Encrypt bundles in case the OS writes them to unencrypted storage.

Only the bundle contents created by Briar classes are encrypted.
This commit is contained in:
akwizgran
2013-02-18 18:56:00 +00:00
parent 49e6113e5d
commit 51db9ce1fd
12 changed files with 166 additions and 22 deletions

View File

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

View File

@@ -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;
}
}

View File

@@ -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));

View File

@@ -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

View File

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

View File

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

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -67,4 +67,8 @@ class AuthenticatedCipherImpl implements AuthenticatedCipher {
public int getMacLength() {
return macLength;
}
public int getBlockSize() {
return cipher.getUnderlyingCipher().getBlockSize();
}
}

View File

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

View File

@@ -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

View File

@@ -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());