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);
+	}
+}