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

Details are in doc/db.md
This commit is contained in:
Impyy 2017-05-03 21:08:38 +02:00 committed by Alexander Bakker
parent 9de0f35104
commit e8e7a822c5
16 changed files with 564 additions and 310 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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