diff --git a/briar-android/src/net/sf/briar/android/AndroidModule.java b/briar-android/src/net/sf/briar/android/AndroidModule.java
index 29a745489a7104f5c9f8485a67912c6dfe4525fb..83ec34737014019708dcf2a0e0dcbeea57c9a106 100644
--- a/briar-android/src/net/sf/briar/android/AndroidModule.java
+++ b/briar-android/src/net/sf/briar/android/AndroidModule.java
@@ -1,6 +1,7 @@
 package net.sf.briar.android;
 
 import net.sf.briar.api.android.AndroidExecutor;
+import net.sf.briar.api.android.BundleEncrypter;
 import net.sf.briar.api.android.ReferenceManager;
 
 import com.google.inject.AbstractModule;
@@ -11,6 +12,8 @@ public class AndroidModule extends AbstractModule {
 	@Override
 	protected void configure() {
 		bind(AndroidExecutor.class).to(AndroidExecutorImpl.class);
+		bind(BundleEncrypter.class).to(BundleEncrypterImpl.class).in(
+			Singleton.class);
 		bind(ReferenceManager.class).to(ReferenceManagerImpl.class).in(
 				Singleton.class);
 	}
diff --git a/briar-android/src/net/sf/briar/android/BundleEncrypterImpl.java b/briar-android/src/net/sf/briar/android/BundleEncrypterImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..ce8f9912d75cdb9dd70330b4c6fbbebb9d719b2e
--- /dev/null
+++ b/briar-android/src/net/sf/briar/android/BundleEncrypterImpl.java
@@ -0,0 +1,89 @@
+package net.sf.briar.android;
+
+import static javax.crypto.Cipher.DECRYPT_MODE;
+import static javax.crypto.Cipher.ENCRYPT_MODE;
+
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+
+import net.sf.briar.api.android.BundleEncrypter;
+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.util.ByteUtils;
+import android.os.Bundle;
+import android.os.Parcel;
+
+import com.google.inject.Inject;
+
+// This class is not thread-safe
+class BundleEncrypterImpl implements BundleEncrypter {
+
+	private final AuthenticatedCipher cipher;
+	private final SecureRandom random;
+	private final ErasableKey key;
+	private final int blockSize, macLength;
+
+	@Inject
+	BundleEncrypterImpl(CryptoComponent crypto) {
+		cipher = crypto.getBundleCipher();
+		random = crypto.getSecureRandom();
+		key = crypto.generateSecretKey();
+		blockSize = cipher.getBlockSize();
+		macLength = cipher.getMacLength();
+	}
+
+	@Override
+	public void encrypt(Bundle b) {
+		// Marshall the plaintext contents into a byte array
+		Parcel p = Parcel.obtain();
+		b.writeToParcel(p, 0);
+		byte[] plaintext = p.marshall();
+		p.recycle();
+		// Encrypt the byte array using the storage key and a random IV
+		byte[] iv = new byte[blockSize];
+		random.nextBytes(iv);
+		byte[] ciphertext = new byte[plaintext.length + macLength];
+		try {
+			cipher.init(ENCRYPT_MODE, key, iv, null);
+			cipher.doFinal(plaintext, 0, plaintext.length, ciphertext, 0);
+		} catch(GeneralSecurityException e) {
+			throw new RuntimeException(e);
+		}
+		ByteUtils.erase(plaintext);
+		// Replace the plaintext contents with the IV and the ciphertext
+		b.clear();
+		b.putByteArray("net.sf.briar.IV", iv);
+		b.putByteArray("net.sf.briar.CIPHERTEXT", ciphertext);
+	}
+
+	@Override
+	public boolean decrypt(Bundle b) {
+		// Retrieve the IV and the ciphertext
+		byte[] iv = b.getByteArray("net.sf.briar.IV");
+		if(iv == null) throw new IllegalArgumentException();
+		if(iv.length != blockSize) throw new IllegalArgumentException();
+		byte[] ciphertext = b.getByteArray("net.sf.briar.CIPHERTEXT");
+		if(ciphertext == null) throw new IllegalArgumentException();
+		if(ciphertext.length < macLength) throw new IllegalArgumentException();
+		// Decrypt the ciphertext using the storage key and the IV
+		byte[] plaintext = new byte[ciphertext.length - macLength];
+		try {
+			cipher.init(DECRYPT_MODE, key, iv, null);
+			cipher.doFinal(ciphertext, 0, ciphertext.length, plaintext, 0);
+		} catch(GeneralSecurityException e) {
+			return false; // Invalid ciphertext
+		}
+		// Unmarshall the byte array
+		Parcel p = Parcel.obtain();
+		p.unmarshall(plaintext, 0, plaintext.length);
+		ByteUtils.erase(plaintext);
+		// Replace the IV and the ciphertext with the plaintext contents
+		b.remove("net.sf.briar.IV");
+		b.remove("net.sf.briar.CIPHERTEXT");
+		p.setDataPosition(0);
+		b.readFromParcel(p);
+		p.recycle();
+		return true;
+	}
+}
diff --git a/briar-android/src/net/sf/briar/android/helloworld/HelloWorldActivity.java b/briar-android/src/net/sf/briar/android/helloworld/HelloWorldActivity.java
index afa9c32e8d348496a153215b2a0fe2a3c050db0c..e253024c6064978ea7d1cc29fc57576c2c5d3fc2 100644
--- a/briar-android/src/net/sf/briar/android/helloworld/HelloWorldActivity.java
+++ b/briar-android/src/net/sf/briar/android/helloworld/HelloWorldActivity.java
@@ -28,8 +28,8 @@ implements OnClickListener {
 			Logger.getLogger(HelloWorldActivity.class.getName());
 
 	@Override
-	public void onCreate(Bundle savedInstanceState) {
-		super.onCreate(savedInstanceState);
+	public void onCreate(Bundle state) {
+		super.onCreate(state);
 		if(LOG.isLoggable(INFO)) LOG.info("Created");
 		LinearLayout layout = new LinearLayout(this);
 		layout.setLayoutParams(new LayoutParams(MATCH_PARENT, MATCH_PARENT));
diff --git a/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java b/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java
index 1b1172c10523ba1dda044140d8ff741acf56238b..5de5df339ecced46fadd2d55addad3bd1c380e71 100644
--- a/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java
+++ b/briar-android/src/net/sf/briar/android/invitation/AddContactActivity.java
@@ -5,6 +5,7 @@ import static java.util.logging.Level.WARNING;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
+import net.sf.briar.api.android.BundleEncrypter;
 import net.sf.briar.api.android.ReferenceManager;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.db.DatabaseComponent;
@@ -30,6 +31,7 @@ implements InvitationListener {
 	@Inject @DatabaseExecutor private Executor dbExecutor;
 	@Inject private InvitationTaskFactory invitationTaskFactory;
 	@Inject private ReferenceManager referenceManager;
+	@Inject private BundleEncrypter bundleEncrypter;
 
 	// All of the following must be accessed on the UI thread
 	private AddContactView view = null;
@@ -46,6 +48,7 @@ implements InvitationListener {
 	@Override
 	public void onCreate(Bundle state) {
 		super.onCreate(state);
+		if(state != null && !bundleEncrypter.decrypt(state)) state = null;
 		if(state == null) {
 			// This is a new activity
 			setView(new NetworkSetupView(this));
@@ -120,14 +123,16 @@ implements InvitationListener {
 	@Override
 	public void onSaveInstanceState(Bundle state) {
 		super.onSaveInstanceState(state);
-		state.putString("net.sf.briar.NETWORK_NAME", networkName);
-		state.putBoolean("net.sf.briar.USE_BLUETOOTH", useBluetooth);
-		state.putInt("net.sf.briar.LOCAL_CODE", localInvitationCode);
-		state.putInt("net.sf.briar.REMOTE_CODE", remoteInvitationCode);
-		state.putBoolean("net.sf.briar.FAILED", connectionFailed);
-		state.putBoolean("net.sf.briar.MATCHED", localMatched && remoteMatched);
-		if(task != null)
-			state.putLong("net.sf.briar.TASK_HANDLE", taskHandle);
+		Bundle b = new Bundle();
+		b.putString("net.sf.briar.NETWORK_NAME", networkName);
+		b.putBoolean("net.sf.briar.USE_BLUETOOTH", useBluetooth);
+		b.putInt("net.sf.briar.LOCAL_CODE", localInvitationCode);
+		b.putInt("net.sf.briar.REMOTE_CODE", remoteInvitationCode);
+		b.putBoolean("net.sf.briar.FAILED", connectionFailed);
+		b.putBoolean("net.sf.briar.MATCHED", localMatched && remoteMatched);
+		if(task != null) b.putLong("net.sf.briar.TASK_HANDLE", taskHandle);
+		bundleEncrypter.encrypt(b);
+		state.putAll(b);
 	}
 
 	@Override
diff --git a/briar-android/src/net/sf/briar/android/invitation/ContactAddedView.java b/briar-android/src/net/sf/briar/android/invitation/ContactAddedView.java
index aef34f05fb9bd361cc7d21bcb93c082ef4c3fc4f..50d652f070fd1121958422499690acae7b432ef2 100644
--- a/briar-android/src/net/sf/briar/android/invitation/ContactAddedView.java
+++ b/briar-android/src/net/sf/briar/android/invitation/ContactAddedView.java
@@ -17,6 +17,8 @@ import android.widget.TextView.OnEditorActionListener;
 public class ContactAddedView extends AddContactView implements OnClickListener,
 OnEditorActionListener {
 
+	EditText nicknameEntry = null;
+
 	ContactAddedView(Context ctx) {
 		super(ctx);
 	}
@@ -50,7 +52,7 @@ OnEditorActionListener {
 		innerLayout.setGravity(CENTER);
 
 		final Button done = new Button(ctx);
-		EditText nicknameEntry = new EditText(ctx) {
+		nicknameEntry = new EditText(ctx) {
 			@Override
 			protected void onTextChanged(CharSequence text, int start,
 					int lengthBefore, int lengthAfter) {
@@ -71,11 +73,13 @@ OnEditorActionListener {
 	}
 
 	public boolean onEditorAction(TextView textView, int actionId, KeyEvent e) {
-		if(textView.getText().length() > 0) container.finish();
+		String nickname = textView.getText().toString();
+		if(nickname.length() > 0) container.addContactAndFinish(nickname);
 		return true;
 	}
 
 	public void onClick(View view) {
-		container.finish(); // Done
+		String nickname = nicknameEntry.getText().toString();
+		container.addContactAndFinish(nickname);
 	}
 }
diff --git a/briar-api/src/net/sf/briar/api/android/BundleEncrypter.java b/briar-api/src/net/sf/briar/api/android/BundleEncrypter.java
new file mode 100644
index 0000000000000000000000000000000000000000..e2af1a8324e564645675212c4560d395bc2b2718
--- /dev/null
+++ b/briar-api/src/net/sf/briar/api/android/BundleEncrypter.java
@@ -0,0 +1,27 @@
+package net.sf.briar.api.android;
+
+import android.os.Bundle;
+
+/**
+ * Encrypts and decrypts the contents of bundles in case the operating system
+ * writes them to unencrypted storage.
+ * <p>
+ * This interface is designed to be accessed from the UI thread, so
+ * implementations may not be thread-safe.
+ */
+public interface BundleEncrypter {
+
+	/**
+	 * Encrypts the given bundle, replacing its contents with the encrypted
+	 * data.
+	 */
+	void encrypt(Bundle b);
+
+	/**
+	 * Decrypts the given bundle, replacing its contents with the decrypted
+	 * data, or returns false if the bundle contains invalid data, which may
+	 * occur if the process that created the encrypted bundle was terminated
+	 * and replaced by the current process.
+	 */
+	boolean decrypt(Bundle b);
+}
diff --git a/briar-api/src/net/sf/briar/api/crypto/AuthenticatedCipher.java b/briar-api/src/net/sf/briar/api/crypto/AuthenticatedCipher.java
index e6a8cd8adf20b5eebd12610acb373acf417548fa..0dfa322af5e79bd872598e3408b0b511d12769fe 100644
--- a/briar-api/src/net/sf/briar/api/crypto/AuthenticatedCipher.java
+++ b/briar-api/src/net/sf/briar/api/crypto/AuthenticatedCipher.java
@@ -26,4 +26,7 @@ public interface AuthenticatedCipher {
 
 	/** Returns the length of the message authenticated code (MAC) in bytes. */
 	int getMacLength();
+
+	/** Returns the block size of the cipher in bytes. */
+	int getBlockSize();
 }
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 0c792b068d3723da04da7590095321a73739723a..e59df9c5ef04b667f6646161594147628cdce619 100644
--- a/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java
+++ b/briar-api/src/net/sf/briar/api/crypto/CryptoComponent.java
@@ -65,7 +65,7 @@ public interface CryptoComponent {
 
 	KeyParser getSignatureKeyParser();
 
-	ErasableKey generateTestKey();
+	ErasableKey generateSecretKey();
 
 	MessageDigest getMessageDigest();
 
@@ -77,5 +77,7 @@ public interface CryptoComponent {
 
 	AuthenticatedCipher getFrameCipher();
 
+	AuthenticatedCipher getBundleCipher();
+
 	Signature getSignature();
 }
diff --git a/briar-core/src/net/sf/briar/crypto/AuthenticatedCipherImpl.java b/briar-core/src/net/sf/briar/crypto/AuthenticatedCipherImpl.java
index 0514e1e69b50a0f2c7f1097bd9a85956612b38ab..57d39f792df5672a6a380020b0f5a8c90c76b786 100644
--- a/briar-core/src/net/sf/briar/crypto/AuthenticatedCipherImpl.java
+++ b/briar-core/src/net/sf/briar/crypto/AuthenticatedCipherImpl.java
@@ -67,4 +67,8 @@ class AuthenticatedCipherImpl implements AuthenticatedCipher {
 	public int getMacLength() {
 		return macLength;
 	}
+
+	public int getBlockSize() {
+		return cipher.getUnderlyingCipher().getBlockSize();
+	}
 }
diff --git a/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java b/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java
index a59ee4924b40684dfc361fa43b3bd0c0cbe08e8b..344a863bc3eb6f08da465a201d8fc9f3901a3b7d 100644
--- a/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java
+++ b/briar-core/src/net/sf/briar/crypto/CryptoComponentImpl.java
@@ -332,7 +332,7 @@ class CryptoComponentImpl implements CryptoComponent {
 		return signatureKeyParser;
 	}
 
-	public ErasableKey generateTestKey() {
+	public ErasableKey generateSecretKey() {
 		byte[] b = new byte[SECRET_KEY_BYTES];
 		secureRandom.nextBytes(b);
 		return new ErasableKeyImpl(b, SECRET_KEY_ALGO);
@@ -377,4 +377,11 @@ class CryptoComponentImpl implements CryptoComponent {
 		AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
 		return new AuthenticatedCipherImpl(cipher, GCM_MAC_LENGTH);
 	}
+
+	public AuthenticatedCipher getBundleCipher() {
+		// 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);
+	}
 }
diff --git a/briar-tests/src/net/sf/briar/transport/IncomingEncryptionLayerTest.java b/briar-tests/src/net/sf/briar/transport/IncomingEncryptionLayerTest.java
index 65ed6844e0287a1549a3193809e4c1445bb9f17f..84234fa3ae8ab1ac1ca3e32ab0d24955104de805 100644
--- a/briar-tests/src/net/sf/briar/transport/IncomingEncryptionLayerTest.java
+++ b/briar-tests/src/net/sf/briar/transport/IncomingEncryptionLayerTest.java
@@ -37,7 +37,7 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 		Injector i = Guice.createInjector(new CryptoModule());
 		crypto = i.getInstance(CryptoComponent.class);
 		frameCipher = crypto.getFrameCipher();
-		frameKey = crypto.generateTestKey();
+		frameKey = crypto.generateSecretKey();
 	}
 
 	@Test
diff --git a/briar-tests/src/net/sf/briar/transport/OutgoingEncryptionLayerTest.java b/briar-tests/src/net/sf/briar/transport/OutgoingEncryptionLayerTest.java
index 475e5d9adb4d26237e533194994056c6ac86c504..5c9e129fee84a40910fe11529a326a833f46594d 100644
--- a/briar-tests/src/net/sf/briar/transport/OutgoingEncryptionLayerTest.java
+++ b/briar-tests/src/net/sf/briar/transport/OutgoingEncryptionLayerTest.java
@@ -46,7 +46,7 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		byte[] iv = new byte[IV_LENGTH], aad = new byte[AAD_LENGTH];
 		byte[] plaintext = new byte[FRAME_LENGTH - MAC_LENGTH];
 		byte[] ciphertext = new byte[FRAME_LENGTH];
-		ErasableKey frameKey = crypto.generateTestKey();
+		ErasableKey frameKey = crypto.generateSecretKey();
 		// Calculate the expected ciphertext
 		FrameEncoder.encodeIv(iv, 0);
 		FrameEncoder.encodeAad(aad, 0, plaintext.length);
@@ -71,7 +71,7 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		// Initiator's constructor
 		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(),
+				10 * FRAME_LENGTH, frameCipher, crypto.generateSecretKey(),
 				FRAME_LENGTH, tag);
 		// Write an empty final frame without having written any other frames
 		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], 0, true);
@@ -84,7 +84,7 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		// Responder's constructor
 		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(),
+				10 * FRAME_LENGTH, frameCipher, crypto.generateSecretKey(),
 				FRAME_LENGTH);
 		// Write an empty final frame without having written any other frames
 		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], 0, true);
@@ -98,7 +98,7 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		// Initiator's constructor
 		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(),
+				10 * FRAME_LENGTH, frameCipher, crypto.generateSecretKey(),
 				FRAME_LENGTH, tag);
 		// There should be space for nine full frames and one partial frame
 		byte[] frame = new byte[FRAME_LENGTH - MAC_LENGTH];
@@ -122,7 +122,7 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		// Responder's constructor
 		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(),
+				10 * FRAME_LENGTH, frameCipher, crypto.generateSecretKey(),
 				FRAME_LENGTH);
 		// There should be space for ten full frames
 		assertEquals(10 * MAX_PAYLOAD_LENGTH, o.getRemainingCapacity());
@@ -145,7 +145,7 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		// The connection has plenty of space so we're limited by frame numbers
 		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				Long.MAX_VALUE, frameCipher, crypto.generateTestKey(),
+				Long.MAX_VALUE, frameCipher, crypto.generateSecretKey(),
 				FRAME_LENGTH);
 		// There should be enough frame numbers for 2^32 frames
 		assertEquals((1L << 32) * MAX_PAYLOAD_LENGTH, o.getRemainingCapacity());