security(lib,storage): enforce single-use encryption contexts, encrypt keyring and harden key import

This commit is contained in:
2026-07-29 23:18:22 +02:00
parent 9bbcab7522
commit 8b2f3df41f
64 changed files with 7473 additions and 1799 deletions

View File

@@ -47,7 +47,7 @@ import zeroecho.core.context.EncryptionContext;
import zeroecho.core.spec.VoidSpec;
import zeroecho.core.spi.SymmetricKeyGenerator;
import zeroecho.core.spi.SymmetricKeyImporter;
import zeroecho.sdk.util.RandomSupport;
import zeroecho.core.util.RandomSupport;
/**
* AES algorithm registration and capability wiring.

View File

@@ -56,7 +56,7 @@ import zeroecho.core.context.EncryptionContext;
import zeroecho.core.err.ProviderFailureException;
import zeroecho.core.io.CipherTransformInputStreamBuilder;
import zeroecho.core.spi.ContextAware;
import zeroecho.sdk.util.RandomSupport;
import zeroecho.core.util.RandomSupport;
/**
* Streaming AES cipher context for GCM / CBC / CTR.

View File

@@ -54,7 +54,7 @@ import zeroecho.core.context.EncryptionContext;
import zeroecho.core.err.ProviderFailureException;
import zeroecho.core.io.CipherTransformInputStreamBuilder;
import zeroecho.core.spi.ContextAware;
import zeroecho.sdk.util.RandomSupport;
import zeroecho.core.util.RandomSupport;
/**
* <h2>Abstract streaming cipher context for ChaCha algorithms</h2>

View File

@@ -34,7 +34,7 @@
package zeroecho.core.alg.chacha;
import zeroecho.core.SymmetricHeaderCodec;
import zeroecho.sdk.util.RandomSupport;
import zeroecho.core.util.RandomSupport;
/**
* <h2>ChaCha20-Poly1305 (AEAD) algorithm</h2>

View File

@@ -33,7 +33,7 @@
******************************************************************************/
package zeroecho.core.alg.chacha;
import zeroecho.sdk.util.RandomSupport;
import zeroecho.core.util.RandomSupport;
/**
* <h2>ChaCha20 (stream) algorithm</h2>
*

View File

@@ -0,0 +1,29 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
******************************************************************************/
package zeroecho.core.spi;
import java.io.IOException;
import zeroecho.core.storage.KeyringPassword;
/**
* Supplies a fresh destroyable password for one keyring open operation.
*
* <p>Ownership of the returned object transfers to the receiver, which must
* destroy it in a {@code finally} block immediately after the keyring has been
* opened. Implementations must not source passwords from immutable strings,
* process arguments, system properties, environment fallbacks, persistent
* files, or global mutable state.</p>
*/
@FunctionalInterface
public interface KeyringUnlockProvider {
/**
* Supplies unlock material for one operation.
*
* @return a fresh password owner whose ownership transfers to the receiver
* @throws IOException if unlock material cannot be supplied
*/
KeyringPassword acquire() throws IOException;
}

View File

@@ -0,0 +1,58 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
******************************************************************************/
package zeroecho.core.storage;
import java.io.IOException;
import java.util.Objects;
/**
* Redacted checked failure raised by encrypted keyring operations.
*
* <p>The public message contains only the stable error code. Filesystem paths,
* aliases, key material, ciphertext, and provider-controlled messages are
* deliberately excluded.</p>
*/
public final class KeyringException extends IOException {
private static final long serialVersionUID = 1L;
/**
* Stable keyring failure categories.
*/
public enum Code {
KEYRING_ALREADY_OPEN,
KEYRING_FILESYSTEM_UNSUPPORTED,
KEYRING_FORMAT_INVALID,
KEYRING_LIMIT_EXCEEDED,
KEYRING_UNLOCK_FAILED,
KEYRING_IO_FAILED,
KEYRING_DURABILITY_UNCONFIRMED,
KEYRING_CLOSED,
KEYRING_NON_EXPORTABLE_KEY,
KEYRING_IMPORT_MAPPING_INVALID,
KEYRING_IMPORT_METADATA_INVALID,
KEYRING_KEY_NOT_CANONICALIZABLE
}
private final Code code;
/**
* Creates a redacted keyring exception.
*
* @param code stable error code
*/
public KeyringException(Code code) {
super(Objects.requireNonNull(code, "code must not be null").name());
this.code = code;
}
/**
* Returns the stable error code.
*
* @return failure code
*/
public Code code() {
return code;
}
}

View File

@@ -0,0 +1,97 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
******************************************************************************/
package zeroecho.core.storage;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.PosixFilePermission;
import java.util.Set;
/**
* Narrow package-local boundary for atomic keyring persistence operations.
*/
interface KeyringFileOperations {
KeyringFileOperations NIO = new NioKeyringFileOperations();
/** Atomic persistence destination. */
enum Target {
MAIN_IMAGE,
NONCE_RESERVATION
}
/** Creates one owner-only temporary file beside its destination. */
Path createTemporary(Target target, Path parent, String prefix, String suffix,
FileAttribute<Set<PosixFilePermission>> permissions) throws IOException;
/** Writes the complete encrypted image to its temporary file. */
void writeTemporary(Target target, Path temporary, byte[] image) throws IOException;
/** Forces temporary-file contents to durable storage. */
void forceTemporary(Target target, Path temporary) throws IOException;
/** Atomically replaces the destination with the temporary file. */
void atomicReplace(Target target, Path temporary, Path destination) throws IOException;
/** Forces the destination directory after atomic replacement. */
void forceDirectory(Target target, Path parent) throws IOException;
/** Deletes a temporary file that did not become authoritative. */
void deleteTemporary(Target target, Path temporary) throws IOException;
}
/** Production NIO implementation of the atomic persistence boundary. */
final class NioKeyringFileOperations implements KeyringFileOperations {
@Override
public Path createTemporary(Target target, Path parent, String prefix, String suffix,
FileAttribute<Set<PosixFilePermission>> permissions) throws IOException {
return Files.createTempFile(parent, prefix, suffix, permissions);
}
@Override
public void writeTemporary(Target target, Path temporary, byte[] image)
throws IOException {
try (FileChannel channel = FileChannel.open(temporary,
StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS)) {
ByteBuffer buffer = ByteBuffer.wrap(image);
while (buffer.hasRemaining()) {
channel.write(buffer);
}
}
}
@Override
public void forceTemporary(Target target, Path temporary) throws IOException {
try (FileChannel channel = FileChannel.open(temporary,
StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS)) {
channel.force(true);
}
}
@Override
public void atomicReplace(Target target, Path temporary, Path destination)
throws IOException {
Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
}
@Override
public void forceDirectory(Target target, Path parent) throws IOException {
try (FileChannel directory = FileChannel.open(parent, StandardOpenOption.READ)) {
directory.force(true);
}
}
@Override
public void deleteTemporary(Target target, Path temporary) throws IOException {
Files.deleteIfExists(temporary);
}
}

