security(lib,storage): enforce single-use encryption contexts, encrypt keyring and harden key import
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.core.alg.chacha;
|
||||
|
||||
import zeroecho.sdk.util.RandomSupport;
|
||||
import zeroecho.core.util.RandomSupport;
|
||||
/**
|
||||
* <h2>ChaCha20 (stream) algorithm</h2>
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
120
lib/src/main/java/zeroecho/core/storage/KeyringPassword.java
Normal file
120
lib/src/main/java/zeroecho/core/storage/KeyringPassword.java
Normal 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]";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
* @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;
|
||||
|
||||
70
lib/src/main/java/zeroecho/core/util/RandomSupport.java
Normal file
70
lib/src/main/java/zeroecho/core/util/RandomSupport.java
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.util;
|
||||
|
||||
import zeroecho.core.util.RandomSupport;
|
||||
|
||||
|
||||
/**
|
||||
* Utility class for generating random passwords and secure random byte arrays.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user