HSM JSP Provider | Docs | TitaniumGuard

HSM JSP Provider

HSM JSP Provider

TgHsmProvider is the standard Java Security Provider surface for the HSM. Use it when you want to work with familiar JCA/JCE APIs such as Cipher, KeyGenerator, MessageDigest, Signature, KeyPairGenerator, KeyAgreement, and KEM.

Add the JAR

Maven

<dependency>
  <groupId>in.titaniumguard.hsm</groupId>
  <artifactId>jsp</artifactId>
  <version>0.0.1</version>
</dependency>

Gradle

implementation("in.titaniumguard.hsm:jsp:0.0.1")

Register the provider

The provider is an AutoCloseable wrapper around the HSM transport. For local development, the plaintext target path is the simplest way to connect.

import in.titaniumguard.hsm.jce.appliance.HsmPartition;
import in.titaniumguard.hsm.jsp.provider.HsmJspProvider;
import java.security.Security;

try (var provider = HsmJspProvider.forPlaintextTarget(
    "localhost:50051",
    new HsmPartition("alpha", "partition-secret")
)) {
  

  // Use Cipher, KeyGenerator, Signature, etc. with the provider instance.
}

What the provider exposes

JCA/JCE typeAlgorithms
CipherAES/GCM/NoPadding, AES/GCM-SIV/NoPadding, ChaCha20-Poly1305, XChaCha20-Poly1305
KeyGeneratorAES, ChaCha20, XChaCha20
MessageDigestSHA-224, SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512, SM3
SignatureRSA, ECDSA, Ed25519, Ed448, MLDSA44, MLDSA65, MLDSA87
KeyPairGeneratorRSA, EC, Ed25519, Ed448, X25519, X448, MLDSA44, MLDSA65, MLDSA87, MLKEM512, MLKEM768, MLKEM1024
KeyAgreementX25519, X448
KEMMLKEM512, MLKEM768, MLKEM1024

AEAD ciphers

The AEAD cipher SPI supports AAD and both generated and caller-supplied nonces.

  • AES modes use GCMParameterSpec(128, nonce)
  • ChaCha20-Poly1305 uses IvParameterSpec
  • XChaCha20-Poly1305 uses IvParameterSpec
  • Cipher.updateAAD(...) is supported for all AEAD modes
  • Cipher.getIV() returns the nonce used for encryption

AES/GCM example

import java.security.Security;
import java.util.Arrays;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;

try (var provider = HsmJspProvider.forPlaintextTarget("localhost:50051", "alpha", "partition-secret")) {
  SecretKey key = KeyGenerator.getInstance("AES", provider).generateKey();

  Cipher encrypt = Cipher.getInstance("AES/GCM/NoPadding", provider);
  encrypt.init(Cipher.ENCRYPT_MODE, key);
  encrypt.updateAAD("aad".getBytes());

  byte[] plaintext = "hello".getBytes();
  byte[] ciphertext = encrypt.doFinal(plaintext);
  byte[] nonce = encrypt.getIV();

  Cipher decrypt = Cipher.getInstance("AES/GCM/NoPadding", provider);
  decrypt.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, nonce));
  decrypt.updateAAD("aad".getBytes());

  byte[] recovered = decrypt.doFinal(ciphertext);
}

ChaCha20-Poly1305 example

import java.security.Security;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

try (var provider = HsmJspProvider.forPlaintextTarget("localhost:50051", "alpha", "partition-secret")) {
  SecretKey key = KeyGenerator.getInstance("ChaCha20", provider).generateKey();
  byte[] nonce = new byte[12];

  Cipher encrypt = Cipher.getInstance("ChaCha20-Poly1305", provider);
  encrypt.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(nonce));
  encrypt.updateAAD("aad".getBytes());

  byte[] ciphertext = encrypt.doFinal("hello".getBytes());
  byte[] usedNonce = encrypt.getIV();

  Cipher decrypt = Cipher.getInstance("ChaCha20-Poly1305", provider);
  decrypt.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(usedNonce));
  decrypt.updateAAD("aad".getBytes());
  byte[] recovered = decrypt.doFinal(ciphertext);
}

XChaCha20-Poly1305 example

XChaCha20-Poly1305 uses a 24-byte nonce. The API flow is the same as ChaCha20, but the nonce size is larger.

import java.security.Security;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