View File

@@ -0,0 +1,452 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
******************************************************************************/
package zeroecho.core.storage;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;
import zeroecho.core.CryptoAlgorithm;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.KeyOperation;
import zeroecho.core.KeyOperationInfo;
import zeroecho.core.alg.aes.AesKeyImportSpec;
import zeroecho.core.alg.bike.BikePrivateKeySpec;
import zeroecho.core.alg.bike.BikePublicKeySpec;
import zeroecho.core.alg.chacha.ChaChaKeyImportSpec;
import zeroecho.core.alg.cmce.CmcePrivateKeySpec;
import zeroecho.core.alg.cmce.CmcePublicKeySpec;
import zeroecho.core.alg.dh.DhPrivateKeySpec;
import zeroecho.core.alg.dh.DhPublicKeySpec;
import zeroecho.core.alg.ecdsa.EcdsaPrivateKeySpec;
import zeroecho.core.alg.ecdsa.EcdsaPublicKeySpec;
import zeroecho.core.alg.ed25519.Ed25519PrivateKeySpec;
import zeroecho.core.alg.ed25519.Ed25519PublicKeySpec;
import zeroecho.core.alg.ed448.Ed448PrivateKeySpec;
import zeroecho.core.alg.ed448.Ed448PublicKeySpec;
import zeroecho.core.alg.elgamal.ElgamalPrivateKeySpec;
import zeroecho.core.alg.elgamal.ElgamalPublicKeySpec;
import zeroecho.core.alg.frodo.FrodoPrivateKeySpec;
import zeroecho.core.alg.frodo.FrodoPublicKeySpec;
import zeroecho.core.alg.hmac.HmacKeyImportSpec;
import zeroecho.core.alg.hqc.HqcPrivateKeySpec;
import zeroecho.core.alg.hqc.HqcPublicKeySpec;
import zeroecho.core.alg.kyber.KyberPrivateKeySpec;
import zeroecho.core.alg.kyber.KyberPublicKeySpec;
import zeroecho.core.alg.mldsa.MldsaPrivateKeySpec;
import zeroecho.core.alg.mldsa.MldsaPublicKeySpec;
import zeroecho.core.alg.ntru.NtruPrivateKeySpec;
import zeroecho.core.alg.ntru.NtruPublicKeySpec;
import zeroecho.core.alg.ntruprime.NtrulPrimePrivateKeySpec;
import zeroecho.core.alg.ntruprime.NtrulPrimePublicKeySpec;
import zeroecho.core.alg.ntruprime.SntruPrimePrivateKeySpec;
import zeroecho.core.alg.ntruprime.SntruPrimePublicKeySpec;
import zeroecho.core.alg.rsa.RsaPrivateKeySpec;
import zeroecho.core.alg.rsa.RsaPublicKeySpec;
import zeroecho.core.alg.saber.SaberPrivateKeySpec;
import zeroecho.core.alg.saber.SaberPublicKeySpec;
import zeroecho.core.alg.slhdsa.SlhDsaPrivateKeySpec;
import zeroecho.core.alg.slhdsa.SlhDsaPublicKeySpec;
import zeroecho.core.alg.sphincsplus.SphincsPlusPrivateKeySpec;
import zeroecho.core.alg.sphincsplus.SphincsPlusPublicKeySpec;
import zeroecho.core.alg.xdh.XdhPrivateKeySpec;
import zeroecho.core.alg.xdh.XdhPublicKeySpec;
import zeroecho.core.spec.AlgorithmKeySpec;
import zeroecho.core.spi.PrivateKeyImporter;
import zeroecho.core.spi.PublicKeyImporter;
import zeroecho.core.spi.SymmetricKeyImporter;
/**
* Closed trusted mapping from persistent key identities to canonical registered
* import operations.
*
* <p>Provider identity is deliberately absent. Standard encoded key material
* is reconstructed by the current runtime's canonical ZeroEcho importer. The
* original JCA provider is neither persisted nor reproduced.</p>
*/
final class KeyringImportRegistry {
private static final String ALGORITHM_AES = "AES";
private static final String ALGORITHM_HMAC = "HMAC";
private static final String ALGORITHM_CHACHA20 = "CHACHA20";
private static final String ALGORITHM_CHACHA20_POLY1305 = "CHACHA20-POLY1305";
private static final Map<Class<? extends AlgorithmKeySpec>,
Function<byte[], ? extends AlgorithmKeySpec>> SPEC_FACTORIES =
createSpecFactories();
private static final Map<Tuple, PersistentMapping> MAPPINGS = createMappings();
private KeyringImportRegistry() {
}
/**
* Closed HMAC variants admitted by the persistent format.
*/
/* default */
enum HmacVariant {
NONE(0, null),
SHA256(1, "HmacSHA256"),
SHA384(2, "HmacSHA384"),
SHA512(3, "HmacSHA512");
private final int code;
private final String jcaName;
HmacVariant(int code, String jcaName) {
this.code = code;
this.jcaName = jcaName;
}
/* default */ int code() {
return code;
}
/* default */ String jcaName() {
return jcaName;
}
/* default */ static HmacVariant fromCode(int code) throws KeyringException {
for (HmacVariant value : values()) {
if (value.code == code) {
return value;
}
}
throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_METADATA_INVALID);
}
/* default */ static HmacVariant forStoredKey(String algorithmId, Key key)
throws KeyringException {
if (!ALGORITHM_HMAC.equals(algorithmId)) {
return NONE;
}
String algorithm = key.getAlgorithm();
for (HmacVariant value : values()) {
if (value != NONE && value.jcaName.equals(algorithm)) {
return value;
}
}
throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_METADATA_INVALID);
}
}
/**
* Immutable description used by the finite importer-matrix test.
*
* @param algorithmId canonical ZeroEcho algorithm identifier
* @param kind key kind
* @param encoding standard encoding
* @param hmacVariant closed HMAC variant, or {@link HmacVariant#NONE}
* @param specType exact registered importer specification type
*/
/* default */
record PersistentMapping(String algorithmId, KeyringStore.Kind kind,
KeyringStore.Encoding encoding, HmacVariant hmacVariant,
Class<? extends AlgorithmKeySpec> specType) {
}
@SuppressWarnings("PMD.AvoidCatchingGenericException")
/* default */ static Key importKey(String algorithmId, KeyringStore.Kind kind,
KeyringStore.Encoding encoding, HmacVariant hmacVariant, byte[] encoded)
throws GeneralSecurityException, KeyringException {
PersistentMapping mapping = requireMapping(algorithmId, kind, encoding, hmacVariant);
CryptoAlgorithm algorithm = requireAlgorithm(mapping.algorithmId);
AlgorithmKeySpec spec = createSpec(mapping, encoded);
Throwable primary = null;
try {
return invokeImporter(algorithm, kind, spec);
} catch (GeneralSecurityException | RuntimeException exception) {
primary = exception;
throw exception;
} finally {
KeyringStore.destroyTemporarySpec(spec, primary);
}
}
/* default */ static void validateMapping(String algorithmId, KeyringStore.Kind kind,
KeyringStore.Encoding encoding, HmacVariant hmacVariant)
throws KeyringException {
PersistentMapping mapping = requireMapping(algorithmId, kind, encoding, hmacVariant);
CryptoAlgorithm algorithm = requireAlgorithm(mapping.algorithmId);
List<KeyOperationInfo> operations = matchingOperations(algorithm, kind);
if (operations.size() != 1 || operations.get(0).specType() != mapping.specType) {
throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_MAPPING_INVALID);
}
}
@SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.AvoidCatchingGenericException" })
/* default */ static void validateCanonical(String algorithmId, KeyringStore.Kind kind,
KeyringStore.Encoding encoding, HmacVariant hmacVariant, Key source,
byte[] encoded)
throws KeyringException {
Key imported = null;
byte[] canonical = null;
try {
if (!matchesSourceAlgorithm(source, algorithmId, hmacVariant)) {
throw new KeyringException(
KeyringException.Code.KEYRING_KEY_NOT_CANONICALIZABLE);
}
imported = importKey(algorithmId, kind, encoding, hmacVariant, encoded);
canonical = imported.getEncoded();
if (canonical == null || !matchesFormat(imported.getFormat(), encoding)
|| !MessageDigest.isEqual(encoded, canonical)
|| !matchesAlgorithm(imported, algorithmId, hmacVariant)) {
throw new KeyringException(
KeyringException.Code.KEYRING_KEY_NOT_CANONICALIZABLE);
}
} catch (GeneralSecurityException | RuntimeException exception) {
throw new KeyringException(
KeyringException.Code.KEYRING_KEY_NOT_CANONICALIZABLE);
} finally {
if (canonical != null) {
Arrays.fill(canonical, (byte) 0);
}
destroyImported(imported);
}
}
/* default */ static List<PersistentMapping> mappings() {
return List.copyOf(MAPPINGS.values());
}
private static PersistentMapping requireMapping(String algorithmId,
KeyringStore.Kind kind, KeyringStore.Encoding encoding,
HmacVariant hmacVariant) throws KeyringException {
PersistentMapping mapping = MAPPINGS.get(
new Tuple(algorithmId, kind, encoding, hmacVariant));
if (mapping == null) {
throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_MAPPING_INVALID);
}
return mapping;
}
@SuppressWarnings("PMD.PreserveStackTrace")
private static CryptoAlgorithm requireAlgorithm(String algorithmId)
throws KeyringException {
try {
return CryptoAlgorithms.require(algorithmId);
} catch (IllegalArgumentException exception) {
throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_MAPPING_INVALID);
}
}
private static List<KeyOperationInfo> matchingOperations(CryptoAlgorithm algorithm,
KeyringStore.Kind kind) {
return algorithm.keyOperations().stream()
.filter(info -> info.operation() == operation(kind))
.toList();
}
private static KeyOperation operation(KeyringStore.Kind kind) {
return switch (kind) {
case PUBLIC_KEY -> KeyOperation.ASYMMETRIC_PUBLIC_IMPORT;
case PRIVATE_KEY -> KeyOperation.ASYMMETRIC_PRIVATE_IMPORT;
case SECRET_KEY -> KeyOperation.SYMMETRIC_IMPORT;
};
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static Key invokeImporter(CryptoAlgorithm algorithm, KeyringStore.Kind kind,
AlgorithmKeySpec spec) throws GeneralSecurityException {
return switch (kind) {
case PUBLIC_KEY -> ((PublicKeyImporter) algorithm.publicKeyImporter(spec.getClass()))
.importPublic(spec);
case PRIVATE_KEY -> ((PrivateKeyImporter) algorithm.privateKeyImporter(spec.getClass()))
.importPrivate(spec);
case SECRET_KEY -> ((SymmetricKeyImporter) algorithm.symmetricKeyImporter(spec.getClass()))
.importSecret(spec);
};
}
private static AlgorithmKeySpec createSpec(PersistentMapping mapping, byte[] encoded)
throws KeyringException {
if (mapping.specType == HmacKeyImportSpec.class) {
return new HmacKeyImportSpec(mapping.hmacVariant.jcaName, encoded);
}
Function<byte[], ? extends AlgorithmKeySpec> factory =
SPEC_FACTORIES.get(mapping.specType);
AlgorithmKeySpec result = factory == null ? null : factory.apply(encoded);
if (result == null) {
throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_MAPPING_INVALID);
}
return result;
}
private static boolean matchesFormat(String actual, KeyringStore.Encoding encoding) {
return actual != null && switch (encoding) {
case X509 -> "X.509".equalsIgnoreCase(actual) || "X509".equalsIgnoreCase(actual);
case PKCS8 -> "PKCS#8".equalsIgnoreCase(actual) || "PKCS8".equalsIgnoreCase(actual);
case RAW -> "RAW".equalsIgnoreCase(actual);
};
}
private static boolean matchesAlgorithm(Key imported, String algorithmId,
HmacVariant hmacVariant) {
if (ALGORITHM_HMAC.equals(algorithmId)) {
return hmacVariant.jcaName.equals(imported.getAlgorithm());
}
if (ALGORITHM_AES.equals(algorithmId)) {
return ALGORITHM_AES.equals(imported.getAlgorithm());
}
if (ALGORITHM_CHACHA20.equals(algorithmId)
|| ALGORITHM_CHACHA20_POLY1305.equals(algorithmId)) {
return "ChaCha20".equals(imported.getAlgorithm());
}
return true;
}
private static boolean matchesSourceAlgorithm(Key source, String algorithmId,
HmacVariant hmacVariant) {
if (ALGORITHM_HMAC.equals(algorithmId)) {
return hmacVariant.jcaName.equals(source.getAlgorithm());
}
if (ALGORITHM_AES.equals(algorithmId)) {
return ALGORITHM_AES.equals(source.getAlgorithm());
}
if (ALGORITHM_CHACHA20.equals(algorithmId)
|| ALGORITHM_CHACHA20_POLY1305.equals(algorithmId)) {
return "ChaCha20".equals(source.getAlgorithm());
}
return true;
}
@SuppressWarnings("PMD.EmptyCatchBlock")
private static void destroyImported(Key imported) {
if (imported instanceof Destroyable destroyable) {
try {
destroyable.destroy();
} catch (DestroyFailedException exception) {
// Some JCA key implementations advertise but do not support destruction.
}
}
}
private static Map<Class<? extends AlgorithmKeySpec>,
Function<byte[], ? extends AlgorithmKeySpec>> createSpecFactories() {
return Map.ofEntries(
Map.entry(AesKeyImportSpec.class, AesKeyImportSpec::fromRaw),
Map.entry(ChaChaKeyImportSpec.class, ChaChaKeyImportSpec::fromRaw),
Map.entry(BikePublicKeySpec.class, BikePublicKeySpec::new),
Map.entry(BikePrivateKeySpec.class, BikePrivateKeySpec::new),
Map.entry(CmcePublicKeySpec.class, CmcePublicKeySpec::new),
Map.entry(CmcePrivateKeySpec.class, CmcePrivateKeySpec::new),
Map.entry(DhPublicKeySpec.class, DhPublicKeySpec::new),
Map.entry(DhPrivateKeySpec.class, DhPrivateKeySpec::new),
Map.entry(EcdsaPublicKeySpec.class, EcdsaPublicKeySpec::new),
Map.entry(EcdsaPrivateKeySpec.class, EcdsaPrivateKeySpec::new),
Map.entry(Ed25519PublicKeySpec.class, Ed25519PublicKeySpec::new),
Map.entry(Ed25519PrivateKeySpec.class, Ed25519PrivateKeySpec::new),
Map.entry(Ed448PublicKeySpec.class, Ed448PublicKeySpec::new),
Map.entry(Ed448PrivateKeySpec.class, Ed448PrivateKeySpec::new),
Map.entry(ElgamalPublicKeySpec.class, ElgamalPublicKeySpec::new),
Map.entry(ElgamalPrivateKeySpec.class, ElgamalPrivateKeySpec::new),
Map.entry(FrodoPublicKeySpec.class, FrodoPublicKeySpec::new),
Map.entry(FrodoPrivateKeySpec.class, FrodoPrivateKeySpec::new),
Map.entry(HqcPublicKeySpec.class, HqcPublicKeySpec::new),
Map.entry(HqcPrivateKeySpec.class, HqcPrivateKeySpec::new),
Map.entry(KyberPublicKeySpec.class, KyberPublicKeySpec::new),
Map.entry(KyberPrivateKeySpec.class, KyberPrivateKeySpec::new),
Map.entry(MldsaPublicKeySpec.class, MldsaPublicKeySpec::new),
Map.entry(MldsaPrivateKeySpec.class, MldsaPrivateKeySpec::new),
Map.entry(NtruPublicKeySpec.class, NtruPublicKeySpec::new),
Map.entry(NtruPrivateKeySpec.class, NtruPrivateKeySpec::new),
Map.entry(NtrulPrimePublicKeySpec.class, NtrulPrimePublicKeySpec::new),
Map.entry(NtrulPrimePrivateKeySpec.class, NtrulPrimePrivateKeySpec::new),
Map.entry(SntruPrimePublicKeySpec.class, SntruPrimePublicKeySpec::new),
Map.entry(SntruPrimePrivateKeySpec.class, SntruPrimePrivateKeySpec::new),
Map.entry(RsaPublicKeySpec.class, RsaPublicKeySpec::new),
Map.entry(RsaPrivateKeySpec.class, RsaPrivateKeySpec::new),
Map.entry(SaberPublicKeySpec.class, SaberPublicKeySpec::new),
Map.entry(SaberPrivateKeySpec.class, SaberPrivateKeySpec::new),
Map.entry(SlhDsaPublicKeySpec.class, SlhDsaPublicKeySpec::new),
Map.entry(SlhDsaPrivateKeySpec.class, SlhDsaPrivateKeySpec::new),
Map.entry(SphincsPlusPublicKeySpec.class, SphincsPlusPublicKeySpec::new),
Map.entry(SphincsPlusPrivateKeySpec.class, SphincsPlusPrivateKeySpec::new),
Map.entry(XdhPublicKeySpec.class, XdhPublicKeySpec::new),
Map.entry(XdhPrivateKeySpec.class, XdhPrivateKeySpec::new));
}
private static Map<Tuple, PersistentMapping> createMappings() {
Map<Tuple, PersistentMapping> mappings = new LinkedHashMap<>();
addAsymmetric(mappings, "BIKE", BikePublicKeySpec.class, BikePrivateKeySpec.class);
addAsymmetric(mappings, "CMCE", CmcePublicKeySpec.class, CmcePrivateKeySpec.class);
addAsymmetric(mappings, "DH", DhPublicKeySpec.class, DhPrivateKeySpec.class);
addAsymmetric(mappings, "ECDSA", EcdsaPublicKeySpec.class, EcdsaPrivateKeySpec.class);
addAsymmetric(mappings, "Ed25519", Ed25519PublicKeySpec.class,
Ed25519PrivateKeySpec.class);
addAsymmetric(mappings, "Ed448", Ed448PublicKeySpec.class,
Ed448PrivateKeySpec.class);
addAsymmetric(mappings, "ElGamal", ElgamalPublicKeySpec.class,
ElgamalPrivateKeySpec.class);
addAsymmetric(mappings, "Frodo", FrodoPublicKeySpec.class,
FrodoPrivateKeySpec.class);
addAsymmetric(mappings, "HQC", HqcPublicKeySpec.class, HqcPrivateKeySpec.class);
addAsymmetric(mappings, "ML-KEM", KyberPublicKeySpec.class,
KyberPrivateKeySpec.class);
addAsymmetric(mappings, "ML-DSA", MldsaPublicKeySpec.class,
MldsaPrivateKeySpec.class);
addAsymmetric(mappings, "NTRU", NtruPublicKeySpec.class, NtruPrivateKeySpec.class);
addAsymmetric(mappings, "NTRULPRime", NtrulPrimePublicKeySpec.class,
NtrulPrimePrivateKeySpec.class);
addAsymmetric(mappings, "SNTRUPrime", SntruPrimePublicKeySpec.class,
SntruPrimePrivateKeySpec.class);
addAsymmetric(mappings, "RSA", RsaPublicKeySpec.class, RsaPrivateKeySpec.class);
addAsymmetric(mappings, "SABER", SaberPublicKeySpec.class,
SaberPrivateKeySpec.class);
addAsymmetric(mappings, "SLH-DSA", SlhDsaPublicKeySpec.class,
SlhDsaPrivateKeySpec.class);
addAsymmetric(mappings, "SPHINCS+", SphincsPlusPublicKeySpec.class,
SphincsPlusPrivateKeySpec.class);
addAsymmetric(mappings, "Xdh", XdhPublicKeySpec.class, XdhPrivateKeySpec.class);
add(mappings, ALGORITHM_AES, KeyringStore.Kind.SECRET_KEY,
KeyringStore.Encoding.RAW,
HmacVariant.NONE, AesKeyImportSpec.class);
add(mappings, ALGORITHM_CHACHA20, KeyringStore.Kind.SECRET_KEY,
KeyringStore.Encoding.RAW,
HmacVariant.NONE, ChaChaKeyImportSpec.class);
add(mappings, ALGORITHM_CHACHA20_POLY1305, KeyringStore.Kind.SECRET_KEY,
KeyringStore.Encoding.RAW, HmacVariant.NONE, ChaChaKeyImportSpec.class);
add(mappings, ALGORITHM_HMAC, KeyringStore.Kind.SECRET_KEY,
KeyringStore.Encoding.RAW,
HmacVariant.SHA256, HmacKeyImportSpec.class);
add(mappings, ALGORITHM_HMAC, KeyringStore.Kind.SECRET_KEY,
KeyringStore.Encoding.RAW,
HmacVariant.SHA384, HmacKeyImportSpec.class);
add(mappings, ALGORITHM_HMAC, KeyringStore.Kind.SECRET_KEY,
KeyringStore.Encoding.RAW,
HmacVariant.SHA512, HmacKeyImportSpec.class);
return Collections.unmodifiableMap(mappings);
}
private static void addAsymmetric(Map<Tuple, PersistentMapping> mappings,
String algorithmId, Class<? extends AlgorithmKeySpec> publicSpec,
Class<? extends AlgorithmKeySpec> privateSpec) {
add(mappings, algorithmId, KeyringStore.Kind.PUBLIC_KEY,
KeyringStore.Encoding.X509, HmacVariant.NONE, publicSpec);
add(mappings, algorithmId, KeyringStore.Kind.PRIVATE_KEY,
KeyringStore.Encoding.PKCS8, HmacVariant.NONE, privateSpec);
}
private static void add(Map<Tuple, PersistentMapping> mappings, String algorithmId,
KeyringStore.Kind kind, KeyringStore.Encoding encoding,
HmacVariant hmacVariant, Class<? extends AlgorithmKeySpec> specType) {
Tuple tuple = new Tuple(algorithmId, kind, encoding, hmacVariant);
PersistentMapping mapping = new PersistentMapping(algorithmId, kind,
encoding, hmacVariant, specType);
if (mappings.put(tuple, mapping) != null) {
throw new IllegalStateException("Duplicate persistent key importer tuple");
}
}
private record Tuple(String algorithmId, KeyringStore.Kind kind,
KeyringStore.Encoding encoding, HmacVariant hmacVariant) {
}
}

