6

Hi there I am trying to implement the RC4 algorithm in Java. I found this code as an example that help me to understand the idea:

public class RC4 { private int[] S = new int[256]; private int[] T = new int[256]; private int keylen; public RC4(byte[] key) throws Exception { if (key.length < 1 || key.length > 256) { throw new Exception("key must be between 1 and 256 bytes"); } else { keylen = key.length; for (int i = 0; i < 256; i++) { S[i] = i; T[i] = key[i % keylen]; } int j = 0; for (int i = 0; i < 256; i++) { j = (j + S[i] + T[i]) % 256; S[i] ^= S[j]; S[j] ^= S[i]; S[i] ^= S[j]; } } } public int[] encrypt(int[] plaintext) { int[] ciphertext = new int[plaintext.length]; int i = 0, j = 0, k, t; for (int counter = 0; counter < plaintext.length; counter++) { i = (i + 1) % 256; j = (j + S[i]) % 256; S[i] ^= S[j]; S[j] ^= S[i]; S[i] ^= S[j]; t = (S[i] + S[j]) % 256; k = S[t]; ciphertext[counter] = plaintext[counter] ^ k; } return ciphertext; } public int[] decrypt(int[] ciphertext) { return encrypt(ciphertext); } } 

I have few question:

  1. Why is the plain-text an int array in the above code?

  2. When I test this code I get strange result, can somebody explain to me? Here my code to test:

    public class RC4_Main { public static void main(String args[]) throws Exception { String keyword = "hello"; byte[] keytest = keyword.getBytes(); //convert keyword to byte int[] text = {1, 2, 3, 4, 5}; // text as 12345 RC4 rc4 = new RC4(keytest); System.out.print("\noriginal text: "); for (int i = 0; i < text.length; i++) { System.out.print(text[i]); } int[] cipher = rc4.encrypt(text); //encryption System.out.print("\ncipher: "); for (int i = 0; i < cipher.length; i++) { System.out.print(cipher[i]); } int[] backtext = rc4.decrypt(cipher); //decryption System.out.print("\nback to text: "); for (int i = 0; i < backtext.length; i++) { System.out.print(backtext[i]); } System.out.println(); } } 

Here is the result: (original and back to text are not SAME) why???

original text: 12345 cipher: 1483188254174 back to text: 391501310217 

6 Answers 6

11

There are a few things to notice:

  • Java is not very easy to use when you require unsigned bytes (e.g. for indexing);
  • if you create a state in S and T, you should really notice that these values change, when you decrypt with the same instance you take the state used for encryption;
  • the above code is not very efficient memory wise, and you can easily rewrite it to take byte arrays;
  • to use a String, after refactoring the arguments to byte[], you first need to use first, e.g. using String.getBytes(Charset charset);

To make life easier, and to have some fun late night hacking, I improved your code and tested it against a single vector in rfc6229 using a zero'd out byte array.

UPDATE: As micahk points out below, the evil C XOR swap that was used prevented this code from encrypting the final byte of input in Java. Using regular old swaps fixes it.

Warning: the code below should be considered a coding exercise. Please use a well vetted library instead of the code snippet below to perform RC4 (or Ron's Code 4, ARC4 etc.) in your application. That means using Cipher.getInstance("RC4"); or the ARC4 classes in Bouncy Castle.

public class RC4 { private final byte[] S = new byte[256]; private final byte[] T = new byte[256]; private final int keylen; public RC4(final byte[] key) { if (key.length < 1 || key.length > 256) { throw new IllegalArgumentException( "key must be between 1 and 256 bytes"); } else { keylen = key.length; for (int i = 0; i < 256; i++) { S[i] = (byte) i; T[i] = key[i % keylen]; } int j = 0; byte tmp; for (int i = 0; i < 256; i++) { j = (j + S[i] + T[i]) & 0xFF; tmp = S[j]; S[j] = S[i]; S[i] = tmp; } } } public byte[] encrypt(final byte[] plaintext) { final byte[] ciphertext = new byte[plaintext.length]; int i = 0, j = 0, k, t; byte tmp; for (int counter = 0; counter < plaintext.length; counter++) { i = (i + 1) & 0xFF; j = (j + S[i]) & 0xFF; tmp = S[j]; S[j] = S[i]; S[i] = tmp; t = (S[i] + S[j]) & 0xFF; k = S[t]; ciphertext[counter] = (byte) (plaintext[counter] ^ k); } return ciphertext; } public byte[] decrypt(final byte[] ciphertext) { return encrypt(ciphertext); } } 

Happy coding.

Sign up to request clarification or add additional context in comments.

10 Comments

Hi there! Thank you so much for helping me but can you provide a simple pieces of codes that used to test your above code. I want "key" and "plain-text" that will be read from files (key file and plain-text file) in binary mode (is that mean convert contain of file to byte?). But now I just need a sample code to test this class, can you help me?
Pls help me how to use your code, I tested it but give me very strange result....
The output of most known ciphers is a number of bits or bytes that can have any value. Those bytes may not encode printable characters (using e.g. ASCII decoding). If you want ciphertext that can be displayed, use hexadecimal encoding. For transport use Base64. Both are available in the Apache commons libraries, Google them!
Try hexadecimals first. A good IDE such as Eclipse can also show you the byte array in hexadecimals, but you have to configure the "Variables" view correctly using the drop down box first. Then click the plus sign in front of the array to see all elements.
|
5

The Java code has a bug due to the use of the xor-swap technique:

 S[i] ^= S[j]; S[j] ^= S[i]; S[i] ^= S[j]; 

Instead of this, you'll want to use a temp variable as in the below. I haven't delved into why the result isn't as expected with the xor swap, but I had decryption errors with this that were resolved by simply doing a straight-forward swap. I suspect it to be a subtle-side effect of the implicit cast from byte to int that occurs in order to do the xor operation.

public class RC4 { private final byte[] S = new byte[256]; private final byte[] T = new byte[256]; private final int keylen; public RC4(final byte[] key) { if (key.length < 1 || key.length > 256) { throw new IllegalArgumentException( "key must be between 1 and 256 bytes"); } else { keylen = key.length; for (int i = 0; i < 256; i++) { S[i] = (byte) i; T[i] = key[i % keylen]; } int j = 0; for (int i = 0; i < 256; i++) { j = (j + S[i] + T[i]) & 0xFF; byte temp = S[i]; S[i] = S[j]; S[j] = temp; } } } public byte[] encrypt(final byte[] plaintext) { final byte[] ciphertext = new byte[plaintext.length]; int i = 0, j = 0, k, t; for (int counter = 0; counter < plaintext.length; counter++) { i = (i + 1) & 0xFF; j = (j + S[i]) & 0xFF; byte temp = S[i]; S[i] = S[j]; S[j] = temp; t = (S[i] + S[j]) & 0xFF; k = S[t]; ciphertext[counter] = (byte) (plaintext[counter] ^ k); } return ciphertext; } public byte[] decrypt(final byte[] ciphertext) { return encrypt(ciphertext); } } 

Comments

3

Your integer arrays S andT have not been constructed. Hence you get a NullPointerException as soon as you attempt to use them.

Looking at the rest of the code, I guess they should have been 256-item arrays:

private int[] S = new int[256]; private int[] T = new int[256]; 

1 Comment

Yes I fixed as your code above, it worked now but why plain text must be an integer array, how can I use string instead?
3

(I know this is a old thread, but maybe my answer can help who is reading it)

The problem is not in the RC4 code but in how you are using it. What you have to understand is every time that encript method is invoked, the S array is modified to generate a pseudo random key.

In this code your are using the decript method after encript over the same instance of RC4 class. But RC4 class have the key creation in the constructor, so when you execute decript method, the key is not recently created as it has been modified by the previous encript. Instead of this code:

int[] cipher = rc4.encrypt(text); //encryption System.out.print("\ncipher: "); for (int i = 0; i < cipher.length; i++) { System.out.print(cipher[i]); } int[] backtext = rc4.decrypt(cipher); //decryption System.out.print("\nback to text: "); for (int i = 0; i < backtext.length; i++) { System.out.print(backtext[i]); } 

Use a rc4 new instance before decript:

int[] cipher = rc4.encrypt(text); //encryption System.out.print("\ncipher: "); for (int i = 0; i < cipher.length; i++) { System.out.print(cipher[i]); } rc4 = new RC4(keytest); int[] backtext = rc4.decrypt(cipher); //decryption System.out.print("\nback to text: "); for (int i = 0; i < backtext.length; i++) { System.out.print(backtext[i]); } 

So the decript method will be have a clean S array, and it will be able to obtain S sequence in the same order than the previous encript method.

Comments

2

1) int array: probably because Java doesn't support unsigned bytes.

2) Null exception: I counted line 12 being this one: S[i] = i; It looks like the S array is not being constructed before it's used.

4 Comments

what do you mean by "unsigned bytes"? I mean the text should be anythings ( at least it should be string) why did the author implement it as an int array, I just don't understand that point?
I'm only guessing, because i didn't write that code. In order to encrypt, you will be treating your string as a sequence of bytes, and doing mathematical operations on those bytes. For whatever historical reason, Java lacks an unsigned byte type, and often people use integers to perform math on Java bytes. The code as presented seems incomplete.
I edited my question, I test the code, but the encryption and decryption give me different result, you know why?
There is really no reason to use int[] instead of a byte[] except if the author only wants to show positive numbers internally. All required operations work identically on signed bytes values as on unsigned byte values. The code gets a bit uglier though as most expressions (that do not use assignment operators) will return an integer as result, requiring a cast back to byte.
1

RC4 is a broken algorithm and recommendation is to not use the same anymore if the data is to be kept highly secure.

If you still need a working implementation, you don't need to recreate the algorithm in your code. Java API javax.crypto can do it for you. Just generate a key and call the init method with mode set to encryption/decryption.

static String decryptRC4() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException{ byte[] testDataBytes = "testString".getBytes(); KeyGenerator rc4KeyGenerator = KeyGenerator.getInstance("RC4"); SecretKey key = rc4KeyGenerator.generateKey(); // Create Cipher instance and initialize it to encrytion mode Cipher cipher = Cipher.getInstance("RC4"); // Transformation of the algorithm cipher.init(Cipher.ENCRYPT_MODE, key); byte[] cipherBytes = cipher.doFinal(testDataBytes); // Reinitialize the Cipher to decryption mode cipher.init(Cipher.DECRYPT_MODE,key, cipher.getParameters()); byte[] testDataBytesDecrypted = cipher.doFinal(cipherBytes); System.out.println("Decrypted Data : "+new String(testDataBytesDecrypted)); return new String(testDataBytesDecrypted); } 

Output:

enter image description here

If you need to send the encrypted data as part of a url then use Base64Encoding and then send.

e.g.

 static String decryptRC4() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException{ byte[] plainBytes = "testString".getBytes(); KeyGenerator rc4KeyGenerator = KeyGenerator.getInstance("RC4"); SecretKey key = rc4KeyGenerator.generateKey(); // Create Cipher instance and initialize it to encrytion mode Cipher cipher = Cipher.getInstance("RC4"); // Transformation of the algorithm cipher.init(Cipher.ENCRYPT_MODE, key); byte[] cipherBytes = cipher.doFinal(plainBytes); String encoded = encodeBase64(cipherBytes); String decoded = decodeBase64(encoded); // Reinitialize the Cipher to decryption mode cipher.init(Cipher.DECRYPT_MODE,key, cipher.getParameters()); byte[] plainBytesDecrypted = cipher.doFinal(Hex.decode(decoded)); System.out.println("Decrypted Data : "+new String(plainBytesDecrypted)); return new String(plainBytesDecrypted); } static String decodeBase64(String encodedData){ byte[] b = Base64.getDecoder().decode(encodedData); String decodedData = DatatypeConverter.printHexBinary(b); return decodedData; } static String encodeBase64(byte[] data){ byte[] b = Base64.getEncoder().encode(data); String encodedData = new String(b); /*String encodedData = DatatypeConverter.printHexBinary(b);*/ return encodedData; } 

Tip: Use Hex.decode as shown above to get bytes from the base64 decoded string or else you will get encoding issues. As much as possible do the conversions using Hex and convert to bytes array using bouncycastle methods.

Imports needed:

import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.xml.bind.DatatypeConverter; import org.apache.commons.codec.DecoderException; import org.bouncycastle.util.encoders.Hex; 

Also if you are generating a key from your own string you can use MD5Hashing for the same.

Please refer this for help on how to create a key using custom String: https://stackoverflow.com/a/52463858/5912424

3 Comments

Thanks @Pierre.Vriens. Wasn't sure if I should duplicate the answer here. I have corrected the same.
@Pierre.Vriens can you please revert the downvote now that the correction has been made?
Hahaha. Sure. So that means it is possible.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.