From e8e7a822c57b2730b102a52ed3793d03e71a915e Mon Sep 17 00:00:00 2001 From: Impyy Date: Wed, 3 May 2017 21:08:38 +0200 Subject: [PATCH] Push some work on yet another rework of the database. This time with slots! Details are in doc/db.md --- .../main/java/me/impy/aegis/MainActivity.java | 262 ++++++++---------- .../java/me/impy/aegis/ScannerActivity.java | 1 - .../me/impy/aegis/crypto/CryptoUtils.java | 16 +- .../aegis/crypto/DerivationParameters.java | 6 - .../me/impy/aegis/crypto/KeyStoreHandle.java | 15 +- .../java/me/impy/aegis/crypto/MasterKey.java | 48 ++++ .../aegis/crypto/slots/FingerprintSlot.java | 5 + .../impy/aegis/crypto/slots/PasswordSlot.java | 60 ++++ .../me/impy/aegis/crypto/slots/RawSlot.java | 40 +++ .../java/me/impy/aegis/crypto/slots/Slot.java | 54 ++++ .../aegis/crypto/slots/SlotCollection.java | 84 ++++++ .../main/java/me/impy/aegis/db/Database.java | 2 +- .../java/me/impy/aegis/db/DatabaseFile.java | 128 +++------ .../me/impy/aegis/db/DatabaseManager.java | 16 ++ .../me/impy/aegis/util/LittleByteBuffer.java | 38 +++ doc/db.md | 99 +++---- 16 files changed, 564 insertions(+), 310 deletions(-) delete mode 100644 app/src/main/java/me/impy/aegis/crypto/DerivationParameters.java create mode 100644 app/src/main/java/me/impy/aegis/crypto/MasterKey.java create mode 100644 app/src/main/java/me/impy/aegis/crypto/slots/FingerprintSlot.java create mode 100644 app/src/main/java/me/impy/aegis/crypto/slots/PasswordSlot.java create mode 100644 app/src/main/java/me/impy/aegis/crypto/slots/RawSlot.java create mode 100644 app/src/main/java/me/impy/aegis/crypto/slots/Slot.java create mode 100644 app/src/main/java/me/impy/aegis/crypto/slots/SlotCollection.java create mode 100644 app/src/main/java/me/impy/aegis/db/DatabaseManager.java create mode 100644 app/src/main/java/me/impy/aegis/util/LittleByteBuffer.java diff --git a/app/src/main/java/me/impy/aegis/MainActivity.java b/app/src/main/java/me/impy/aegis/MainActivity.java index c8b6e8f0..028eb26f 100644 --- a/app/src/main/java/me/impy/aegis/MainActivity.java +++ b/app/src/main/java/me/impy/aegis/MainActivity.java @@ -29,6 +29,8 @@ import android.widget.LinearLayout; import android.widget.Toast; import java.io.IOException; +import java.lang.reflect.UndeclaredThrowableException; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -36,15 +38,15 @@ import java.util.Comparator; import java.util.Objects; import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; import javax.crypto.SecretKey; -import me.impy.aegis.crypto.CryptParameters; import me.impy.aegis.crypto.CryptResult; import me.impy.aegis.crypto.CryptoUtils; -import me.impy.aegis.crypto.DerivationParameters; -import me.impy.aegis.crypto.KeyStoreHandle; +import me.impy.aegis.crypto.MasterKey; import me.impy.aegis.crypto.OTP; +import me.impy.aegis.crypto.slots.PasswordSlot; +import me.impy.aegis.crypto.slots.Slot; +import me.impy.aegis.crypto.slots.SlotCollection; import me.impy.aegis.db.Database; import me.impy.aegis.db.DatabaseFile; import me.impy.aegis.finger.FingerprintAuthenticationDialogFragment; @@ -58,10 +60,10 @@ public class MainActivity extends AppCompatActivity { RecyclerView rvKeyProfiles; KeyProfileAdapter mKeyProfileAdapter; ArrayList mKeyProfiles = new ArrayList<>(); + MasterKey masterKey; Database database; DatabaseFile databaseFile; - boolean blockSave = false; boolean nightMode = false; int clickedItemPosition = -1; @@ -88,9 +90,8 @@ public class MainActivity extends AppCompatActivity { setSupportActionBar(toolbar); initializeAppShortcuts(); FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); - fab.setEnabled(false); + fab.setEnabled(true); fab.setOnClickListener(view -> { - blockSave = true; Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class); startActivityForResult(scannerActivity, GET_KEYINFO); }); @@ -121,12 +122,11 @@ public class MainActivity extends AppCompatActivity { }; Collections.sort(mKeyProfiles, comparator); - loadDatabase(null); + loadDatabase(); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - blockSave = false; if (requestCode == GET_KEYINFO) { if (resultCode == RESULT_OK) { final KeyProfile keyProfile = (KeyProfile)data.getSerializableExtra("KeyProfile"); @@ -160,7 +160,7 @@ public class MainActivity extends AppCompatActivity { mKeyProfiles.add(keyProfile); mKeyProfileAdapter.notifyDataSetChanged(); - saveDatabase(true, null); + saveDatabase(); } } } @@ -174,19 +174,17 @@ public class MainActivity extends AppCompatActivity { @Override protected void onPause() { - if (!blockSave) { - // update order of keys - for (int i = 0; i < mKeyProfiles.size(); i++) { - try { - database.updateKey(mKeyProfiles.get(i)); - } catch (Exception e) { - e.printStackTrace(); - } + // update order of keys + for (int i = 0; i < mKeyProfiles.size(); i++) { + try { + database.updateKey(mKeyProfiles.get(i)); + } catch (Exception e) { + e.printStackTrace(); } - - saveDatabase(false, null); } + saveDatabase(); + super.onPause(); } @@ -205,50 +203,24 @@ public class MainActivity extends AppCompatActivity { Cipher cipher = obj.getCipher(); switch (action) { case SAVE: - saveDatabase(false, cipher); + saveDatabase(); break; case LOAD: - loadDatabase(cipher); + loadDatabase(); break; } } } - private void saveDatabase(boolean allowPrompt, Cipher cipher) { + private void saveDatabase() { try { byte[] bytes = database.serialize(); - CryptParameters cryptParams = null; - DerivationParameters derParams = null; - - switch (databaseFile.getLevel()) { - case DatabaseFile.SEC_LEVEL_DERIVED: - // TODO - break; - case DatabaseFile.SEC_LEVEL_KEYSTORE: - if (cipher == null) { - KeyStoreHandle keyStore = new KeyStoreHandle(); - SecretKey key = keyStore.getKey(); - cipher = CryptoUtils.createCipher(key, Cipher.ENCRYPT_MODE); - } - - CryptResult result = CryptoUtils.encrypt(bytes, cipher); - bytes = result.Data; - cryptParams = result.Parameters; - break; - } - - databaseFile.setContent(bytes); - databaseFile.setCryptParameters(cryptParams); - databaseFile.setDerivationParameters(derParams); - databaseFile.save(); - } catch (IllegalBlockSizeException e) { - // TODO: is there a way to catch "Key user not authenticated" specifically aside from checking the exception message? - if (causeIsKeyUserNotAuthenticated(e) && allowPrompt && cipher != null) { - promptFingerPrint(FingerprintAuthenticationDialogFragment.Action.SAVE, cipher); - } - e.printStackTrace(); + CryptResult result = masterKey.encrypt(bytes); + databaseFile.setContent(result.Data); + databaseFile.setCryptParameters(result.Parameters); + databaseFile.save(getApplicationContext()); } catch (Exception e) { - e.printStackTrace(); + throw new UndeclaredThrowableException(e); } } @@ -293,23 +265,22 @@ public class MainActivity extends AppCompatActivity { private void deleteProfile(KeyProfile profile) { new AlertDialog.Builder(MainActivity.this) - .setTitle("Delete entry") - .setMessage("Are you sure you want to delete this profile?") - .setPositiveButton(android.R.string.yes, (dialog, which) -> { - try { - database.removeKey(profile); - } catch (Exception e) { - e.printStackTrace(); - //TODO: feedback - return; - } - mKeyProfiles.remove(clickedItemPosition); - mKeyProfileAdapter.notifyItemRemoved(clickedItemPosition); - }) - .setNegativeButton(android.R.string.no, (dialog, which) -> { - - }) - .show(); + .setTitle("Delete entry") + .setMessage("Are you sure you want to delete this profile?") + .setPositiveButton(android.R.string.yes, (dialog, which) -> { + try { + database.removeKey(profile); + } catch (Exception e) { + e.printStackTrace(); + //TODO: feedback + return; + } + mKeyProfiles.remove(clickedItemPosition); + mKeyProfileAdapter.notifyItemRemoved(clickedItemPosition); + }) + .setNegativeButton(android.R.string.no, (dialog, which) -> { + }) + .show(); } @Override @@ -320,14 +291,9 @@ public class MainActivity extends AppCompatActivity { @Override public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - + if (item.getItemId() == R.id.action_settings) { Intent preferencesActivity = new Intent(this, PreferencesActivity.class); startActivity(preferencesActivity); - return true; } @@ -396,103 +362,91 @@ public class MainActivity extends AppCompatActivity { } } - private void loadDatabase(Cipher cipher) { + private void createDatabase() { + database = new Database(); + try { + databaseFile = new DatabaseFile(); + } catch (Exception e) { + // TODO: tell the user to stop using a weird platform + throw new UndeclaredThrowableException(e); + } + + try { + masterKey = new MasterKey(null); + } catch (NoSuchAlgorithmException e) { + // TODO: tell the user to stop using a weird platform + throw new UndeclaredThrowableException(e); + } + + SlotCollection slots = databaseFile.getSlots(); + + try { + PasswordSlot slot = new PasswordSlot(); + byte[] salt = CryptoUtils.generateSalt(); + SecretKey derivedKey = slot.deriveKey("testpassword".toCharArray(), salt, 1000); + Cipher cipher = Slot.createCipher(derivedKey, Cipher.ENCRYPT_MODE); + masterKey.encryptSlot(slot, cipher); + slots.add(slot); + } catch (Exception e) { + throw new UndeclaredThrowableException(e); + } + } + + private void loadDatabase() { try { databaseFile = DatabaseFile.load(getApplicationContext()); } catch (IOException e) { - // this file doesn't exist yet - try { - // TODO: prompt for security settings (level, auth, etc) - database = new Database(); - databaseFile = new DatabaseFile(getApplicationContext()); - databaseFile.setLevel(DatabaseFile.SEC_LEVEL_KEYSTORE); - - if (databaseFile.getLevel() == DatabaseFile.SEC_LEVEL_KEYSTORE) { - KeyStoreHandle store = new KeyStoreHandle(); - if (!store.keyExists()) { - store.generateKey(true); - } - } - } catch (Exception ex) { - e.printStackTrace(); - return; - } + // the database file doesn't exist yet + createDatabase(); + saveDatabase(); + return; } catch (Exception e) { // something else went wrong - e.printStackTrace(); - return; + throw new UndeclaredThrowableException(e); } - if (database == null) { - byte[] content = databaseFile.getContent(); - switch (databaseFile.getLevel()) { - case DatabaseFile.SEC_LEVEL_NONE: + byte[] content = databaseFile.getContent(); + if (databaseFile.isEncrypted()) { + SlotCollection slots = databaseFile.getSlots(); + for (Slot slot : slots) { + if (slot instanceof PasswordSlot) { try { - Database temp = new Database(); - temp.deserialize(content); - database = temp; + PasswordSlot derSlot = (PasswordSlot)slot; + SecretKey derivedKey = derSlot.deriveKey("testpassword".toCharArray()); + Cipher cipher = Slot.createCipher(derivedKey, Cipher.DECRYPT_MODE); + masterKey = MasterKey.decryptSlot(slot, cipher); } catch (Exception e) { - // TODO: handle corrupt database - e.printStackTrace(); - return; + throw new UndeclaredThrowableException(e); } break; - case DatabaseFile.SEC_LEVEL_DERIVED: - // TODO: prompt for pin/pass - /*CryptParameters cryptParams = dbFile.getCryptParameters(); - DerivationParameters derParams = dbFile.getDerivationParameters(); - SecretKey key = CryptoUtils.deriveKey("password".toCharArray(), derParams.Salt, (int)derParams.IterationCount);*/ + } else { - break; - case DatabaseFile.SEC_LEVEL_KEYSTORE: - // TODO: prompt for fingerprint if auth is required - try { - CryptParameters params = databaseFile.getCryptParameters(); - - if (cipher == null) { - KeyStoreHandle store = new KeyStoreHandle(); - SecretKey key = store.getKey(); - cipher = CryptoUtils.createCipher(key, Cipher.DECRYPT_MODE, params.Nonce); - } - - CryptResult result = null; - //try { - result = CryptoUtils.decrypt(content, cipher, params); - //} catch (Exception e) { - // // we probably need to authenticate ourselves - // promptFingerPrint(1, cipher); - //} - if (result != null) { - database = new Database(); - database.deserialize(result.Data); - } - } catch (IllegalBlockSizeException e) { - if (causeIsKeyUserNotAuthenticated(e) && cipher != null) { - promptFingerPrint(FingerprintAuthenticationDialogFragment.Action.LOAD, cipher); - } - e.printStackTrace(); - return; - } catch (Exception e) { - e.printStackTrace(); - return; - } - break; - default: - // TODO: handle unknown security level - return; + } } + + CryptResult result; + try { + result = masterKey.decrypt(content, databaseFile.getCryptParameters()); + } catch (Exception e) { + throw new UndeclaredThrowableException(e); + } + + content = result.Data; + } + + database = new Database(); + try { + database.deserialize(content); + } catch (Exception e) { + throw new UndeclaredThrowableException(e); } try { mKeyProfiles.addAll(database.getKeys()); mKeyProfileAdapter.notifyDataSetChanged(); } catch (Exception e) { - e.printStackTrace(); - return; + throw new UndeclaredThrowableException(e); } - - FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab); - fab.setEnabled(true); } private boolean causeIsKeyUserNotAuthenticated(Exception e) { diff --git a/app/src/main/java/me/impy/aegis/ScannerActivity.java b/app/src/main/java/me/impy/aegis/ScannerActivity.java index 00818ccc..ffc90f37 100644 --- a/app/src/main/java/me/impy/aegis/ScannerActivity.java +++ b/app/src/main/java/me/impy/aegis/ScannerActivity.java @@ -124,4 +124,3 @@ public class ScannerActivity extends Activity implements ZXingScannerView.Result return supportedFormats; } } - diff --git a/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java b/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java index 3719d6dc..37d3c4b3 100644 --- a/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java +++ b/app/src/main/java/me/impy/aegis/crypto/CryptoUtils.java @@ -13,6 +13,7 @@ import java.util.Arrays; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; @@ -24,8 +25,11 @@ public class CryptoUtils { public static final byte CRYPTO_KEY_SIZE = 32; public static final byte CRYPTO_NONCE_SIZE = 12; public static final byte CRYPTO_SALT_SIZE = 32; - public static final String CRYPTO_CIPHER = "AES/GCM/NoPadding"; - public static final String CRYPTO_DERIVE_ALGO = "PBKDF2WithHmacSHA256"; + public static final short CRYPTO_ITERATION_COUNT = 10000; + public static final String CRYPTO_CIPHER_RAW = "AES/ECB/NoPadding"; + public static final String CRYPTO_CIPHER_AEAD = "AES/GCM/NoPadding"; + // TODO: use a separate library for an HMAC-SHA256 implementation + public static final String CRYPTO_DERIVE_ALGO = "PBKDF2WithHmacSHA1"; public static SecretKey deriveKey(char[] password, byte[] salt, int iterations) throws NoSuchAlgorithmException, InvalidKeySpecException { SecretKeyFactory factory = SecretKeyFactory.getInstance(CRYPTO_DERIVE_ALGO); @@ -40,7 +44,7 @@ public class CryptoUtils { public static Cipher createCipher(SecretKey key, int opmode, byte[] nonce) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException { GCMParameterSpec spec = new GCMParameterSpec(CRYPTO_TAG_SIZE * 8, nonce); - Cipher cipher = Cipher.getInstance(CRYPTO_CIPHER); + Cipher cipher = Cipher.getInstance(CRYPTO_CIPHER_AEAD); cipher.init(opmode, key, spec); return cipher; } @@ -75,6 +79,12 @@ public class CryptoUtils { }}; } + public static SecretKey generateKey() throws NoSuchAlgorithmException { + KeyGenerator generator = KeyGenerator.getInstance("AES"); + generator.init(CRYPTO_KEY_SIZE * 8); + return generator.generateKey(); + } + public static byte[] generateSalt() { return generateRandomBytes(CRYPTO_KEY_SIZE); } diff --git a/app/src/main/java/me/impy/aegis/crypto/DerivationParameters.java b/app/src/main/java/me/impy/aegis/crypto/DerivationParameters.java deleted file mode 100644 index e81111db..00000000 --- a/app/src/main/java/me/impy/aegis/crypto/DerivationParameters.java +++ /dev/null @@ -1,6 +0,0 @@ -package me.impy.aegis.crypto; - -public class DerivationParameters { - public long IterationCount; - public byte[] Salt; -} diff --git a/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java index 3374fca0..4bf64156 100644 --- a/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java +++ b/app/src/main/java/me/impy/aegis/crypto/KeyStoreHandle.java @@ -32,14 +32,13 @@ public class KeyStoreHandle { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME); generator.init(new KeyGenParameterSpec.Builder(KEY_NAME, - KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .setUserAuthenticationRequired(authRequired) - .setRandomizedEncryptionRequired(false) - .setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8) - //.setUserAuthenticationValidityDurationSeconds(30); - .build()); + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_ECB) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) + .setUserAuthenticationRequired(authRequired) + .setRandomizedEncryptionRequired(false) + .setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8) + .build()); return generator.generateKey(); } else { diff --git a/app/src/main/java/me/impy/aegis/crypto/MasterKey.java b/app/src/main/java/me/impy/aegis/crypto/MasterKey.java new file mode 100644 index 00000000..5676a3c7 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/MasterKey.java @@ -0,0 +1,48 @@ +package me.impy.aegis.crypto; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +import me.impy.aegis.crypto.slots.Slot; + +public class MasterKey { + private SecretKey _key; + + public MasterKey(SecretKey key) throws NoSuchAlgorithmException { + if (key == null) { + key = CryptoUtils.generateKey(); + } + _key = key; + } + + public static MasterKey decryptSlot(Slot slot, Cipher cipher) + throws BadPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException { + return new MasterKey(slot.getKey(cipher)); + } + + public CryptResult encrypt(byte[] bytes) + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, + NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException { + Cipher cipher = CryptoUtils.createCipher(_key, Cipher.ENCRYPT_MODE); + return CryptoUtils.encrypt(bytes, cipher); + } + + public CryptResult decrypt(byte[] bytes, CryptParameters params) + throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, + NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException, IOException { + Cipher cipher = CryptoUtils.createCipher(_key, Cipher.DECRYPT_MODE, params.Nonce); + return CryptoUtils.decrypt(bytes, cipher, params); + } + + public void encryptSlot(Slot slot, Cipher cipher) throws BadPaddingException, IllegalBlockSizeException { + slot.setKey(_key, cipher); + } +} diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/FingerprintSlot.java b/app/src/main/java/me/impy/aegis/crypto/slots/FingerprintSlot.java new file mode 100644 index 00000000..09157180 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/slots/FingerprintSlot.java @@ -0,0 +1,5 @@ +package me.impy.aegis.crypto.slots; + +public class FingerprintSlot extends RawSlot { + +} diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/PasswordSlot.java b/app/src/main/java/me/impy/aegis/crypto/slots/PasswordSlot.java new file mode 100644 index 00000000..b7d7c287 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/slots/PasswordSlot.java @@ -0,0 +1,60 @@ +package me.impy.aegis.crypto.slots; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +import javax.crypto.SecretKey; + +import me.impy.aegis.crypto.CryptoUtils; +import me.impy.aegis.util.LittleByteBuffer; + +public class PasswordSlot extends RawSlot { + private long _iterationCount; + private byte[] _salt; + + public PasswordSlot() { + super(); + } + + @Override + public byte[] serialize() { + byte[] bytes = super.serialize(); + LittleByteBuffer buffer = LittleByteBuffer.wrap(bytes); + buffer.position(super.getSize()); + buffer.putLong(_iterationCount); + buffer.put(_salt); + return buffer.array(); + } + + @Override + public void deserialize(byte[] data) throws Exception { + super.deserialize(data); + LittleByteBuffer buffer = LittleByteBuffer.wrap(data); + buffer.position(super.getSize()); + _iterationCount = buffer.getLong(); + _salt = new byte[CryptoUtils.CRYPTO_SALT_SIZE]; + buffer.get(_salt); + } + + public SecretKey deriveKey(char[] password, byte[] salt, int iterations) throws InvalidKeySpecException, NoSuchAlgorithmException { + SecretKey key = CryptoUtils.deriveKey(password, salt, iterations); + _iterationCount = iterations; + _salt = salt; + return key; + } + + public SecretKey deriveKey(char[] password) throws InvalidKeySpecException, NoSuchAlgorithmException { + SecretKey key = CryptoUtils.deriveKey(password, _salt, (int)_iterationCount); + return key; + } + + @Override + public int getSize() { + return 1 + CryptoUtils.CRYPTO_KEY_SIZE + /* iterations */ 8 + CryptoUtils.CRYPTO_SALT_SIZE; + } + + @Override + public byte getType() { + return TYPE_DERIVED; + } +} diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/RawSlot.java b/app/src/main/java/me/impy/aegis/crypto/slots/RawSlot.java new file mode 100644 index 00000000..9c8856ad --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/slots/RawSlot.java @@ -0,0 +1,40 @@ +package me.impy.aegis.crypto.slots; + +import me.impy.aegis.crypto.CryptoUtils; +import me.impy.aegis.util.LittleByteBuffer; + +public class RawSlot extends Slot { + + public RawSlot() { + super(); + } + + @Override + public byte[] serialize() { + LittleByteBuffer buffer = LittleByteBuffer.allocate(getSize()); + buffer.put(getType()); + buffer.put(_encryptedMasterKey); + return buffer.array(); + } + + @Override + public void deserialize(byte[] data) throws Exception { + LittleByteBuffer buffer = LittleByteBuffer.wrap(data); + if (buffer.get() != getType()) { + throw new Exception("slot type mismatch"); + } + buffer.get(); + _encryptedMasterKey = new byte[CryptoUtils.CRYPTO_KEY_SIZE]; + buffer.get(_encryptedMasterKey); + } + + @Override + public int getSize() { + return 1 + CryptoUtils.CRYPTO_KEY_SIZE; + } + + @Override + public byte getType() { + return TYPE_RAW; + } +} diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/Slot.java b/app/src/main/java/me/impy/aegis/crypto/slots/Slot.java new file mode 100644 index 00000000..84876020 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/slots/Slot.java @@ -0,0 +1,54 @@ +package me.impy.aegis.crypto.slots; + +import android.annotation.SuppressLint; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +import me.impy.aegis.crypto.CryptoUtils; + +public abstract class Slot { + public final static byte TYPE_RAW = 0x00; + public final static byte TYPE_DERIVED = 0x01; + public final static byte TYPE_FINGERPRINT = 0x02; + + protected byte[] _encryptedMasterKey; + + // getKey decrypts the encrypted master key in this slot with the given key and returns it. + public SecretKey getKey(Cipher cipher) throws BadPaddingException, IllegalBlockSizeException { + byte[] decryptedKeyBytes = cipher.doFinal(_encryptedMasterKey); + SecretKey decryptedKey = new SecretKeySpec(decryptedKeyBytes, CryptoUtils.CRYPTO_CIPHER_RAW); + CryptoUtils.zero(decryptedKeyBytes); + return decryptedKey; + } + + // setKey encrypts the given master key with the given key and stores the result in this slot. + public void setKey(SecretKey masterKey, Cipher cipher) throws BadPaddingException, IllegalBlockSizeException { + byte[] masterKeyBytes = masterKey.getEncoded(); + _encryptedMasterKey = cipher.doFinal(masterKeyBytes); + CryptoUtils.zero(masterKeyBytes); + } + + // suppressing the AES ECB warning + // this is perfectly safe because we discard this cipher after passing CryptoUtils.CRYPTO_KEY_SIZE bytes through it + @SuppressLint("getInstance") + public static Cipher createCipher(SecretKey key, int mode) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException { + Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_CIPHER_RAW); + cipher.init(mode, key); + return cipher; + } + + public abstract int getSize(); + public abstract byte getType(); + + // a slot has a binary representation + public abstract byte[] serialize(); + public abstract void deserialize(byte[] data) throws Exception; +} diff --git a/app/src/main/java/me/impy/aegis/crypto/slots/SlotCollection.java b/app/src/main/java/me/impy/aegis/crypto/slots/SlotCollection.java new file mode 100644 index 00000000..b4eb01f9 --- /dev/null +++ b/app/src/main/java/me/impy/aegis/crypto/slots/SlotCollection.java @@ -0,0 +1,84 @@ +package me.impy.aegis.crypto.slots; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import me.impy.aegis.util.LittleByteBuffer; + +public class SlotCollection implements Iterable { + private List _slots = new ArrayList<>(); + + public static byte[] serialize(SlotCollection slots) { + // yep, no streams at this api level + int size = 0; + for (Slot slot : slots) { + size += slot.getSize(); + } + + LittleByteBuffer buffer = LittleByteBuffer.allocate(size); + + for (Slot slot : slots) { + byte[] bytes = slot.serialize(); + buffer.put(bytes); + } + return buffer.array(); + } + + public static SlotCollection deserialize(byte[] data) throws Exception { + SlotCollection slots = new SlotCollection(); + LittleByteBuffer buffer = LittleByteBuffer.wrap(data); + + while (buffer.remaining() > 0) { + Slot slot; + + switch (buffer.peek()) { + case Slot.TYPE_RAW: + slot = new RawSlot(); + break; + case Slot.TYPE_DERIVED: + slot = new PasswordSlot(); + break; + case Slot.TYPE_FINGERPRINT: + slot = new FingerprintSlot(); + break; + default: + throw new Exception("unrecognized slot type"); + } + + byte[] bytes = new byte[slot.getSize()]; + buffer.get(bytes); + + slot.deserialize(bytes); + slots.add(slot); + } + + return slots; + } + + public void add(Slot slot) { + _slots.add(slot); + } + + public void remove(Slot slot) { + _slots.remove(slot); + } + + public int size() { + return _slots.size(); + } + + public T find(Class type) { + for (Slot slot : this) { + if (slot.getClass() == type) { + return type.cast(slot); + } + } + return null; + } + + @Override + public Iterator iterator() { + return _slots.iterator(); + } +} diff --git a/app/src/main/java/me/impy/aegis/db/Database.java b/app/src/main/java/me/impy/aegis/db/Database.java index c6e9bc65..6ea9b8c9 100644 --- a/app/src/main/java/me/impy/aegis/db/Database.java +++ b/app/src/main/java/me/impy/aegis/db/Database.java @@ -91,4 +91,4 @@ public class Database { throw new Exception("Key doesn't exist"); } -} \ No newline at end of file +} diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java index fb9d722d..8c974dcf 100644 --- a/app/src/main/java/me/impy/aegis/db/DatabaseFile.java +++ b/app/src/main/java/me/impy/aegis/db/DatabaseFile.java @@ -7,68 +7,56 @@ import java.io.DataOutputStream; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; +import java.lang.reflect.UndeclaredThrowableException; import java.util.Arrays; import me.impy.aegis.crypto.CryptParameters; +import me.impy.aegis.crypto.slots.SlotCollection; import me.impy.aegis.crypto.CryptoUtils; -import me.impy.aegis.crypto.DerivationParameters; +import me.impy.aegis.util.LittleByteBuffer; public class DatabaseFile { - public static final byte SEC_LEVEL_NONE = 0x00; - public static final byte SEC_LEVEL_DERIVED = 0x01; - public static final byte SEC_LEVEL_KEYSTORE = 0x02; private static final byte bSectionEncryptionParameters = 0x00; - private static final byte bSectionDerivationParameters = 0x01; + private static final byte bSectionSlots = 0x01; private static final byte bSectionEnd = (byte) 0xFF; private static final byte bVersion = 1; private static final String dbFilename = "aegis.db"; private final byte[] bHeader; - private final Context context; - private byte level; private byte[] content; private CryptParameters cryptParameters; - private DerivationParameters derivationParameters; + private SlotCollection slots; - public DatabaseFile(Context ctx) throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { - context = ctx; - bHeader = "AEGIS".getBytes("US_ASCII"); + public DatabaseFile() { + try { + bHeader = "AEGIS".getBytes("US_ASCII"); + } catch (Exception e) { + throw new UndeclaredThrowableException(e); + } + slots = new SlotCollection(); } public byte[] serialize() throws IOException { CryptParameters cryptParams = getCryptParameters(); - DerivationParameters derParams = getDerivationParameters(); byte[] content = getContent(); - byte level = getLevel(); // this is dumb, java doesn't provide an endianness-aware data stream ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); DataOutputStream stream = new DataOutputStream(byteStream); stream.write(bHeader); stream.write(bVersion); - stream.write(level); - // we assume that all of the needed params for the security level are set - // if that's not the case, a NullPointerException will be thrown. - switch (level) { - case SEC_LEVEL_DERIVED: - ByteBuffer paramBuffer = newBuffer(/* iterations */ 8 + CryptoUtils.CRYPTO_SALT_SIZE); - paramBuffer.putLong(derParams.IterationCount); - paramBuffer.put(derParams.Salt); - writeSection(stream, bSectionDerivationParameters, paramBuffer.array()); - // intentional fallthrough - case SEC_LEVEL_KEYSTORE: - paramBuffer = newBuffer(CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE); - paramBuffer.put(cryptParams.Nonce); - paramBuffer.put(cryptParams.Tag); - writeSection(stream, bSectionEncryptionParameters, paramBuffer.array()); - break; + if (cryptParams != null) { + LittleByteBuffer paramBuffer = LittleByteBuffer.allocate(CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE); + paramBuffer.put(cryptParams.Nonce); + paramBuffer.put(cryptParams.Tag); + writeSection(stream, bSectionEncryptionParameters, paramBuffer.array()); + } + + if (slots != null) { + byte[] bytes = SlotCollection.serialize(slots); + writeSection(stream, bSectionSlots, bytes); } writeSection(stream, bSectionEnd, null); @@ -77,7 +65,7 @@ public class DatabaseFile { } public void deserialize(byte[] data) throws Exception { - ByteBuffer buffer = newBuffer(data); + LittleByteBuffer buffer = LittleByteBuffer.wrap(data); byte[] header = new byte[bHeader.length]; buffer.get(header); @@ -91,17 +79,11 @@ public class DatabaseFile { throw new Exception("Unsupported version"); } - byte level = buffer.get(); - if (level > SEC_LEVEL_KEYSTORE) { - throw new Exception("Unsupported security level"); - } - setLevel(level); - CryptParameters cryptParams = null; - DerivationParameters derParams = null; + SlotCollection slots = null; for (section s = readSection(buffer); s.ID != bSectionEnd; s = readSection(buffer)) { - ByteBuffer sBuff = newBuffer(s.Data); + LittleByteBuffer sBuff = LittleByteBuffer.wrap(s.Data); switch (s.ID) { case bSectionEncryptionParameters: assertLength(s.Data, CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE); @@ -116,35 +98,25 @@ public class DatabaseFile { Tag = tag; }}; break; - case bSectionDerivationParameters: - assertLength(s.Data, /* iterations */ 8 + CryptoUtils.CRYPTO_SALT_SIZE); - - long iterations = sBuff.getLong(); - byte[] salt = new byte[CryptoUtils.CRYPTO_SALT_SIZE]; - sBuff.get(salt); - - derParams = new DerivationParameters() {{ - IterationCount = iterations; - Salt = salt; - }}; + case bSectionSlots: + slots = SlotCollection.deserialize(s.Data); break; } } - if ((level == SEC_LEVEL_DERIVED && (cryptParams == null || derParams == null)) - || (level == SEC_LEVEL_KEYSTORE && cryptParams == null)) { - throw new Exception("Security level parameters missing"); - } - setCryptParameters(cryptParams); - setDerivationParameters(derParams); + setSlots(slots); byte[] content = new byte[buffer.remaining()]; buffer.get(content); setContent(content); } - public void save() throws IOException { + public boolean isEncrypted() { + return slots != null && cryptParameters != null; + } + + public void save(Context context) throws IOException { byte[] data = serialize(); FileOutputStream file = context.openFileOutput(dbFilename, Context.MODE_PRIVATE); @@ -158,7 +130,7 @@ public class DatabaseFile { file.read(data); file.close(); - DatabaseFile db = new DatabaseFile(context); + DatabaseFile db = new DatabaseFile(); db.deserialize(data); return db; } @@ -166,7 +138,7 @@ public class DatabaseFile { private static void writeSection(DataOutputStream stream, byte id, byte[] data) throws IOException { stream.write(id); - ByteBuffer buffer = newBuffer(/* sizeof uint32_t */ 4); + LittleByteBuffer buffer = LittleByteBuffer.allocate(/* sizeof uint32_t */ 4); if (data == null) { buffer.putInt(0); } else { @@ -179,7 +151,7 @@ public class DatabaseFile { } } - private static section readSection(ByteBuffer buffer) { + private static section readSection(LittleByteBuffer buffer) { section s = new section(); s.ID = buffer.get(); @@ -190,18 +162,6 @@ public class DatabaseFile { return s; } - private static ByteBuffer newBuffer(byte[] data) { - ByteBuffer buffer = ByteBuffer.wrap(data); - buffer.order(ByteOrder.LITTLE_ENDIAN); - return buffer; - } - - private static ByteBuffer newBuffer(int size) { - ByteBuffer buffer = ByteBuffer.allocate(size); - buffer.order(ByteOrder.LITTLE_ENDIAN); - return buffer; - } - private static void assertLength(byte[] bytes, int length) throws Exception { if (bytes.length != length) { throw new Exception("Bad length"); @@ -224,20 +184,12 @@ public class DatabaseFile { this.cryptParameters = parameters; } - public DerivationParameters getDerivationParameters() { - return derivationParameters; + public SlotCollection getSlots() { + return slots; } - public void setDerivationParameters(DerivationParameters derivationParameters) { - this.derivationParameters = derivationParameters; - } - - public byte getLevel() { - return level; - } - - public void setLevel(byte level) { - this.level = level; + public void setSlots(SlotCollection slots) { + this.slots = slots; } private static class section { diff --git a/app/src/main/java/me/impy/aegis/db/DatabaseManager.java b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java new file mode 100644 index 00000000..b40bc33a --- /dev/null +++ b/app/src/main/java/me/impy/aegis/db/DatabaseManager.java @@ -0,0 +1,16 @@ +package me.impy.aegis.db; + +public class DatabaseManager +{ + private DatabaseManager() { + + } + + public static DatabaseManager load() { + return null; + } + + public static DatabaseManager create() { + return null; + } +} diff --git a/app/src/main/java/me/impy/aegis/util/LittleByteBuffer.java b/app/src/main/java/me/impy/aegis/util/LittleByteBuffer.java new file mode 100644 index 00000000..314abecc --- /dev/null +++ b/app/src/main/java/me/impy/aegis/util/LittleByteBuffer.java @@ -0,0 +1,38 @@ +package me.impy.aegis.util; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +// LittleByteBuffer wraps a ByteBuffer to extend its API a little. +// Its byte order is set to little endian by default. +// All this boilerplate just to change the default byte order and add a peek method... Is it worth it? Probably not. +public class LittleByteBuffer { + private ByteBuffer _buffer; + + private LittleByteBuffer(ByteBuffer buffer) { + _buffer = buffer; + _buffer.order(ByteOrder.LITTLE_ENDIAN); + } + + public byte peek() { + _buffer.mark(); + byte b = _buffer.get(); + _buffer.reset(); + return b; + } + + public byte get() { return _buffer.get(); } + public LittleByteBuffer get(byte[] dst) {_buffer.get(dst); return this; } + public LittleByteBuffer put(byte b) { _buffer.put(b); return this; } + public LittleByteBuffer put(byte[] bytes) { _buffer.put(bytes); return this; } + public int remaining() { return _buffer.remaining(); } + public byte[] array() { return _buffer.array(); } + public LittleByteBuffer putInt(int i) { _buffer.putInt(i); return this; } + public LittleByteBuffer putLong(long l) { _buffer.putLong(l); return this; } + public int getInt() { return _buffer.getInt(); } + public long getLong() { return _buffer.getLong(); } + public int position() { return _buffer.position(); } + public LittleByteBuffer position(int i) { _buffer.position(i); return this; } + public static LittleByteBuffer allocate(int size) { return new LittleByteBuffer(ByteBuffer.allocate(size)); } + public static LittleByteBuffer wrap(byte[] bytes) { return new LittleByteBuffer(ByteBuffer.wrap(bytes)); } +} diff --git a/doc/db.md b/doc/db.md index 0ecea38d..102db1fd 100644 --- a/doc/db.md +++ b/doc/db.md @@ -1,21 +1,22 @@ # Database -The database is encoded in a simple binary format with JSON content at its -core. +The database is encoded in a simple binary format with JSON content at its core. ## Encryption -The content of the database can be encrypted with AES in GCM mode. The nonce -and authentication tag are stored in the plain section of this file. The -storage place for the key depends on the security level that is used. This will -be discussed later. +The content of the database can be encrypted with AES in GCM mode. The nonce and +authentication tag are stored in the plain section of this file. + +If there is no Slots and/or EncryptionParameters section in the file, it is +implied that the content of is unencrypted and Aegis will try to interpret it as +such. ## Format -The file format starts with a small header that contains some magic, the -version number and the level of security. A list of sections follows. These -sections contain some information needed to perform decryption of the database. -The (encrypted) content of the database starts after the end marker section. +The file format starts with a small header that contains some magic and a +version number. A list of sections follows. These sections contain some +information needed to perform decryption of the database. The (encrypted) +content of the database starts after the end marker section. All integers are encoded in Little Endian. @@ -25,43 +26,9 @@ All integers are encoded in Little Endian. |:-------|:-------------------------| | `5` | "AEGIS" encoded in ASCII | | `1` | `uint8_t` Version | -| `1` | `uint8_t` Level | | `?` | List of sections | | `?` | Content | -#### Levels - -As mentioned above, there are different levels of security that a user can -choose from. No encryption, encryption using a derived key and encryption using -a key that's stored in the Android KeyStore. - -| Value | Name | -|:-------|:---------| -| `0x00` | None | -| `0x01` | Derived | -| `0x02` | KeyStore | - -The 'KeyStore' level expects an EncryptionParameters section. The 'Derived' -level expects an EncryptionParameters section **and** a DerivationParameters section. -The 'None' level expects no additional sections. - -##### None - -No encryption at all. The content of the database is stored in plain text. - -##### Derived - -If this level is used, the key is derived from a user-provided password using -PBKDF2 with SHA256 as the underlying PRF. The parameters used for PBKDF2 (salt, -number of iterations) are stored in the plain section of this file. The key is -not stored anywhere. - -##### KeyStore - -The key is kept in the Android keystore and can optionally be set up to require -user authentication (fingerprint). This security level is only available on -Android M and above. - ### Sections | Length | Contents | @@ -75,7 +42,7 @@ ID can be one of: | Value | Name | |:-------|:---------------------| | `0x00` | EncryptionParameters | -| `0x01` | DerivationParameters | +| `0x01` | Slots | | `0xFF` | End marker | #### EncryptionParameters @@ -85,13 +52,48 @@ ID can be one of: | `12` | Nonce | | `16` | Tag | -#### DerivationParameters +#### Slots + +This section contains a list of slots. All slots contain the master key +encrypted with raw AES. The key that is used for encryption depends on the slot +type. + +A slot has the following structure. + +| Length | Contents | +|:-------|:--------------------| +| `1` | `uint8_t` Type | +| `32` | Encrypted key | +| `?` | Additional data | + +Type can be one of: + +| Value | Name | +|:-------|:------------| +| `0x00` | Raw | +| `0x01` | Password | +| `0x02` | Fingerprint | + +##### Raw + +This slot type contains no additional data. + +##### Password + +With this slot type the key used for the master key encryption is derived from a +user-provided password using PBKDF2 with SHA1 (should be changed to SHA256 +before initial release) as the underlying PRF. The parameters used for PBKDF2 +are stored as additional data. | Length | Contents | |:-------|:--------------------------------| | `8` | `uint64_t` Number of iterations | | `32` | Salt | +##### Fingerprint + +A fingerprint slot is exactly the same as a Raw slot. + #### End marker This section indicates the end of the list of sections. This section doesn't @@ -99,10 +101,9 @@ have any content and thus its length is 0. ### Content -The content of the database is a JSON file encoded in UTF-8. As mentioned -above, it's encrypted. +The content of the database is a JSON file encoded in UTF-8. -``` json +```json { "version": 1, "entries":