mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-02 19:44: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 android.widget.Toast;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.UndeclaredThrowableException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -36,15 +38,15 @@ import java.util.Comparator;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
|
|
||||||
import me.impy.aegis.crypto.CryptParameters;
|
|
||||||
import me.impy.aegis.crypto.CryptResult;
|
import me.impy.aegis.crypto.CryptResult;
|
||||||
import me.impy.aegis.crypto.CryptoUtils;
|
import me.impy.aegis.crypto.CryptoUtils;
|
||||||
import me.impy.aegis.crypto.DerivationParameters;
|
import me.impy.aegis.crypto.MasterKey;
|
||||||
import me.impy.aegis.crypto.KeyStoreHandle;
|
|
||||||
import me.impy.aegis.crypto.OTP;
|
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.Database;
|
||||||
import me.impy.aegis.db.DatabaseFile;
|
import me.impy.aegis.db.DatabaseFile;
|
||||||
import me.impy.aegis.finger.FingerprintAuthenticationDialogFragment;
|
import me.impy.aegis.finger.FingerprintAuthenticationDialogFragment;
|
||||||
|
@ -58,10 +60,10 @@ public class MainActivity extends AppCompatActivity {
|
||||||
RecyclerView rvKeyProfiles;
|
RecyclerView rvKeyProfiles;
|
||||||
KeyProfileAdapter mKeyProfileAdapter;
|
KeyProfileAdapter mKeyProfileAdapter;
|
||||||
ArrayList<KeyProfile> mKeyProfiles = new ArrayList<>();
|
ArrayList<KeyProfile> mKeyProfiles = new ArrayList<>();
|
||||||
|
MasterKey masterKey;
|
||||||
Database database;
|
Database database;
|
||||||
DatabaseFile databaseFile;
|
DatabaseFile databaseFile;
|
||||||
|
|
||||||
boolean blockSave = false;
|
|
||||||
boolean nightMode = false;
|
boolean nightMode = false;
|
||||||
int clickedItemPosition = -1;
|
int clickedItemPosition = -1;
|
||||||
|
|
||||||
|
@ -88,9 +90,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
initializeAppShortcuts();
|
initializeAppShortcuts();
|
||||||
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
|
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
|
||||||
fab.setEnabled(false);
|
fab.setEnabled(true);
|
||||||
fab.setOnClickListener(view -> {
|
fab.setOnClickListener(view -> {
|
||||||
blockSave = true;
|
|
||||||
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
|
Intent scannerActivity = new Intent(getApplicationContext(), ScannerActivity.class);
|
||||||
startActivityForResult(scannerActivity, GET_KEYINFO);
|
startActivityForResult(scannerActivity, GET_KEYINFO);
|
||||||
});
|
});
|
||||||
|
@ -121,12 +122,11 @@ public class MainActivity extends AppCompatActivity {
|
||||||
};
|
};
|
||||||
Collections.sort(mKeyProfiles, comparator);
|
Collections.sort(mKeyProfiles, comparator);
|
||||||
|
|
||||||
loadDatabase(null);
|
loadDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
blockSave = false;
|
|
||||||
if (requestCode == GET_KEYINFO) {
|
if (requestCode == GET_KEYINFO) {
|
||||||
if (resultCode == RESULT_OK) {
|
if (resultCode == RESULT_OK) {
|
||||||
final KeyProfile keyProfile = (KeyProfile)data.getSerializableExtra("KeyProfile");
|
final KeyProfile keyProfile = (KeyProfile)data.getSerializableExtra("KeyProfile");
|
||||||
|
@ -160,7 +160,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
mKeyProfiles.add(keyProfile);
|
mKeyProfiles.add(keyProfile);
|
||||||
mKeyProfileAdapter.notifyDataSetChanged();
|
mKeyProfileAdapter.notifyDataSetChanged();
|
||||||
saveDatabase(true, null);
|
saveDatabase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,19 +174,17 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
if (!blockSave) {
|
// update order of keys
|
||||||
// update order of keys
|
for (int i = 0; i < mKeyProfiles.size(); i++) {
|
||||||
for (int i = 0; i < mKeyProfiles.size(); i++) {
|
try {
|
||||||
try {
|
database.updateKey(mKeyProfiles.get(i));
|
||||||
database.updateKey(mKeyProfiles.get(i));
|
} catch (Exception e) {
|
||||||
} catch (Exception e) {
|
e.printStackTrace();
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveDatabase(false, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveDatabase();
|
||||||
|
|
||||||
super.onPause();
|
super.onPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,50 +203,24 @@ public class MainActivity extends AppCompatActivity {
|
||||||
Cipher cipher = obj.getCipher();
|
Cipher cipher = obj.getCipher();
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case SAVE:
|
case SAVE:
|
||||||
saveDatabase(false, cipher);
|
saveDatabase();
|
||||||
break;
|
break;
|
||||||
case LOAD:
|
case LOAD:
|
||||||
loadDatabase(cipher);
|
loadDatabase();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void saveDatabase(boolean allowPrompt, Cipher cipher) {
|
private void saveDatabase() {
|
||||||
try {
|
try {
|
||||||
byte[] bytes = database.serialize();
|
byte[] bytes = database.serialize();
|
||||||
CryptParameters cryptParams = null;
|
CryptResult result = masterKey.encrypt(bytes);
|
||||||
DerivationParameters derParams = null;
|
databaseFile.setContent(result.Data);
|
||||||
|
databaseFile.setCryptParameters(result.Parameters);
|
||||||
switch (databaseFile.getLevel()) {
|
databaseFile.save(getApplicationContext());
|
||||||
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();
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
throw new UndeclaredThrowableException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -293,23 +265,22 @@ public class MainActivity extends AppCompatActivity {
|
||||||
private void deleteProfile(KeyProfile profile)
|
private void deleteProfile(KeyProfile profile)
|
||||||
{
|
{
|
||||||
new AlertDialog.Builder(MainActivity.this)
|
new AlertDialog.Builder(MainActivity.this)
|
||||||
.setTitle("Delete entry")
|
.setTitle("Delete entry")
|
||||||
.setMessage("Are you sure you want to delete this profile?")
|
.setMessage("Are you sure you want to delete this profile?")
|
||||||
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
.setPositiveButton(android.R.string.yes, (dialog, which) -> {
|
||||||
try {
|
try {
|
||||||
database.removeKey(profile);
|
database.removeKey(profile);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
//TODO: feedback
|
//TODO: feedback
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mKeyProfiles.remove(clickedItemPosition);
|
mKeyProfiles.remove(clickedItemPosition);
|
||||||
mKeyProfileAdapter.notifyItemRemoved(clickedItemPosition);
|
mKeyProfileAdapter.notifyItemRemoved(clickedItemPosition);
|
||||||
})
|
})
|
||||||
.setNegativeButton(android.R.string.no, (dialog, which) -> {
|
.setNegativeButton(android.R.string.no, (dialog, which) -> {
|
||||||
|
})
|
||||||
})
|
.show();
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -320,14 +291,9 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
int id = item.getItemId();
|
if (item.getItemId() == R.id.action_settings) {
|
||||||
|
|
||||||
//noinspection SimplifiableIfStatement
|
|
||||||
if (id == R.id.action_settings) {
|
|
||||||
|
|
||||||
Intent preferencesActivity = new Intent(this, PreferencesActivity.class);
|
Intent preferencesActivity = new Intent(this, PreferencesActivity.class);
|
||||||
startActivity(preferencesActivity);
|
startActivity(preferencesActivity);
|
||||||
|
|
||||||
return true;
|
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 {
|
try {
|
||||||
databaseFile = DatabaseFile.load(getApplicationContext());
|
databaseFile = DatabaseFile.load(getApplicationContext());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
// this file doesn't exist yet
|
// the database file doesn't exist yet
|
||||||
try {
|
createDatabase();
|
||||||
// TODO: prompt for security settings (level, auth, etc)
|
saveDatabase();
|
||||||
database = new Database();
|
return;
|
||||||
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;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// something else went wrong
|
// something else went wrong
|
||||||
e.printStackTrace();
|
throw new UndeclaredThrowableException(e);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (database == null) {
|
byte[] content = databaseFile.getContent();
|
||||||
byte[] content = databaseFile.getContent();
|
if (databaseFile.isEncrypted()) {
|
||||||
switch (databaseFile.getLevel()) {
|
SlotCollection slots = databaseFile.getSlots();
|
||||||
case DatabaseFile.SEC_LEVEL_NONE:
|
for (Slot slot : slots) {
|
||||||
|
if (slot instanceof PasswordSlot) {
|
||||||
try {
|
try {
|
||||||
Database temp = new Database();
|
PasswordSlot derSlot = (PasswordSlot)slot;
|
||||||
temp.deserialize(content);
|
SecretKey derivedKey = derSlot.deriveKey("testpassword".toCharArray());
|
||||||
database = temp;
|
Cipher cipher = Slot.createCipher(derivedKey, Cipher.DECRYPT_MODE);
|
||||||
|
masterKey = MasterKey.decryptSlot(slot, cipher);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// TODO: handle corrupt database
|
throw new UndeclaredThrowableException(e);
|
||||||
e.printStackTrace();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case DatabaseFile.SEC_LEVEL_DERIVED:
|
} else {
|
||||||
// TODO: prompt for pin/pass
|
|
||||||
/*CryptParameters cryptParams = dbFile.getCryptParameters();
|
|
||||||
DerivationParameters derParams = dbFile.getDerivationParameters();
|
|
||||||
SecretKey key = CryptoUtils.deriveKey("password".toCharArray(), derParams.Salt, (int)derParams.IterationCount);*/
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
mKeyProfiles.addAll(database.getKeys());
|
mKeyProfiles.addAll(database.getKeys());
|
||||||
mKeyProfileAdapter.notifyDataSetChanged();
|
mKeyProfileAdapter.notifyDataSetChanged();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
throw new UndeclaredThrowableException(e);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
|
|
||||||
fab.setEnabled(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean causeIsKeyUserNotAuthenticated(Exception e) {
|
private boolean causeIsKeyUserNotAuthenticated(Exception e) {
|
||||||
|
|
|
@ -124,4 +124,3 @@ public class ScannerActivity extends Activity implements ZXingScannerView.Result
|
||||||
return supportedFormats;
|
return supportedFormats;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ import java.util.Arrays;
|
||||||
import javax.crypto.BadPaddingException;
|
import javax.crypto.BadPaddingException;
|
||||||
import javax.crypto.Cipher;
|
import javax.crypto.Cipher;
|
||||||
import javax.crypto.IllegalBlockSizeException;
|
import javax.crypto.IllegalBlockSizeException;
|
||||||
|
import javax.crypto.KeyGenerator;
|
||||||
import javax.crypto.NoSuchPaddingException;
|
import javax.crypto.NoSuchPaddingException;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.SecretKeyFactory;
|
import javax.crypto.SecretKeyFactory;
|
||||||
|
@ -24,8 +25,11 @@ public class CryptoUtils {
|
||||||
public static final byte CRYPTO_KEY_SIZE = 32;
|
public static final byte CRYPTO_KEY_SIZE = 32;
|
||||||
public static final byte CRYPTO_NONCE_SIZE = 12;
|
public static final byte CRYPTO_NONCE_SIZE = 12;
|
||||||
public static final byte CRYPTO_SALT_SIZE = 32;
|
public static final byte CRYPTO_SALT_SIZE = 32;
|
||||||
public static final String CRYPTO_CIPHER = "AES/GCM/NoPadding";
|
public static final short CRYPTO_ITERATION_COUNT = 10000;
|
||||||
public static final String CRYPTO_DERIVE_ALGO = "PBKDF2WithHmacSHA256";
|
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 {
|
public static SecretKey deriveKey(char[] password, byte[] salt, int iterations) throws NoSuchAlgorithmException, InvalidKeySpecException {
|
||||||
SecretKeyFactory factory = SecretKeyFactory.getInstance(CRYPTO_DERIVE_ALGO);
|
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 {
|
public static Cipher createCipher(SecretKey key, int opmode, byte[] nonce) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, InvalidKeyException {
|
||||||
GCMParameterSpec spec = new GCMParameterSpec(CRYPTO_TAG_SIZE * 8, nonce);
|
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);
|
cipher.init(opmode, key, spec);
|
||||||
return cipher;
|
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() {
|
public static byte[] generateSalt() {
|
||||||
return generateRandomBytes(CRYPTO_KEY_SIZE);
|
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) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME);
|
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME);
|
||||||
generator.init(new KeyGenParameterSpec.Builder(KEY_NAME,
|
generator.init(new KeyGenParameterSpec.Builder(KEY_NAME,
|
||||||
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
|
||||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
|
||||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||||
.setUserAuthenticationRequired(authRequired)
|
.setUserAuthenticationRequired(authRequired)
|
||||||
.setRandomizedEncryptionRequired(false)
|
.setRandomizedEncryptionRequired(false)
|
||||||
.setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8)
|
.setKeySize(CryptoUtils.CRYPTO_KEY_SIZE * 8)
|
||||||
//.setUserAuthenticationValidityDurationSeconds(30);
|
.build());
|
||||||
.build());
|
|
||||||
|
|
||||||
return generator.generateKey();
|
return generator.generateKey();
|
||||||
} else {
|
} 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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,68 +7,56 @@ import java.io.DataOutputStream;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.lang.reflect.UndeclaredThrowableException;
|
||||||
import java.nio.ByteOrder;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import me.impy.aegis.crypto.CryptParameters;
|
import me.impy.aegis.crypto.CryptParameters;
|
||||||
|
import me.impy.aegis.crypto.slots.SlotCollection;
|
||||||
import me.impy.aegis.crypto.CryptoUtils;
|
import me.impy.aegis.crypto.CryptoUtils;
|
||||||
import me.impy.aegis.crypto.DerivationParameters;
|
import me.impy.aegis.util.LittleByteBuffer;
|
||||||
|
|
||||||
public class DatabaseFile {
|
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 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 bSectionEnd = (byte) 0xFF;
|
||||||
private static final byte bVersion = 1;
|
private static final byte bVersion = 1;
|
||||||
private static final String dbFilename = "aegis.db";
|
private static final String dbFilename = "aegis.db";
|
||||||
|
|
||||||
private final byte[] bHeader;
|
private final byte[] bHeader;
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
private byte level;
|
|
||||||
private byte[] content;
|
private byte[] content;
|
||||||
private CryptParameters cryptParameters;
|
private CryptParameters cryptParameters;
|
||||||
private DerivationParameters derivationParameters;
|
private SlotCollection slots;
|
||||||
|
|
||||||
public DatabaseFile(Context ctx) throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException {
|
public DatabaseFile() {
|
||||||
context = ctx;
|
try {
|
||||||
bHeader = "AEGIS".getBytes("US_ASCII");
|
bHeader = "AEGIS".getBytes("US_ASCII");
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new UndeclaredThrowableException(e);
|
||||||
|
}
|
||||||
|
slots = new SlotCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] serialize() throws IOException {
|
public byte[] serialize() throws IOException {
|
||||||
CryptParameters cryptParams = getCryptParameters();
|
CryptParameters cryptParams = getCryptParameters();
|
||||||
DerivationParameters derParams = getDerivationParameters();
|
|
||||||
byte[] content = getContent();
|
byte[] content = getContent();
|
||||||
byte level = getLevel();
|
|
||||||
|
|
||||||
// this is dumb, java doesn't provide an endianness-aware data stream
|
// this is dumb, java doesn't provide an endianness-aware data stream
|
||||||
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
|
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
|
||||||
DataOutputStream stream = new DataOutputStream(byteStream);
|
DataOutputStream stream = new DataOutputStream(byteStream);
|
||||||
stream.write(bHeader);
|
stream.write(bHeader);
|
||||||
stream.write(bVersion);
|
stream.write(bVersion);
|
||||||
stream.write(level);
|
|
||||||
|
|
||||||
// we assume that all of the needed params for the security level are set
|
if (cryptParams != null) {
|
||||||
// if that's not the case, a NullPointerException will be thrown.
|
LittleByteBuffer paramBuffer = LittleByteBuffer.allocate(CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE);
|
||||||
switch (level) {
|
paramBuffer.put(cryptParams.Nonce);
|
||||||
case SEC_LEVEL_DERIVED:
|
paramBuffer.put(cryptParams.Tag);
|
||||||
ByteBuffer paramBuffer = newBuffer(/* iterations */ 8 + CryptoUtils.CRYPTO_SALT_SIZE);
|
writeSection(stream, bSectionEncryptionParameters, paramBuffer.array());
|
||||||
paramBuffer.putLong(derParams.IterationCount);
|
}
|
||||||
paramBuffer.put(derParams.Salt);
|
|
||||||
writeSection(stream, bSectionDerivationParameters, paramBuffer.array());
|
if (slots != null) {
|
||||||
// intentional fallthrough
|
byte[] bytes = SlotCollection.serialize(slots);
|
||||||
case SEC_LEVEL_KEYSTORE:
|
writeSection(stream, bSectionSlots, bytes);
|
||||||
paramBuffer = newBuffer(CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE);
|
|
||||||
paramBuffer.put(cryptParams.Nonce);
|
|
||||||
paramBuffer.put(cryptParams.Tag);
|
|
||||||
writeSection(stream, bSectionEncryptionParameters, paramBuffer.array());
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
writeSection(stream, bSectionEnd, null);
|
writeSection(stream, bSectionEnd, null);
|
||||||
|
@ -77,7 +65,7 @@ public class DatabaseFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deserialize(byte[] data) throws Exception {
|
public void deserialize(byte[] data) throws Exception {
|
||||||
ByteBuffer buffer = newBuffer(data);
|
LittleByteBuffer buffer = LittleByteBuffer.wrap(data);
|
||||||
|
|
||||||
byte[] header = new byte[bHeader.length];
|
byte[] header = new byte[bHeader.length];
|
||||||
buffer.get(header);
|
buffer.get(header);
|
||||||
|
@ -91,17 +79,11 @@ public class DatabaseFile {
|
||||||
throw new Exception("Unsupported version");
|
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;
|
CryptParameters cryptParams = null;
|
||||||
DerivationParameters derParams = null;
|
SlotCollection slots = null;
|
||||||
|
|
||||||
for (section s = readSection(buffer); s.ID != bSectionEnd; s = readSection(buffer)) {
|
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) {
|
switch (s.ID) {
|
||||||
case bSectionEncryptionParameters:
|
case bSectionEncryptionParameters:
|
||||||
assertLength(s.Data, CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE);
|
assertLength(s.Data, CryptoUtils.CRYPTO_NONCE_SIZE + CryptoUtils.CRYPTO_TAG_SIZE);
|
||||||
|
@ -116,35 +98,25 @@ public class DatabaseFile {
|
||||||
Tag = tag;
|
Tag = tag;
|
||||||
}};
|
}};
|
||||||
break;
|
break;
|
||||||
case bSectionDerivationParameters:
|
case bSectionSlots:
|
||||||
assertLength(s.Data, /* iterations */ 8 + CryptoUtils.CRYPTO_SALT_SIZE);
|
slots = SlotCollection.deserialize(s.Data);
|
||||||
|
|
||||||
long iterations = sBuff.getLong();
|
|
||||||
byte[] salt = new byte[CryptoUtils.CRYPTO_SALT_SIZE];
|
|
||||||
sBuff.get(salt);
|
|
||||||
|
|
||||||
derParams = new DerivationParameters() {{
|
|
||||||
IterationCount = iterations;
|
|
||||||
Salt = salt;
|
|
||||||
}};
|
|
||||||
break;
|
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);
|
setCryptParameters(cryptParams);
|
||||||
setDerivationParameters(derParams);
|
setSlots(slots);
|
||||||
|
|
||||||
byte[] content = new byte[buffer.remaining()];
|
byte[] content = new byte[buffer.remaining()];
|
||||||
buffer.get(content);
|
buffer.get(content);
|
||||||
setContent(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();
|
byte[] data = serialize();
|
||||||
|
|
||||||
FileOutputStream file = context.openFileOutput(dbFilename, Context.MODE_PRIVATE);
|
FileOutputStream file = context.openFileOutput(dbFilename, Context.MODE_PRIVATE);
|
||||||
|
@ -158,7 +130,7 @@ public class DatabaseFile {
|
||||||
file.read(data);
|
file.read(data);
|
||||||
file.close();
|
file.close();
|
||||||
|
|
||||||
DatabaseFile db = new DatabaseFile(context);
|
DatabaseFile db = new DatabaseFile();
|
||||||
db.deserialize(data);
|
db.deserialize(data);
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
@ -166,7 +138,7 @@ public class DatabaseFile {
|
||||||
private static void writeSection(DataOutputStream stream, byte id, byte[] data) throws IOException {
|
private static void writeSection(DataOutputStream stream, byte id, byte[] data) throws IOException {
|
||||||
stream.write(id);
|
stream.write(id);
|
||||||
|
|
||||||
ByteBuffer buffer = newBuffer(/* sizeof uint32_t */ 4);
|
LittleByteBuffer buffer = LittleByteBuffer.allocate(/* sizeof uint32_t */ 4);
|
||||||
if (data == null) {
|
if (data == null) {
|
||||||
buffer.putInt(0);
|
buffer.putInt(0);
|
||||||
} else {
|
} else {
|
||||||
|
@ -179,7 +151,7 @@ public class DatabaseFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static section readSection(ByteBuffer buffer) {
|
private static section readSection(LittleByteBuffer buffer) {
|
||||||
section s = new section();
|
section s = new section();
|
||||||
s.ID = buffer.get();
|
s.ID = buffer.get();
|
||||||
|
|
||||||
|
@ -190,18 +162,6 @@ public class DatabaseFile {
|
||||||
return s;
|
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 {
|
private static void assertLength(byte[] bytes, int length) throws Exception {
|
||||||
if (bytes.length != length) {
|
if (bytes.length != length) {
|
||||||
throw new Exception("Bad length");
|
throw new Exception("Bad length");
|
||||||
|
@ -224,20 +184,12 @@ public class DatabaseFile {
|
||||||
this.cryptParameters = parameters;
|
this.cryptParameters = parameters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DerivationParameters getDerivationParameters() {
|
public SlotCollection getSlots() {
|
||||||
return derivationParameters;
|
return slots;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDerivationParameters(DerivationParameters derivationParameters) {
|
public void setSlots(SlotCollection slots) {
|
||||||
this.derivationParameters = derivationParameters;
|
this.slots = slots;
|
||||||
}
|
|
||||||
|
|
||||||
public byte getLevel() {
|
|
||||||
return level;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLevel(byte level) {
|
|
||||||
this.level = level;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class section {
|
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
|
# Database
|
||||||
|
|
||||||
The database is encoded in a simple binary format with JSON content at its
|
The database is encoded in a simple binary format with JSON content at its core.
|
||||||
core.
|
|
||||||
|
|
||||||
## Encryption
|
## Encryption
|
||||||
|
|
||||||
The content of the database can be encrypted with AES in GCM mode. The nonce
|
The content of the database can be encrypted with AES in GCM mode. The nonce and
|
||||||
and authentication tag are stored in the plain section of this file. The
|
authentication tag are stored in the plain section of this file.
|
||||||
storage place for the key depends on the security level that is used. This will
|
|
||||||
be discussed later.
|
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
|
## Format
|
||||||
|
|
||||||
The file format starts with a small header that contains some magic, the
|
The file format starts with a small header that contains some magic and a
|
||||||
version number and the level of security. A list of sections follows. These
|
version number. A list of sections follows. These sections contain some
|
||||||
sections contain some information needed to perform decryption of the database.
|
information needed to perform decryption of the database. The (encrypted)
|
||||||
The (encrypted) content of the database starts after the end marker section.
|
content of the database starts after the end marker section.
|
||||||
|
|
||||||
All integers are encoded in Little Endian.
|
All integers are encoded in Little Endian.
|
||||||
|
|
||||||
|
@ -25,43 +26,9 @@ All integers are encoded in Little Endian.
|
||||||
|:-------|:-------------------------|
|
|:-------|:-------------------------|
|
||||||
| `5` | "AEGIS" encoded in ASCII |
|
| `5` | "AEGIS" encoded in ASCII |
|
||||||
| `1` | `uint8_t` Version |
|
| `1` | `uint8_t` Version |
|
||||||
| `1` | `uint8_t` Level |
|
|
||||||
| `?` | List of sections |
|
| `?` | List of sections |
|
||||||
| `?` | Content |
|
| `?` | 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
|
### Sections
|
||||||
|
|
||||||
| Length | Contents |
|
| Length | Contents |
|
||||||
|
@ -75,7 +42,7 @@ ID can be one of:
|
||||||
| Value | Name |
|
| Value | Name |
|
||||||
|:-------|:---------------------|
|
|:-------|:---------------------|
|
||||||
| `0x00` | EncryptionParameters |
|
| `0x00` | EncryptionParameters |
|
||||||
| `0x01` | DerivationParameters |
|
| `0x01` | Slots |
|
||||||
| `0xFF` | End marker |
|
| `0xFF` | End marker |
|
||||||
|
|
||||||
#### EncryptionParameters
|
#### EncryptionParameters
|
||||||
|
@ -85,13 +52,48 @@ ID can be one of:
|
||||||
| `12` | Nonce |
|
| `12` | Nonce |
|
||||||
| `16` | Tag |
|
| `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 |
|
| Length | Contents |
|
||||||
|:-------|:--------------------------------|
|
|:-------|:--------------------------------|
|
||||||
| `8` | `uint64_t` Number of iterations |
|
| `8` | `uint64_t` Number of iterations |
|
||||||
| `32` | Salt |
|
| `32` | Salt |
|
||||||
|
|
||||||
|
##### Fingerprint
|
||||||
|
|
||||||
|
A fingerprint slot is exactly the same as a Raw slot.
|
||||||
|
|
||||||
#### End marker
|
#### End marker
|
||||||
|
|
||||||
This section indicates the end of the list of sections. This section doesn't
|
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
|
### Content
|
||||||
|
|
||||||
The content of the database is a JSON file encoded in UTF-8. As mentioned
|
The content of the database is a JSON file encoded in UTF-8.
|
||||||
above, it's encrypted.
|
|
||||||
|
|
||||||
``` json
|
```json
|
||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"entries":
|
"entries":
|
||||||
|
|
Loading…
Add table
Reference in a new issue