package com.subgraph.orchid.crypto;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import com.subgraph.orchid.TorException;
/**
* The <code>HybridEncryption</code> class implements the "hybrid encryption" scheme
* as described in section 0.3 of the main Tor specification (tor-spec.txt).
*/
public class HybridEncryption {
private final static int PK_ENC_LEN = 128;
private final static int PK_PAD_LEN = 42;
private final static int PK_DATA_LEN = PK_ENC_LEN - PK_PAD_LEN; // 86 bytes
private final static int PK_DATA_LEN_WITH_KEY = PK_DATA_LEN - TorStreamCipher.KEY_LEN; // 70 bytes
/*
* The "hybrid encryption" of a byte sequence M with a public key PK is
* computed as follows:
*
* 1. If M is less than PK_ENC_LEN-PK_PAD_LEN (86), pad and encrypt M with PK.
* 2. Otherwise, generate a KEY_LEN byte random key K.
* Let M1 = the first PK_ENC_LEN-PK_PAD_LEN-KEY_LEN (70) bytes of M,
* and let M2 = the rest of M.
* Pad and encrypt K|M1 with PK. Encrypt M2 with our stream cipher,
* using the key K. Concatenate these encrypted values.
*/
final private Cipher cipher;
/**
* Create a new <code>HybridEncryption</code> instance which can be used for performing
* "hybrid encryption" operations as described in the main Tor specification (tor-spec.txt).
*/
public HybridEncryption() {
try {
cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding");
} catch (NoSuchAlgorithmException e) {
throw new TorException(e);
} catch (NoSuchPaddingException e) {
throw new TorException(e);
}
}
/**
* Encrypt the entire contents of the byte array <code>data</code> with the given <code>TorPublicKey</code>
* according to the "hybrid encryption" scheme described in the main Tor specification (tor-spec.txt).
*
* @param data The bytes to be encrypted.
* @param publicKey The public key to use for encryption.
* @return A new array containing the encrypted data.
*/
public byte[] encrypt(byte[] data, TorPublicKey publicKey) {
if(data.length < PK_DATA_LEN)
return encryptSimple(data, publicKey);
// RSA( K | M1 ) --> C1
TorStreamCipher randomKeyCipher = TorStreamCipher.createWithRandomKey();
final byte[] kAndM1 = new byte[PK_DATA_LEN];
System.arraycopy(randomKeyCipher.getKeyBytes(), 0, kAndM1, 0, TorStreamCipher.KEY_LEN);
System.arraycopy(data, 0, kAndM1, TorStreamCipher.KEY_LEN, PK_DATA_LEN_WITH_KEY);
final byte[] c1 = encryptSimple(kAndM1, publicKey);
// AES_CTR(M2) --> C2
final byte[] c2 = new byte[data.length - PK_DATA_LEN_WITH_KEY];
System.arraycopy(data, PK_DATA_LEN_WITH_KEY, c2, 0, c2.length);
randomKeyCipher.encrypt(c2);
//final byte[] c2 = randomKeyCipher.doFinal(data, PK_DATA_LEN_WITH_KEY, data.length - PK_DATA_LEN_WITH_KEY);
// C1 | C2
final byte[] output = new byte[c1.length + c2.length];
System.arraycopy(c1, 0, output, 0, c1.length);
System.arraycopy(c2, 0, output, c1.length, c2.length);
return output;
}
private byte[] encryptSimple(byte[] data, TorPublicKey publicKey) {
try {
cipher.init(Cipher.ENCRYPT_MODE, publicKey.getRSAPublicKey());
return cipher.doFinal(data);
} catch (InvalidKeyException e) {
throw new TorException(e);
} catch (IllegalBlockSizeException e) {
throw new TorException(e);
} catch (BadPaddingException e) {
throw new TorException(e);
}
}
/**
* Decrypt the contents of the byte array <code>data</code> with the given <code>TorPrivateKey</code>
* according to the "hybrid encryption" scheme described in the main Tor specification (tor-spec.txt).
*
* @param data Encrypted data to decrypt.
* @param privateKey The private key to use to decrypt the data.
* @return A new byte array containing the decrypted data.
*/
public byte[] decrypt(byte[] data, TorPrivateKey privateKey) {
if(data.length < PK_ENC_LEN)
throw new TorException("Message is too short");
if(data.length == PK_ENC_LEN)
return decryptSimple(data, privateKey);
// ( C1 | C2 ) --> C1, C2
final byte[] c1 = new byte[PK_ENC_LEN];
final byte[] c2 = new byte[data.length - PK_ENC_LEN];
System.arraycopy(data, 0, c1, 0, PK_ENC_LEN);
System.arraycopy(data, PK_ENC_LEN, c2, 0, c2.length);
// RSA( C1 ) --> ( K | M1 ) --> K, M1
final byte[] kAndM1 = decryptSimple(c1, privateKey);
final byte[] streamKey = new byte[TorStreamCipher.KEY_LEN];
final int m1Length = kAndM1.length - TorStreamCipher.KEY_LEN;
final byte[] m1 = new byte[m1Length];
System.arraycopy(kAndM1, 0, streamKey, 0, TorStreamCipher.KEY_LEN);
System.arraycopy(kAndM1, TorStreamCipher.KEY_LEN, m1, 0, m1Length);
// AES_CTR( C2 ) --> M2
final TorStreamCipher streamCipher = TorStreamCipher.createFromKeyBytes(streamKey);
streamCipher.encrypt(c2);
final byte[] m2 = c2;
final byte[] output = new byte[m1.length + m2.length];
System.arraycopy(m1, 0, output, 0, m1.length);
System.arraycopy(m2, 0, output, m1.length, m2.length);
return output;
}
private byte[] decryptSimple(byte[] data, TorPrivateKey privateKey) {
try {
cipher.init(Cipher.DECRYPT_MODE, privateKey.getRSAPrivateKey());
return cipher.doFinal(data);
} catch (InvalidKeyException e) {
throw new TorException(e);
} catch (IllegalBlockSizeException e) {
throw new TorException(e);
} catch (BadPaddingException e) {
throw new TorException(e);
}
}
}