183 lines
5.9 KiB
Java
183 lines
5.9 KiB
Java
/*
|
|
* Copyright (c) 2026 QPQ AG <info@qpq.swiss>. All rights reserved.
|
|
* Project: Gajumaru Mobile Wallet
|
|
*/
|
|
|
|
package swiss.qpq.gajumobile.security;
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.security.SecureRandom;
|
|
import java.util.UUID;
|
|
import javax.crypto.SecretKey;
|
|
import javax.crypto.spec.SecretKeySpec;
|
|
import swiss.qpq.gajumaru.core.crypto.Ed25519;
|
|
import swiss.qpq.gajumaru.core.crypto.Vault;
|
|
import swiss.qpq.gajumaru.core.encoding.Mnemonic;
|
|
import swiss.qpq.gajumaru.core.tools.CryptoUtils;
|
|
import swiss.qpq.gajumobile.data.models.Account;
|
|
|
|
/**
|
|
* AccountAirlock provides a secure Java-based bridge for identity operations.
|
|
* It ensures that sensitive material only exists in local stack-allocated
|
|
* buffers and is wiped immediately after use.
|
|
*/
|
|
public final class AccountAirlock {
|
|
|
|
private AccountAirlock() {}
|
|
|
|
/**
|
|
* Returns the 4096-word Gajumaru dictionary.
|
|
*/
|
|
public static String[] getWordList() {
|
|
return Mnemonic.WORDS;
|
|
}
|
|
|
|
/**
|
|
* Generates a new random mnemonic phrase.
|
|
*
|
|
* @return The 23-word mnemonic phrase as a byte array of UTF-8 words.
|
|
*/
|
|
public static byte[][] generateMnemonic() {
|
|
byte[] entropy = new byte[32];
|
|
new SecureRandom().nextBytes(entropy);
|
|
try {
|
|
return Mnemonic.encode(entropy);
|
|
} finally {
|
|
CryptoUtils.wipe(entropy);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a new Account object from a mnemonic phrase.
|
|
*
|
|
* @param label The user-defined label for the account.
|
|
* @param phrase The mnemonic phrase as a list of UTF-8 word byte arrays.
|
|
* @param masterKey The hardware-backed Master Key handle.
|
|
* @return The new Account object.
|
|
* @throws Exception if creation or encryption fails.
|
|
*/
|
|
public static Account createAccount(
|
|
String label,
|
|
byte[][] phrase,
|
|
SecretKey masterKey
|
|
) throws Exception {
|
|
|
|
byte[] seed = null;
|
|
byte[] dekBytes = null;
|
|
|
|
try {
|
|
// 1. Decode mnemonic to seed
|
|
seed = Mnemonic.decode(phrase);
|
|
|
|
// 2. Generate Public Key
|
|
byte[] publicKey = Ed25519.publicKey(seed);
|
|
|
|
// 3. Generate random Data Encryption Key (DEK)
|
|
SecureRandom random = new SecureRandom();
|
|
dekBytes = new byte[32];
|
|
random.nextBytes(dekBytes);
|
|
SecretKey dek = new SecretKeySpec(dekBytes, "AES");
|
|
|
|
// 4. Encrypt Private Key (seed) with DEK
|
|
// We use the provider-generated IV here for maximum hardware compatibility
|
|
Vault.Ciphertext dataCiphertext = Vault.encrypt(dek, seed);
|
|
|
|
// 5. Encrypt DEK with Master Key
|
|
// Android Keystore REQUIRED to generate the IV when randomizedEncryption is true
|
|
Vault.Ciphertext dekCiphertext = Vault.encrypt(masterKey, dekBytes);
|
|
|
|
EncryptedEnvelope envelope = new EncryptedEnvelope(
|
|
dataCiphertext.getData(),
|
|
dekCiphertext.getData(),
|
|
dataCiphertext.getIv(),
|
|
dekCiphertext.getIv()
|
|
);
|
|
|
|
String id = UUID.randomUUID().toString();
|
|
return new Account(id, label, publicKey, envelope, null);
|
|
|
|
} finally {
|
|
// 6. Memory Hygiene: Wipe sensitive data from the heap
|
|
if (seed != null) {
|
|
CryptoUtils.wipe(seed);
|
|
}
|
|
if (dekBytes != null) {
|
|
CryptoUtils.wipe(dekBytes);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Utility to convert a UI String into the internal airlock byte[][] format.
|
|
*/
|
|
public static byte[][] stringToPhrase(String phrase) {
|
|
String[] words = phrase.trim().split("\\s+");
|
|
byte[][] result = new byte[words.length][];
|
|
for (int i = 0; i < words.length; i++) {
|
|
result[i] = words[i].getBytes(StandardCharsets.UTF_8);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Utility to wipe a phrase returned by the airlock.
|
|
*/
|
|
public static void wipePhrase(byte[][] phrase) {
|
|
if (phrase != null) {
|
|
for (byte[] word : phrase) {
|
|
CryptoUtils.wipe(word);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrypts an account's private key and signs a message.
|
|
*
|
|
* @param envelope The encrypted private key envelope.
|
|
* @param masterKey The hardware-backed Master Key handle from Android Keystore.
|
|
* @param message The message to sign.
|
|
* @return The Ed25519 signature.
|
|
* @throws Exception if decryption or signing fails.
|
|
*/
|
|
public static byte[] sign(
|
|
EncryptedEnvelope envelope,
|
|
SecretKey masterKey,
|
|
byte[] message
|
|
) throws Exception {
|
|
|
|
byte[] plaintextDek = null;
|
|
byte[] plaintextPrivateKey = null;
|
|
|
|
try {
|
|
// 1. Decrypt the Data Encryption Key (DEK) using the Master Key
|
|
plaintextDek = Vault.decrypt(
|
|
masterKey,
|
|
envelope.getDekIv(),
|
|
envelope.getEncryptedDek()
|
|
);
|
|
SecretKey dek = new SecretKeySpec(plaintextDek, "AES");
|
|
|
|
// 2. Decrypt the Private Key using the DEK
|
|
plaintextPrivateKey = Vault.decrypt(
|
|
dek,
|
|
envelope.getDataIv(),
|
|
envelope.getEncryptedData()
|
|
);
|
|
|
|
// 3. Perform Ed25519 signature
|
|
// The Ed25519 implementation in gm-java uses internal scratch pads
|
|
// that are also wiped upon completion.
|
|
return Ed25519.sign(plaintextPrivateKey, message);
|
|
|
|
} finally {
|
|
// 4. Memory Hygiene: Wipe sensitive data from the heap
|
|
if (plaintextDek != null) {
|
|
CryptoUtils.wipe(plaintextDek);
|
|
}
|
|
if (plaintextPrivateKey != null) {
|
|
CryptoUtils.wipe(plaintextPrivateKey);
|
|
}
|
|
}
|
|
}
|
|
}
|