View File

@@ -0,0 +1,76 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
******************************************************************************/
package zeroecho.core.storage;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* Fixed-purpose RFC 5869 derivation for nonce-reservation authentication.
*/
final class KeyringNonceReservationKdf {
private static final int MASTER_KEY_BYTES = 32;
private static final int STORE_ID_BYTES = 16;
private static final int OUTPUT_BYTES = 32;
private static final String HMAC_SHA256 = "HmacSHA256";
private static final String DOMAIN_LABEL =
"zeroecho:keyring:nonce-reservation-mac:v1";
private KeyringNonceReservationKdf() {
}
/**
* Derives the store-specific nonce-reservation MAC key.
*
* @param masterKey borrowed 256-bit store master key
* @param storeId borrowed canonical 128-bit binary store UUID
* @return newly owned 256-bit derived key
* @throws GeneralSecurityException if HMAC-SHA-256 is unavailable
*/
/* default */ static byte[] derive(byte[] masterKey, byte[] storeId)
throws GeneralSecurityException {
if (masterKey == null || masterKey.length != MASTER_KEY_BYTES
|| storeId == null || storeId.length != STORE_ID_BYTES) {
throw new IllegalArgumentException("Invalid keyring derivation input");
}
byte[] salt = storeId.clone();
byte[] info = DOMAIN_LABEL.getBytes(StandardCharsets.US_ASCII);
byte[] prk = null;
byte[] expandInput = null;
try {
prk = hmac(salt, masterKey);
expandInput = Arrays.copyOf(info, info.length + 1);
expandInput[expandInput.length - 1] = 1;
byte[] output = hmac(prk, expandInput);
if (output.length != OUTPUT_BYTES) {
Arrays.fill(output, (byte) 0);
throw new GeneralSecurityException("KEYRING_DERIVATION_FAILED");
}
return output;
} finally {
Arrays.fill(salt, (byte) 0);
Arrays.fill(info, (byte) 0);
wipe(prk);
wipe(expandInput);
}
}
private static byte[] hmac(byte[] key, byte[] input)
throws GeneralSecurityException {
Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(new SecretKeySpec(key, HMAC_SHA256));
return mac.doFinal(input);
}
private static void wipe(byte[] value) {
if (value != null) {
Arrays.fill(value, (byte) 0);
}
}
}

