Java API Reference

Complete API documentation for MetaMUI Crypto Primitives in Java.

Installation

Maven

<dependency>
    <groupId>com.metamui</groupId>
    <artifactId>metamui-crypto</artifactId>
    <version>3.0.0</version>
</dependency>

Gradle

dependencies {
    implementation 'com.metamui:metamui-crypto:3.0.0'
}

Gradle (Kotlin DSL)

dependencies {
    implementation("com.metamui:metamui-crypto:3.0.0")
}

Quick Start

import com.metamui.crypto.signature.Ed25519;
import com.metamui.crypto.signature.Ed25519Keypair;
import com.metamui.crypto.encryption.ChaCha20Poly1305;

public class QuickStart {
    public static void main(String[] args) throws Exception {
        // Generate Ed25519 keypair
        Ed25519Keypair keypair = Ed25519.generateKeypair();
        
        // Sign a message
        byte[] message = "Hello, World!".getBytes();
        byte[] signature = Ed25519.sign(message, keypair.getPrivateKey());
        
        // Verify signature
        boolean isValid = Ed25519.verify(signature, message, 
                                        keypair.getPublicKey());
        
        System.out.println("Signature valid: " + isValid);
    }
}

API Documentation

For detailed API documentation, see:

Package Structure

// Core packages
import com.metamui.crypto.*;                // Core classes and interfaces
import com.metamui.crypto.hash.*;          // Hash functions
import com.metamui.crypto.signature.*;     // Digital signatures
import com.metamui.crypto.encryption.*;    // Symmetric encryption
import com.metamui.crypto.kex.*;          // Key exchange
import com.metamui.crypto.kdf.*;          // Key derivation
import com.metamui.crypto.pqc.*;          // Post-quantum algorithms
import com.metamui.crypto.util.*;         // Utility classes
import com.metamui.crypto.random.*;       // Secure random

JNI Integration

The library uses JNI for performance-critical operations while maintaining pure Java fallbacks.

public class CryptoProvider {
    static {
        try {
            // Load native library for performance
            System.loadLibrary("metamui_crypto_jni");
            useNative = true;
        } catch (UnsatisfiedLinkError e) {
            // Fall back to pure Java implementation
            useNative = false;
        }
    }
    
    public static boolean isNativeAvailable() {
        return useNative;
    }
}

Exception Handling

Checked Exceptions

import com.metamui.crypto.exception.*;

try {
    byte[] encrypted = cipher.encrypt(plaintext, key);
} catch (InvalidKeyException e) {
    System.err.println("Invalid key: " + e.getMessage());
} catch (EncryptionException e) {
    System.err.println("Encryption failed: " + e.getMessage());
} catch (CryptoException e) {
    System.err.println("Crypto error: " + e.getMessage());
}

Runtime Exceptions

// Runtime exceptions for programming errors
try {
    Ed25519.sign(null, privateKey); // Throws NullPointerException
} catch (NullPointerException e) {
    System.err.println("Null input provided");
} catch (IllegalArgumentException e) {
    System.err.println("Invalid argument: " + e.getMessage());
}

Memory Management

Try-With-Resources

// Automatic resource management
try (SecureBytes key = new SecureBytes(32)) {
    // Generate random key
    SecureRandom.fill(key.getBytes());
    
    // Use key for encryption
    ChaCha20Poly1305 cipher = new ChaCha20Poly1305(key.getBytes());
    byte[] encrypted = cipher.encrypt(data, nonce);
} // Key automatically cleared from memory

// Multiple resources
try (SecureBytes key = new SecureBytes(32);
     SecureBytes nonce = new SecureBytes(12)) {
    // Use resources
} // All resources cleaned up

Direct ByteBuffers

import java.nio.ByteBuffer;

// Use direct buffers for better performance
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
directBuffer.put(data);
directBuffer.flip();

// Process with zero-copy
Blake3.hash(directBuffer);

// Clean up direct buffer
if (directBuffer instanceof DirectBuffer) {
    ((DirectBuffer) directBuffer).cleaner().clean();
}

Hash Functions

SHA-256/512

import com.metamui.crypto.hash.Sha256;
import com.metamui.crypto.hash.Sha512;

// Simple hashing
byte[] hash256 = Sha256.hash(data);
byte[] hash512 = Sha512.hash(data);

