diff --git a/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java b/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java
index c500cc3ccc13394a617d9a1e9d2634fc0e737983..517a7c6d2afdaef844de9cb7ff30eabfed202a57 100644
--- a/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java
+++ b/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java
@@ -96,15 +96,33 @@ public interface CryptoComponent {
 			long connection);
 
 	/**
-	 * Encrypts the given plaintext so it can be written to temporary storage.
-	 * The ciphertext will not be decryptable after the app restarts.
+	 * Encrypts and authenticates the given plaintext so it can be written to
+	 * temporary storage. The ciphertext will not be decryptable after the app
+	 * restarts.
 	 */
 	byte[] encryptTemporaryStorage(byte[] plaintext);
 
 	/**
-	 * Decrypts the given ciphertext that has been read from temporary storage.
-	 * Returns null if the ciphertext is not decryptable (for example, if it
-	 * was written before the app restarted).
+	 * Decrypts and authenticates the given ciphertext that has been read from
+	 * temporary storage. Returns null if the ciphertext cannot be decrypted
+	 * and authenticated (for example, if it was written before the app
+	 * restarted).
 	 */
 	byte[] decryptTemporaryStorage(byte[] ciphertext);
+
+	/**
+	 * Encrypts and authenticates the given plaintext so it can be written to
+	 * storage. The encryption and authentication keys are derived from the
+	 * given password. The ciphertext will be decryptable using the same
+	 * password after the app restarts.
+	 */
+	byte[] encryptWithPassword(byte[] plaintext, char[] password);
+
+	/**
+	 * Decrypts and authenticates the given ciphertext that has been read from
+	 * storage. The encryption and authentication keys are derived from the
+	 * given password. Returns null if the ciphertext cannot be decrypted and
+	 * authenticated (for example, if the password is wrong).
+	 */
+	byte[] decryptWithPassword(byte[] ciphertext, char[] password);
 }
diff --git a/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java b/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java
index 8b634030aa6cafc5de0fff32930439c3b594f91e..7526c3708da9fba24eece4402d02f33c9883c330 100644
--- a/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java
+++ b/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java
@@ -6,6 +6,8 @@ import static net.sf.briar.api.invitation.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.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.math.BigInteger;
 import java.security.GeneralSecurityException;
 import java.security.KeyFactory;
@@ -37,10 +39,14 @@ import net.sf.briar.api.crypto.MessageDigest;
 import net.sf.briar.api.crypto.PseudoRandom;
 import net.sf.briar.util.ByteUtils;
 
+import org.spongycastle.crypto.CipherParameters;
 import org.spongycastle.crypto.engines.AESEngine;
+import org.spongycastle.crypto.generators.PKCS5S2ParametersGenerator;
 import org.spongycastle.crypto.modes.AEADBlockCipher;
 import org.spongycastle.crypto.modes.GCMBlockCipher;
+import org.spongycastle.crypto.params.KeyParameter;
 import org.spongycastle.jce.provider.BouncyCastleProvider;
