Files
GajuMobile-Android/app/src/main/java/swiss/qpq/gajumobile/security/AccountAirlock.java
T

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