View File

@@ -0,0 +1,120 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
******************************************************************************/
package zeroecho.core.storage;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.locks.ReentrantLock;
import javax.security.auth.DestroyFailedException;
import javax.security.auth.Destroyable;
/**
* Destroyable owner of a keyring password.
*
* <p>The constructor and {@link #copy()} use defensive copies. Callers retain
* ownership of the array supplied to the constructor and must clear it. The
* returned copy belongs to the receiver and must be cleared immediately after
* key derivation. This object never creates an immutable password
* {@link String}.</p>
*
* <p>Instances are thread-safe. Destruction is idempotent and makes subsequent
* access fail deterministically.</p>
*/
public final class KeyringPassword implements Destroyable, AutoCloseable {
private final ReentrantLock lifecycleLock = new ReentrantLock();
private final char[] password;
private boolean destroyed;
/**
* Creates a password owner.
*
* @param password password characters, which are defensively copied
* @throws NullPointerException if {@code password} is {@code null}
* @throws IllegalArgumentException if {@code password} is empty
*/
@SuppressWarnings("PMD.UseVarargs")
public KeyringPassword(char[] password) {
Objects.requireNonNull(password, "password must not be null");
if (password.length == 0) {
throw new IllegalArgumentException("password must not be empty");
}
this.password = password.clone();
}
/**
* Returns a receiver-owned password copy.
*
* @return a new mutable array that the receiver must clear
* @throws IllegalStateException if this object has been destroyed
*/
public char[] copy() {
lifecycleLock.lock();
try {
if (destroyed) {
throw new IllegalStateException("Keyring password is destroyed");
}
return password.clone();
} finally {
lifecycleLock.unlock();
}
}
/**
* Clears the owned password characters.
*
* @throws DestroyFailedException never thrown by this implementation
*/
@Override
public void destroy() throws DestroyFailedException {
lifecycleLock.lock();
try {
if (!destroyed) {
Arrays.fill(password, '\0');
destroyed = true;
}
} finally {
lifecycleLock.unlock();
}
}
/**
* Reports whether the password has been destroyed.
*
* @return {@code true} after destruction
*/
@Override
public boolean isDestroyed() {
lifecycleLock.lock();
try {
return destroyed;
} finally {
lifecycleLock.unlock();
}
}
/**
* Destroys the password.
*/
@Override
@SuppressWarnings("PMD.PreserveStackTrace")
public void close() {
try {
destroy();
} catch (DestroyFailedException impossible) {
throw new IllegalStateException("Keyring password destruction failed");
}
}
/**
* Returns a redacted representation.
*
* @return a constant redacted value
*/
@Override
public String toString() {
return "KeyringPassword[REDACTED]";
}
}

