mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-02 03:24:27 +00:00
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:
parent
9de0f35104
commit
e8e7a822c5
16 changed files with 564 additions and 310 deletions
|
@ -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 {
|
||||
|
|
48
app/src/main/java/me/impy/aegis/crypto/MasterKey.java
Normal file
48
app/src/main/java/me/impy/aegis/crypto/MasterKey.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
40
app/src/main/java/me/impy/aegis/crypto/slots/RawSlot.java
Normal file
40
app/src/main/java/me/impy/aegis/crypto/slots/RawSlot.java
Normal 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;
|
||||
}
|
||||
}
|
54
app/src/main/java/me/impy/aegis/crypto/slots/Slot.java
Normal file
54
app/src/main/java/me/impy/aegis/crypto/slots/Slot.java
Normal 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;
|
||||
}
|
|
@ -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 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 {
|
||||
|
|
16
app/src/main/java/me/impy/aegis/db/DatabaseManager.java
Normal file
16
app/src/main/java/me/impy/aegis/db/DatabaseManager.java
Normal 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;
|
||||
}
|
||||
}
|
38
app/src/main/java/me/impy/aegis/util/LittleByteBuffer.java
Normal file
38
app/src/main/java/me/impy/aegis/util/LittleByteBuffer.java
Normal 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)); }
|
||||
}
|
99
doc/db.md
99
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":
|
||||
|
|
Loading…
Add table
Reference in a new issue