// Streaming hash
Sha256 hasher = new Sha256();
hasher.update(chunk1);
hasher.update(chunk2);
byte[] hash = hasher.digest();

// Hash string directly
String hashHex = Sha256.hashToHex("Hello, World!");

// HMAC
byte[] hmac = Sha256.hmac(key, message);
boolean valid = Sha256.verifyHmac(key, message, hmac);

BLAKE2b/2s

import com.metamui.crypto.hash.Blake2b;
import com.metamui.crypto.hash.Blake2s;

// Standard BLAKE2b (64 bytes)
byte[] hash = Blake2b.hash(data);

// Custom output size
byte[] hash32 = Blake2b.hash(data, 32);

// Keyed hashing (MAC)
byte[] mac = Blake2b.keyedHash(data, key);

// With parameters
Blake2b.Parameters params = new Blake2b.Parameters()
    .setDigestLength(32)
    .setKey(key)
    .setSalt(salt)
    .setPersonal("MyApp".getBytes());

Blake2b hasher = Blake2b.withParameters(params);
hasher.update(data);
byte[] hash = hasher.digest();

BLAKE3

import com.metamui.crypto.hash.Blake3;

// Standard hash
byte[] hash = Blake3.hash(data);

// Keyed hash
byte[] keyedHash = Blake3.keyedHash(data, key);

// Derive key
byte[] derivedKey = Blake3.deriveKey(
    "my-context",
    inputKeyMaterial,
    32  // output length
);

// Streaming
Blake3 hasher = new Blake3();
for (byte[] chunk : chunks) {
    hasher.update(chunk);
}
byte[] hash = hasher.digest();

// Extended output (XOF)
Blake3.XOF xof = Blake3.newXOF();
xof.update(data);
byte[] output = new byte[100]; // Any size
xof.squeeze(output);

Digital Signatures

Ed25519

import com.metamui.crypto.signature.Ed25519;
import com.metamui.crypto.signature.Ed25519Keypair;
import com.metamui.crypto.signature.Ed25519PublicKey;
import com.metamui.crypto.signature.Ed25519PrivateKey;

// Generate keypair
Ed25519Keypair keypair = Ed25519.generateKeypair();

// Generate from seed
byte[] seed = new byte[32];
SecureRandom.fill(seed);
Ed25519Keypair seededKeypair = Ed25519.generateKeypair(seed);

// Sign
byte[] signature = Ed25519.sign(message, keypair.getPrivateKey());

// Verify
boolean valid = Ed25519.verify(signature, message, keypair.getPublicKey());

// Export/Import keys
byte[] publicKeyBytes = keypair.getPublicKey().toBytes();
byte[] privateKeyBytes = keypair.getPrivateKey().toBytes();

Ed25519PublicKey publicKey = Ed25519PublicKey.fromBytes(publicKeyBytes);
Ed25519PrivateKey privateKey = Ed25519PrivateKey.fromBytes(privateKeyBytes);

// Batch verification
List<byte[]> signatures = Arrays.asList(sig1, sig2, sig3);
List<byte[]> messages = Arrays.asList(msg1, msg2, msg3);
List<Ed25519PublicKey> publicKeys = Arrays.asList(pk1, pk2, pk3);

boolean[] results = Ed25519.batchVerify(signatures, messages, publicKeys);

Sr25519

import com.metamui.crypto.signature.Sr25519;
import com.metamui.crypto.signature.Sr25519Keypair;

// Generate keypair
Sr25519Keypair keypair = Sr25519.generateKeypair();

// Mini secret key from seed
Sr25519.MiniSecretKey miniSecret = Sr25519.miniSecretFromSeed(seed);
Sr25519Keypair keypair = miniSecret.expandToKeypair();

// Sign with context
byte[] signature = Sr25519.signWithContext(
    message, 
    keypair.getPrivateKey(),
    "substrate".getBytes()
);

// Verify with context
boolean valid = Sr25519.verifyWithContext(
    signature,
    message,
    keypair.getPublicKey(),
    "substrate".getBytes()
);

// VRF (Verifiable Random Function)
Sr25519.VRFResult vrfResult = Sr25519.vrfSign(message, keypair.getPrivateKey());
byte[] output = vrfResult.getOutput();
byte[] proof = vrfResult.getProof();

Sr25519.VRFVerifyResult verifyResult = Sr25519.vrfVerify(
    message, keypair.getPublicKey(), proof
);