+import org.spongycastle.util.Strings;
 
 class CryptoComponentImpl implements CryptoComponent {
 
@@ -55,9 +61,11 @@ class CryptoComponentImpl implements CryptoComponent {
 	private static final String SIGNATURE_KEY_PAIR_ALGO = "ECDSA";
 	private static final int SIGNATURE_KEY_PAIR_BITS = 384;
 	private static final String TAG_CIPHER_ALGO = "AES/ECB/NoPadding";
-	private static final int GCM_MAC_LENGTH = 16; // 128 bits
+	private static final int GCM_MAC_BYTES = 16; // 128 bits
 	private static final String STORAGE_CIPHER_ALGO = "AES/GCM/NoPadding";
-	private static final int STORAGE_IV_LENGTH = 32; // 256 bits
+	private static final int STORAGE_IV_BYTES = 16; // 128 bits
+	private static final int PBKDF_SALT_BYTES = 16; // 128 bits
+	private static final int PBKDF_ITERATIONS = 10 * 1000; // FIXME: How many?
 	private static final String KEY_DERIVATION_ALGO = "AES/CTR/NoPadding";
 	private static final int KEY_DERIVATION_IV_BYTES = 16; // 128 bits
 
@@ -345,7 +353,7 @@ class CryptoComponentImpl implements CryptoComponent {
 		// This code is specific to Spongy Castle 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);
+		return new AuthenticatedCipherImpl(cipher, GCM_MAC_BYTES);
 	}
 
 	public void encodeTag(byte[] tag, Cipher tagCipher, ErasableKey tagKey,
@@ -366,19 +374,19 @@ class CryptoComponentImpl implements CryptoComponent {
 
 	public byte[] encryptTemporaryStorage(byte[] input) {
 		// Generate a random IV
-		byte[] ivBytes = new byte[STORAGE_IV_LENGTH];
+		byte[] ivBytes = new byte[STORAGE_IV_BYTES];
 		secureRandom.nextBytes(ivBytes);
 		IvParameterSpec iv = new IvParameterSpec(ivBytes);
 		// The output contains the IV, ciphertext and MAC
-		int outputLen = STORAGE_IV_LENGTH + input.length + GCM_MAC_LENGTH;
+		int outputLen = STORAGE_IV_BYTES + input.length + GCM_MAC_BYTES;
 		byte[] output = new byte[outputLen];
-		System.arraycopy(ivBytes, 0, output, 0, STORAGE_IV_LENGTH);
+		System.arraycopy(ivBytes, 0, output, 0, STORAGE_IV_BYTES);
 		// Initialise the cipher and encrypt the plaintext
 		Cipher cipher;
 		try {
 			cipher = Cipher.getInstance(STORAGE_CIPHER_ALGO, PROVIDER);
 			cipher.init(ENCRYPT_MODE, temporaryStorageKey, iv);
-			cipher.doFinal(input, 0, input.length, output, STORAGE_IV_LENGTH);
+			cipher.doFinal(input, 0, input.length, output, STORAGE_IV_BYTES);
 			return output;
 		} catch(GeneralSecurityException e) {
 			throw new RuntimeException(e);
@@ -387,9 +395,9 @@ class CryptoComponentImpl implements CryptoComponent {
 
 	public byte[] decryptTemporaryStorage(byte[] input) {
 		// The input contains the IV, ciphertext and MAC
-		if(input.length < STORAGE_IV_LENGTH + GCM_MAC_LENGTH)
+		if(input.length < STORAGE_IV_BYTES + GCM_MAC_BYTES)
 			return null; // Invalid
-		IvParameterSpec iv = new IvParameterSpec(input, 0, STORAGE_IV_LENGTH);
+		IvParameterSpec iv = new IvParameterSpec(input, 0, STORAGE_IV_BYTES);
 		// Initialise the cipher
 		Cipher cipher;
 		try {
@@ -400,13 +408,77 @@ class CryptoComponentImpl implements CryptoComponent {
 		}
 		// Try to decrypt the ciphertext (may be invalid)
 		try {
-			return cipher.doFinal(input, STORAGE_IV_LENGTH,
-					input.length - STORAGE_IV_LENGTH);
+			return cipher.doFinal(input, STORAGE_IV_BYTES,
+					input.length - STORAGE_IV_BYTES);
 		} catch(GeneralSecurityException e) {
 			return null; // Invalid
 		}
 	}
 
+	public byte[] encryptWithPassword(byte[] input, char[] password) {
+		// Generate a random salt
+		byte[] salt = new byte[PBKDF_SALT_BYTES];
+		secureRandom.nextBytes(salt);
+		// Derive the key from the password
+		byte[] keyBytes = pbkdf2(password, salt);
+		ErasableKey key = new ErasableKeyImpl(keyBytes, SECRET_KEY_ALGO);
+		// Generate a random IV
+		byte[] ivBytes = new byte[STORAGE_IV_BYTES];
+		secureRandom.nextBytes(ivBytes);
+		IvParameterSpec iv = new IvParameterSpec(ivBytes);
+		// The output contains the salt, IV, ciphertext and MAC
+		int outputLen = PBKDF_SALT_BYTES + STORAGE_IV_BYTES + input.length
+				+ GCM_MAC_BYTES;
+		byte[] output = new byte[outputLen];
+		System.arraycopy(salt, 0, output, 0, PBKDF_SALT_BYTES);
+		System.arraycopy(ivBytes, 0, output, PBKDF_SALT_BYTES,
+				STORAGE_IV_BYTES);
+		// Initialise the cipher and encrypt the plaintext
+		Cipher cipher;
+		try {
+			cipher = Cipher.getInstance(STORAGE_CIPHER_ALGO, PROVIDER);
+			cipher.init(ENCRYPT_MODE, key, iv);
+			cipher.doFinal(input, 0, input.length, output,
+					PBKDF_SALT_BYTES + STORAGE_IV_BYTES);
+			return output;
+		} catch(GeneralSecurityException e) {
+			throw new RuntimeException(e);
+		} finally {
+			key.erase();
+		}
+	}
+
+	public byte[] decryptWithPassword(byte[] input, char[] password) {
+		// The input contains the salt, IV, ciphertext and MAC
+		if(input.length < PBKDF_SALT_BYTES + STORAGE_IV_BYTES + GCM_MAC_BYTES)
+			return null; // Invalid
+		byte[] salt = new byte[PBKDF_SALT_BYTES];
+		System.arraycopy(input, 0, salt, 0, PBKDF_SALT_BYTES);
+		IvParameterSpec iv = new IvParameterSpec(input, PBKDF_SALT_BYTES,
+				STORAGE_IV_BYTES);
+		// Derive the key from the password
+		byte[] keyBytes = pbkdf2(password, salt);
+		ErasableKey key = new ErasableKeyImpl(keyBytes, SECRET_KEY_ALGO);
+		// Initialise the cipher
+		Cipher cipher;
+		try {
+			cipher = Cipher.getInstance(STORAGE_CIPHER_ALGO, PROVIDER);
+			cipher.init(DECRYPT_MODE, key, iv);
+		} catch(GeneralSecurityException e) {
+			key.erase();
+			throw new RuntimeException(e);
+		}
+		// Try to decrypt the ciphertext (may be invalid)
+		try {
+			return cipher.doFinal(input, PBKDF_SALT_BYTES + STORAGE_IV_BYTES,
+					input.length - PBKDF_SALT_BYTES - STORAGE_IV_BYTES);
+		} catch(GeneralSecurityException e) {
+			return null; // Invalid
+		} finally {
+			key.erase();
+		}
+	}
+
 	private ECPublicKey checkP384Params(PublicKey publicKey) {
 		if(!(publicKey instanceof ECPublicKey)) throw new RuntimeException();
 		ECPublicKey ecPublicKey = (ECPublicKey) publicKey;
@@ -480,4 +552,31 @@ class CryptoComponentImpl implements CryptoComponent {
 			throw new RuntimeException(e);
 		}
 	}
+
+	// Password-based key derivation function - see PKCS#5 v2.1, section 5.2
+	private byte[] pbkdf2(char[] password, byte[] salt) {
+		// This code is specific to Spongy Castle because the password-based
+		// KDF exposed through the JCE interface is PKCS#12
+		byte[] utf8 = toUtf8ByteArray(password);
+		PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator();
+		gen.init(utf8, salt, PBKDF_ITERATIONS);
+		int keyLengthInBits = SECRET_KEY_BYTES * 8;
+		CipherParameters p = gen.generateDerivedParameters(keyLengthInBits);
+		ByteUtils.erase(utf8);
+		return ((KeyParameter) p).getKey();
+	}
+
+	byte[] toUtf8ByteArray(char[] c) {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		try {
+			Strings.toUTF8ByteArray(c, out);
+			byte[] utf8 = out.toByteArray();
+			// Erase the output stream's buffer
+			out.reset();
+			out.write(new byte[utf8.length]);
+			return utf8;
+		} catch(IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
 }
diff --git a/briar-tests/build.xml b/briar-tests/build.xml
index cf248ed5b3b87cdf199046e5403d75638173792a..655e6f80edbd4e7c6b3a48b5d6e4955ef31c3c64 100644
--- a/briar-tests/build.xml
+++ b/briar-tests/build.xml
@@ -75,6 +75,7 @@
 			<test name='net.sf.briar.crypto.KeyAgreementTest'/>
 			<test name='net.sf.briar.crypto.KeyDerivationTest'/>
 			<test name='net.sf.briar.crypto.KeyEncodingAndParsingTest'/>
+			<test name="net.sf.briar.crypto.PasswordBasedKdfTest"/>
 			<test name='net.sf.briar.db.BasicH2Test'/>
 			<test name='net.sf.briar.db.DatabaseCleanerImplTest'/>
 			<test name='net.sf.briar.db.DatabaseComponentImplTest'/>
diff --git a/briar-tests/src/net/sf/briar/crypto/PasswordBasedKdfTest.java b/briar-tests/src/net/sf/briar/crypto/PasswordBasedKdfTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..93b92638c45e270e1bf3a93a8c76f5bc7d340677
--- /dev/null
+++ b/briar-tests/src/net/sf/briar/crypto/PasswordBasedKdfTest.java
@@ -0,0 +1,41 @@
+package net.sf.briar.crypto;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import java.util.Random;
+
+import net.sf.briar.BriarTestCase;
+import net.sf.briar.api.crypto.CryptoComponent;
+
+import org.junit.Test;
+
+public class PasswordBasedKdfTest extends BriarTestCase {
+
+	@Test
+	public void testEncryptionAndDecryption() {
+		CryptoComponent crypto = new CryptoComponentImpl();
+		Random random = new Random();
+		byte[] input = new byte[123];
+		random.nextBytes(input);
+		char[] password = "password".toCharArray();
+		byte[] ciphertext = crypto.encryptWithPassword(input, password);
+		byte[] output = crypto.decryptWithPassword(ciphertext, password);
+		assertArrayEquals(input, output);
+	}
+
+	@Test
+	public void testInvalidCiphertextReturnsNull() {
+		CryptoComponent crypto = new CryptoComponentImpl();
+		Random random = new Random();
+		byte[] input = new byte[123];
+		random.nextBytes(input);
+		char[] password = "password".toCharArray();
+		byte[] ciphertext = crypto.encryptWithPassword(input, password);
+		// Modify the ciphertext
+		int position = random.nextInt(ciphertext.length);
+		int value = random.nextInt(256);
+		ciphertext[position] = (byte) value;
+		byte[] output = crypto.decryptWithPassword(ciphertext, password);
+		assertNull(output);
+	}
+}