Push some work on yet another rework of the database. This time with slots!

Details are in doc/db.md
pull/41/head
Impyy 8 years ago committed by Alexander Bakker
parent 9de0f35104
commit e8e7a822c5

@ -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<KeyProfile> 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) {

@ -124,4 +124,3 @@ public class ScannerActivity extends Activity implements ZXingScannerView.Result
return supportedFormats;
}
}

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

@ -1,6 +0,0 @@
package me.impy.aegis.crypto;
public class DerivationParameters {
public long IterationCount;
public byte[] Salt;
}

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

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

@ -0,0 +1,5 @@
package me.impy.aegis.crypto.slots;
public class FingerprintSlot extends RawSlot {
}

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

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

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

@ -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<Slot> {
private List<Slot> _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 extends Slot> T find(Class<T> type) {
for (Slot slot : this) {
if (slot.getClass() == type) {
return type.cast(slot);
}
}
return null;
}
@Override
public Iterator<Slot> iterator() {
return _slots.iterator();
}
}

@ -91,4 +91,4 @@ public class Database {
throw new Exception("Key doesn't exist");
}
}
}

@ -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 void setDerivationParameters(DerivationParameters derivationParameters) {
this.derivationParameters = derivationParameters;
}
public byte getLevel() {
return level;
public SlotCollection getSlots() {
return slots;
}
public void setLevel(byte level) {
this.level = level;
public void setSlots(SlotCollection slots) {
this.slots = slots;
}
private static class section {

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

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

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

Loading…
Cancel
Save