Encryption

ChaCha20-Poly1305

import com.metamui.crypto.encryption.ChaCha20Poly1305;

// Create cipher
ChaCha20Poly1305 cipher = new ChaCha20Poly1305(key);

// Generate nonce
byte[] nonce = new byte[ChaCha20Poly1305.NONCE_SIZE];
SecureRandom.fill(nonce);

// Encrypt with associated data
byte[] associatedData = "metadata".getBytes();
ChaCha20Poly1305.EncryptResult result = cipher.encrypt(
    plaintext, nonce, associatedData
);
byte[] ciphertext = result.getCiphertext();
byte[] tag = result.getTag();

// Decrypt and verify
byte[] decrypted = cipher.decrypt(ciphertext, nonce, tag, associatedData);

// Using XChaCha20-Poly1305 (24-byte nonce)
ChaCha20Poly1305 xcipher = ChaCha20Poly1305.xchacha20poly1305(key);
byte[] xnonce = new byte[ChaCha20Poly1305.XNONCE_SIZE];

AES-256-GCM

import com.metamui.crypto.encryption.Aes256Gcm;
import javax.crypto.spec.GCMParameterSpec;

// Create cipher
Aes256Gcm aes = new Aes256Gcm(key);

// Generate IV
byte[] iv = new byte[Aes256Gcm.IV_SIZE];
SecureRandom.fill(iv);

// Encrypt
Aes256Gcm.EncryptResult result = aes.encrypt(
    plaintext, iv, associatedData
);
byte[] ciphertext = result.getCiphertext();
byte[] tag = result.getTag();

// Decrypt
byte[] plaintext = aes.decrypt(ciphertext, iv, tag, associatedData);

// Check hardware support
if (Aes256Gcm.isHardwareAccelerated()) {
    System.out.println("Using AES-NI hardware acceleration");
}

Key Exchange

X25519

import com.metamui.crypto.kex.X25519;
import com.metamui.crypto.kex.X25519Keypair;

// Generate keypairs
X25519Keypair alice = X25519.generateKeypair();
X25519Keypair bob = X25519.generateKeypair();

// Compute shared secrets
byte[] aliceShared = X25519.computeSharedSecret(
    alice.getPrivateKey(), bob.getPublicKey()
);

byte[] bobShared = X25519.computeSharedSecret(
    bob.getPrivateKey(), alice.getPublicKey()
);

// aliceShared.equals(bobShared)
assert Arrays.equals(aliceShared, bobShared);

// From seed
byte[] privateKey = X25519.generatePrivateKey(seed);
byte[] publicKey = X25519.derivePublicKey(privateKey);

Post-Quantum Algorithms

ML-KEM-768

import com.metamui.crypto.pqc.MlKem768;
import com.metamui.crypto.pqc.MlKem768Keypair;

// Generate keypair
MlKem768Keypair keypair = MlKem768.generateKeypair();

// Encapsulation (sender)
MlKem768.EncapsulateResult encapResult = MlKem768.encapsulate(
    keypair.getPublicKey()
);
byte[] ciphertext = encapResult.getCiphertext();
byte[] sharedSecret = encapResult.getSharedSecret();

// Decapsulation (receiver)
byte[] sharedSecret2 = MlKem768.decapsulate(
    ciphertext, keypair.getPrivateKey()
);

// Both parties have same shared secret
assert Arrays.equals(sharedSecret, sharedSecret2);

// Serialize keys
byte[] publicKeyBytes = keypair.getPublicKey().serialize();
byte[] privateKeyBytes = keypair.getPrivateKey().serialize();

// Deserialize
MlKem768PublicKey publicKey = MlKem768PublicKey.deserialize(publicKeyBytes);
MlKem768PrivateKey privateKey = MlKem768PrivateKey.deserialize(privateKeyBytes);

Dilithium

import com.metamui.crypto.pqc.Dilithium3;
import com.metamui.crypto.pqc.Dilithium3Keypair;

// Generate keypair
Dilithium3Keypair keypair = Dilithium3.generateKeypair();

// Sign message
byte[] signature = Dilithium3.sign(message, keypair.getPrivateKey());

// Verify signature
boolean valid = Dilithium3.verify(
    signature, message, keypair.getPublicKey()
);

