mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 18:59:06 +01:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
27
briar-api/src/net/sf/briar/api/android/BundleEncrypter.java
Normal file
27
briar-api/src/net/sf/briar/api/android/BundleEncrypter.java
Normal 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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -67,4 +67,8 @@ class AuthenticatedCipherImpl implements AuthenticatedCipher {
|
||||
public int getMacLength() {
|
||||
return macLength;
|
||||
}
|
||||
|
||||
public int getBlockSize() {
|
||||
return cipher.getUnderlyingCipher().getBlockSize();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user