try (var provider = HsmJspProvider.forPlaintextTarget("localhost:50051", "alpha", "partition-secret")) {
  SecretKey key = KeyGenerator.getInstance("XChaCha20", provider).generateKey();
  byte[] nonce = new byte[24];

  Cipher encrypt = Cipher.getInstance("XChaCha20-Poly1305", provider);
  encrypt.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(nonce));
  encrypt.updateAAD("aad".getBytes());

  byte[] ciphertext = encrypt.doFinal("hello".getBytes());
  byte[] usedNonce = encrypt.getIV();

  Cipher decrypt = Cipher.getInstance("XChaCha20-Poly1305", provider);
  decrypt.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(usedNonce));
  decrypt.updateAAD("aad".getBytes());
  byte[] recovered = decrypt.doFinal(ciphertext);
}

Digests

Use MessageDigest for SHA-2, SHA-3, and SM3. The provider exposes each digest as a standard Java security algorithm.

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.Security;

try (var provider = HsmJspProvider.forPlaintextTarget("localhost:50051", "alpha", "partition-secret")) {
  byte[] payload = "payload".getBytes(StandardCharsets.UTF_8);

  byte[] sha256 = MessageDigest.getInstance("SHA-256", provider).digest(payload);
  byte[] sha3 = MessageDigest.getInstance("SHA3-512", provider).digest(payload);
  byte[] sm3 = MessageDigest.getInstance("SM3", provider).digest(payload);
}

Notes:

  • The provider supports SHA-224, SHA-256, SHA-384, SHA-512, SHA3-256, SHA3-384, SHA3-512, and SM3.
  • You can create a fresh MessageDigest per algorithm and use it exactly like any other Java security provider.

Signatures

Use Signature with a key pair from the matching KeyPairGenerator. The provider signs and verifies the bytes you pass into Signature.update(...). If your protocol expects a digest-first workflow, hash the message yourself before calling sign() or verify().

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.Security;

try (var provider = HsmJspProvider.forPlaintextTarget("localhost:50051", "alpha", "partition-secret")) {
  KeyPair pair = KeyPairGenerator.getInstance("Ed25519", provider).generateKeyPair();
  byte[] message = "signed payload".getBytes(StandardCharsets.UTF_8);
  byte[] digest = MessageDigest.getInstance("SHA-512").digest(message);

  Signature signer = Signature.getInstance("Ed25519", provider);
  signer.initSign(pair.getPrivate());
  signer.update(digest);
  byte[] signature = signer.sign();

  Signature verifier = Signature.getInstance("Ed25519", provider);
  verifier.initVerify(pair.getPublic());
  verifier.update(digest);
  boolean valid = verifier.verify(signature);
}

For RSA, the API is the same, but you choose RSA as the algorithm name:

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.Security;

try (var provider = HsmJspProvider.forPlaintextTarget("localhost:50051", "alpha", "partition-secret")) {
  KeyPair pair = KeyPairGenerator.getInstance("RSA", provider).generateKeyPair();
  byte[] message = "signed payload".getBytes(StandardCharsets.UTF_8);
  byte[] digest = MessageDigest.getInstance("SHA-256").digest(message);

  Signature signer = Signature.getInstance("RSA", provider);
  signer.initSign(pair.getPrivate());
  signer.update(digest);
  byte[] signature = signer.sign();

  Signature verifier = Signature.getInstance("RSA", provider);
  verifier.initVerify(pair.getPublic());
  verifier.update(digest);
  boolean valid = verifier.verify(signature);
}

Notes:

  • The provider supports RSA, ECDSA, Ed25519, Ed448, MLDSA44, MLDSA65, and MLDSA87.
  • Treat the bytes passed to Signature.update(...) as the digest input for your protocol.
  • Use the provider’s key pair generators to obtain keys whose ids stay attached to the returned key wrappers.

Key agreement

Use KeyAgreement for X25519 and X448 shared-secret derivation. Both peers generate a key pair, initialize the agreement with the private key, and perform a single phase with the peer public key.

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Security;
import javax.crypto.KeyAgreement;