// Deterministic signatures
byte[] detSignature = Dilithium3.signDeterministic(
    message, keypair.getPrivateKey()
);

// Serialize/Deserialize
byte[] publicKeyBytes = keypair.getPublicKey().toBytes();
byte[] privateKeyBytes = keypair.getPrivateKey().toBytes();

Dilithium3PublicKey publicKey = Dilithium3PublicKey.fromBytes(publicKeyBytes);
Dilithium3PrivateKey privateKey = Dilithium3PrivateKey.fromBytes(privateKeyBytes);

Key Derivation

Argon2

import com.metamui.crypto.kdf.Argon2;
import com.metamui.crypto.kdf.Argon2Config;
import com.metamui.crypto.kdf.Argon2Type;

// Generate salt
byte[] salt = new byte[16];
SecureRandom.fill(salt);

// Hash password with default parameters
byte[] hash = Argon2.hashPassword(
    "user_password".toCharArray(),
    salt
);

// Hash with custom parameters
Argon2Config config = new Argon2Config.Builder()
    .type(Argon2Type.ARGON2ID)
    .timeCost(3)        // iterations
    .memoryCost(65536)  // 64 MB
    .parallelism(4)     // threads
    .hashLength(32)     // output length
    .build();

byte[] customHash = Argon2.hashPassword(
    "password".toCharArray(),
    salt,
    config
);

// Verify password
boolean valid = Argon2.verifyPassword(
    "user_password".toCharArray(),
    storedHash
);

// Derive encryption key
byte[] key = Argon2.deriveKey(
    password,
    salt,
    32,  // key length
    config
);

PBKDF2

import com.metamui.crypto.kdf.Pbkdf2;
import com.metamui.crypto.kdf.Pbkdf2.HashAlgorithm;

// Derive key from password
byte[] salt = new byte[32];
SecureRandom.fill(salt);

byte[] key = Pbkdf2.deriveKey(
    "password".toCharArray(),
    salt,
    100000,                    // iterations
    32,                        // key length
    HashAlgorithm.SHA256       // PRF
);

// With progress callback
Pbkdf2.ProgressListener listener = (current, total) -> {
    System.out.printf("Progress: %d/%d%n", current, total);
};

byte[] keyWithProgress = Pbkdf2.deriveKey(
    password, salt, 100000, 32,
    HashAlgorithm.SHA256, listener
);

HKDF

import com.metamui.crypto.kdf.Hkdf;
import com.metamui.crypto.kdf.Hkdf.HashAlgorithm;

// Extract
byte[] prk = Hkdf.extract(
    HashAlgorithm.SHA256,
    inputKeyMaterial,
    salt
);

// Expand
byte[] okm = Hkdf.expand(
    HashAlgorithm.SHA256,
    prk,
    info,
    42  // output length
);

// One-shot operation
byte[] derivedKey = Hkdf.deriveKey(
    HashAlgorithm.SHA256,
    inputKeyMaterial,
    32,    // output length
    salt,
    info
);

Android Compatibility

Android-Specific Setup

public class CryptoApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        
        // Initialize MetaMUI Crypto for Android
        MetaMUICrypto.init(this);
        
        // Enable Android Keystore integration
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            MetaMUICrypto.enableAndroidKeystore();
        }
    }
}

Android Keystore Integration

import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;

// Store key in Android Keystore
public class AndroidKeyManager {
    private static final String KEYSTORE_ALIAS = "MetaMUIKey";
    
    public void generateAndStoreKey() throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"
        );
        
        KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(
            KEYSTORE_ALIAS,
            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setUserAuthenticationRequired(true)
            .build();
            
        keyGen.init(spec);
        keyGen.generateKey();
    }
    
    public SecretKey getKey() throws Exception {
        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);
        return (SecretKey) keyStore.getKey(KEYSTORE_ALIAS, null);
    }
}

Spring Boot Integration

Configuration

@Configuration
@EnableConfigurationProperties(CryptoProperties.class)
public class CryptoConfig {
    
    @Bean
    public Ed25519Keypair signingKeypair(CryptoProperties props) 
            throws Exception {
        if (props.getSigningKey() != null) {
            byte[] seed = Base64.getDecoder().decode(props.getSigningKey());
            return Ed25519.generateKeypair(seed);
        }
        return Ed25519.generateKeypair();
    }
    