View File

@@ -0,0 +1,42 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
******************************************************************************/
package zeroecho.core.storage;
/**
* Operational limits applied while opening an encrypted software keyring.
*
* @param operationalIterationMaximum maximum accepted PBKDF2 iteration count;
* it may restrict but never exceed the absolute decoded maximum
*/
public record KeyringProtection(int operationalIterationMaximum) {
/** Iterations used when a new keyring is created. */
public static final int CREATION_ITERATIONS = 600_000;
/** Largest operationally accepted PBKDF2 iteration count. */
public static final int MAX_OPERATIONAL_ITERATIONS = 1_000_000;
/** Absolute safety ceiling for decoded PBKDF2 iteration counts. */
public static final int MAX_DECODED_ITERATIONS = 10_000_000;
/**
* Validates the operational limit.
*
* @throws IllegalArgumentException if the limit is below the creation
* setting or above the absolute operational maximum
*/
public KeyringProtection {
if (operationalIterationMaximum < CREATION_ITERATIONS
|| operationalIterationMaximum > MAX_OPERATIONAL_ITERATIONS) {
throw new IllegalArgumentException("operationalIterationMaximum must be in [600000,1000000]");
}
}
/**
* Returns the standard protection policy.
*
* @return policy accepting up to one million iterations
*/
public static KeyringProtection standard() {
return new KeyringProtection(MAX_OPERATIONAL_ITERATIONS);
}
}