try (var provider = HsmJspProvider.forPlaintextTarget("localhost:50051", "alpha", "partition-secret")) {
  KeyPair left = KeyPairGenerator.getInstance("X25519", provider).generateKeyPair();
  KeyPair right = KeyPairGenerator.getInstance("X25519", provider).generateKeyPair();

  KeyAgreement leftAgreement = KeyAgreement.getInstance("X25519", provider);
  leftAgreement.init(left.getPrivate());
  leftAgreement.doPhase(right.getPublic(), true);
  byte[] leftSecret = leftAgreement.generateSecret();

  KeyAgreement rightAgreement = KeyAgreement.getInstance("X25519", provider);
  rightAgreement.init(right.getPrivate());
  rightAgreement.doPhase(left.getPublic(), true);
  byte[] rightSecret = rightAgreement.generateSecret();
}

Notes:

  • The provider supports X25519 and X448.
  • The shared secret returned by each side should match exactly when both sides use the same curve and peer keys.

KEM

Use KEM for ML-KEM encapsulation and decapsulation. The sender creates an encapsulator from the public key, and the receiver creates a decapsulator from the private key.

Sender side

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Security;
import javax.crypto.KEM;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

try (var provider = HsmJspProvider.forPlaintextTarget("localhost:50051", "alpha", "partition-secret")) {
  KeyPair pair = KeyPairGenerator.getInstance("MLKEM512", provider).generateKeyPair();
  KEM kem = KEM.getInstance("MLKEM512", provider);

  KEM.Encapsulator encapsulator = kem.newEncapsulator(pair.getPublic());
  KEM.Encapsulated encapsulated = encapsulator.encapsulate();
  SecretKey sharedFromEncapsulation = encapsulated.key();
  byte[] encapsulation = encapsulated.encapsulation();

  byte[] sharedKeyBytes = sharedFromEncapsulation.getEncoded();
  SecretKey aesKey = new SecretKeySpec(sharedKeyBytes, "AES");
  byte[] nonce = new byte[12];

  Cipher encrypt = Cipher.getInstance("AES/GCM/NoPadding", provider);
  encrypt.init(Cipher.ENCRYPT_MODE, aesKey, new GCMParameterSpec(128, nonce));
  encrypt.updateAAD("payload-aad".getBytes());
  byte[] ciphertext = encrypt.doFinal("payload".getBytes());

  // Send `encapsulation` and `ciphertext` to the receiver.
}

Receiver side

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Security;
import javax.crypto.KEM;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

try (var provider = HsmJspProvider.forPlaintextTarget("localhost:50051", "alpha", "partition-secret")) {
  KeyPair pair = KeyPairGenerator.getInstance("MLKEM512", provider).generateKeyPair();
  KEM kem = KEM.getInstance("MLKEM512", provider);

  byte[] encapsulation = /* bytes received from the sender */;
  byte[] nonce = /* nonce received from the sender */;
  byte[] ciphertext = /* ciphertext received from the sender */;

  KEM.Decapsulator decapsulator = kem.newDecapsulator(pair.getPrivate());
  SecretKey sharedFromDecapsulation = decapsulator.decapsulate(encapsulation);
  SecretKey aesKey = new SecretKeySpec(sharedFromDecapsulation.getEncoded(), "AES");

  Cipher decrypt = Cipher.getInstance("AES/GCM/NoPadding", provider);
  decrypt.init(Cipher.DECRYPT_MODE, aesKey, new GCMParameterSpec(128, nonce));
  decrypt.updateAAD("payload-aad".getBytes());
  byte[] plaintext = decrypt.doFinal(ciphertext);
}

Notes:

  • The provider supports MLKEM512, MLKEM768, and MLKEM1024.
  • The encapsulated shared secret and the decapsulated shared secret should match byte-for-byte.
  • Use KEM.Encapsulated.key() to obtain the sender-side shared secret.
  • Use KEM.Encapsulated.encapsulation() to obtain the bytes you transmit to the recipient.
  • Use KEM.Decapsulator.decapsulate(...) on the recipient side to recover the same shared secret from the encapsulation bytes.
  • A common next step is to feed the derived secret into another symmetric algorithm, such as AES/GCM/NoPadding, as shown above.
  • The same flow works with the other ML-KEM variants; only the algorithm name changes.

Notes

  • The provider returns TitaniumGuard key wrappers that preserve the HSM key id.
  • Use the provider instance directly in Cipher.getInstance(...), KeyGenerator.getInstance(...), and similar factory calls.
  • For decrypt paths, pass the same nonce and AAD that were used during encryption.