    @Bean
    public ChaCha20Poly1305 cipher(CryptoProperties props) 
            throws Exception {
        byte[] key = Base64.getDecoder().decode(props.getEncryptionKey());
        return new ChaCha20Poly1305(key);
    }
}

@ConfigurationProperties(prefix = "crypto")
@Data
public class CryptoProperties {
    private String signingKey;
    private String encryptionKey;
}

Service Layer

@Service
public class CryptoService {
    private final Ed25519Keypair signingKeypair;
    private final ChaCha20Poly1305 cipher;
    
    public CryptoService(Ed25519Keypair signingKeypair, 
                        ChaCha20Poly1305 cipher) {
        this.signingKeypair = signingKeypair;
        this.cipher = cipher;
    }
    
    public String signAndEncode(String data) throws Exception {
        byte[] signature = Ed25519.sign(
            data.getBytes(), 
            signingKeypair.getPrivateKey()
        );
        return Base64.getEncoder().encodeToString(signature);
    }
    
    @Cacheable("encrypted-data")
    public byte[] encrypt(byte[] data) throws Exception {
        byte[] nonce = new byte[12];
        SecureRandom.fill(nonce);
        
        var result = cipher.encrypt(data, nonce, null);
        
        // Combine nonce + ciphertext + tag
        return ByteBuffer.allocate(nonce.length + 
                                  result.getCiphertext().length + 
                                  result.getTag().length)
            .put(nonce)
            .put(result.getCiphertext())
            .put(result.getTag())
            .array();
    }
}

Security Provider Registration

import java.security.Security;
import com.metamui.crypto.provider.MetaMUIProvider;

public class SecurityConfig {
    static {
        // Register MetaMUI as security provider
        Security.addProvider(new MetaMUIProvider());
        
        // Set as preferred provider for specific algorithms
        Security.setProperty("crypto.policy", "unlimited");
        Security.insertProviderAt(new MetaMUIProvider(), 1);
    }
    
    public static void listProviders() {
        for (Provider provider : Security.getProviders()) {
            System.out.println(provider.getName() + " v" + 
                             provider.getVersion());
            
            provider.getServices().stream()
                .filter(s -> s.getType().equals("Signature"))
                .forEach(s -> System.out.println("  " + s.getAlgorithm()));
        }
    }
}

Performance Tuning

Buffer Pools

import java.nio.ByteBuffer;
import java.util.concurrent.ArrayBlockingQueue;

public class BufferPool {
    private final ArrayBlockingQueue<ByteBuffer> pool;
    private final int bufferSize;
    
    public BufferPool(int poolSize, int bufferSize) {
        this.bufferSize = bufferSize;
        this.pool = new ArrayBlockingQueue<>(poolSize);
        
        // Pre-allocate buffers
        for (int i = 0; i < poolSize; i++) {
            pool.offer(ByteBuffer.allocateDirect(bufferSize));
        }
    }
    
    public ByteBuffer acquire() throws InterruptedException {
        ByteBuffer buffer = pool.take();
        buffer.clear();
        return buffer;
    }
    
    public void release(ByteBuffer buffer) {
        if (buffer.capacity() == bufferSize) {
            pool.offer(buffer);
        }
    }
}

Parallel Processing

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ParallelCrypto {
    private final ExecutorService executor = 
        Executors.newWorkStealingPool();
    
    public CompletableFuture<byte[][]> hashFilesAsync(List<File> files) {
        List<CompletableFuture<byte[]>> futures = files.stream()
            .map(file -> CompletableFuture.supplyAsync(() -> {
                try {
                    byte[] data = Files.readAllBytes(file.toPath());
                    return Blake3.hash(data);
                } catch (IOException e) {
                    throw new CompletionException(e);
                }
            }, executor))
            .collect(Collectors.toList());
        
        return CompletableFuture.allOf(
            futures.toArray(new CompletableFuture[0]))
            .thenApply(v -> futures.stream()
                .map(CompletableFuture::join)
                .toArray(byte[][]::new));
    }
    
    public void shutdown() {
        executor.shutdown();
    }
}

Native Optimization

public class NativeOptimization {
    static {
        // Load platform-specific native library
        String os = System.getProperty("os.name").toLowerCase();
        String arch = System.getProperty("os.arch");
        
        if (os.contains("win")) {
            System.loadLibrary("metamui_crypto_win_" + arch);
        } else if (os.contains("mac")) {
            System.loadLibrary("metamui_crypto_mac_" + arch);
        } else if (os.contains("linux")) {
            System.loadLibrary("metamui_crypto_linux_" + arch);
        }
    }
    
