/* * Copyright (c) 2026 QPQ AG . 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); } } } }