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
e-sushi
  • 13,786
  • 10
  • 38
  • 57
Ronaldinho Learn Coding
  • 13,254
  • 24
  • 83
  • 110

6 Answers6

10

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.

Community
  • 1
  • 1
Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
  • 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? – Ronaldinho Learn Coding Sep 06 '12 at 01:23
  • 1
    Pls help me how to use your code, I tested it but give me very strange result.... – Ronaldinho Learn Coding Sep 06 '12 at 02:07
  • I tested code, create 2 separated RC4 objects(for encrypt and decrypt). Result after decryption is same as input plain-text => code maybe good, but I can't display the cipher-text (gave me strange characters after I convert byte to String) , I mean how can I verify if cipher-text is correct (compare to other implementation in other languages C# or C++), how to display that ciphertext? – Ronaldinho Learn Coding Sep 06 '12 at 02:40
  • 1
    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 [tag:encoding]. For transport use Base64. Both are available in the Apache commons libraries, Google them! – Maarten Bodewes Sep 06 '12 at 07:23
  • Did you get it to work? I haven't tested bigger data yet, just a few bytes. – Maarten Bodewes Sep 06 '12 at 22:15
  • Yes it worked, I mean after you encrypted original input text you get a cipher-text, decrypt that cipher-text again you will get exactly original input but I just dont know to see the cipher - text, still have difficult to convert it to Base64 as you said – Ronaldinho Learn Coding Sep 07 '12 at 02:33
  • 1
    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. – Maarten Bodewes Sep 07 '12 at 23:33
  • 2
    Or try http://stackoverflow.com/questions/2817752/java-code-to-convert-byte-to-hexadecimal – Maarten Bodewes Sep 07 '12 at 23:35
  • Thanks very much for great support! I'll try to figure it out. – Ronaldinho Learn Coding Sep 09 '12 at 17:08
  • I've copied your code and run it, but the encrypted result seems not correct. Key is `key` and plaintext is `hello`, the online [RC4 encryption tools](http://www.fyneworks.com/encryption/rc4-encryption/) show that the result be `63 09 58 81 4B`, but your result is `63 09 58 81 6F`. Please give it a check. – ChandlerQ May 22 '13 at 16:50
4

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);
    }
}
micahk
  • 41
  • 1
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];
Duncan Jones
  • 67,400
  • 29
  • 193
  • 254
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.

dmantil
  • 31
  • 1
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.

theglauber
  • 28,367
  • 7
  • 29
  • 47
  • 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? – Ronaldinho Learn Coding Sep 05 '12 at 21:30
  • 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. – theglauber Sep 05 '12 at 21:36
  • I edited my question, I test the code, but the encryption and decryption give me different result, you know why? – Ronaldinho Learn Coding Sep 05 '12 at 21:41
  • 3
    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](http://docs.oracle.com/javase/tutorial/java/nutsandbolts/operators.html)) will return an integer as result, requiring a cast back to `byte`. – Maarten Bodewes Sep 05 '12 at 22:07
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