    // Native method declarations
    private static native byte[] blake3HashNative(byte[] data);
    private static native byte[] chacha20Poly1305EncryptNative(
        byte[] plaintext, byte[] key, byte[] nonce);
    
    // Java fallback implementations
    public static byte[] blake3Hash(byte[] data) {
        if (isNativeAvailable()) {
            return blake3HashNative(data);
        }
        return Blake3.hash(data);
    }
}

Examples

JWT Token Generation

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;

public class JwtHelper {
    private final Ed25519Keypair keypair;
    
    public JwtHelper(Ed25519Keypair keypair) {
        this.keypair = keypair;
    }
    
    public String createToken(Map<String, Object> claims) {
        // Create custom EdDSA algorithm
        Algorithm algorithm = new EdDSAAlgorithm(keypair);
        
        return JWT.create()
            .withIssuer("metamui")
            .withIssuedAt(new Date())
            .withExpiresAt(new Date(System.currentTimeMillis() + 3600000))
            .withClaim("user_id", claims.get("user_id"))
            .withClaim("roles", (List<String>) claims.get("roles"))
            .sign(algorithm);
    }
    
    public DecodedJWT verifyToken(String token) {
        Algorithm algorithm = new EdDSAAlgorithm(keypair);
        JWTVerifier verifier = JWT.require(algorithm)
            .withIssuer("metamui")
            .build();
            
        return verifier.verify(token);
    }
    
    // Custom EdDSA algorithm implementation
    private static class EdDSAAlgorithm extends Algorithm {
        private final Ed25519Keypair keypair;
        
        public EdDSAAlgorithm(Ed25519Keypair keypair) {
            super("EdDSA", "EdDSA");
            this.keypair = keypair;
        }
        
        @Override
        public byte[] sign(byte[] contentBytes) throws SignatureException {
            try {
                return Ed25519.sign(contentBytes, keypair.getPrivateKey());
            } catch (Exception e) {
                throw new SignatureException("Failed to sign", e);
            }
        }
        
        @Override
        public void verify(DecodedJWT jwt) throws SignatureException {
            byte[] signature = Base64.getUrlDecoder().decode(
                jwt.getSignature());
            byte[] content = (jwt.getHeader() + "." + 
                            jwt.getPayload()).getBytes();
            
            if (!Ed25519.verify(signature, content, keypair.getPublicKey())) {
                throw new SignatureException("Invalid signature");
            }
        }
    }
}

Encrypted Database Fields

import javax.persistence.*;
import com.metamui.crypto.encryption.ChaCha20Poly1305;

@Entity
public class SecureUser {
    @Id
    @GeneratedValue
    private Long id;
    
    private String username;
    
    @Lob
    @Column(name = "encrypted_ssn")
    private byte[] encryptedSsn;
    
    @Column(name = "ssn_nonce")
    private byte[] ssnNonce;
    
    @Transient
    private static final ChaCha20Poly1305 cipher;
    
    static {
        // Load encryption key from secure configuration
        byte[] key = loadEncryptionKey();
        cipher = new ChaCha20Poly1305(key);
    }
    
    public void setSsn(String ssn) throws Exception {
        this.ssnNonce = new byte[12];
        SecureRandom.fill(this.ssnNonce);
        
        var result = cipher.encrypt(
            ssn.getBytes(), 
            this.ssnNonce, 
            this.username.getBytes() // Use username as AAD
        );
        
        // Store ciphertext + tag
        this.encryptedSsn = ByteBuffer.allocate(
            result.getCiphertext().length + result.getTag().length)
            .put(result.getCiphertext())
            .put(result.getTag())
            .array();
    }
    
    public String getSsn() throws Exception {
        if (encryptedSsn == null) return null;
        
        // Split ciphertext and tag
        byte[] ciphertext = Arrays.copyOfRange(
            encryptedSsn, 0, encryptedSsn.length - 16);
        byte[] tag = Arrays.copyOfRange(
            encryptedSsn, encryptedSsn.length - 16, encryptedSsn.length);
        
        byte[] plaintext = cipher.decrypt(
            ciphertext, ssnNonce, tag, 
            this.username.getBytes()
        );
        
        return new String(plaintext);
    }
}