View File

@@ -0,0 +1,21 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
******************************************************************************/
package zeroecho.core.storage;
/**
* Fills keyring randomness buffers.
*
* <p>This package-private seam supports deterministic format tests; production
* creation uses the authoritative shared secure random source.</p>
*/
@FunctionalInterface
interface KeyringRandomBytes {
/**
* Fills a destination buffer.
*
* @param destination buffer to fill completely
*/
void nextBytes(byte[] destination);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,25 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
@@ -32,94 +32,51 @@
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
/**
* Human-editable key storage persisted in a compact UTF-8 text format.
* Encrypted local software-keyring storage.
*
* <p>
* This package provides a lightweight keyring that applications can read and
* write without specialized tooling. The store keeps entries in insertion
* order, allows simple alias-based lookups, and materializes keys through the
* core catalog.
* {@link zeroecho.core.storage.KeyringStore} binds an unlocked store to one
* filesystem path and holds exclusive process-lifetime ownership until close.
* It wraps one random store master key with PBKDF2-HMAC-SHA-256 and
* AES-256-GCM, encrypts every entry independently, and authenticates the
* ordered entry manifest. Mutations use owner-only temporary files and atomic
* replacement.
* </p>
*
* <h2>Key elements</h2>
* <ul>
* <li>{@link KeyringStore} - in-memory map of aliases to immutable records with
* helpers to add, load, save, import, export, and resolve keys.</li>
* <li>{@link KeyringStore.Record} - value object describing one entry: alias,
* algorithm id, {@link KeyringStore.Record.Kind kind}, spec class, and a
* {@link zeroecho.core.marshal.PairSeq} payload.</li>
* <li><i>Resolution helpers</i> - {@link KeyringStore#getPublic(String)},
* {@link KeyringStore#getPrivate(String)}, and
* {@link KeyringStore#getSecret(String)} plus
* {@link KeyringStore.PublicWithId}, {@link KeyringStore.PrivateWithId},
* {@link KeyringStore.SecretWithId} to return the algorithm id together with
* the key.</li>
* </ul>
*
* <h2>File format</h2>
* <p>
* Files begin with a magic header followed by one or more <code>@entry</code>
* blocks. Keys that belong to the spec payload are prefixed with
* <code>s.</code> to avoid collisions with top-level fields; in-memory they are
* stored without the prefix. Lines beginning with <code>#</code> are comments.
* A blank line terminates an entry.
* Nonce-reservation authentication is cryptographically separated from AES
* entry and manifest encryption. A dedicated 256-bit MAC key is derived with
* HKDF-HMAC-SHA-256 from the store master key, canonical binary store UUID, and
* fixed nonce-reservation domain. It is never persisted, remains owned by the
* open store, and is destroyed on close. Only the current sidecar version is
* accepted.
* </p>
*
* <pre>{@code
* # KeyringStore v1
* &#64;entry
* alias=my-rsa
* algorithm=RSA
* kind=PUBLIC_KEY
* spec=zeroecho.core.alg.rsa.RsaPublicKeySpec
* s.x509B64=MIIBIjANBgkqh...
* }</pre>
*
* <h2>Spec marshaling contract</h2>
* <p>
* Each spec class named in the <code>spec</code> field must expose two public
* static methods discovered by reflection:
* Unlock passwords are destroyable, transfer ownership to the receiver, and
* are destroyed immediately after the master key is unwrapped. The unlocked
* store retains the master key, its domain-separated nonce-reservation MAC
* key, and encrypted entry records; closing the store clears this material.
* The store requires a POSIX filesystem on which owner-only permissions can be
* verified. A directory-force failure after atomic replacement makes the open
* instance unusable until close and authenticated reopen resolves which
* complete image is current. Non-exportable keys must remain behind an
* external provider reference.
* </p>
* <ul>
* <li><code>static PairSeq marshal(SpecType spec)</code></li>
* <li><code>static SpecType unmarshal(PairSeq pairs)</code></li>
* </ul>
*
* <p>
* {@link KeyringStore} validates each persisted specification type against the
* selected algorithm's registered import operation before invoking these
* methods.
* Only the current binary format is accepted. Earlier plaintext development
* formats are rejected without migration. Persisted input contains no Java
* class or provider names and cannot trigger runtime class loading. Import
* specifications are selected from a closed mapping of canonical ZeroEcho
* algorithm, key kind, standard encoding, and (only for HMAC) one of the
* explicit SHA-256, SHA-384, or SHA-512 variants. Standard encodings are
* reconstructed through the current runtime's canonical importer; the
* originating JCA provider identity is not persisted or guaranteed. Keys that
* are provider-bound, non-exportable, or not canonically reconstructable must
* remain behind an external provider reference.
* </p>
*
* <h2>Typical usage</h2> <pre>{@code
* // Create and persist a keyring.
* zeroecho.sdk.ZeroEchoSession session = new zeroecho.sdk.ZeroEchoSession();
* KeyringStore ks = new KeyringStore(session);
* ks.putPublic("site-signing", "Ed25519", myEd25519PublicSpec);
* ks.putPrivate("site-signing", "Ed25519", myEd25519PrivateSpec);
* ks.save(java.nio.file.Path.of("keyring.txt"));
*
* // Load and resolve a key later.
* KeyringStore reloaded = KeyringStore.load(session, java.nio.file.Path.of("keyring.txt"));
* java.security.PublicKey pub = reloaded.getPublic("site-signing");
* }</pre>
*
* <h2>Notes and recommendations</h2>
* <ul>
* <li>Persistence is plaintext. Treat files as sensitive, protect with OS
* permissions, and avoid committing to VCS.</li>
* <li>Resolution delegates to {@link zeroecho.core.CryptoAlgorithms}; the
* algorithm id must be one that the catalog recognizes.</li>
* <li>Records are immutable;
* {@link KeyringStore#putPublic(String, String, zeroecho.core.spec.AlgorithmKeySpec)},
* {@link KeyringStore#putPrivate(String, String, zeroecho.core.spec.AlgorithmKeySpec)},
* and
* {@link KeyringStore#putSecret(String, String, zeroecho.core.spec.AlgorithmKeySpec)}
* replace entries by alias.</li>
* <li>Lookups validate kind; for example,
* {@link KeyringStore#getPublic(String)} fails if the alias stores a private
* key.</li>
* </ul>
*
* @since 1.0
*/
package zeroecho.core.storage;

View File

@@ -0,0 +1,70 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
******************************************************************************/
package zeroecho.core.util;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Provides the authoritative process-wide cryptographic random source.
*/
public final class RandomSupport {
private static final Logger LOG = Logger.getLogger(RandomSupport.class.getName());
private static final SecureRandom RANDOM = createRandom();
private RandomSupport() {
}
/**
* Returns the shared thread-safe random source.
*
* @return shared secure random generator
*/
public static SecureRandom getRandom() {
return RANDOM;
}
/**
* Allocates and fills a random byte array.
*
* @param size required array length
* @return random bytes
*/
public static byte[] generateRandom(int size) {
return generateRandom(new byte[size]);
}
/**
* Fills the supplied array with random bytes.
*
* @param buffer destination array
* @return {@code buffer}
*/
public static byte[] generateRandom(byte[] buffer) {
RANDOM.nextBytes(buffer);
return buffer;
}
/**
* Returns a uniform value below the bound.
*
* @param bound exclusive positive upper bound
* @return generated value
*/
public static int nextInt(int bound) {
return RANDOM.nextInt(bound);
}
private static SecureRandom createRandom() {
try {
return SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException exception) {
LOG.log(Level.WARNING, "Strong SecureRandom unavailable; using the platform default");
return new SecureRandom();
}
}
}

View File

@@ -52,7 +52,7 @@ import zeroecho.sdk.builders.alg.AesDataContentBuilder;
import zeroecho.sdk.builders.alg.ChaChaDataContentBuilder;
import zeroecho.sdk.content.api.DataContent;
import zeroecho.sdk.content.api.EncryptedContent;
import zeroecho.sdk.util.RandomSupport;
import zeroecho.core.util.RandomSupport;
/**
* Encrypting stage that emits the recipient table followed by the symmetric

View File

@@ -42,7 +42,7 @@ import java.util.Objects;
import zeroecho.core.context.KemContext;
import zeroecho.core.context.KemContext.KemResult;
import zeroecho.core.io.Util;
import zeroecho.sdk.util.RandomSupport;
import zeroecho.core.util.RandomSupport;
/**
* Recipient implementation that derives a key-encryption key (KEK) via a

View File

@@ -44,7 +44,7 @@ import javax.security.auth.Destroyable;
import zeroecho.core.io.Util;
import zeroecho.sdk.Pbkdf2Limits;
import zeroecho.sdk.util.RandomSupport;
import zeroecho.core.util.RandomSupport;
/**
* Password recipient that derives a KEK via PBKDF2(HMAC-SHA-256) and wraps the

View File

@@ -33,6 +33,8 @@
******************************************************************************/
package zeroecho.sdk.util;
import zeroecho.core.util.RandomSupport;
/**
* Utility class for generating random passwords and secure random byte arrays.

View File

@@ -1,115 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.util;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Utility class providing support for secure random number generation.
* <p>
* This class provides cryptographically secure random data through one
* process-wide, thread-safe {@link SecureRandom}. It selects the platform's
* strong implementation when available and otherwise uses the platform default.
* </p>
*
* @author Leo Galambos
*/
public final class RandomSupport {
private static final Logger LOG = Logger.getLogger(RandomSupport.class.getName());
/** Shared {@link SecureRandom} instance. */
private static final SecureRandom RANDOM = createRandom();
private RandomSupport() {
// this is a utility class
}
/**
* Returns the shared {@link SecureRandom} instance.
*
* @return the shared random source
*/
public static SecureRandom getRandom() {
return RANDOM;
}
private static SecureRandom createRandom() {
try {
return SecureRandom.getInstanceStrong();
} catch (NoSuchAlgorithmException exception) {
LOG.log(Level.WARNING, "Strong SecureRandom unavailable; using the platform default");
return new SecureRandom();
}
}
/**
* Generates a secure random byte array of the specified size.
*
* @param size The size of the byte array to generate.
* @return A byte array filled with cryptographically secure random bytes.
*/
public static byte[] generateRandom(final int size) {
return generateRandom(new byte[size]);
}
/**
* Fills the given byte array with random bytes and returns it.
* <p>
* This method uses a thread-safe {@code SecureRandom} instance to generate
* cryptographically strong random values.
*
* @param buffer the byte array to fill with random bytes
* @return the same byte array, now containing random data
* @throws NullPointerException if {@code buffer} is {@code null}
*/
public static byte[] generateRandom(final byte[] buffer) {
RANDOM.nextBytes(buffer);
return buffer;
}
/**
* Returns a uniformly distributed value between zero (inclusive) and the
* specified bound (exclusive).
*
* @param bound exclusive upper bound; must be positive
* @return a uniformly distributed value
* @throws IllegalArgumentException if {@code bound} is not positive
*/
public static int nextInt(final int bound) {
return RANDOM.nextInt(bound);
}
}

View File

@@ -60,9 +60,6 @@
* stream.</li>
* <li>{@link Password} - helpers for generating random bytes and printable
* passwords.</li>
* <li>{@link RandomSupport} - shared or per-call
* {@link java.security.SecureRandom} access with thread-safe helpers for
* filling arrays.</li>
* <li>{@link X509Support} - minimal PEM-based load and print helpers for
* certificates, private keys, and certificate signing requests.</li>
* </ul>