package net.sf.briar.crypto; import static javax.crypto.Cipher.ENCRYPT_MODE; import static net.sf.briar.api.plugins.InvitationConstants.CODE_BITS; import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH; import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PublicKey; import java.security.SecureRandom; import java.security.Security; import java.security.Signature; import javax.crypto.Cipher; import javax.crypto.KeyAgreement; import javax.crypto.spec.IvParameterSpec; import net.sf.briar.api.crypto.AuthenticatedCipher; import net.sf.briar.api.crypto.CryptoComponent; import net.sf.briar.api.crypto.ErasableKey; import net.sf.briar.api.crypto.KeyParser; import net.sf.briar.api.crypto.MessageDigest; import net.sf.briar.api.crypto.PseudoRandom; import net.sf.briar.util.ByteUtils; import org.spongycastle.crypto.engines.AESEngine; import org.spongycastle.crypto.modes.AEADBlockCipher; import org.spongycastle.crypto.modes.GCMBlockCipher; import org.spongycastle.jce.provider.BouncyCastleProvider; import com.google.inject.Inject; class CryptoComponentImpl implements CryptoComponent { private static final String PROVIDER = "SC"; private static final String AGREEMENT_KEY_PAIR_ALGO = "ECDH"; private static final int AGREEMENT_KEY_PAIR_BITS = 384; private static final String AGREEMENT_ALGO = "ECDHC"; private static final String SECRET_KEY_ALGO = "AES"; private static final int SECRET_KEY_BYTES = 32; // 256 bits private static final String KEY_DERIVATION_ALGO = "AES/CTR/NoPadding"; private static final int KEY_DERIVATION_IV_BYTES = 16; // 128 bits private static final String DIGEST_ALGO = "SHA-384"; private static final String SIGNATURE_KEY_PAIR_ALGO = "ECDSA"; private static final int SIGNATURE_KEY_PAIR_BITS = 384; private static final String SIGNATURE_ALGO = "ECDSA"; private static final String TAG_CIPHER_ALGO = "AES/ECB/NoPadding"; private static final int GCM_MAC_LENGTH = 16; // 128 bits // Labels for key derivation private static final byte[] A_TAG = { 'A', '_', 'T', 'A', 'G', '\0' }; private static final byte[] B_TAG = { 'B', '_', 'T', 'A', 'G', '\0' }; private static final byte[] A_FRAME_A = { 'A', '_', 'F', 'R', 'A', 'M', 'E', '_', 'A', '\0' }; private static final byte[] A_FRAME_B = { 'A', '_', 'F', 'R', 'A', 'M', 'E', '_', 'B', '\0' }; private static final byte[] B_FRAME_A = { 'B', '_', 'F', 'R', 'A', 'M', 'E', '_', 'A', '\0' }; private static final byte[] B_FRAME_B = { 'B', '_', 'F', 'R', 'A', 'M', 'E', '_', 'B', '\0' }; // Labels for secret derivation private static final byte[] FIRST = { 'F', 'I', 'R', 'S', 'T', '\0' }; private static final byte[] ROTATE = { 'R', 'O', 'T', 'A', 'T', 'E', '\0' }; // Label for confirmation code derivation private static final byte[] CODE = { 'C', 'O', 'D', 'E', '\0' }; // Blank plaintext for key derivation private static final byte[] KEY_DERIVATION_BLANK_PLAINTEXT = new byte[SECRET_KEY_BYTES]; private final KeyParser agreementKeyParser, signatureKeyParser; private final KeyPairGenerator agreementKeyPairGenerator; private final KeyPairGenerator signatureKeyPairGenerator; private final SecureRandom secureRandom; @Inject CryptoComponentImpl() { Security.addProvider(new BouncyCastleProvider()); try { agreementKeyParser = new KeyParserImpl(AGREEMENT_KEY_PAIR_ALGO, PROVIDER); signatureKeyParser = new KeyParserImpl(SIGNATURE_KEY_PAIR_ALGO, PROVIDER); agreementKeyPairGenerator = KeyPairGenerator.getInstance( AGREEMENT_KEY_PAIR_ALGO, PROVIDER); agreementKeyPairGenerator.initialize(AGREEMENT_KEY_PAIR_BITS); signatureKeyPairGenerator = KeyPairGenerator.getInstance( SIGNATURE_KEY_PAIR_ALGO, PROVIDER); signatureKeyPairGenerator.initialize(SIGNATURE_KEY_PAIR_BITS); } catch(GeneralSecurityException e) { throw new RuntimeException(e); } secureRandom = new SecureRandom(); } public ErasableKey deriveTagKey(byte[] secret, boolean alice) { if(alice) return deriveKey(secret, A_TAG, 0L); else return deriveKey(secret, B_TAG, 0L); } public ErasableKey deriveFrameKey(byte[] secret, long connection, boolean alice, boolean initiator) { if(alice) { if(initiator) return deriveKey(secret, A_FRAME_A, connection); else return deriveKey(secret, A_FRAME_B, connection); } else { if(initiator) return deriveKey(secret, B_FRAME_A, connection); else return deriveKey(secret, B_FRAME_B, connection); } } private ErasableKey deriveKey(byte[] secret, byte[] label, long context) { byte[] key = counterModeKdf(secret, label, context); return new ErasableKeyImpl(key, SECRET_KEY_ALGO); } // Key derivation function based on a block cipher in CTR mode - see // NIST SP 800-108, section 5.1 private byte[] counterModeKdf(byte[] secret, byte[] label, long context) { // The secret must be usable as a key if(secret.length != SECRET_KEY_BYTES) throw new IllegalArgumentException(); // The label and context must leave a byte free for the counter if(label.length + 4 >= KEY_DERIVATION_IV_BYTES) throw new IllegalArgumentException(); byte[] ivBytes = new byte[KEY_DERIVATION_IV_BYTES]; System.arraycopy(label, 0, ivBytes, 0, label.length); ByteUtils.writeUint32(context, ivBytes, label.length); // Use the secret and the IV to encrypt a blank plaintext IvParameterSpec iv = new IvParameterSpec(ivBytes); ErasableKey key = new ErasableKeyImpl(secret, SECRET_KEY_ALGO); try { Cipher cipher = Cipher.getInstance(KEY_DERIVATION_ALGO, PROVIDER); cipher.init(Cipher.ENCRYPT_MODE, key, iv); byte[] output = cipher.doFinal(KEY_DERIVATION_BLANK_PLAINTEXT); assert output.length == SECRET_KEY_BYTES; return output; } catch(GeneralSecurityException e) { throw new RuntimeException(e); } } public byte[] deriveInitialSecret(byte[] theirPublicKey, KeyPair ourKeyPair, boolean alice) throws GeneralSecurityException { PublicKey theirPublic = agreementKeyParser.parsePublicKey( theirPublicKey); MessageDigest messageDigest = getMessageDigest(); byte[] ourPublicKey = ourKeyPair.getPublic().getEncoded(); byte[] ourHash = messageDigest.digest(ourPublicKey); byte[] theirHash = messageDigest.digest(theirPublicKey); byte[] aliceInfo, bobInfo; if(alice) { aliceInfo = ourHash; bobInfo = theirHash; } else { aliceInfo = theirHash; bobInfo = ourHash; } // The raw secret comes from the key agreement algorithm KeyAgreement keyAgreement = KeyAgreement.getInstance(AGREEMENT_ALGO, PROVIDER); keyAgreement.init(ourKeyPair.getPrivate()); keyAgreement.doPhase(theirPublic, true); byte[] rawSecret = keyAgreement.generateSecret(); // Derive the cooked secret from the raw secret using the // concatenation KDF byte[] cookedSecret = concatenationKdf(rawSecret, FIRST, aliceInfo, bobInfo); ByteUtils.erase(rawSecret); return cookedSecret; } // Key derivation function based on a hash function - see NIST SP 800-56A, // section 5.8 private byte[] concatenationKdf(byte[] rawSecret, byte[] label, byte[] initiatorInfo, byte[] responderInfo) { // The output of the hash function must be long enough to use as a key MessageDigest messageDigest = getMessageDigest(); if(messageDigest.getDigestLength() < SECRET_KEY_BYTES) throw new RuntimeException(); // All fields are length-prefixed byte[] length = new byte[1]; ByteUtils.writeUint8(rawSecret.length, length, 0); messageDigest.update(length); messageDigest.update(rawSecret); ByteUtils.writeUint8(label.length, length, 0); messageDigest.update(length); messageDigest.update(label); ByteUtils.writeUint8(initiatorInfo.length, length, 0); messageDigest.update(length); messageDigest.update(initiatorInfo); ByteUtils.writeUint8(responderInfo.length, length, 0); messageDigest.update(length); messageDigest.update(responderInfo); byte[] hash = messageDigest.digest(); // The secret is the first SECRET_KEY_BYTES bytes of the hash byte[] output = new byte[SECRET_KEY_BYTES]; System.arraycopy(hash, 0, output, 0, SECRET_KEY_BYTES); ByteUtils.erase(hash); return output; } public byte[] deriveNextSecret(byte[] secret, long period) { if(period < 0 || period > MAX_32_BIT_UNSIGNED) throw new IllegalArgumentException(); return counterModeKdf(secret, ROTATE, period); } public void encodeTag(byte[] tag, Cipher tagCipher, ErasableKey tagKey, long connection) { if(tag.length < TAG_LENGTH) throw new IllegalArgumentException(); if(connection < 0 || connection > MAX_32_BIT_UNSIGNED) throw new IllegalArgumentException(); for(int i = 0; i < TAG_LENGTH; i++) tag[i] = 0; ByteUtils.writeUint32(connection, tag, 0); try { tagCipher.init(ENCRYPT_MODE, tagKey); int encrypted = tagCipher.doFinal(tag, 0, TAG_LENGTH, tag); if(encrypted != TAG_LENGTH) throw new IllegalArgumentException(); } catch(GeneralSecurityException e) { // Unsuitable cipher or key throw new IllegalArgumentException(e); } } public int generateInvitationCode() { int codeBytes = (int) Math.ceil(CODE_BITS / 8.0); byte[] random = new byte[codeBytes]; secureRandom.nextBytes(random); return ByteUtils.readUint(random, CODE_BITS); } public int[] deriveConfirmationCodes(byte[] secret) { byte[] alice = counterModeKdf(secret, CODE, 0); byte[] bob = counterModeKdf(secret, CODE, 1); int[] codes = new int[2]; codes[0] = ByteUtils.readUint(alice, CODE_BITS); codes[1] = ByteUtils.readUint(bob, CODE_BITS); ByteUtils.erase(alice); ByteUtils.erase(bob); return codes; } public KeyPair generateAgreementKeyPair() { return agreementKeyPairGenerator.generateKeyPair(); } public KeyParser getAgreementKeyParser() { return agreementKeyParser; } public KeyPair generateSignatureKeyPair() { return signatureKeyPairGenerator.generateKeyPair(); } public KeyParser getSignatureKeyParser() { return signatureKeyParser; } public ErasableKey generateTestKey() { byte[] b = new byte[SECRET_KEY_BYTES]; getSecureRandom().nextBytes(b); return new ErasableKeyImpl(b, SECRET_KEY_ALGO); } public MessageDigest getMessageDigest() { try { return new DoubleDigest(java.security.MessageDigest.getInstance( DIGEST_ALGO, PROVIDER)); } catch(GeneralSecurityException e) { throw new RuntimeException(e); } } public PseudoRandom getPseudoRandom(int seed1, int seed2) { return new PseudoRandomImpl(getMessageDigest(), seed1, seed2); } public SecureRandom getSecureRandom() { return secureRandom; } public Signature getSignature() { try { return Signature.getInstance(SIGNATURE_ALGO, PROVIDER); } catch(GeneralSecurityException e) { throw new RuntimeException(e); } } public Cipher getTagCipher() { try { return Cipher.getInstance(TAG_CIPHER_ALGO, PROVIDER); } catch(GeneralSecurityException e) { throw new RuntimeException(e); } } public AuthenticatedCipher getFrameCipher() { // This code is specific to BouncyCastle because javax.crypto.Cipher // doesn't support additional authenticated data until Java 7 AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine()); return new AuthenticatedCipherImpl(cipher, GCM_MAC_LENGTH); } }