File Encryption Service

import java.nio.file.*;
import java.nio.channels.*;

public class FileEncryptionService {
    private final byte[] masterKey;
    
    public FileEncryptionService(byte[] masterKey) {
        this.masterKey = masterKey;
    }
    
    public void encryptFile(Path inputFile, Path outputFile, 
                           String password) throws Exception {
        // Derive file key from password
        byte[] salt = new byte[16];
        SecureRandom.fill(salt);
        
        byte[] fileKey = Argon2.deriveKey(
            password.toCharArray(),
            salt,
            32,
            new Argon2Config.Builder()
                .timeCost(3)
                .memoryCost(65536)
                .parallelism(4)
                .build()
        );
        
        // Create cipher
        ChaCha20Poly1305 cipher = new ChaCha20Poly1305(fileKey);
        
        try (FileChannel input = FileChannel.open(inputFile, 
                StandardOpenOption.READ);
             FileChannel output = FileChannel.open(outputFile,
                StandardOpenOption.CREATE, 
                StandardOpenOption.WRITE)) {
            
            // Write header (salt + nonce)
            ByteBuffer header = ByteBuffer.allocate(28);
            header.put(salt);
            
            byte[] nonce = new byte[12];
            SecureRandom.fill(nonce);
            header.put(nonce);
            header.flip();
            output.write(header);
            
            // Encrypt file in chunks
            ByteBuffer buffer = ByteBuffer.allocateDirect(64 * 1024);
            
            while (input.read(buffer) != -1) {
                buffer.flip();
                
                byte[] chunk = new byte[buffer.remaining()];
                buffer.get(chunk);
                
                var result = cipher.encrypt(chunk, nonce, null);
                
                ByteBuffer encrypted = ByteBuffer.allocate(
                    result.getCiphertext().length + result.getTag().length);
                encrypted.put(result.getCiphertext());
                encrypted.put(result.getTag());
                encrypted.flip();
                
                output.write(encrypted);
                
                buffer.clear();
                incrementNonce(nonce);
            }
        } finally {
            // Clear sensitive data
            Arrays.fill(fileKey, (byte) 0);
        }
    }
    
    private static void incrementNonce(byte[] nonce) {
        for (int i = nonce.length - 1; i >= 0; i--) {
            if (++nonce[i] != 0) break;
        }
    }
}

Testing

JUnit 5 Tests

import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class CryptoTest {
    
    @Test
    @Order(1)
    void testEd25519SignVerify() throws Exception {
        // Arrange
        Ed25519Keypair keypair = Ed25519.generateKeypair();
        byte[] message = "test message".getBytes();
        
        // Act
        byte[] signature = Ed25519.sign(message, keypair.getPrivateKey());
        boolean valid = Ed25519.verify(signature, message, 
                                      keypair.getPublicKey());
        
        // Assert
        assertTrue(valid);
        assertEquals(64, signature.length);
    }
    
    @ParameterizedTest
    @ValueSource(ints = {16, 32, 64, 128})
    void testSecureRandomGeneratesUniqueBytes(int size) {
        // Act
        byte[] random1 = new byte[size];
        byte[] random2 = new byte[size];
        SecureRandom.fill(random1);
        SecureRandom.fill(random2);
        
        // Assert
        assertFalse(Arrays.equals(random1, random2));
        assertEquals(size, random1.length);
    }
    
    @Test
    @Timeout(5)
    void testArgon2Performance() throws Exception {
        byte[] password = "password".getBytes();
        byte[] salt = new byte[16];
        
        assertTimeout(Duration.ofSeconds(2), () -> {
            Argon2.hashPassword(password, salt);
        });
    }
}

Mockito Integration

import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class CryptoServiceTest {
    
    @Mock
    private KeyStore keyStore;
    
    @InjectMocks
    private CryptoService service;
    
    @Test
    void testEncryptionWithMockedKeyStore() throws Exception {
        // Given
        byte[] key = new byte[32];
        when(keyStore.getKey("encryption-key"))
            .thenReturn(key);
        
        // When
        byte[] encrypted = service.encrypt("data".getBytes());
        
        // Then
        assertNotNull(encrypted);
        verify(keyStore).getKey("encryption-key");
    }
}

See Also