diff --git a/briar-core/src/org/briarproject/crypto/AsciiArmour.java b/briar-core/src/org/briarproject/crypto/AsciiArmour.java new file mode 100644 index 0000000000000000000000000000000000000000..5e00461d475ecab8d107bee86f95908e57a8d792 --- /dev/null +++ b/briar-core/src/org/briarproject/crypto/AsciiArmour.java @@ -0,0 +1,27 @@ +package org.briarproject.crypto; + +import org.briarproject.api.FormatException; +import org.briarproject.util.StringUtils; + +class AsciiArmour { + + static String wrap(byte[] b, int lineLength) { + String wrapped = StringUtils.toHexString(b); + StringBuilder s = new StringBuilder(); + int length = wrapped.length(); + for (int i = 0; i < length; i += lineLength) { + int end = Math.min(i + lineLength, length); + s.append(wrapped.substring(i, end)); + s.append("\r\n"); + } + return s.toString(); + } + + static byte[] unwrap(String s) throws FormatException { + try { + return StringUtils.fromHexString(s.replaceAll("[^0-9a-fA-F]", "")); + } catch (IllegalArgumentException e) { + throw new FormatException(); + } + } +} diff --git a/briar-core/src/org/briarproject/crypto/MessageEncrypter.java b/briar-core/src/org/briarproject/crypto/MessageEncrypter.java new file mode 100644 index 0000000000000000000000000000000000000000..a92eed0abd568987fdb06f7f2c9b630932c48b8f --- /dev/null +++ b/briar-core/src/org/briarproject/crypto/MessageEncrypter.java @@ -0,0 +1,207 @@ +package org.briarproject.crypto; + +import org.briarproject.util.StringUtils; +import org.spongycastle.asn1.teletrust.TeleTrusTNamedCurves; +import org.spongycastle.asn1.x9.X9ECParameters; +import org.spongycastle.crypto.AsymmetricCipherKeyPair; +import org.spongycastle.crypto.BasicAgreement; +import org.spongycastle.crypto.BlockCipher; +import org.spongycastle.crypto.CipherParameters; +import org.spongycastle.crypto.CryptoException; +import org.spongycastle.crypto.DerivationFunction; +import org.spongycastle.crypto.KeyEncoder; +import org.spongycastle.crypto.KeyParser; +import org.spongycastle.crypto.Mac; +import org.spongycastle.crypto.agreement.ECDHCBasicAgreement; +import org.spongycastle.crypto.digests.SHA256Digest; +import org.spongycastle.crypto.engines.AESLightEngine; +import org.spongycastle.crypto.engines.IESEngine; +import org.spongycastle.crypto.generators.ECKeyPairGenerator; +import org.spongycastle.crypto.generators.EphemeralKeyPairGenerator; +import org.spongycastle.crypto.generators.KDF2BytesGenerator; +import org.spongycastle.crypto.macs.HMac; +import org.spongycastle.crypto.modes.CBCBlockCipher; +import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher; +import org.spongycastle.crypto.params.AsymmetricKeyParameter; +import org.spongycastle.crypto.params.ECDomainParameters; +import org.spongycastle.crypto.params.ECKeyGenerationParameters; +import org.spongycastle.crypto.params.ECPrivateKeyParameters; +import org.spongycastle.crypto.params.ECPublicKeyParameters; +import org.spongycastle.crypto.params.IESWithCipherParameters; +import org.spongycastle.crypto.parsers.ECIESPublicKeyParser; + +import java.io.ByteArrayInputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.security.SecureRandom; +import java.util.Scanner; + +public class MessageEncrypter { + + private static final ECDomainParameters PARAMETERS; + private static final int MAC_KEY_BITS = 256; + private static final int CIPHER_KEY_BITS = 256; + private static final int LINE_LENGTH = 70; + + static { + X9ECParameters x9 = TeleTrusTNamedCurves.getByName("brainpoolp512r1"); + PARAMETERS = new ECDomainParameters(x9.getCurve(), x9.getG(), + x9.getN(), x9.getH()); + } + + private final ECKeyPairGenerator generator; + private final EphemeralKeyPairGenerator ephemeralGenerator; + private final KeyParser parser; + + MessageEncrypter(SecureRandom random) { + generator = new ECKeyPairGenerator(); + generator.init(new ECKeyGenerationParameters(PARAMETERS, random)); + KeyEncoder encoder = new PublicKeyEncoder(); + ephemeralGenerator = new EphemeralKeyPairGenerator(generator, encoder); + parser = new PublicKeyParser(PARAMETERS); + } + + AsymmetricCipherKeyPair generateKeyPair() { + return generator.generateKeyPair(); + } + + byte[] encrypt(ECPublicKeyParameters pubKey, byte[] plaintext) + throws CryptoException { + IESEngine engine = getEngine(); + engine.init(pubKey, getCipherParameters(), ephemeralGenerator); + return engine.processBlock(plaintext, 0, plaintext.length); + } + + byte[] decrypt(ECPrivateKeyParameters privKey, byte[] ciphertext) + throws CryptoException { + IESEngine engine = getEngine(); + engine.init(privKey, getCipherParameters(), parser); + return engine.processBlock(ciphertext, 0, ciphertext.length); + } + + private IESEngine getEngine() { + BasicAgreement agreement = new ECDHCBasicAgreement(); + DerivationFunction kdf = new KDF2BytesGenerator(new SHA256Digest()); + Mac mac = new HMac(new SHA256Digest()); + BlockCipher cipher = new CBCBlockCipher(new AESLightEngine()); + PaddedBufferedBlockCipher pad = new PaddedBufferedBlockCipher(cipher); + return new IESEngine(agreement, kdf, mac, pad); + } + + private CipherParameters getCipherParameters() { + return new IESWithCipherParameters(null, null, MAC_KEY_BITS, + CIPHER_KEY_BITS); + } + + private static class PublicKeyEncoder implements KeyEncoder { + + @Override + public byte[] getEncoded(AsymmetricKeyParameter key) { + if (!(key instanceof ECPublicKeyParameters)) + throw new IllegalArgumentException(); + return ((ECPublicKeyParameters) key).getQ().getEncoded(false); + } + } + + private static class PublicKeyParser extends ECIESPublicKeyParser { + + private PublicKeyParser(ECDomainParameters ecParams) { + super(ecParams); + } + + @Override + public AsymmetricKeyParameter readKey(InputStream in) + throws IOException { + try { + return super.readKey(in); + } catch (IllegalArgumentException e) { + throw new IOException(e); + } + } + } + + public static void main(String[] args) throws Exception { + if (args.length < 1) { + printUsage(); + return; + } + SecureRandom random = new SecureRandom(); + MessageEncrypter encrypter = new MessageEncrypter(random); + if (args[0].equals("generate")) { + if (args.length != 3) { + printUsage(); + return; + } + // Generate a key pair + AsymmetricCipherKeyPair keyPair = encrypter.generateKeyPair(); + ECPublicKeyParameters publicKey = + (ECPublicKeyParameters) keyPair.getPublic(); + byte[] publicKeyBytes = publicKey.getQ().getEncoded(false); + PrintStream out = new PrintStream(new FileOutputStream(args[1])); + out.print(StringUtils.toHexString(publicKeyBytes)); + out.flush(); + out.close(); + ECPrivateKeyParameters privateKey = + (ECPrivateKeyParameters) keyPair.getPrivate(); + out = new PrintStream(new FileOutputStream(args[2])); + out.print(privateKey.getD().toString(16).toUpperCase()); + out.flush(); + out.close(); + } else if (args[0].equals("encrypt")) { + if (args.length != 2) { + printUsage(); + return; + } + // Encrypt a decrypted message + InputStream in = new FileInputStream(args[1]); + byte[] b = StringUtils.fromHexString(readFully(in).trim()); + in = new ByteArrayInputStream(b); + ECPublicKeyParameters publicKey = + (ECPublicKeyParameters) encrypter.parser.readKey(in); + String message = readFully(System.in); + byte[] plaintext = message.getBytes(Charset.forName("UTF-8")); + byte[] ciphertext = encrypter.encrypt(publicKey, plaintext); + System.out.println(AsciiArmour.wrap(ciphertext, LINE_LENGTH)); + } else if (args[0].equals("decrypt")) { + if (args.length != 2) { + printUsage(); + return; + } + // Decrypt an encrypted message + InputStream in = new FileInputStream(args[1]); + byte[] b = StringUtils.fromHexString(readFully(in).trim()); + BigInteger d = new BigInteger(1, b); + ECPrivateKeyParameters privateKey = new ECPrivateKeyParameters(d, + PARAMETERS); + byte[] ciphertext = AsciiArmour.unwrap(readFully(System.in)); + byte[] plaintext = encrypter.decrypt(privateKey, ciphertext); + System.out.println(new String(plaintext, Charset.forName("UTF-8"))); + } else { + printUsage(); + } + } + + private static void printUsage() { + System.err.println("Usage:"); + System.err.println("MessageEncrypter generate <public_key_file> <private_key_file>"); + System.err.println("MessageEncrypter encrypt <public_key_file>"); + System.err.println("MessageEncrypter decrypt <private_key_file>"); + } + + private static String readFully(InputStream in) throws IOException { + StringBuilder stringBuilder = new StringBuilder(); + Scanner scanner = new Scanner(in); + while (scanner.hasNextLine()) { + stringBuilder.append(scanner.nextLine()); + stringBuilder.append(System.lineSeparator()); + } + scanner.close(); + in.close(); + return stringBuilder.toString(); + } +} diff --git a/briar-tests/src/org/briarproject/crypto/AsciiArmourTest.java b/briar-tests/src/org/briarproject/crypto/AsciiArmourTest.java new file mode 100644 index 0000000000000000000000000000000000000000..e07f3e6c39a50ceeede823db656299dda522d7d1 --- /dev/null +++ b/briar-tests/src/org/briarproject/crypto/AsciiArmourTest.java @@ -0,0 +1,47 @@ +package org.briarproject.crypto; + +import org.briarproject.BriarTestCase; +import org.junit.Test; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +public class AsciiArmourTest extends BriarTestCase { + + @Test + public void testWrapOnSingleLine() { + byte[] b = new byte[8]; + for (int i = 0; i < b.length; i++) b[i] = (byte) i; + String expected = "0001020304050607\r\n"; + assertEquals(expected, AsciiArmour.wrap(b, 70)); + } + + @Test + public void testWrapOnMultipleLines() { + byte[] b = new byte[8]; + for (int i = 0; i < b.length; i++) b[i] = (byte) i; + String expected = "0001020\r\n3040506\r\n07\r\n"; + assertEquals(expected, AsciiArmour.wrap(b, 7)); + } + + @Test + public void testUnwrapOnSingleLine() throws Exception { + String s = "0001020304050607"; + byte[] expected = new byte[] {0, 1, 2, 3, 4, 5, 6, 7}; + assertArrayEquals(expected, AsciiArmour.unwrap(s)); + } + + @Test + public void testUnwrapOnMultipleLines() throws Exception { + String s = "0001020\r\n3040506\r\n07"; + byte[] expected = new byte[] {0, 1, 2, 3, 4, 5, 6, 7}; + assertArrayEquals(expected, AsciiArmour.unwrap(s)); + } + + @Test + public void testUnwrapWithJunkCharacters() throws Exception { + String s = "0001??020\rzz\n30z40..506\r\n07;;"; + byte[] expected = new byte[] {0, 1, 2, 3, 4, 5, 6, 7}; + assertArrayEquals(expected, AsciiArmour.unwrap(s)); + } +} diff --git a/briar-tests/src/org/briarproject/crypto/MessageEncrypterTest.java b/briar-tests/src/org/briarproject/crypto/MessageEncrypterTest.java new file mode 100644 index 0000000000000000000000000000000000000000..29c07984e5b5d3e66b059cc4f6a7940c80d88d26 --- /dev/null +++ b/briar-tests/src/org/briarproject/crypto/MessageEncrypterTest.java @@ -0,0 +1,41 @@ +package org.briarproject.crypto; + +import org.briarproject.BriarTestCase; +import org.junit.Test; +import org.spongycastle.crypto.AsymmetricCipherKeyPair; +import org.spongycastle.crypto.CryptoException; +import org.spongycastle.crypto.params.ECPrivateKeyParameters; +import org.spongycastle.crypto.params.ECPublicKeyParameters; + +import java.security.SecureRandom; + +import static org.junit.Assert.assertArrayEquals; + +public class MessageEncrypterTest extends BriarTestCase { + + private final SecureRandom random = new SecureRandom(); + + @Test + public void testEncryptionAndDecryption() throws Exception { + MessageEncrypter m = new MessageEncrypter(random); + AsymmetricCipherKeyPair kp = m.generateKeyPair(); + ECPublicKeyParameters pub = (ECPublicKeyParameters) kp.getPublic(); + ECPrivateKeyParameters priv = (ECPrivateKeyParameters) kp.getPrivate(); + byte[] plaintext = new byte[123]; + random.nextBytes(plaintext); + byte[] ciphertext = m.encrypt(pub, plaintext); + byte[] decrypted = m.decrypt(priv, ciphertext); + assertArrayEquals(plaintext, decrypted); + } + + @Test(expected = CryptoException.class) + public void testDecryptionFailsWithAlteredCiphertext() throws Exception { + MessageEncrypter m = new MessageEncrypter(random); + AsymmetricCipherKeyPair kp = m.generateKeyPair(); + ECPublicKeyParameters pub = (ECPublicKeyParameters) kp.getPublic(); + ECPrivateKeyParameters priv = (ECPrivateKeyParameters) kp.getPrivate(); + byte[] ciphertext = m.encrypt(pub, new byte[123]); + ciphertext[random.nextInt(ciphertext.length)] ^= 0xFF; + m.decrypt(priv, ciphertext); + } +}