chore(text): source code format

This commit is contained in:
2026-07-30 20:28:43 +02:00
parent bc68e0433c
commit dedd16f584
262 changed files with 4867 additions and 5717 deletions

View File

@@ -138,18 +138,16 @@ public final class Guard {
/** /**
* Executes Guard with an explicit keyring unlock source. * Executes Guard with an explicit keyring unlock source.
* *
* @param args command arguments * @param args command arguments
* @param options dispatcher options * @param options dispatcher options
* @param keyringUnlockProvider destroyable-password provider * @param keyringUnlockProvider destroyable-password provider
* @return process exit code * @return process exit code
* @throws ParseException if parsing fails * @throws ParseException if parsing fails
* @throws IOException if I/O fails * @throws IOException if I/O fails
* @throws GeneralSecurityException if cryptographic processing fails * @throws GeneralSecurityException if cryptographic processing fails
*/ */
@SuppressWarnings({ "PMD.NcssCount", "PMD.CognitiveComplexity", @SuppressWarnings({ "PMD.NcssCount", "PMD.CognitiveComplexity", "PMD.CyclomaticComplexity", "PMD.NPathComplexity" })
"PMD.CyclomaticComplexity", "PMD.NPathComplexity" }) public static int main(final String[] args, final Options options, KeyringUnlockProvider keyringUnlockProvider)
public static int main(final String[] args, final Options options,
KeyringUnlockProvider keyringUnlockProvider)
throws ParseException, IOException, GeneralSecurityException { throws ParseException, IOException, GeneralSecurityException {
// ---- operation selection // ---- operation selection
final Option OPT_ENCRYPT = Option.builder("e").longOpt("encrypt").hasArg().argName("in-file") final Option OPT_ENCRYPT = Option.builder("e").longOpt("encrypt").hasArg().argName("in-file")
@@ -206,8 +204,7 @@ public final class Guard {
.desc("Recipient KEK length: exactly 16 or 32 bytes (default 32)").get(); .desc("Recipient KEK length: exactly 16 or 32 bytes (default 32)").get();
final Option OPT_PBKDF2_MAX = Option.builder().longOpt("pbkdf2-max").hasArg().argName("iterations") final Option OPT_PBKDF2_MAX = Option.builder().longOpt("pbkdf2-max").hasArg().argName("iterations")
.desc("Operational PBKDF2 ceiling; required for password operations").get(); .desc("Operational PBKDF2 ceiling; required for password operations").get();
final Option OPT_PBKDF2_HARD_MAX = Option.builder().longOpt("pbkdf2-hard-max").hasArg() final Option OPT_PBKDF2_HARD_MAX = Option.builder().longOpt("pbkdf2-hard-max").hasArg().argName("iterations")
.argName("iterations")
.desc("Absolute decoded PBKDF2 safety ceiling; required for password operations").get(); .desc("Absolute decoded PBKDF2 safety ceiling; required for password operations").get();
// ---- decoys (all types) // ---- decoys (all types)
@@ -260,8 +257,7 @@ public final class Guard {
final CommandLine cmd = parser.parse(options, args); final CommandLine cmd = parser.parse(options, args);
final boolean passwordOperation = cmd.hasOption(OPT_TO_PSW) || cmd.hasOption(OPT_DECOY_PSW) final boolean passwordOperation = cmd.hasOption(OPT_TO_PSW) || cmd.hasOption(OPT_DECOY_PSW)
|| cmd.hasOption(OPT_DECOY_PSW_RAND) || cmd.hasOption(OPT_PASSWORD); || cmd.hasOption(OPT_DECOY_PSW_RAND) || cmd.hasOption(OPT_PASSWORD);
final ZeroEchoSession session = createSession(cmd, OPT_PBKDF2_MAX, OPT_PBKDF2_HARD_MAX, final ZeroEchoSession session = createSession(cmd, OPT_PBKDF2_MAX, OPT_PBKDF2_HARD_MAX, passwordOperation);
passwordOperation);
final boolean encrypt = cmd.hasOption(OPT_ENCRYPT); final boolean encrypt = cmd.hasOption(OPT_ENCRYPT);
final Path inPath = Paths.get(cmd.getOptionValue(encrypt ? OPT_ENCRYPT : OPT_DECRYPT)); final Path inPath = Paths.get(cmd.getOptionValue(encrypt ? OPT_ENCRYPT : OPT_DECRYPT));
@@ -359,8 +355,7 @@ public final class Guard {
// envelope builder (new API) // envelope builder (new API)
final MultiRecipientDataSourceBuilder env = MultiRecipientDataSourceBuilder.builder(session) final MultiRecipientDataSourceBuilder env = MultiRecipientDataSourceBuilder.builder(session)
.payloadKeyBytes(cekBytes) .payloadKeyBytes(cekBytes).headerLimits(maxRecipients, maxEntryLen);
.headerLimits(maxRecipients, maxEntryLen);
UnlockMaterial borrowedUnlockMaterial = null; UnlockMaterial borrowedUnlockMaterial = null;
try (env) { try (env) {
if (aes != null) { if (aes != null) {
@@ -374,11 +369,10 @@ public final class Guard {
if (encrypt) { if (encrypt) {
final int iter = Integer.parseInt(cmd.getOptionValue(OPT_PSW_ITER, "200000")); final int iter = Integer.parseInt(cmd.getOptionValue(OPT_PSW_ITER, "200000"));
final int saltLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_SALT, "16")); final int saltLen = Integer.parseInt(cmd.getOptionValue(OPT_PSW_SALT, "16"));
final int kekLen = RecipientKekSizes.requireSupported( final int kekLen = RecipientKekSizes
Integer.parseInt(cmd.getOptionValue(OPT_PSW_KEK, "32"))); .requireSupported(Integer.parseInt(cmd.getOptionValue(OPT_PSW_KEK, "32")));
KeyringStore ks = loadKeyringIfPresent(cmd, OPT_KEYRING, KeyringStore ks = loadKeyringIfPresent(cmd, OPT_KEYRING, keyringUnlockProvider);
keyringUnlockProvider);
try (ks) { try (ks) {
for (String alias : cmd.getOptionValues(OPT_TO_ALIAS) == null ? new String[0] for (String alias : cmd.getOptionValues(OPT_TO_ALIAS) == null ? new String[0]
: cmd.getOptionValues(OPT_TO_ALIAS)) { : cmd.getOptionValues(OPT_TO_ALIAS)) {
@@ -424,8 +418,7 @@ public final class Guard {
throw new ParseException("Specify exactly one of --priv-alias or --password for decryption"); throw new ParseException("Specify exactly one of --priv-alias or --password for decryption");
} }
if (privAlias != null) { if (privAlias != null) {
try (KeyringStore ks = requireKeyring(cmd, OPT_KEYRING, try (KeyringStore ks = requireKeyring(cmd, OPT_KEYRING, keyringUnlockProvider)) {
keyringUnlockProvider)) {
final KeyringStore.PrivateWithId pr = ks.getPrivateWithId(privAlias); final KeyringStore.PrivateWithId pr = ks.getPrivateWithId(privAlias);
borrowedUnlockMaterial = new UnlockMaterial.Private(pr.key()); borrowedUnlockMaterial = new UnlockMaterial.Private(pr.key());
} }
@@ -474,8 +467,7 @@ public final class Guard {
boolean hasAbsoluteMaximum = cmd.hasOption(absoluteMaximumOption); boolean hasAbsoluteMaximum = cmd.hasOption(absoluteMaximumOption);
if (!hasOperationalMaximum && !hasAbsoluteMaximum) { if (!hasOperationalMaximum && !hasAbsoluteMaximum) {
if (passwordOperation) { if (passwordOperation) {
throw new ParseException( throw new ParseException("Password operations require --pbkdf2-max and --pbkdf2-hard-max");
"Password operations require --pbkdf2-max and --pbkdf2-hard-max");
} }
return new ZeroEchoSession(); return new ZeroEchoSession();
} }
@@ -485,11 +477,9 @@ public final class Guard {
try { try {
int operationalMaximum = Integer.parseInt(cmd.getOptionValue(operationalMaximumOption)); int operationalMaximum = Integer.parseInt(cmd.getOptionValue(operationalMaximumOption));
int absoluteMaximum = Integer.parseInt(cmd.getOptionValue(absoluteMaximumOption)); int absoluteMaximum = Integer.parseInt(cmd.getOptionValue(absoluteMaximumOption));
return new ZeroEchoSession().withPbkdf2Limits( return new ZeroEchoSession().withPbkdf2Limits(new Pbkdf2Limits(operationalMaximum, absoluteMaximum));
new Pbkdf2Limits(operationalMaximum, absoluteMaximum));
} catch (IllegalArgumentException exception) { } catch (IllegalArgumentException exception) {
ParseException parseException = ParseException parseException = new ParseException("Invalid PBKDF2 limits: " + exception.getMessage());
new ParseException("Invalid PBKDF2 limits: " + exception.getMessage());
parseException.initCause(exception); parseException.initCause(exception);
throw parseException; throw parseException;
} }
@@ -524,9 +514,9 @@ public final class Guard {
* </ul> * </ul>
* *
* <p> * <p>
* In both cases, the created context is consumed by * In both cases, the created context is consumed by the matching
* the matching {@link MultiRecipientDataSourceBuilder} recipient method and is * {@link MultiRecipientDataSourceBuilder} recipient method and is closed
* closed internally by the resulting content. * internally by the resulting content.
* </p> * </p>
* *
* @param env target builder to which the recipient is added * @param env target builder to which the recipient is added
@@ -542,8 +532,8 @@ public final class Guard {
*/ */
@SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" }) @SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" })
private static void addRecipientFromAlias(ZeroEchoSession session, MultiRecipientDataSourceBuilder env, private static void addRecipientFromAlias(ZeroEchoSession session, MultiRecipientDataSourceBuilder env,
KeyringStore ks, String alias, KeyringStore ks, String alias, int kekBytes, int saltLen, boolean decoy)
int kekBytes, int saltLen, boolean decoy) throws GeneralSecurityException, IOException { throws GeneralSecurityException, IOException {
KeyringStore.PublicWithId r = ks.getPublicWithId(alias); KeyringStore.PublicWithId r = ks.getPublicWithId(alias);
final String algId = r.algorithm(); final String algId = r.algorithm();
final java.security.PublicKey pub = r.key(); final java.security.PublicKey pub = r.key();
@@ -602,16 +592,14 @@ public final class Guard {
} }
private static KeyringStore loadKeyringIfPresent(CommandLine cmd, Option optKs, private static KeyringStore loadKeyringIfPresent(CommandLine cmd, Option optKs,
KeyringUnlockProvider unlockProvider) KeyringUnlockProvider unlockProvider) throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
if (!cmd.hasOption(optKs)) { if (!cmd.hasOption(optKs)) {
return null; return null;
} }
return KeyringUnlocks.open(Paths.get(cmd.getOptionValue(optKs)), unlockProvider); return KeyringUnlocks.open(Paths.get(cmd.getOptionValue(optKs)), unlockProvider);
} }
private static KeyringStore requireKeyring(CommandLine cmd, Option optKs, private static KeyringStore requireKeyring(CommandLine cmd, Option optKs, KeyringUnlockProvider unlockProvider)
KeyringUnlockProvider unlockProvider)
throws IOException, ParseException, GeneralSecurityException { throws IOException, ParseException, GeneralSecurityException {
if (!cmd.hasOption(optKs)) { if (!cmd.hasOption(optKs)) {
throw new ParseException("--keyring <file> is required when aliases are used"); throw new ParseException("--keyring <file> is required when aliases are used");

View File

@@ -210,30 +210,27 @@ public final class Kem { // NOPMD
* @param args command arguments * @param args command arguments
* @param opts command options * @param opts command options
* @return process exit code * @return process exit code
* @throws ParseException if arguments are invalid * @throws ParseException if arguments are invalid
* @throws IOException if I/O fails * @throws IOException if I/O fails
* @throws GeneralSecurityException if cryptographic processing fails * @throws GeneralSecurityException if cryptographic processing fails
*/ */
public static int main(String[] args, Options opts) public static int main(String[] args, Options opts) throws ParseException, IOException, GeneralSecurityException {
throws ParseException, IOException, GeneralSecurityException {
return main(args, opts, KeyringUnlocks.console()); return main(args, opts, KeyringUnlocks.console());
} }
/** /**
* Executes the KEM command with an explicit keyring unlock source. * Executes the KEM command with an explicit keyring unlock source.
* *
* @param args command arguments * @param args command arguments
* @param opts command options * @param opts command options
* @param unlockProvider keyring password provider * @param unlockProvider keyring password provider
* @return process exit code * @return process exit code
* @throws ParseException if arguments are invalid * @throws ParseException if arguments are invalid
* @throws IOException if I/O fails * @throws IOException if I/O fails
* @throws GeneralSecurityException if cryptographic processing fails * @throws GeneralSecurityException if cryptographic processing fails
*/ */
@SuppressWarnings({ "PMD.NcssCount", "PMD.CyclomaticComplexity", @SuppressWarnings({ "PMD.NcssCount", "PMD.CyclomaticComplexity", "PMD.NPathComplexity" })
"PMD.NPathComplexity" }) public static int main(String[] args, Options opts, KeyringUnlockProvider unlockProvider)
public static int main(String[] args, Options opts,
KeyringUnlockProvider unlockProvider)
throws ParseException, IOException, GeneralSecurityException { throws ParseException, IOException, GeneralSecurityException {
ZeroEchoSession session = new ZeroEchoSession(); ZeroEchoSession session = new ZeroEchoSession();
defineOptions(opts); defineOptions(opts);
@@ -275,105 +272,105 @@ public final class Kem { // NOPMD
final Path keyringPath = Path.of(cmd.getOptionValue(OPT_KEYRING.getLongOpt())); final Path keyringPath = Path.of(cmd.getOptionValue(OPT_KEYRING.getLongOpt()));
try (KeyringStore keyring = KeyringUnlocks.open(keyringPath, unlockProvider)) { try (KeyringStore keyring = KeyringUnlocks.open(keyringPath, unlockProvider)) {
// Configure KEM envelope // Configure KEM envelope
KemDataContentBuilder kem = KemDataContentBuilder.builder(session).kem(kemId); KemDataContentBuilder kem = KemDataContentBuilder.builder(session).kem(kemId);
if (cmd.hasOption(OPT_DIRECT.getLongOpt())) { if (cmd.hasOption(OPT_DIRECT.getLongOpt())) {
kem = kem.directSecret(); kem = kem.directSecret();
} else { } else {
byte[] info = parseOptionalHex(cmd, OPT_HKDF, "ZeroEcho-KEM".getBytes()); byte[] info = parseOptionalHex(cmd, OPT_HKDF, "ZeroEcho-KEM".getBytes());
kem = kem.hkdfSha256(info); kem = kem.hkdfSha256(info);
} }
// typed numeric options // typed numeric options
Integer keyBytes = parsedIntOpt(cmd, OPT_KEY_BYTES); Integer keyBytes = parsedIntOpt(cmd, OPT_KEY_BYTES);
if (keyBytes != null) { if (keyBytes != null) {
kem = kem.derivedKeyBytes(keyBytes); kem = kem.derivedKeyBytes(keyBytes);
} }
Integer maxKemCt = parsedIntOpt(cmd, OPT_MAX_KEM_CT); Integer maxKemCt = parsedIntOpt(cmd, OPT_MAX_KEM_CT);
if (maxKemCt != null) { if (maxKemCt != null) {
kem = kem.maxKemCiphertextLen(maxKemCt); kem = kem.maxKemCiphertextLen(maxKemCt);
} }
// Common symmetric knobs // Common symmetric knobs
final byte[] aad = parseHexOpt(cmd, OPT_AAD); final byte[] aad = parseHexOpt(cmd, OPT_AAD);
final boolean wantHeader = cmd.hasOption(OPT_HEADER.getLongOpt()); final boolean wantHeader = cmd.hasOption(OPT_HEADER.getLongOpt());
// AES payload // AES payload
if (wantAes) { if (wantAes) {
String mode = cmd.getOptionValue(OPT_AES_CIPHER.getLongOpt(), "gcm").toLowerCase(Locale.ROOT); String mode = cmd.getOptionValue(OPT_AES_CIPHER.getLongOpt(), "gcm").toLowerCase(Locale.ROOT);
AesDataContentBuilder aes = AesDataContentBuilder.builder(session); AesDataContentBuilder aes = AesDataContentBuilder.builder(session);
switch (mode) { switch (mode) {
case "gcm" -> { case "gcm" -> {
Integer tagBitsOpt = parsedIntOpt(cmd, OPT_AES_TAG_BITS); Integer tagBitsOpt = parsedIntOpt(cmd, OPT_AES_TAG_BITS);
int tagBits = tagBitsOpt == null ? 128 : tagBitsOpt; int tagBits = tagBitsOpt == null ? 128 : tagBitsOpt;
aes = aes.modeGcm(tagBits); aes = aes.modeGcm(tagBits);
}
case "ctr" -> aes = aes.modeCtr();
case "cbc" -> aes = aes.modeCbcPkcs5();
default -> throw new ParseException("Unsupported --aes-cipher: " + mode);
} }
case "ctr" -> aes = aes.modeCtr(); byte[] iv = parseHexOpt(cmd, OPT_AES_IV);
case "cbc" -> aes = aes.modeCbcPkcs5(); if (iv != null) {
default -> throw new ParseException("Unsupported --aes-cipher: " + mode); aes = aes.withDecryptionIv(iv);
}
if (aad != null && aad.length > 0) {
aes = aes.withAad(aad);
}
if (wantHeader) {
aes = aes.withHeader();
}
kem = kem.withAes(aes);
} }
byte[] iv = parseHexOpt(cmd, OPT_AES_IV);
if (iv != null) {
aes = aes.withDecryptionIv(iv);
}
if (aad != null && aad.length > 0) {
aes = aes.withAad(aad);
}
if (wantHeader) {
aes = aes.withHeader();
}
kem = kem.withAes(aes);
}
// ChaCha payload // ChaCha payload
if (wantChaCha) { if (wantChaCha) {
ChaChaDataContentBuilder cc = ChaChaDataContentBuilder.builder(session); ChaChaDataContentBuilder cc = ChaChaDataContentBuilder.builder(session);
byte[] nonce = parseHexOpt(cmd, OPT_CHACHA_NONCE); byte[] nonce = parseHexOpt(cmd, OPT_CHACHA_NONCE);
if (nonce != null) { if (nonce != null) {
cc = cc.withDecryptionNonce(nonce); cc = cc.withDecryptionNonce(nonce);
}
// counter is an integer, not bytes; use typed parsed option
Integer counter = parsedIntOpt(cmd, OPT_CHACHA_COUNTER);
if (counter != null) {
cc = cc.withCounter(counter);
}
Integer initial = parsedIntOpt(cmd, OPT_CHACHA_INITIAL);
if (initial != null) {
cc = cc.initialCounter(initial);
}
if (aad != null && aad.length > 0) {
cc = cc.withAad(aad); // selects AEAD
}
if (wantHeader) {
cc = cc.withHeader();
}
kem = kem.withChaCha(cc);
} }
// counter is an integer, not bytes; use typed parsed option
Integer counter = parsedIntOpt(cmd, OPT_CHACHA_COUNTER);
if (counter != null) {
cc = cc.withCounter(counter);
}
Integer initial = parsedIntOpt(cmd, OPT_CHACHA_INITIAL);
if (initial != null) {
cc = cc.initialCounter(initial);
}
if (aad != null && aad.length > 0) {
cc = cc.withAad(aad); // selects AEAD
}
if (wantHeader) {
cc = cc.withHeader();
}
kem = kem.withChaCha(cc);
}
// Pipeline: source -> kem payload stage // Pipeline: source -> kem payload stage
DataContent chain; DataContent chain;
if (encrypt) { if (encrypt) {
String alias = require(cmd, OPT_PUB, "Missing --pub for encryption"); String alias = require(cmd, OPT_PUB, "Missing --pub for encryption");
PublicKey recipient = keyring.getPublic(alias); PublicKey recipient = keyring.getPublic(alias);
chain = DataContentChainBuilder.encrypt() chain = DataContentChainBuilder.encrypt()
.add(PlainFileBuilder.builder().url(Path.of(input).toUri().toURL())) .add(PlainFileBuilder.builder().url(Path.of(input).toUri().toURL()))
.add(kem.recipientPublic(recipient)).build(); .add(kem.recipientPublic(recipient)).build();
} else { } else {
String alias = require(cmd, OPT_PRIV, "Missing --priv for decryption"); String alias = require(cmd, OPT_PRIV, "Missing --priv for decryption");
PrivateKey recipient = keyring.getPrivate(alias); PrivateKey recipient = keyring.getPrivate(alias);
chain = DataContentChainBuilder.decrypt() chain = DataContentChainBuilder.decrypt()
.add(PlainFileBuilder.builder().url(Path.of(input).toUri().toURL())) .add(PlainFileBuilder.builder().url(Path.of(input).toUri().toURL()))
.add(kem.recipientPrivate(recipient)).build(); .add(kem.recipientPrivate(recipient)).build();
} }
try (InputStream in = chain.getStream(); OutputStream out = Files.newOutputStream(output)) { try (InputStream in = chain.getStream(); OutputStream out = Files.newOutputStream(output)) {
in.transferTo(out); in.transferTo(out);
} catch (IOException ex) { } catch (IOException ex) {
if (LOG.isLoggable(Level.SEVERE)) { if (LOG.isLoggable(Level.SEVERE)) {
LOG.log(Level.SEVERE, "I/O error", ex); LOG.log(Level.SEVERE, "I/O error", ex);
}
return 1;
} }
return 1;
}
return 0; return 0;
} }
} }

View File

@@ -192,16 +192,15 @@ public final class KeyStoreManagement {
/** /**
* Executes the command with an explicit unlock source. * Executes the command with an explicit unlock source.
* *
* @param args arguments passed by the application dispatcher * @param args arguments passed by the application dispatcher
* @param dispatcherOptions dispatcher options * @param dispatcherOptions dispatcher options
* @param unlockProvider explicit destroyable-password provider * @param unlockProvider explicit destroyable-password provider
* @return process exit code * @return process exit code
* @throws ParseException if parsing fails * @throws ParseException if parsing fails
* @throws IOException if keyring I/O fails * @throws IOException if keyring I/O fails
* @throws GeneralSecurityException if cryptographic processing fails * @throws GeneralSecurityException if cryptographic processing fails
*/ */
public static int main(final String[] args, final Options dispatcherOptions, public static int main(final String[] args, final Options dispatcherOptions, KeyringUnlockProvider unlockProvider)
KeyringUnlockProvider unlockProvider)
throws ParseException, IOException, GeneralSecurityException { throws ParseException, IOException, GeneralSecurityException {
ZeroEchoSession session = new ZeroEchoSession(); ZeroEchoSession session = new ZeroEchoSession();
defineOptions(dispatcherOptions); defineOptions(dispatcherOptions);
@@ -218,8 +217,7 @@ public final class KeyStoreManagement {
} }
Path keyringPath = Path.of(cmd.getOptionValue(KEYSTORE_OPTION.getLongOpt())); Path keyringPath = Path.of(cmd.getOptionValue(KEYSTORE_OPTION.getLongOpt()));
try (KeyringStore store = Files.exists(keyringPath) try (KeyringStore store = Files.exists(keyringPath) ? KeyringUnlocks.open(keyringPath, unlockProvider)
? KeyringUnlocks.open(keyringPath, unlockProvider)
: KeyringUnlocks.create(keyringPath, unlockProvider)) { : KeyringUnlocks.create(keyringPath, unlockProvider)) {
if (cmd.hasOption(LIST_ALIASES_OPTION.getLongOpt())) { if (cmd.hasOption(LIST_ALIASES_OPTION.getLongOpt())) {
listAliases(store); listAliases(store);
@@ -310,8 +308,8 @@ public final class KeyStoreManagement {
* @param store keyring store to mutate * @param store keyring store to mutate
* @param cmd parsed command line * @param cmd parsed command line
*/ */
public static void doGenerate(final ZeroEchoSession session, final KeyringStore store, public static void doGenerate(final ZeroEchoSession session, final KeyringStore store, final CommandLine cmd)
final CommandLine cmd) throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
String algId = required(cmd, ALG_OPTION, "--alg is required for --generate"); String algId = required(cmd, ALG_OPTION, "--alg is required for --generate");
String aliasBase = required(cmd, ALIAS_OPTION, "--alias is required for --generate"); String aliasBase = required(cmd, ALIAS_OPTION, "--alias is required for --generate");
String kind = cmd.getOptionValue(KIND_OPTION.getLongOpt()); String kind = cmd.getOptionValue(KIND_OPTION.getLongOpt());
@@ -391,8 +389,7 @@ public final class KeyStoreManagement {
} }
private static void generateSymmetric(ZeroEchoSession session, KeyringStore store, CryptoAlgorithm algorithm, private static void generateSymmetric(ZeroEchoSession session, KeyringStore store, CryptoAlgorithm algorithm,
String algorithmId, String alias, boolean overwrite) String algorithmId, String alias, boolean overwrite) throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
GeneratedSecret generated = firstGeneratedSecret(session, algorithm, algorithmId); GeneratedSecret generated = firstGeneratedSecret(session, algorithm, algorithmId);
ensureWritable(store, alias, overwrite); ensureWritable(store, alias, overwrite);
store.putSecret(alias, algorithmId, generated.key()); store.putSecret(alias, algorithmId, generated.key());
@@ -430,9 +427,7 @@ public final class KeyStoreManagement {
/** Selects the exact key-generation operation requested by the command. */ /** Selects the exact key-generation operation requested by the command. */
private enum GenerationKind { private enum GenerationKind {
ASYMMETRIC, ASYMMETRIC, SYMMETRIC, NONE
SYMMETRIC,
NONE
} }
private static String required(CommandLine cmd, Option opt, String message) { private static String required(CommandLine cmd, Option opt, String message) {

View File

@@ -53,8 +53,7 @@ final class KeyringUnlocks {
} }
} }
private static KeyringPassword acquire(KeyringUnlockProvider provider) private static KeyringPassword acquire(KeyringUnlockProvider provider) throws IOException {
throws IOException {
KeyringPassword password = provider.acquire(); KeyringPassword password = provider.acquire();
if (password == null) { if (password == null) {
throw new IOException("Keyring unlock provider returned no password"); throw new IOException("Keyring unlock provider returned no password");

View File

@@ -158,24 +158,22 @@ public final class Tag { // NOPMD
* @throws GeneralSecurityException if a cryptographic error occurs during * @throws GeneralSecurityException if a cryptographic error occurs during
* signature or digest processing * signature or digest processing
*/ */
public static int main(String[] args, Options root) public static int main(String[] args, Options root) throws ParseException, IOException, GeneralSecurityException {
throws ParseException, IOException, GeneralSecurityException {
return main(args, root, KeyringUnlocks.console()); return main(args, root, KeyringUnlocks.console());
} }
/** /**
* Executes the command with an explicit keyring unlock source. * Executes the command with an explicit keyring unlock source.
* *
* @param args command arguments * @param args command arguments
* @param root root options * @param root root options
* @param unlockProvider keyring password provider * @param unlockProvider keyring password provider
* @return process exit code * @return process exit code
* @throws ParseException if parsing fails * @throws ParseException if parsing fails
* @throws IOException if I/O fails * @throws IOException if I/O fails
* @throws GeneralSecurityException if cryptographic processing fails * @throws GeneralSecurityException if cryptographic processing fails
*/ */
public static int main(String[] args, Options root, public static int main(String[] args, Options root, KeyringUnlockProvider unlockProvider)
KeyringUnlockProvider unlockProvider)
throws ParseException, IOException, GeneralSecurityException { throws ParseException, IOException, GeneralSecurityException {
ZeroEchoSession session = new ZeroEchoSession(); ZeroEchoSession session = new ZeroEchoSession();
Options opts = root; Options opts = root;
@@ -222,13 +220,13 @@ public final class Tag { // NOPMD
if (produce) { if (produce) {
String privAlias = require(cli, PRIV_OPT, "signature produce requires --priv <alias>"); String privAlias = require(cli, PRIV_OPT, "signature produce requires --priv <alias>");
PrivateKey priv = keyring.getPrivate(privAlias); PrivateKey priv = keyring.getPrivate(privAlias);
tail = new TagTrailerDataContentBuilder<>( tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, priv, spec))
TagEngineBuilder.signature(session, alg, priv, spec)).build(true); .build(true);
} else { } else {
String pubAlias = require(cli, PUB_OPT, "signature verify requires --pub <alias>"); String pubAlias = require(cli, PUB_OPT, "signature verify requires --pub <alias>");
PublicKey pub = keyring.getPublic(pubAlias); PublicKey pub = keyring.getPublic(pubAlias);
tail = new TagTrailerDataContentBuilder<>( tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, pub, spec))
TagEngineBuilder.signature(session, alg, pub, spec)).build(false); .build(false);
} }
} }
} else { // digest } else { // digest

View File

@@ -124,16 +124,16 @@ public class GuardTest {
// Encrypt // Encrypt
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", password, String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", password,
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "aes-gcm",
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex }; "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex };
System.out.println("...encrypt: " + Arrays.toString(encArgs)); System.out.println("...encrypt: " + Arrays.toString(encArgs));
int e = Guard.main(encArgs, new Options(), TestKeyringUnlocks.provider()); int e = Guard.main(encArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, e, "... encrypt expected exit code 0"); assertEquals(0, e, "... encrypt expected exit code 0");
// Decrypt (using password) // Decrypt (using password)
String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--password", password, String[] decArgs = { "--decrypt", enc.toString(), "--output", dec.toString(), "--password", password,
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "aes-gcm",
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex }; "--tag-bits", Integer.toString(tagBits), "--aad-hex", aadHex };
System.out.println("...decrypt: " + Arrays.toString(decArgs)); System.out.println("...decrypt: " + Arrays.toString(decArgs));
int d = Guard.main(decArgs, new Options(), TestKeyringUnlocks.provider()); int d = Guard.main(decArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, d, "... decrypt expected exit code 0"); assertEquals(0, d, "... decrypt expected exit code 0");
@@ -163,10 +163,8 @@ public class GuardTest {
System.out.println(method); System.out.println(method);
Path input = writeRandom(tmp.resolve("invalid-kek.bin"), 32, 0x4B454B); Path input = writeRandom(tmp.resolve("invalid-kek.bin"), 32, 0x4B454B);
Path output = tmp.resolve("invalid-kek.enc"); Path output = tmp.resolve("invalid-kek.enc");
String[] arguments = { "--encrypt", input.toString(), "--output", output.toString(), String[] arguments = { "--encrypt", input.toString(), "--output", output.toString(), "--to-psw", "controlled",
"--to-psw", "controlled", "--to-kek-bytes", "24", "--to-kek-bytes", "24", "--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM,
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM,
"--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM,
"--alg", "aes-gcm" }; "--alg", "aes-gcm" };
IllegalArgumentException failure = assertThrows(IllegalArgumentException.class, IllegalArgumentException failure = assertThrows(IllegalArgumentException.class,
@@ -290,8 +288,8 @@ public class GuardTest {
// plus 2 random password decoys. Recipients are shuffled by default. // plus 2 random password decoys. Recipients are shuffled by default.
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(), String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--keyring", ring.toString(),
"--to-alias", rsa.pub, "--to-psw", password, "--decoy-alias", elg.pub, "--decoy-psw-rand", "2", "--to-alias", rsa.pub, "--to-psw", password, "--decoy-alias", elg.pub, "--decoy-psw-rand", "2",
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "aes-gcm",
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad }; "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad };
System.out.println("...encrypt: " + Arrays.toString(encArgs)); System.out.println("...encrypt: " + Arrays.toString(encArgs));
int e = Guard.main(encArgs, new Options(), TestKeyringUnlocks.provider()); int e = Guard.main(encArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, e, "... encrypt rc"); assertEquals(0, e, "... encrypt rc");
@@ -308,8 +306,8 @@ public class GuardTest {
// Decrypt via password instead of key // Decrypt via password instead of key
String[] decPwd = { "--decrypt", enc.toString(), "--output", dec2.toString(), "--password", password, String[] decPwd = { "--decrypt", enc.toString(), "--output", dec2.toString(), "--password", password,
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "aes-gcm",
"aes-gcm", "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad }; "--tag-bits", Integer.toString(tagBits), "--aad-hex", aad };
System.out.println("...decrypt(password): " + Arrays.toString(decPwd)); System.out.println("...decrypt(password): " + Arrays.toString(decPwd));
int d2 = Guard.main(decPwd, new Options(), TestKeyringUnlocks.provider()); int d2 = Guard.main(decPwd, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, d2, "... decrypt(password) rc"); assertEquals(0, d2, "... decrypt(password) rc");
@@ -334,9 +332,9 @@ public class GuardTest {
Path enc = tmp.resolve("pt-neg.bin.enc"); Path enc = tmp.resolve("pt-neg.bin.enc");
String pwd = "x"; String pwd = "x";
String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", pwd, String[] encArgs = { "--encrypt", in.toString(), "--output", enc.toString(), "--to-psw", pwd, "--pbkdf2-max",
"--pbkdf2-max", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", TEST_PBKDF2_MAXIMUM, "--pbkdf2-hard-max", TEST_PBKDF2_MAXIMUM, "--alg", "aes-gcm", "--tag-bits",
"aes-gcm", "--tag-bits", "128" }; "128" };
int e = Guard.main(encArgs, new Options(), TestKeyringUnlocks.provider()); int e = Guard.main(encArgs, new Options(), TestKeyringUnlocks.provider());
assertEquals(0, e, "... encrypt rc"); assertEquals(0, e, "... encrypt rc");

View File

@@ -165,8 +165,7 @@ public class KemTest {
KeyAliases aliases = generateKemIntoKeyStore(ring, kemId, "alias-" + shortId(kemId)); KeyAliases aliases = generateKemIntoKeyStore(ring, kemId, "alias-" + shortId(kemId));
// Sanity: re-open to ensure the file is valid // Sanity: re-open to ensure the file is valid
try (zeroecho.core.storage.KeyringPassword password = try (zeroecho.core.storage.KeyringPassword password = TestKeyringUnlocks.provider().acquire();
TestKeyringUnlocks.provider().acquire();
KeyringStore ks = KeyringStore.open(ring, password)) { KeyringStore ks = KeyringStore.open(ring, password)) {
if (!(ks.contains(aliases.pub) && ks.contains(aliases.prv))) { if (!(ks.contains(aliases.pub) && ks.contains(aliases.prv))) {
throw new IllegalStateException("Keyring does not contain expected aliases for " + kemId); throw new IllegalStateException("Keyring does not contain expected aliases for " + kemId);
@@ -210,15 +209,15 @@ public class KemTest {
Files.write(plain, content); Files.write(plain, content);
System.out.println("...[" + kemId + "] ChaCha encrypt"); System.out.println("...[" + kemId + "] ChaCha encrypt");
int e = Kem.main(new String[] { "--encrypt", plain.toString(), "--output", enc.toString(), int e = Kem.main(new String[] { "--encrypt", plain.toString(), "--output", enc.toString(),
"--keyring", ring.toString(), "--pub", aliases.pub, "--kem", kemId, "--chacha", "--keyring", ring.toString(), "--pub", aliases.pub, "--kem", kemId, "--chacha", "--aad",
"--aad", aadChaCha, "--header" }, new Options(), TestKeyringUnlocks.provider()); aadChaCha, "--header" }, new Options(), TestKeyringUnlocks.provider());
if (e != 0) { if (e != 0) {
throw new IllegalStateException("ChaCha encrypt rc=" + e); throw new IllegalStateException("ChaCha encrypt rc=" + e);
} }
System.out.println("...[" + kemId + "] ChaCha decrypt"); System.out.println("...[" + kemId + "] ChaCha decrypt");
int d = Kem.main(new String[] { "--decrypt", enc.toString(), "--output", dec.toString(), int d = Kem.main(new String[] { "--decrypt", enc.toString(), "--output", dec.toString(),
"--keyring", ring.toString(), "--priv", aliases.prv, "--kem", kemId, "--chacha", "--keyring", ring.toString(), "--priv", aliases.prv, "--kem", kemId, "--chacha", "--aad",
"--aad", aadChaCha, "--header" }, new Options(), TestKeyringUnlocks.provider()); aadChaCha, "--header" }, new Options(), TestKeyringUnlocks.provider());
if (d != 0) { if (d != 0) {
throw new IllegalStateException("ChaCha decrypt rc=" + d); throw new IllegalStateException("ChaCha decrypt rc=" + d);
} }

View File

@@ -139,8 +139,7 @@ public class KeyStoreManagementTest {
assertTrue(attempted > 0, "No generation attempts were successful"); assertTrue(attempted > 0, "No generation attempts were successful");
// Verify by reloading and materializing. // Verify by reloading and materializing.
zeroecho.core.storage.KeyringPassword password = zeroecho.core.storage.KeyringPassword password = TestKeyringUnlocks.provider().acquire();
TestKeyringUnlocks.provider().acquire();
KeyringStore store; KeyringStore store;
try { try {
store = KeyringStore.open(ring, password); store = KeyringStore.open(ring, password);
@@ -197,15 +196,13 @@ public class KeyStoreManagementTest {
// ---- helpers ---- // ---- helpers ----
private static boolean hasAsymmetricDefault(CryptoAlgorithm alg) { private static boolean hasAsymmetricDefault(CryptoAlgorithm alg) {
return alg.keyOperations().stream() return alg.keyOperations().stream().anyMatch(
.anyMatch(info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE info -> info.operation() == KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE && info.defaultSpec() != null);
&& info.defaultSpec() != null);
} }
private static boolean hasSymmetricDefault(CryptoAlgorithm alg) { private static boolean hasSymmetricDefault(CryptoAlgorithm alg) {
return alg.keyOperations().stream() return alg.keyOperations().stream()
.anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE .anyMatch(info -> info.operation() == KeyOperation.SYMMETRIC_GENERATE && info.defaultSpec() != null);
&& info.defaultSpec() != null);
} }
private static String sanitize(String id) { private static String sanitize(String id) {

View File

@@ -127,8 +127,7 @@ public class TagTest {
Path ring = tmp.resolve("ring-ed25519.txt"); Path ring = tmp.resolve("ring-ed25519.txt");
KeyAliases ed = generateIntoKeyStore(ring, "Ed25519", "ed"); KeyAliases ed = generateIntoKeyStore(ring, "Ed25519", "ed");
// sanity // sanity
try (zeroecho.core.storage.KeyringPassword password = try (zeroecho.core.storage.KeyringPassword password = TestKeyringUnlocks.provider().acquire();
TestKeyringUnlocks.provider().acquire();
KeyringStore ks = KeyringStore.open(ring, password)) { KeyringStore ks = KeyringStore.open(ring, password)) {
assertTrue(ks.contains(ed.pub) && ks.contains(ed.prv), "missing expected aliases"); assertTrue(ks.contains(ed.pub) && ks.contains(ed.prv), "missing expected aliases");
} }
@@ -200,12 +199,18 @@ public class TagTest {
Files.write(plain, pt); Files.write(plain, pt);
// produce // produce
assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in", assertEquals(0,
plain.toString(), "--out", tagged.toString() }, new Options(), TestKeyringUnlocks.provider())); Tag.main(
new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in",
plain.toString(), "--out", tagged.toString() },
new Options(), TestKeyringUnlocks.provider()));
// verify (match) // verify (match)
assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "verify", "--alg", "SHA-256", "--in", assertEquals(0,
tagged.toString(), "--out", recovered.toString() }, new Options(), TestKeyringUnlocks.provider())); Tag.main(
new String[] { "--type", "digest", "--mode", "verify", "--alg", "SHA-256", "--in",
tagged.toString(), "--out", recovered.toString() },
new Options(), TestKeyringUnlocks.provider()));
assertArrayEquals(pt, Files.readAllBytes(recovered), "digest round-trip mismatch"); assertArrayEquals(pt, Files.readAllBytes(recovered), "digest round-trip mismatch");
@@ -223,15 +228,21 @@ public class TagTest {
Files.write(plain, pt); Files.write(plain, pt);
// produce // produce
assertEquals(0, Tag.main(new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in", assertEquals(0,
plain.toString(), "--out", tagged.toString() }, new Options(), TestKeyringUnlocks.provider())); Tag.main(
new String[] { "--type", "digest", "--mode", "produce", "--alg", "SHA-256", "--in",
plain.toString(), "--out", tagged.toString() },
new Options(), TestKeyringUnlocks.provider()));
// corrupt last byte -> break digest // corrupt last byte -> break digest
flipLastByte(tagged); flipLastByte(tagged);
// verify (mismatch): expect throw + default marker ("digest invalid") // verify (mismatch): expect throw + default marker ("digest invalid")
assertEquals(1, Tag.main(new String[] { "--type", "digest", "--mode", "verify", "--alg", "SHA-256", "--in", assertEquals(1,
tagged.toString(), "--out", out.toString() }, new Options(), TestKeyringUnlocks.provider())); Tag.main(
new String[] { "--type", "digest", "--mode", "verify", "--alg", "SHA-256", "--in",
tagged.toString(), "--out", out.toString() },
new Options(), TestKeyringUnlocks.provider()));
assertTrue(Files.notExists(out, LinkOption.NOFOLLOW_LINKS)); assertTrue(Files.notExists(out, LinkOption.NOFOLLOW_LINKS));

View File

@@ -8,7 +8,6 @@ final class TestKeyringUnlocks {
} }
static KeyringUnlockProvider provider() { static KeyringUnlockProvider provider() {
return () -> new KeyringPassword( return () -> new KeyringPassword(new char[] { 't', 'e', 's', 't', '-', 'k', 'e', 'y', 'r', 'i', 'n', 'g' });
new char[] { 't', 'e', 's', 't', '-', 'k', 'e', 'y', 'r', 'i', 'n', 'g' });
} }
} }

View File

@@ -91,17 +91,17 @@ class JpegExifIntegrationTest {
// AES encryption setup // AES encryption setup
/* /*
* SecretKey key = zeroEchoSession.keyBuilders().symmetric() * SecretKey key = zeroEchoSession.keyBuilders().symmetric() .generate("AES",
* .generate("AES", AesKeyGenSpec.aes256()); AesSpec spec = * AesKeyGenSpec.aes256()); AesSpec spec =
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build(); * AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
* EncryptionContext enc = zeroEchoSession.createContext("AES", KeyUsage.ENCRYPT, key, * EncryptionContext enc = zeroEchoSession.createContext("AES",
* spec); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + * KeyUsage.ENCRYPT, key, spec); CtxInterface session =
* System.nanoTime()); session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware) * Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
* session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware)
* enc).setContext(session); * enc).setContext(session);
*/ */
ZeroEchoSession zeroEchoSession = new ZeroEchoSession(); ZeroEchoSession zeroEchoSession = new ZeroEchoSession();
SecretKey key = zeroEchoSession.keyBuilders().symmetric() SecretKey key = zeroEchoSession.keyBuilders().symmetric().generate("AES", AesKeyGenSpec.aes256());
.generate("AES", AesKeyGenSpec.aes256());
CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime()); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
byte[] encryptedBytes; byte[] encryptedBytes;
@@ -152,7 +152,8 @@ class JpegExifIntegrationTest {
// input // input
.add(PlainBytesBuilder.builder().bytes(extractedEncryptedBytes)) .add(PlainBytesBuilder.builder().bytes(extractedEncryptedBytes))
// encryption // encryption
.add(AesDataContentBuilder.builder(zeroEchoSession).importKeyRaw(key.getEncoded()).spec(AesSpec.gcm128(null)) .add(AesDataContentBuilder.builder(zeroEchoSession).importKeyRaw(key.getEncoded())
.spec(AesSpec.gcm128(null))
// let us use the default header for AAD and IV // let us use the default header for AAD and IV
.withHeader().withAad(aad).context(session)) .withHeader().withAad(aad).context(session))
// and create the pipeline // and create the pipeline
@@ -164,9 +165,9 @@ class JpegExifIntegrationTest {
/* /*
* AesSpec spec = * AesSpec spec =
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build(); * AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
* EncryptionContext dec1 = zeroEchoSession.createContext("AES", KeyUsage.DECRYPT, * EncryptionContext dec1 = zeroEchoSession.createContext("AES",
* key, spec); ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx * KeyUsage.DECRYPT, key, spec); ((ContextAware) dec1).setContext(session); //
* byte[] pt1 = readAll(dec1.attach(new * same IV/AAD in ctx byte[] pt1 = readAll(dec1.attach(new
* ByteArrayInputStream(extractedEncryptedBytes))); dec1.close(); * ByteArrayInputStream(extractedEncryptedBytes))); dec1.close();
*/ */
String decrypted = new String(pt1, StandardCharsets.UTF_8); String decrypted = new String(pt1, StandardCharsets.UTF_8);

View File

@@ -16,29 +16,31 @@ import zeroecho.core.spec.ContextSpec;
/** /**
* Immutable value descriptor of one algorithm context capability. * Immutable value descriptor of one algorithm context capability.
* *
* <p>The default specification is resolved once during provider construction. * <p>
* All components therefore have stable value semantics and are safe for * The default specification is resolved once during provider construction. All
* concurrent reads.</p> * components therefore have stable value semantics and are safe for concurrent
* reads.
* </p>
* *
* @param algorithmId canonical algorithm identifier * @param algorithmId canonical algorithm identifier
* @param family algorithm family * @param family algorithm family
* @param role supported key usage * @param role supported key usage
* @param contextType produced context type * @param contextType produced context type
* @param keyType accepted key type * @param keyType accepted key type
* @param specType accepted specification type * @param specType accepted specification type
* @param defaultSpec non-null resolved default specification * @param defaultSpec non-null resolved default specification
* @since 1.0 * @since 1.0
*/ */
public record Capability(String algorithmId, AlgorithmFamily family, KeyUsage role, public record Capability(String algorithmId, AlgorithmFamily family, KeyUsage role,
Class<? extends CryptoContext> contextType, Class<? extends Key> keyType, Class<? extends CryptoContext> contextType, Class<? extends Key> keyType, Class<? extends ContextSpec> specType,
Class<? extends ContextSpec> specType, ContextSpec defaultSpec) { ContextSpec defaultSpec) {
/** /**
* Validates the capability metadata. * Validates the capability metadata.
* *
* @throws NullPointerException if a component is {@code null} * @throws NullPointerException if a component is {@code null}
* @throws IllegalArgumentException if {@code defaultSpec} is incompatible * @throws IllegalArgumentException if {@code defaultSpec} is incompatible with
* with {@code specType} * {@code specType}
*/ */
public Capability { public Capability {
Objects.requireNonNull(algorithmId, "algorithmId must not be null"); Objects.requireNonNull(algorithmId, "algorithmId must not be null");

View File

@@ -106,8 +106,8 @@ import zeroecho.core.spi.SymmetricKeyImporter;
* <p> * <p>
* <b>Security note:</b> Algorithms must enforce strong validation of keys and * <b>Security note:</b> Algorithms must enforce strong validation of keys and
* specs during registration and * specs during registration and
* {@link #createContext(KeyUsage, Key, ContextSpec)} to * {@link #createContext(KeyUsage, Key, ContextSpec)} to prevent downgrade or
* prevent downgrade or misuse attacks. * misuse attacks.
* </p> * </p>
* *
* @since 1.0 * @since 1.0
@@ -123,16 +123,11 @@ public abstract class CryptoAlgorithm { // NOPMD
private final List<Capability> capabilities = new ArrayList<>(); private final List<Capability> capabilities = new ArrayList<>();
private final Map<KeyUsage, List<RoleBinding<?, ?, ?>>> ctxBindings = new EnumMap<>(KeyUsage.class); private final Map<KeyUsage, List<RoleBinding<?, ?, ?>>> ctxBindings = new EnumMap<>(KeyUsage.class);
private final Map<Class<? extends AlgorithmKeySpec>, AsymmetricKeyPairGenerator<?>> keyPairGenerators = private final Map<Class<? extends AlgorithmKeySpec>, AsymmetricKeyPairGenerator<?>> keyPairGenerators = new LinkedHashMap<>();
new LinkedHashMap<>(); private final Map<Class<? extends AlgorithmKeySpec>, PublicKeyImporter<?>> publicKeyImporters = new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, PublicKeyImporter<?>> publicKeyImporters = private final Map<Class<? extends AlgorithmKeySpec>, PrivateKeyImporter<?>> privateKeyImporters = new LinkedHashMap<>();
new LinkedHashMap<>(); private final Map<Class<? extends AlgorithmKeySpec>, SymmetricKeyGenerator<?>> symmetricKeyGenerators = new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, PrivateKeyImporter<?>> privateKeyImporters = private final Map<Class<? extends AlgorithmKeySpec>, SymmetricKeyImporter<?>> symmetricKeyImporters = new LinkedHashMap<>();
new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, SymmetricKeyGenerator<?>> symmetricKeyGenerators =
new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, SymmetricKeyImporter<?>> symmetricKeyImporters =
new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, AlgorithmKeySpec> asymmetricDefaults = new LinkedHashMap<>(); private final Map<Class<? extends AlgorithmKeySpec>, AlgorithmKeySpec> asymmetricDefaults = new LinkedHashMap<>();
private final Map<Class<? extends AlgorithmKeySpec>, AlgorithmKeySpec> symmetricDefaults = new LinkedHashMap<>(); private final Map<Class<? extends AlgorithmKeySpec>, AlgorithmKeySpec> symmetricDefaults = new LinkedHashMap<>();
@@ -296,8 +291,9 @@ public abstract class CryptoAlgorithm { // NOPMD
* <p> * <p>
* Concrete algorithms call this during construction to declare support for * Concrete algorithms call this during construction to declare support for
* specific roles (e.g., {@code ENCRYPT}, {@code VERIFY}). When * specific roles (e.g., {@code ENCRYPT}, {@code VERIFY}). When
* {@link #createContext(KeyUsage, Key, ContextSpec)} is later invoked, the provided * {@link #createContext(KeyUsage, Key, ContextSpec)} is later invoked, the
* {@code key} and optional {@code spec} are matched against these bindings. * provided {@code key} and optional {@code spec} are matched against these
* bindings.
* </p> * </p>
* *
* @param role supported {@link KeyUsage} role * @param role supported {@link KeyUsage} role
@@ -397,8 +393,7 @@ public abstract class CryptoAlgorithm { // NOPMD
if (rb.accepts(key, spec)) { if (rb.accepts(key, spec)) {
S resolved = (spec != null) ? spec S resolved = (spec != null) ? spec
: Objects.requireNonNull(rb.defaultSpec.get(), "defaultSpec value must not be null"); : Objects.requireNonNull(rb.defaultSpec.get(), "defaultSpec value must not be null");
C ctx = Objects.requireNonNull(rb.factory.createContext(key, resolved), C ctx = Objects.requireNonNull(rb.factory.createContext(key, resolved), _id + " factory returned null");
_id + " factory returned null");
// Enforce the declared context type contract: // Enforce the declared context type contract:
if (!rb.ctxType.isInstance(ctx)) { if (!rb.ctxType.isInstance(ctx)) {
throw new IllegalStateException(_id + " factory returned " + ctx.getClass().getName() throw new IllegalStateException(_id + " factory returned " + ctx.getClass().getName()
@@ -411,8 +406,7 @@ public abstract class CryptoAlgorithm { // NOPMD
+ (spec == null ? " (default spec)" : " and spec=" + spec.getClass().getName())); + (spec == null ? " (default spec)" : " and spec=" + spec.getClass().getName()));
} }
private <S extends AlgorithmKeySpec> S resolveDefault(Class<S> specType, private <S extends AlgorithmKeySpec> S resolveDefault(Class<S> specType, Supplier<? extends S> defaultSpecOrNull) {
Supplier<? extends S> defaultSpecOrNull) {
if (defaultSpecOrNull == null) { if (defaultSpecOrNull == null) {
return null; return null;
} }
@@ -426,16 +420,18 @@ public abstract class CryptoAlgorithm { // NOPMD
/** /**
* Registers asymmetric key-pair generation for one exact specification class. * Registers asymmetric key-pair generation for one exact specification class.
* *
* <p>The optional default is resolved and validated during registration. * <p>
* The optional default is resolved and validated during registration.
* Registered generators must be safe for concurrent invocation after the * Registered generators must be safe for concurrent invocation after the
* algorithm is published.</p> * algorithm is published.
* </p>
* *
* @param specType exact specification class * @param specType exact specification class
* @param generator non-null generator * @param generator non-null generator
* @param defaultSpecOrNull optional default supplier, evaluated once * @param defaultSpecOrNull optional default supplier, evaluated once
* @param <S> specification type * @param <S> specification type
* @throws NullPointerException if a required argument or supplied default is * @throws NullPointerException if a required argument or supplied default
* {@code null} * is {@code null}
* @throws IllegalArgumentException if the supplied default has the wrong type * @throws IllegalArgumentException if the supplied default has the wrong type
*/ */
protected final <S extends AlgorithmKeySpec> void registerAsymmetricKeyPairGenerator(Class<S> specType, protected final <S extends AlgorithmKeySpec> void registerAsymmetricKeyPairGenerator(Class<S> specType,
@@ -450,7 +446,7 @@ public abstract class CryptoAlgorithm { // NOPMD
* *
* @param specType exact specification class * @param specType exact specification class
* @param importer non-null importer safe for concurrent invocation * @param importer non-null importer safe for concurrent invocation
* @param <S> specification type * @param <S> specification type
* @throws NullPointerException if an argument is {@code null} * @throws NullPointerException if an argument is {@code null}
*/ */
protected final <S extends AlgorithmKeySpec> void registerPublicKeyImporter(Class<S> specType, protected final <S extends AlgorithmKeySpec> void registerPublicKeyImporter(Class<S> specType,
@@ -464,7 +460,7 @@ public abstract class CryptoAlgorithm { // NOPMD
* *
* @param specType exact specification class * @param specType exact specification class
* @param importer non-null importer safe for concurrent invocation * @param importer non-null importer safe for concurrent invocation
* @param <S> specification type * @param <S> specification type
* @throws NullPointerException if an argument is {@code null} * @throws NullPointerException if an argument is {@code null}
*/ */
protected final <S extends AlgorithmKeySpec> void registerPrivateKeyImporter(Class<S> specType, protected final <S extends AlgorithmKeySpec> void registerPrivateKeyImporter(Class<S> specType,
@@ -476,14 +472,16 @@ public abstract class CryptoAlgorithm { // NOPMD
/** /**
* Registers symmetric-key generation for one exact specification class. * Registers symmetric-key generation for one exact specification class.
* *
* <p>The optional default is resolved and validated during registration.</p> * <p>
* The optional default is resolved and validated during registration.
* </p>
* *
* @param specType exact specification class * @param specType exact specification class
* @param generator non-null generator safe for concurrent invocation * @param generator non-null generator safe for concurrent invocation
* @param defaultSpecOrNull optional default supplier, evaluated once * @param defaultSpecOrNull optional default supplier, evaluated once
* @param <S> specification type * @param <S> specification type
* @throws NullPointerException if a required argument or supplied default is * @throws NullPointerException if a required argument or supplied default
* {@code null} * is {@code null}
* @throws IllegalArgumentException if the supplied default has the wrong type * @throws IllegalArgumentException if the supplied default has the wrong type
*/ */
protected final <S extends AlgorithmKeySpec> void registerSymmetricKeyGenerator(Class<S> specType, protected final <S extends AlgorithmKeySpec> void registerSymmetricKeyGenerator(Class<S> specType,
@@ -498,7 +496,7 @@ public abstract class CryptoAlgorithm { // NOPMD
* *
* @param specType exact specification class * @param specType exact specification class
* @param importer non-null importer safe for concurrent invocation * @param importer non-null importer safe for concurrent invocation
* @param <S> specification type * @param <S> specification type
* @throws NullPointerException if an argument is {@code null} * @throws NullPointerException if an argument is {@code null}
*/ */
protected final <S extends AlgorithmKeySpec> void registerSymmetricKeyImporter(Class<S> specType, protected final <S extends AlgorithmKeySpec> void registerSymmetricKeyImporter(Class<S> specType,
@@ -515,12 +513,14 @@ public abstract class CryptoAlgorithm { // NOPMD
* Returns the asymmetric key-pair generator registered for an exact * Returns the asymmetric key-pair generator registered for an exact
* specification class. * specification class.
* *
* <p>The returned implementation may be shared and invoked concurrently.</p> * <p>
* The returned implementation may be shared and invoked concurrently.
* </p>
* *
* @param specType exact specification class; subclasses are not matched * @param specType exact specification class; subclasses are not matched
* @param <S> specification type * @param <S> specification type
* @return registered generator * @return registered generator
* @throws NullPointerException if {@code specType} is {@code null} * @throws NullPointerException if {@code specType} is {@code null}
* @throws IllegalArgumentException if no generator is registered * @throws IllegalArgumentException if no generator is registered
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -537,12 +537,14 @@ public abstract class CryptoAlgorithm { // NOPMD
/** /**
* Returns the public-key importer registered for an exact specification class. * Returns the public-key importer registered for an exact specification class.
* *
* <p>The returned implementation may be shared and invoked concurrently.</p> * <p>
* The returned implementation may be shared and invoked concurrently.
* </p>
* *
* @param specType exact specification class; subclasses are not matched * @param specType exact specification class; subclasses are not matched
* @param <S> specification type * @param <S> specification type
* @return registered importer * @return registered importer
* @throws NullPointerException if {@code specType} is {@code null} * @throws NullPointerException if {@code specType} is {@code null}
* @throws IllegalArgumentException if no importer is registered * @throws IllegalArgumentException if no importer is registered
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -556,15 +558,16 @@ public abstract class CryptoAlgorithm { // NOPMD
} }
/** /**
* Returns the private-key importer registered for an exact specification * Returns the private-key importer registered for an exact specification class.
* class.
* *
* <p>The returned implementation may be shared and invoked concurrently.</p> * <p>
* The returned implementation may be shared and invoked concurrently.
* </p>
* *
* @param specType exact specification class; subclasses are not matched * @param specType exact specification class; subclasses are not matched
* @param <S> specification type * @param <S> specification type
* @return registered importer * @return registered importer
* @throws NullPointerException if {@code specType} is {@code null} * @throws NullPointerException if {@code specType} is {@code null}
* @throws IllegalArgumentException if no importer is registered * @throws IllegalArgumentException if no importer is registered
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -581,12 +584,14 @@ public abstract class CryptoAlgorithm { // NOPMD
* Returns the symmetric-key generator registered for an exact specification * Returns the symmetric-key generator registered for an exact specification
* class. * class.
* *
* <p>The returned implementation may be shared and invoked concurrently.</p> * <p>
* The returned implementation may be shared and invoked concurrently.
* </p>
* *
* @param specType exact specification class; subclasses are not matched * @param specType exact specification class; subclasses are not matched
* @param <S> specification type * @param <S> specification type
* @return registered generator * @return registered generator
* @throws NullPointerException if {@code specType} is {@code null} * @throws NullPointerException if {@code specType} is {@code null}
* @throws IllegalArgumentException if no generator is registered * @throws IllegalArgumentException if no generator is registered
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -603,12 +608,14 @@ public abstract class CryptoAlgorithm { // NOPMD
* Returns the symmetric-key importer registered for an exact specification * Returns the symmetric-key importer registered for an exact specification
* class. * class.
* *
* <p>The returned implementation may be shared and invoked concurrently.</p> * <p>
* The returned implementation may be shared and invoked concurrently.
* </p>
* *
* @param specType exact specification class; subclasses are not matched * @param specType exact specification class; subclasses are not matched
* @param <S> specification type * @param <S> specification type
* @return registered importer * @return registered importer
* @throws NullPointerException if {@code specType} is {@code null} * @throws NullPointerException if {@code specType} is {@code null}
* @throws IllegalArgumentException if no importer is registered * @throws IllegalArgumentException if no importer is registered
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -628,14 +635,12 @@ public abstract class CryptoAlgorithm { // NOPMD
*/ */
public final List<KeyOperationInfo> keyOperations() { public final List<KeyOperationInfo> keyOperations() {
List<KeyOperationInfo> result = new ArrayList<>(); List<KeyOperationInfo> result = new ArrayList<>();
addOperationInfo(result, KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE, keyPairGenerators, addOperationInfo(result, KeyOperation.ASYMMETRIC_KEY_PAIR_GENERATE, keyPairGenerators, asymmetricDefaults);
asymmetricDefaults);
addOperationInfo(result, KeyOperation.ASYMMETRIC_PUBLIC_IMPORT, publicKeyImporters, Map.of()); addOperationInfo(result, KeyOperation.ASYMMETRIC_PUBLIC_IMPORT, publicKeyImporters, Map.of());
addOperationInfo(result, KeyOperation.ASYMMETRIC_PRIVATE_IMPORT, privateKeyImporters, Map.of()); addOperationInfo(result, KeyOperation.ASYMMETRIC_PRIVATE_IMPORT, privateKeyImporters, Map.of());
addOperationInfo(result, KeyOperation.SYMMETRIC_GENERATE, symmetricKeyGenerators, symmetricDefaults); addOperationInfo(result, KeyOperation.SYMMETRIC_GENERATE, symmetricKeyGenerators, symmetricDefaults);
addOperationInfo(result, KeyOperation.SYMMETRIC_IMPORT, symmetricKeyImporters, Map.of()); addOperationInfo(result, KeyOperation.SYMMETRIC_IMPORT, symmetricKeyImporters, Map.of());
result.sort(Comparator.comparing(KeyOperationInfo::operation) result.sort(Comparator.comparing(KeyOperationInfo::operation).thenComparing(info -> info.specType().getName()));
.thenComparing(info -> info.specType().getName()));
return List.copyOf(result); return List.copyOf(result);
} }

View File

@@ -17,10 +17,12 @@ import java.util.TreeMap;
/** /**
* Immutable registry of {@link CryptoAlgorithm} providers. * Immutable registry of {@link CryptoAlgorithm} providers.
* *
* <p>Providers are discovered once through {@link ServiceLoader}, sorted by * <p>
* Providers are discovered once through {@link ServiceLoader}, sorted by
* canonical algorithm identifier, and retained in one immutable registry. * canonical algorithm identifier, and retained in one immutable registry.
* Runtime policy and auditing belong exclusively to explicitly created * Runtime policy and auditing belong exclusively to explicitly created
* {@link zeroecho.sdk.ZeroEchoSession} instances.</p> * {@link zeroecho.sdk.ZeroEchoSession} instances.
* </p>
* *
* @since 1.0 * @since 1.0
*/ */

View File

@@ -93,9 +93,9 @@ public final class CryptoCatalog {
* {@link CryptoAlgorithms}. * {@link CryptoAlgorithms}.
* *
* <p> * <p>
* Provider discovery, deterministic ordering, and duplicate checking occur * Provider discovery, deterministic ordering, and duplicate checking occur once
* once in {@code CryptoAlgorithms}. This method neither scans providers nor * in {@code CryptoAlgorithms}. This method neither scans providers nor copies
* copies their collection. * their collection.
* </p> * </p>
* *
* @return an immutable {@code CryptoCatalog} with all discovered algorithms * @return an immutable {@code CryptoCatalog} with all discovered algorithms
@@ -158,40 +158,16 @@ public final class CryptoCatalog {
/** /**
* Serializes the catalog to a compact JSON document. * Serializes the catalog to a compact JSON document.
* *
* <p> * <p> The schema is: </p> <pre>{@code { "algorithms": [ { "id": "AES/GCM",
* The schema is: * "displayName": "AES-GCM", "capabilities": [ { "family": "SYMMETRIC", "role":
* </p> * "ENCRYPT", "contextType": "AeadEncryptContext", "keyType": "SecretKey",
* <pre>{@code * "specType": "AeadSpec", "defaultSpec": "Random nonce, 128-bit tag" } ],
* { * "asymmetricKeyBuilders": [ { "specType": "Ed25519Spec", "defaultKeySpec":
* "algorithms": [ * "Ed25519 default" } ], "symmetricKeyBuilders": [ { "specType": "AesKeySpec",
* { * "defaultKeySpec": "AES-256" } ] } ] } }</pre>
* "id": "AES/GCM",
* "displayName": "AES-GCM",
* "capabilities": [
* {
* "family": "SYMMETRIC",
* "role": "ENCRYPT",
* "contextType": "AeadEncryptContext",
* "keyType": "SecretKey",
* "specType": "AeadSpec",
* "defaultSpec": "Random nonce, 128-bit tag"
* }
* ],
* "asymmetricKeyBuilders": [
* { "specType": "Ed25519Spec", "defaultKeySpec": "Ed25519 default" }
* ],
* "symmetricKeyBuilders": [
* { "specType": "AesKeySpec", "defaultKeySpec": "AES-256" }
* ]
* }
* ]
* }
* }</pre>
* *
* <p> * <p> String values are escaped for quotes and backslashes. The method does not
* String values are escaped for quotes and backslashes. The method does not * attempt to pretty-print; callers can format the output if needed. </p>
* attempt to pretty-print; callers can format the output if needed.
* </p>
* *
* @return a JSON string describing algorithms, capabilities, and key builders * @return a JSON string describing algorithms, capabilities, and key builders
*/ */
@@ -230,8 +206,7 @@ public final class CryptoCatalog {
} }
firstOperation = false; firstOperation = false;
sb.append('{').append(jsonField("operation", operation.operation().name())).append(',') sb.append('{').append(jsonField("operation", operation.operation().name())).append(',')
.append(jsonField("specType", operation.specType().getSimpleName())) .append(jsonField("specType", operation.specType().getSimpleName())).append(",\"defaultSpec\":")
.append(",\"defaultSpec\":")
.append(operation.defaultSpec() == null ? "null" : jsonString(labelOf(operation.defaultSpec()))) .append(operation.defaultSpec() == null ? "null" : jsonString(labelOf(operation.defaultSpec())))
.append('}'); .append('}');
} }
@@ -273,9 +248,8 @@ public final class CryptoCatalog {
} }
sb.append("</capabilities><keyOperations>"); sb.append("</capabilities><keyOperations>");
for (KeyOperationInfo operation : a.keyOperations()) { for (KeyOperationInfo operation : a.keyOperations()) {
sb.append("<keyOperation operation=\"").append(operation.operation().name()) sb.append("<keyOperation operation=\"").append(operation.operation().name()).append("\" specType=\"")
.append("\" specType=\"").append(esc(operation.specType().getSimpleName())) .append(esc(operation.specType().getSimpleName())).append("\"><defaultSpec>")
.append("\"><defaultSpec>")
.append(operation.defaultSpec() == null ? "" : esc(labelOf(operation.defaultSpec()))) .append(operation.defaultSpec() == null ? "" : esc(labelOf(operation.defaultSpec())))
.append("</defaultSpec></keyOperation>"); .append("</defaultSpec></keyOperation>");
} }

View File

@@ -14,20 +14,20 @@ import zeroecho.core.spec.AlgorithmKeySpec;
/** /**
* Immutable metadata for one exact key operation. * Immutable metadata for one exact key operation.
* *
* @param operation operation guaranteed by the associated lookup * @param operation operation guaranteed by the associated lookup
* @param specType exact accepted specification type * @param specType exact accepted specification type
* @param defaultSpec resolved generation default, or {@code null} for import * @param defaultSpec resolved generation default, or {@code null} for import
* operations and generators without a default * operations and generators without a default
* @since 1.0 * @since 1.0
*/ */
public record KeyOperationInfo(KeyOperation operation, public record KeyOperationInfo(KeyOperation operation, Class<? extends AlgorithmKeySpec> specType,
Class<? extends AlgorithmKeySpec> specType, AlgorithmKeySpec defaultSpec) { AlgorithmKeySpec defaultSpec) {
/** /**
* Validates the metadata invariant. * Validates the metadata invariant.
* *
* @throws NullPointerException if {@code operation} or {@code specType} is * @throws NullPointerException if {@code operation} or {@code specType} is
* {@code null} * {@code null}
* @throws IllegalArgumentException if a default is incompatible with * @throws IllegalArgumentException if a default is incompatible with
* {@code specType}, or an import operation * {@code specType}, or an import operation
* declares a default * declares a default
@@ -38,9 +38,9 @@ public record KeyOperationInfo(KeyOperation operation,
if (defaultSpec != null && !specType.isInstance(defaultSpec)) { if (defaultSpec != null && !specType.isInstance(defaultSpec)) {
throw new IllegalArgumentException("defaultSpec must be an instance of " + specType.getName()); throw new IllegalArgumentException("defaultSpec must be an instance of " + specType.getName());
} }
if (defaultSpec != null && (operation == KeyOperation.SYMMETRIC_IMPORT if (defaultSpec != null
|| operation == KeyOperation.ASYMMETRIC_PUBLIC_IMPORT && (operation == KeyOperation.SYMMETRIC_IMPORT || operation == KeyOperation.ASYMMETRIC_PUBLIC_IMPORT
|| operation == KeyOperation.ASYMMETRIC_PRIVATE_IMPORT)) { || operation == KeyOperation.ASYMMETRIC_PRIVATE_IMPORT)) {
throw new IllegalArgumentException("import operations cannot declare a default specification"); throw new IllegalArgumentException("import operations cannot declare a default specification");
} }
} }

View File

@@ -147,8 +147,8 @@ public abstract class AbstractCryptoAlgorithm extends CryptoAlgorithm {
* *
* <h4>Validation</h4> Type checks happen at creation time (via {@code bind}) * <h4>Validation</h4> Type checks happen at creation time (via {@code bind})
* and again when * and again when
* {@link CryptoAlgorithm#createContext(KeyUsage, Key, ContextSpec)} is * {@link CryptoAlgorithm#createContext(KeyUsage, Key, ContextSpec)} is called.
* called. If a factory returns a context not assignable to {@code ctxType}, an * If a factory returns a context not assignable to {@code ctxType}, an
* {@link IllegalStateException} will be thrown. * {@link IllegalStateException} will be thrown.
* *
* @param family high-level algorithm family classification * @param family high-level algorithm family classification

View File

@@ -63,9 +63,10 @@ import zeroecho.core.util.RandomSupport;
* *
* <p> * <p>
* IV and optional AAD are exchanged via a {@link conflux.CtxInterface} set with * IV and optional AAD are exchanged via a {@link conflux.CtxInterface} set with
* {@link #setContext(conflux.CtxInterface)}. Encryption always generates a fresh * {@link #setContext(conflux.CtxInterface)}. Encryption always generates a
* IV after atomically claiming the context; a caller-provided IV is never used * fresh IV after atomically claiming the context; a caller-provided IV is never
* for encryption. Decryption requires the IV from the context or encoded header. * used for encryption. Decryption requires the IV from the context or encoded
* header.
* </p> * </p>
* *
* <p> * <p>
@@ -311,10 +312,7 @@ public final class AesCipherContext implements EncryptionContext, ContextAware {
/** Single-use encryption lifecycle states. */ /** Single-use encryption lifecycle states. */
private enum OperationState { private enum OperationState {
NEW, NEW, ENCRYPTING, COMPLETED, FAILED
ENCRYPTING,
COMPLETED,
FAILED
} }
/** Marks the owning encryption context terminal as its stream is consumed. */ /** Marks the owning encryption context terminal as its stream is consumed. */

View File

@@ -60,8 +60,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* </p> * </p>
* *
* <p> * <p>
* Objects of this type are thread-safe while active and may be destroyed to wipe * Objects of this type are thread-safe while active and may be destroyed to
* their owned key bytes. Access and marshalling fail after destruction. * wipe their owned key bytes. Access and marshalling fail after destruction.
* </p> * </p>
* *
* @since 1.0 * @since 1.0

View File

@@ -243,8 +243,8 @@ abstract class AbstractChaChaCipherContext<S extends ChaChaBaseSpec> implements
* Ensures a nonce is available in the context. * Ensures a nonce is available in the context.
* *
* <ul> * <ul>
* <li>For encryption, always generates a new nonce after the context has * <li>For encryption, always generates a new nonce after the context has been
* been atomically claimed and stores a copy in the context.</li> * atomically claimed and stores a copy in the context.</li>
* <li>For decryption, validates presence and correct length.</li> * <li>For decryption, validates presence and correct length.</li>
* </ul> * </ul>
* *
@@ -283,10 +283,7 @@ abstract class AbstractChaChaCipherContext<S extends ChaChaBaseSpec> implements
/** Single-use encryption lifecycle states. */ /** Single-use encryption lifecycle states. */
private enum OperationState { private enum OperationState {
NEW, NEW, ENCRYPTING, COMPLETED, FAILED
ENCRYPTING,
COMPLETED,
FAILED
} }
/** Marks the owning encryption context terminal as its stream is consumed. */ /** Marks the owning encryption context terminal as its stream is consumed. */

View File

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

View File

@@ -39,10 +39,10 @@
* the stream cipher ChaCha20 and the AEAD construction ChaCha20-Poly1305. The * the stream cipher ChaCha20 and the AEAD construction ChaCha20-Poly1305. The
* module contains algorithm descriptors, streaming cipher contexts, * module contains algorithm descriptors, streaming cipher contexts,
* configuration specifications, optional header codecs for runtime parameters, * configuration specifications, optional header codecs for runtime parameters,
* and symmetric key import/generation specifications. Key import * and symmetric key import/generation specifications. Key import specifications
* specifications are destroyable. The design favors safe defaults * are destroyable. The design favors safe defaults (12-byte nonces, 128-bit
* (12-byte nonces, 128-bit AEAD tag), explicit role-to-context binding, and a * AEAD tag), explicit role-to-context binding, and a clear separation between
* clear separation between static configuration and per-operation parameters. * static configuration and per-operation parameters.
* </p> * </p>
* *
* <h2>Components</h2> * <h2>Components</h2>

View File

@@ -44,9 +44,9 @@ import zeroecho.core.context.AgreementContext;
* <h2>Generic JCA-based Key Agreement Context</h2> * <h2>Generic JCA-based Key Agreement Context</h2>
* *
* An {@link AgreementContext} backed by the standard JCA key-agreement API. * An {@link AgreementContext} backed by the standard JCA key-agreement API.
* This class supports elliptic-curve and modern Diffie-Hellman variants * This class supports elliptic-curve and modern Diffie-Hellman variants such as
* such as ECDH, XDH (X25519, X448), and others provided by the runtime or * ECDH, XDH (X25519, X448), and others provided by the runtime or configured
* configured provider. * provider.
* *
* <p> * <p>
* Instances of this context are created with a local {@link PrivateKey}, and * Instances of this context are created with a local {@link PrivateKey}, and

View File

@@ -99,9 +99,9 @@
* reconstructs public keys from X.509 encodings via * reconstructs public keys from X.509 encodings via
* {@link java.security.KeyFactory}.</li> * {@link java.security.KeyFactory}.</li>
* <li><b>Signature contexts:</b> * <li><b>Signature contexts:</b>
* {@link zeroecho.core.alg.common.eddsa.CommonEdDSASignatureContext} * {@link zeroecho.core.alg.common.eddsa.CommonEdDSASignatureContext} delegates
* delegates all operations to a generic JCA-backed signature adapter, enforcing * all operations to a generic JCA-backed signature adapter, enforcing a fixed
* a fixed tag length for the selected EdDSA variant.</li> * tag length for the selected EdDSA variant.</li>
* </ul> * </ul>
* *
* <h2>Design notes</h2> * <h2>Design notes</h2>

View File

@@ -116,7 +116,8 @@ public final class SignatureInteropProfile { // NOPMD
* @param keyAlgorithmId ZeroEcho key algorithm identifier used for key * @param keyAlgorithmId ZeroEcho key algorithm identifier used for key
* import and matching, such as {@code RSA} * import and matching, such as {@code RSA}
* @param contextAlgorithmId ZeroEcho context algorithm identifier used * @param contextAlgorithmId ZeroEcho context algorithm identifier used
* with {@code ZeroEchoSession.createContext(...)} * with
* {@code ZeroEchoSession.createContext(...)}
* @param contextSpec explicit ZeroEcho context specification * @param contextSpec explicit ZeroEcho context specification
* @param signatureRepresentation signature representation bridge between * @param signatureRepresentation signature representation bridge between
* external bytes and internal ZeroEcho bytes * external bytes and internal ZeroEcho bytes

View File

@@ -60,9 +60,9 @@
* configured {@link java.security.Signature}, resolves a fixed tag length (via * configured {@link java.security.Signature}, resolves a fixed tag length (via
* resolvers), and exposes a one-shot {@code wrap(InputStream)} API. * resolvers), and exposes a one-shot {@code wrap(InputStream)} API.
* Verification behavior is controlled by a pluggable comparison approach.</li> * Verification behavior is controlled by a pluggable comparison approach.</li>
* <li><b>SignatureStream</b> - internal passthrough input stream that feeds chunks to * <li><b>SignatureStream</b> - internal passthrough input stream that feeds
* the signature engine, emits the trailer in SIGN mode, and performs final * chunks to the signature engine, emits the trailer in SIGN mode, and performs
* verification in VERIFY mode.</li> * final verification in VERIFY mode.</li>
* </ul> * </ul>
* *
* <h2>Length resolution</h2> * <h2>Length resolution</h2>

View File

@@ -117,8 +117,8 @@ public final class Sha2Sha3Algorithm extends AbstractCryptoAlgorithm {
MessageDigest md = MessageDigest.getInstance(s.algorithm().jca()); MessageDigest md = MessageDigest.getInstance(s.algorithm().jca());
return new JcaDigestContext(this, md, s); return new JcaDigestContext(this, md, s);
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
throw new ProviderFailureException( throw new ProviderFailureException("Failed to initialize MessageDigest " + s.algorithm().jca(),
"Failed to initialize MessageDigest " + s.algorithm().jca(), e); e);
} }
}, DigestSpec::sha256 // default for catalog/tests }, DigestSpec::sha256 // default for catalog/tests
); );

View File

@@ -160,8 +160,7 @@ public final class EcdhAlgorithm extends AbstractCryptoAlgorithm {
() -> EcdsaCurveSpec.P256); () -> EcdsaCurveSpec.P256);
// Reuse EC builders/importers // Reuse EC builders/importers
registerAsymmetricKeyPairGenerator(EcdhCurveSpec.class, new EcdhKeyGenBuilder(), registerAsymmetricKeyPairGenerator(EcdhCurveSpec.class, new EcdhKeyGenBuilder(), () -> EcdhCurveSpec.P256);
() -> EcdhCurveSpec.P256);
registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder()); registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder());
registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder()); registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder());
} }

View File

@@ -45,8 +45,8 @@ import zeroecho.core.spi.AsymmetricKeyPairGenerator;
/** /**
* <h2>ECDH Key Pair Generator</h2> * <h2>ECDH Key Pair Generator</h2>
* *
* Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for elliptic curve * Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for
* Diffie-Hellman (ECDH) key pairs. * elliptic curve Diffie-Hellman (ECDH) key pairs.
* *
* <p> * <p>
* This builder generates fresh EC key pairs suitable for ECDH key agreement. It * This builder generates fresh EC key pairs suitable for ECDH key agreement. It

View File

@@ -103,8 +103,8 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm {
* <p> * <p>
* On construction, the algorithm declares its supported roles and registers * On construction, the algorithm declares its supported roles and registers
* builders with the {@link CryptoAlgorithm} infrastructure so they can be * builders with the {@link CryptoAlgorithm} infrastructure so they can be
* discovered by the {@link CryptoCatalog} or invoked through the * discovered by the {@link CryptoCatalog} or invoked through the session-bound
* session-bound {@link zeroecho.sdk.KeyBuilders} entry point. * {@link zeroecho.sdk.KeyBuilders} entry point.
* </p> * </p>
*/ */
public EcdsaAlgorithm() { public EcdsaAlgorithm() {
@@ -134,8 +134,7 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm {
} }
}, () -> EcdsaCurveSpec.P256); }, () -> EcdsaCurveSpec.P256);
registerAsymmetricKeyPairGenerator(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(), registerAsymmetricKeyPairGenerator(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(), () -> EcdsaCurveSpec.P256);
() -> EcdsaCurveSpec.P256);
registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder()); registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder());
registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder()); registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder());
} }

View File

@@ -45,14 +45,14 @@ import zeroecho.core.spi.AsymmetricKeyPairGenerator;
* <h2>ECDSA Key Pair Generator</h2> * <h2>ECDSA Key Pair Generator</h2>
* *
* Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for * Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for
* {@link EcdsaCurveSpec}. * {@link EcdsaCurveSpec}. This builder is responsible for generating new
* This builder is responsible for generating new elliptic curve key pairs for * elliptic curve key pairs for use with the {@link EcdsaAlgorithm}.
* use with the {@link EcdsaAlgorithm}.
* *
* <p>The exact supported operation is * <p>
* {@link #generateKeyPair(EcdsaCurveSpec)}. Public and private import are * The exact supported operation is {@link #generateKeyPair(EcdsaCurveSpec)}.
* registered separately through {@link EcdsaPublicKeyBuilder} and * Public and private import are registered separately through
* {@link EcdsaPrivateKeyBuilder}.</p> * {@link EcdsaPublicKeyBuilder} and {@link EcdsaPrivateKeyBuilder}.
* </p>
* *
* <h2>Usage</h2> Typically accessed through the session key-operation API or * <h2>Usage</h2> Typically accessed through the session key-operation API or
* {@link CryptoAlgorithm#asymmetricKeyPairGenerator(Class)}. * {@link CryptoAlgorithm#asymmetricKeyPairGenerator(Class)}.

View File

@@ -49,9 +49,11 @@ import zeroecho.core.spi.PrivateKeyImporter;
* {@link EcdsaPrivateKeySpec}. This builder is responsible for importing ECDSA * {@link EcdsaPrivateKeySpec}. This builder is responsible for importing ECDSA
* private keys from encoded representations. * private keys from encoded representations.
* *
* <p>The exact supported operation is * <p>
* {@link #importPrivate(EcdsaPrivateKeySpec)}. Generation and public import are * The exact supported operation is {@link #importPrivate(EcdsaPrivateKeySpec)}.
* registered through their own operation-specific implementations.</p> * Generation and public import are registered through their own
* operation-specific implementations.
* </p>
* *
* <h2>Encoding</h2> The {@link EcdsaPrivateKeySpec} stores the private key in * <h2>Encoding</h2> The {@link EcdsaPrivateKeySpec} stores the private key in
* PKCS#8 DER format. This builder delegates to a JCA {@link KeyFactory} for the * PKCS#8 DER format. This builder delegates to a JCA {@link KeyFactory} for the

View File

@@ -48,9 +48,11 @@ import zeroecho.core.spi.PublicKeyImporter;
* {@link EcdsaPublicKeySpec}. This builder is responsible for importing ECDSA * {@link EcdsaPublicKeySpec}. This builder is responsible for importing ECDSA
* public keys from X.509 SubjectPublicKeyInfo encodings. * public keys from X.509 SubjectPublicKeyInfo encodings.
* *
* <p>The exact supported operation is * <p>
* {@link #importPublic(EcdsaPublicKeySpec)}. Generation and private import are * The exact supported operation is {@link #importPublic(EcdsaPublicKeySpec)}.
* registered through their own operation-specific implementations.</p> * Generation and private import are registered through their own
* operation-specific implementations.
* </p>
* *
* <h2>Encoding</h2> The {@link EcdsaPublicKeySpec} stores the public key in * <h2>Encoding</h2> The {@link EcdsaPublicKeySpec} stores the public key in
* standard X.509 DER format. This builder delegates to a JCA {@link KeyFactory} * standard X.509 DER format. This builder delegates to a JCA {@link KeyFactory}

View File

@@ -38,8 +38,8 @@ import zeroecho.core.alg.common.eddsa.AbstractEdDSAKeyGenBuilder;
/** /**
* <h2>Key-pair builder for Ed25519</h2> * <h2>Key-pair builder for Ed25519</h2>
* *
* Concrete {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} implementation for * Concrete {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} implementation
* generating Ed25519 key pairs. * for generating Ed25519 key pairs.
* *
* <p> * <p>
* This builder delegates to the JCA provider under the canonical algorithm name * This builder delegates to the JCA provider under the canonical algorithm name

View File

@@ -38,8 +38,8 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPrivateKeyBuilder;
/** /**
* <h2>Private key builder for Ed25519</h2> * <h2>Private key builder for Ed25519</h2>
* *
* Concrete {@link zeroecho.core.spi.PrivateKeyImporter} for importing * Concrete {@link zeroecho.core.spi.PrivateKeyImporter} for importing wrapping
* wrapping Ed25519 private keys. * Ed25519 private keys.
* *
* <p> * <p>
* This builder integrates with the JCA under the canonical key factory * This builder integrates with the JCA under the canonical key factory

View File

@@ -38,8 +38,8 @@ import zeroecho.core.alg.common.eddsa.AbstractEncodedPublicKeyBuilder;
/** /**
* <h2>Public key builder for Ed25519</h2> * <h2>Public key builder for Ed25519</h2>
* *
* Concrete {@link zeroecho.core.spi.PublicKeyImporter} for importing * Concrete {@link zeroecho.core.spi.PublicKeyImporter} for importing wrapping
* wrapping Ed25519 public keys. * Ed25519 public keys.
* *
* <p> * <p>
* This builder integrates with the JCA under the canonical key factory * This builder integrates with the JCA under the canonical key factory

View File

@@ -66,8 +66,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each call to * <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each call to
* {@link zeroecho.core.spi.PrivateKeyImporter#importPrivate(AlgorithmKeySpec)} * {@link zeroecho.core.spi.PrivateKeyImporter#importPrivate(AlgorithmKeySpec)}
* creates a new * creates a new {@link java.security.KeyFactory}.
* {@link java.security.KeyFactory}.
* *
* @since 1.0 * @since 1.0
*/ */

View File

@@ -65,8 +65,7 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* *
* <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each call to * <h2>Thread-safety</h2> Stateless and safe for concurrent use. Each call to
* {@link zeroecho.core.spi.PublicKeyImporter#importPublic(AlgorithmKeySpec)} * {@link zeroecho.core.spi.PublicKeyImporter#importPublic(AlgorithmKeySpec)}
* creates a new * creates a new {@link java.security.KeyFactory}.
* {@link java.security.KeyFactory}.
* *
* @since 1.0 * @since 1.0
*/ */

View File

@@ -63,7 +63,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* </p> * </p>
* *
* @see KyberAlgorithm * @see KyberAlgorithm
* @see zeroecho.sdk.KeyBuilders.Asymmetric#generateKeyPair(String, AlgorithmKeySpec) * @see zeroecho.sdk.KeyBuilders.Asymmetric#generateKeyPair(String,
* AlgorithmKeySpec)
*/ */
public final class KyberKeyGenSpec implements AlgorithmKeySpec, Describable { public final class KyberKeyGenSpec implements AlgorithmKeySpec, Describable {
/** /**

View File

@@ -47,9 +47,9 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Specification wrapper for a Kyber (ML-KEM) private key encoded in PKCS#8. * Specification wrapper for a Kyber (ML-KEM) private key encoded in PKCS#8.
* *
* <p> * <p>
* Instances of this class carry an owned copy of the PKCS#8-encoded private * Instances of this class carry an owned copy of the PKCS#8-encoded private key
* key bytes. They are used with {@link zeroecho.core.CryptoAlgorithm} key * bytes. They are used with {@link zeroecho.core.CryptoAlgorithm} key builders
* builders to import keys into the providers native representation. * to import keys into the providers native representation.
* </p> * </p>
* *
* <h2>Encoding</h2> * <h2>Encoding</h2>

View File

@@ -102,8 +102,7 @@ public record BlockGeometry(int inChunkSize, int outChunkSize, int finalizationO
"inChunkSize must not exceed outChunkSize: " + inChunkSize + " > " + outChunkSize); "inChunkSize must not exceed outChunkSize: " + inChunkSize + " > " + outChunkSize);
} }
if (finalizationOutputChunks != 0) { if (finalizationOutputChunks != 0) {
throw new IllegalArgumentException( throw new IllegalArgumentException("finalizationOutputChunks must be zero: " + finalizationOutputChunks);
"finalizationOutputChunks must be zero: " + finalizationOutputChunks);
} }
} }

View File

@@ -43,7 +43,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* A {@code SaberKeyGenSpec} selects one of the SABER parameter variants * A {@code SaberKeyGenSpec} selects one of the SABER parameter variants
* standardized in round-3 submissions. Each variant balances performance, * standardized in round-3 submissions. Each variant balances performance,
* bandwidth, and security level. This spec is passed to a registered * bandwidth, and security level. This spec is passed to a registered
* {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} to generate a SABER key pair. * {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} to generate a SABER key
* pair.
* </p> * </p>
* *
* <h2>Variants</h2> The {@link Variant} enumeration identifies supported SABER * <h2>Variants</h2> The {@link Variant} enumeration identifies supported SABER

View File

@@ -54,10 +54,12 @@ import zeroecho.core.spi.AsymmetricKeyPairGenerator;
* Reflection is used to avoid a hard dependency on all parameter variants. * Reflection is used to avoid a hard dependency on all parameter variants.
* </p> * </p>
* *
* <p>The exact supported operation is * <p>
* {@link #generateKeyPair(SphincsPlusKeyGenSpec)}. Public and private import are * The exact supported operation is
* registered separately for {@link SphincsPlusPublicKeySpec} and * {@link #generateKeyPair(SphincsPlusKeyGenSpec)}. Public and private import
* {@link SphincsPlusPrivateKeySpec}.</p> * are registered separately for {@link SphincsPlusPublicKeySpec} and
* {@link SphincsPlusPrivateKeySpec}.
* </p>
* *
* <h2>Example</h2> <pre>{@code * <h2>Example</h2> <pre>{@code
* SphincsPlusKeyGenSpec spec = * SphincsPlusKeyGenSpec spec =

View File

@@ -51,9 +51,11 @@ import zeroecho.core.spi.PrivateKeyImporter;
* pairs but focuses solely on importing private key material. * pairs but focuses solely on importing private key material.
* </p> * </p>
* *
* <p>The exact supported operation is * <p>
* The exact supported operation is
* {@link #importPrivate(SphincsPlusPrivateKeySpec)}. Other key operations are * {@link #importPrivate(SphincsPlusPrivateKeySpec)}. Other key operations are
* registered through their own exact interfaces.</p> * registered through their own exact interfaces.
* </p>
* *
* <h2>Example</h2> <pre>{@code * <h2>Example</h2> <pre>{@code
* // Assuming bytes contain a PKCS#8-encoded SPHINCS+ private key: * // Assuming bytes contain a PKCS#8-encoded SPHINCS+ private key:

View File

@@ -48,7 +48,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <p> * <p>
* {@code SphincsPlusPrivateKeySpec} wraps a PKCS#8-encoded SPHINCS+ private key * {@code SphincsPlusPrivateKeySpec} wraps a PKCS#8-encoded SPHINCS+ private key
* along with the provider name that should be used for import. It is a simple * along with the provider name that should be used for import. It is a simple
* destroyable holder designed for use with {@link SphincsPlusPrivateKeyBuilder}. * destroyable holder designed for use with
* {@link SphincsPlusPrivateKeyBuilder}.
* </p> * </p>
* *
* <h2>Encoding</h2> * <h2>Encoding</h2>

View File

@@ -50,9 +50,11 @@ import zeroecho.core.spi.PublicKeyImporter;
* pairs, but focuses solely on importing public key material. * pairs, but focuses solely on importing public key material.
* </p> * </p>
* *
* <p>The exact supported operation is * <p>
* The exact supported operation is
* {@link #importPublic(SphincsPlusPublicKeySpec)}. Other key operations are * {@link #importPublic(SphincsPlusPublicKeySpec)}. Other key operations are
* registered through their own exact interfaces.</p> * registered through their own exact interfaces.
* </p>
* *
* <h2>Example</h2> <pre>{@code * <h2>Example</h2> <pre>{@code
* // Assuming bytes contain an X.509-encoded SPHINCS+ public key: * // Assuming bytes contain an X.509-encoded SPHINCS+ public key:

View File

@@ -51,7 +51,8 @@ import zeroecho.core.spi.AsymmetricKeyPairGenerator;
* <h2>Design and scope</h2> * <h2>Design and scope</h2>
* <ul> * <ul>
* <li><b>Generation only:</b> This implementation exposes only the exact * <li><b>Generation only:</b> This implementation exposes only the exact
* key-pair generation capability; import operations are registered separately.</li> * key-pair generation capability; import operations are registered
* separately.</li>
* <li><b>Provider resolution:</b> The default JCA provider selection is used. * <li><b>Provider resolution:</b> The default JCA provider selection is used.
* If a specific provider is required, supply or register one that exposes the * If a specific provider is required, supply or register one that exposes the
* requested XDH algorithm name.</li> * requested XDH algorithm name.</li>

View File

@@ -13,10 +13,12 @@ import java.util.Objects;
/** /**
* Utilities for enforcing the best-effort audit-listener contract. * Utilities for enforcing the best-effort audit-listener contract.
* *
* <p>The returned listener suppresses listener failures without logging callback * <p>
* The returned listener suppresses listener failures without logging callback
* arguments, because those arguments may refer to sensitive cryptographic * arguments, because those arguments may refer to sensitive cryptographic
* objects. Cryptographic operation outcomes therefore never depend on an audit * objects. Cryptographic operation outcomes therefore never depend on an audit
* sink's availability.</p> * sink's availability.
* </p>
* *
* @since 1.0 * @since 1.0
*/ */
@@ -36,8 +38,8 @@ public final class AuditListeners {
AuditListener target = Objects.requireNonNull(listener, "listener"); AuditListener target = Objects.requireNonNull(listener, "listener");
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
ClassLoader loader = contextLoader == null ? ClassLoader.getSystemClassLoader() : contextLoader; ClassLoader loader = contextLoader == null ? ClassLoader.getSystemClassLoader() : contextLoader;
return (AuditListener) Proxy.newProxyInstance(loader, return (AuditListener) Proxy.newProxyInstance(loader, new Class<?>[] { AuditListener.class },
new Class<?>[] { AuditListener.class }, (proxy, method, arguments) -> { (proxy, method, arguments) -> {
if (method.getDeclaringClass() == Object.class) { if (method.getDeclaringClass() == Object.class) {
return method.invoke(target, arguments); return method.invoke(target, arguments);
} }

View File

@@ -10,8 +10,10 @@ package zeroecho.core.audit;
/** /**
* Defines the session-owned automatic auditing strategy. * Defines the session-owned automatic auditing strategy.
* *
* <p>Audit listener failures are best-effort diagnostics and never change the * <p>
* outcome of a cryptographic operation.</p> * Audit listener failures are best-effort diagnostics and never change the
* outcome of a cryptographic operation.
* </p>
* *
* @since 1.0 * @since 1.0
*/ */

View File

@@ -105,8 +105,8 @@ import zeroecho.core.spec.ContextSpec;
* <li>Counting is performed by decorating the returned {@code InputStream}s; no * <li>Counting is performed by decorating the returned {@code InputStream}s; no
* buffering beyond normal {@code FilterInputStream} forwarding is * buffering beyond normal {@code FilterInputStream} forwarding is
* introduced.</li> * introduced.</li>
* <li>Idempotent wrapping: contexts already wrapped by this utility are returned * <li>Idempotent wrapping: contexts already wrapped by this utility are
* unchanged. Unrelated JDK proxies are wrapped normally.</li> * returned unchanged. Unrelated JDK proxies are wrapped normally.</li>
* </ul> * </ul>
* *
* <h2>Usage example</h2> <pre>{@code * <h2>Usage example</h2> <pre>{@code
@@ -190,8 +190,7 @@ public final class AuditedContexts {
} }
private static boolean isAuditedProxy(CryptoContext context) { private static boolean isAuditedProxy(CryptoContext context) {
return Proxy.isProxyClass(context.getClass()) return Proxy.isProxyClass(context.getClass()) && Proxy.getInvocationHandler(context) instanceof AuditingHandler;
&& Proxy.getInvocationHandler(context) instanceof AuditingHandler;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -242,8 +241,7 @@ public final class AuditedContexts {
} }
safeAudit.onContextCreatedMeta(ctxId, algoId == null ? UNKNOWN : algoId, safeAudit.onContextCreatedMeta(ctxId, algoId == null ? UNKNOWN : algoId,
provider == null ? UNKNOWN : provider, provider == null ? UNKNOWN : provider, role, keyFp, specMeta);
role, keyFp, specMeta);
} }
ClassLoader cl = ctx.getClass().getClassLoader(); // NOPMD ClassLoader cl = ctx.getClass().getClassLoader(); // NOPMD
@@ -720,8 +718,7 @@ public final class AuditedContexts {
StringBuilder fingerprint = new StringBuilder(16); StringBuilder fingerprint = new StringBuilder(16);
for (int index = 0; index < Math.min(8, digest.length); index++) { for (int index = 0; index < Math.min(8, digest.length); index++) {
int value = digest[index] & 0xff; int value = digest[index] & 0xff;
fingerprint.append(Character.forDigit(value >>> 4, 16)) fingerprint.append(Character.forDigit(value >>> 4, 16)).append(Character.forDigit(value & 0x0f, 16));
.append(Character.forDigit(value & 0x0f, 16));
} }
return key.getAlgorithm() + ":" + fingerprint; return key.getAlgorithm() + ":" + fingerprint;
} catch (NoSuchAlgorithmException exception) { } catch (NoSuchAlgorithmException exception) {

View File

@@ -204,9 +204,9 @@ public final class JulAuditListenerStd implements AuditListener {
* appends a stack trace in addition to the structured summary. * appends a stack trace in addition to the structured summary.
* *
* <p> * <p>
* Stack traces may contain provider exception messages or application * Stack traces may contain provider exception messages or application values.
* values. Enabling them is an explicit diagnostic opt-in and requires a * Enabling them is an explicit diagnostic opt-in and requires a suitably
* suitably protected log destination. * protected log destination.
* </p> * </p>
* *
* @param include true to include stack traces, false to omit them * @param include true to include stack traces, false to omit them
@@ -600,8 +600,7 @@ public final class JulAuditListenerStd implements AuditListener {
StringBuilder sb = new StringBuilder(key.getAlgorithm()).append(':'); StringBuilder sb = new StringBuilder(key.getAlgorithm()).append(':');
for (int i = 0; i < Math.min(8, digest.length); i++) { for (int i = 0; i < Math.min(8, digest.length); i++) {
int value = digest[i] & 0xff; int value = digest[i] & 0xff;
sb.append(Character.forDigit(value >>> 4, 16)) sb.append(Character.forDigit(value >>> 4, 16)).append(Character.forDigit(value & 0x0f, 16));
.append(Character.forDigit(value & 0x0f, 16));
} }
return sb.toString(); return sb.toString();
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {

View File

@@ -46,13 +46,11 @@ package zeroecho.core.err;
* <h2>When it is thrown</h2> * <h2>When it is thrown</h2>
* <ul> * <ul>
* <li>During * <li>During
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String, * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
* zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
* after policy validation, if the resolved algorithm exposes no bindings for * after policy validation, if the resolved algorithm exposes no bindings for
* the given role.</li> * the given role.</li>
* <li>Directly from * <li>Directly from
* {@link zeroecho.core.CryptoAlgorithm#createContext(zeroecho.core.KeyUsage, * {@link zeroecho.core.CryptoAlgorithm#createContext(zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
* java.security.Key, zeroecho.core.spec.ContextSpec)}
* when no binding exists for the role.</li> * when no binding exists for the role.</li>
* </ul> * </ul>
* *

View File

@@ -157,8 +157,8 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
* @param outChunkSize size of output chunks produced by the transform (must be * @param outChunkSize size of output chunks produced by the transform (must be
* > 0) * > 0)
* @param chunks number of chunks buffered at once (must be > 0) * @param chunks number of chunks buffered at once (must be > 0)
* @throws IllegalArgumentException if a size is outside its documented range * @throws IllegalArgumentException if a size is outside its documented range or
* or a buffer size overflows * a buffer size overflows
*/ */
protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks) { protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks) {
super(upstream); super(upstream);
@@ -190,8 +190,8 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
* steady-state (must be > 0) * steady-state (must be > 0)
* @param finalizationOutputChunks number of extra output chunks reserved for * @param finalizationOutputChunks number of extra output chunks reserved for
* finalization (must be >= 0) * finalization (must be >= 0)
* @throws IllegalArgumentException if a size is outside its documented range * @throws IllegalArgumentException if a size is outside its documented range or
* or a buffer size overflows * a buffer size overflows
*/ */
protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks, protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks,
int finalizationOutputChunks) { int finalizationOutputChunks) {
@@ -261,8 +261,7 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
// EOF: run finalization exactly once, even if there's no remainder, // EOF: run finalization exactly once, even if there's no remainder,
// and surface any produced bytes (e.g., padding block, GCM tag). // and surface any produced bytes (e.g., padding block, GCM tag).
if (!eofSeen) { if (!eofSeen) {
int finalOut = validateOutputCount(doFinal(inBuf, 0, 0, outBuf, 0), outBuf.length, int finalOut = validateOutputCount(doFinal(inBuf, 0, 0, outBuf, 0), outBuf.length, "finalization");
"finalization");
outPtr = 0; outPtr = 0;
outLen = finalOut; outLen = finalOut;
eofSeen = true; eofSeen = true;
@@ -273,8 +272,7 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
// all chunks are aligned to the specified boundary (inChunkSize) -> transform // all chunks are aligned to the specified boundary (inChunkSize) -> transform
// can be simply invoked // can be simply invoked
outLen = validateOutputCount(transform(inBuf, 0, inLen / inChunkSize, outBuf), outBuf.length, outLen = validateOutputCount(transform(inBuf, 0, inLen / inChunkSize, outBuf), outBuf.length, "transformation");
"transformation");
outPtr = 0; outPtr = 0;
int left = inLen % inChunkSize; int left = inLen % inChunkSize;
@@ -291,8 +289,7 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
return true; return true;
} }
private static void validateGeometry(int inChunkSize, int outChunkSize, int chunks, private static void validateGeometry(int inChunkSize, int outChunkSize, int chunks, int finalizationOutputChunks) {
int finalizationOutputChunks) {
if (inChunkSize < MIN_INPUT_CHUNK_SIZE) { if (inChunkSize < MIN_INPUT_CHUNK_SIZE) {
throw new IllegalArgumentException("inChunkSize must be greater than 1"); throw new IllegalArgumentException("inChunkSize must be greater than 1");
} }

View File

@@ -53,13 +53,12 @@ import javax.crypto.Cipher;
* block; a final partial block (if any) is processed by a single * block; a final partial block (if any) is processed by a single
* {@code doFinal}. This mode is restricted to RSA and ElGamal.</li> * {@code doFinal}. This mode is restricted to RSA and ElGamal.</li>
* <li><b>Left-padded independent block stream</b> - like the independent block * <li><b>Left-padded independent block stream</b> - like the independent block
* stream, but * stream, but left-pads each transformed output block with zeros up to
* left-pads each transformed output block with zeros up to
* {@code outChunkSize}. Final blocks must be complete; otherwise an * {@code outChunkSize}. Final blocks must be complete; otherwise an
* {@link IllegalStateException} is thrown.</li> * {@link IllegalStateException} is thrown.</li>
* <li><b>Continuous stream</b> - uses * <li><b>Continuous stream</b> - uses {@code Cipher.update(...)} for bulk bytes
* {@code Cipher.update(...)} for bulk bytes and a single {@code doFinal()} at * and a single {@code doFinal()} at end of stream. This is suitable for
* end of stream. This is suitable for CTR/CFB/OFB/GCM and padding modes.</li> * CTR/CFB/OFB/GCM and padding modes.</li>
* </ul> * </ul>
* *
* <h2>Block sizing</h2> * <h2>Block sizing</h2>
@@ -344,7 +343,8 @@ public final class CipherTransformInputStreamBuilder {
* </p> * </p>
* *
* @return a new InputStream that transforms bytes on the fly * @return a new InputStream that transforms bytes on the fly
* @throws NullPointerException if {@code upstream} or {@code cipher} is null * @throws NullPointerException if {@code upstream} or {@code cipher} is
* null
* @throws IllegalArgumentException if independent-block processing is selected * @throws IllegalArgumentException if independent-block processing is selected
* for an unsupported algorithm or buffer * for an unsupported algorithm or buffer
* geometry is invalid * geometry is invalid

View File

@@ -58,6 +58,7 @@ final class SmartBlockStream extends AbstractChunkTransformInputStream {
private static final Logger LOG = Logger.getLogger(SmartBlockStream.class.getName()); private static final Logger LOG = Logger.getLogger(SmartBlockStream.class.getName());
private final Cipher cipher; private final Cipher cipher;
/* package */ SmartBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize, /* package */ SmartBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize,
int bufferedBlocks) { int bufferedBlocks) {
super(upstream, inChunkSize, outChunkSize, bufferedBlocks); super(upstream, inChunkSize, outChunkSize, bufferedBlocks);

View File

@@ -59,6 +59,7 @@ final class SmartPaddedBlockStream extends AbstractChunkTransformInputStream {
private static final Logger LOG = Logger.getLogger(SmartPaddedBlockStream.class.getName()); private static final Logger LOG = Logger.getLogger(SmartPaddedBlockStream.class.getName());
private final Cipher cipher; private final Cipher cipher;
/* package */ SmartPaddedBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize, /* package */ SmartPaddedBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize,
int bufferedBlocks) { int bufferedBlocks) {
super(upstream, inChunkSize, outChunkSize, bufferedBlocks); super(upstream, inChunkSize, outChunkSize, bufferedBlocks);

View File

@@ -54,12 +54,12 @@
* then calls {@code onCompleted()} exactly once at EOF.</li> * then calls {@code onCompleted()} exactly once at EOF.</li>
* <li>{@link CipherTransformInputStreamBuilder} - fluent builder that creates * <li>{@link CipherTransformInputStreamBuilder} - fluent builder that creates
* cipher-backed streams for RSA/ElGamal independent-block processing, * cipher-backed streams for RSA/ElGamal independent-block processing,
* left-zero-padded independent blocks, or * left-zero-padded independent blocks, or continuous
* continuous {@code update}+{@code doFinal} streaming.</li> * {@code update}+{@code doFinal} streaming.</li>
* <li>{@link SmartBlockStream}, {@link SmartPaddedBlockStream}, * <li>{@link SmartBlockStream}, {@link SmartPaddedBlockStream},
* {@link SmartContinuousBlockStream} - internal cipher-backed stream variants; * {@link SmartContinuousBlockStream} - internal cipher-backed stream variants;
* the first two are restricted to independent RSA or ElGamal blocks * the first two are restricted to independent RSA or ElGamal blocks used by the
* used by the builder.</li> * builder.</li>
* <li>{@link TailStrippingInputStream} - withholds the last N bytes from the * <li>{@link TailStrippingInputStream} - withholds the last N bytes from the
* payload and delivers them to a callback at EOF (useful for tags, checksums, * payload and delivers them to a callback at EOF (useful for tags, checksums,
* or footers).</li> * or footers).</li>

View File

@@ -61,8 +61,8 @@ import java.util.List;
* *
* <h2>Serialization</h2> * <h2>Serialization</h2>
* <ul> * <ul>
* <li>{@link #writeTo(Appendable)} outputs each pair as {@code k=v\n} * <li>{@link #writeTo(Appendable)} outputs each pair as {@code k=v\n} lines
* lines without escaping and reports checked I/O failures.</li> * without escaping and reports checked I/O failures.</li>
* <li>{@link #readFrom(java.io.Reader)} parses lines in the same format, * <li>{@link #readFrom(java.io.Reader)} parses lines in the same format,
* ignoring blank lines and comments starting with {@code #}.</li> * ignoring blank lines and comments starting with {@code #}.</li>
* </ul> * </ul>
@@ -99,8 +99,7 @@ public final class PairSeq {
if (kv[elementIndex] == null) { if (kv[elementIndex] == null) {
int pairIndex = elementIndex >>> 1; int pairIndex = elementIndex >>> 1;
String role = (elementIndex & 1) == 0 ? "key" : "value"; String role = (elementIndex & 1) == 0 ? "key" : "value";
throw new IllegalArgumentException( throw new IllegalArgumentException("pair " + pairIndex + " " + role + " must not be null");
"pair " + pairIndex + " " + role + " must not be null");
} }
} }
return new PairSeq(kv.clone()); return new PairSeq(kv.clone());
@@ -201,8 +200,8 @@ public final class PairSeq {
} }
/** /**
* Appends all pairs to the target as {@code key=value} lines, reporting * Appends all pairs to the target as {@code key=value} lines, reporting checked
* checked I/O failures directly. * I/O failures directly.
* *
* <p> * <p>
* No escaping is performed; callers must ensure keys and values do not contain * No escaping is performed; callers must ensure keys and values do not contain

View File

@@ -101,9 +101,9 @@ import java.util.function.Supplier;
* }</pre> * }</pre>
* *
* <h2>Thread-safety</h2> Instances are immutable and thread-safe. Public * <h2>Thread-safety</h2> Instances are immutable and thread-safe. Public
* accessors are resolved once per runtime class and operation type, then invoked * accessors are resolved once per runtime class and operation type, then
* through cached method handles. The unload-safe {@link ClassValue} caches do * invoked through cached method handles. The unload-safe {@link ClassValue}
* not retain otherwise unreachable class loaders. * caches do not retain otherwise unreachable class loaders.
* *
* @param <T> domain type that follows the marshalling and unmarshalling * @param <T> domain type that follows the marshalling and unmarshalling
* conventions * conventions
@@ -274,8 +274,7 @@ public final class PairSeqCodec<T> implements Codec<T, PairSeq> {
try { try {
Method method = runtimeType.getMethod("marshal"); Method method = runtimeType.getMethod("marshal");
if (!PairSeq.class.isAssignableFrom(method.getReturnType())) { if (!PairSeq.class.isAssignableFrom(method.getReturnType())) {
return new MarshalPlan(null, return new MarshalPlan(null, "marshal() must return PairSeq in " + runtimeType.getName(), null);
"marshal() must return PairSeq in " + runtimeType.getName(), null);
} }
MethodHandle handle = MethodHandles.lookup().unreflect(method); MethodHandle handle = MethodHandles.lookup().unreflect(method);
return new MarshalPlan(handle, null, null); return new MarshalPlan(handle, null, null);
@@ -327,14 +326,14 @@ public final class PairSeqCodec<T> implements Codec<T, PairSeq> {
"static unmarshal(PairSeq) must return " + runtimeType.getName(), null); "static unmarshal(PairSeq) must return " + runtimeType.getName(), null);
} }
MethodHandle handle = MethodHandles.lookup().unreflect(method); MethodHandle handle = MethodHandles.lookup().unreflect(method);
return new UnmarshalPlan(handle, return new UnmarshalPlan(handle, "static unmarshal(PairSeq) failed for " + runtimeType.getName(),
"static unmarshal(PairSeq) failed for " + runtimeType.getName(), null, null); null, null);
} }
} catch (NoSuchMethodException ignored) { } catch (NoSuchMethodException ignored) {
// Resolve the constructor fallback below. // Resolve the constructor fallback below.
} catch (IllegalAccessException exception) { } catch (IllegalAccessException exception) {
return new UnmarshalPlan(null, null, return new UnmarshalPlan(null, null, "static unmarshal(PairSeq) failed for " + runtimeType.getName(),
"static unmarshal(PairSeq) failed for " + runtimeType.getName(), exception); exception);
} }
try { try {

View File

@@ -33,7 +33,6 @@
******************************************************************************/ ******************************************************************************/
package zeroecho.core.spec; package zeroecho.core.spec;
/** /**
* Marker interface for algorithm-specific key specifications. * Marker interface for algorithm-specific key specifications.
* <p> * <p>

View File

@@ -11,7 +11,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
/** /**
* Generates asymmetric key pairs for one exact specification type. * Generates asymmetric key pairs for one exact specification type.
* Implementations must be stateless or otherwise safe for concurrent invocation. * Implementations must be stateless or otherwise safe for concurrent
* invocation.
* *
* @param <S> specification type * @param <S> specification type
* @since 1.0 * @since 1.0

View File

@@ -15,11 +15,13 @@ import zeroecho.core.spec.ContextSpec;
/** /**
* Creates a cryptographic context from a key and a context specification. * Creates a cryptographic context from a key and a context specification.
* *
* <p>Implementations report provider and parameter failures with unchecked * <p>
* Implementations report provider and parameter failures with unchecked
* exceptions; context construction is a pure in-memory operation and does not * exceptions; context construction is a pure in-memory operation and does not
* expose an I/O failure contract. Factories must be stateless or otherwise safe * expose an I/O failure contract. Factories must be stateless or otherwise safe
* for concurrent invocation; returned contexts retain their own documented * for concurrent invocation; returned contexts retain their own documented
* thread-safety contracts.</p> * thread-safety contracts.
* </p>
* *
* @param <C> context type produced * @param <C> context type produced
* @param <K> key type accepted * @param <K> key type accepted
@@ -31,7 +33,7 @@ public interface ContextFactoryKS<C extends CryptoContext, K extends Key, S exte
/** /**
* Creates a context bound to the supplied key and specification. * Creates a context bound to the supplied key and specification.
* *
* @param key non-null key * @param key non-null key
* @param spec non-null resolved context specification * @param spec non-null resolved context specification
* @return a newly created context * @return a newly created context
*/ */

View File

@@ -11,11 +11,13 @@ import zeroecho.core.storage.KeyringPassword;
/** /**
* Supplies a fresh destroyable password for one keyring open operation. * Supplies a fresh destroyable password for one keyring open operation.
* *
* <p>Ownership of the returned object transfers to the receiver, which must * <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 * destroy it in a {@code finally} block immediately after the keyring has been
* opened. Implementations must not source passwords from immutable strings, * opened. Implementations must not source passwords from immutable strings,
* process arguments, system properties, environment fallbacks, persistent * process arguments, system properties, environment fallbacks, persistent
* files, or global mutable state.</p> * files, or global mutable state.
* </p>
*/ */
@FunctionalInterface @FunctionalInterface
public interface KeyringUnlockProvider { public interface KeyringUnlockProvider {

View File

@@ -10,8 +10,8 @@ import java.security.PrivateKey;
import zeroecho.core.spec.AlgorithmKeySpec; import zeroecho.core.spec.AlgorithmKeySpec;
/** /**
* Imports private keys for one exact specification type. Implementations must be * Imports private keys for one exact specification type. Implementations must
* stateless or otherwise safe for concurrent invocation. * be stateless or otherwise safe for concurrent invocation.
* *
* @param <S> specification type * @param <S> specification type
* @since 1.0 * @since 1.0

View File

@@ -34,20 +34,26 @@
/** /**
* Provider contracts for context construction and exact key operations. * Provider contracts for context construction and exact key operations.
* *
* <p>Algorithms bind each supported role to a {@link ContextFactoryKS}. Context * <p>
* Algorithms bind each supported role to a {@link ContextFactoryKS}. Context
* construction is an in-memory operation; stream attachment and processing are * construction is an in-memory operation; stream attachment and processing are
* responsible for reporting {@link java.io.IOException}.</p> * responsible for reporting {@link java.io.IOException}.
* </p>
* *
* <p>Key capabilities are registered independently through * <p>
* Key capabilities are registered independently through
* {@link SymmetricKeyGenerator}, {@link SymmetricKeyImporter}, * {@link SymmetricKeyGenerator}, {@link SymmetricKeyImporter},
* {@link AsymmetricKeyPairGenerator}, {@link PublicKeyImporter}, and * {@link AsymmetricKeyPairGenerator}, {@link PublicKeyImporter}, and
* {@link PrivateKeyImporter}. A provider registers only the operations it * {@link PrivateKeyImporter}. A provider registers only the operations it
* implements, so capability lookup fails before invocation instead of returning * implements, so capability lookup fails before invocation instead of returning
* an object with unsupported methods.</p> * an object with unsupported methods.
* </p>
* *
* <p>SPI implementations should be stateless or otherwise safe for concurrent * <p>
* SPI implementations should be stateless or otherwise safe for concurrent
* lookup and invocation. Created cryptographic contexts remain operation-local * lookup and invocation. Created cryptographic contexts remain operation-local
* and are not necessarily thread-safe.</p> * and are not necessarily thread-safe.
* </p>
* *
* @since 1.0 * @since 1.0
*/ */

View File

@@ -10,9 +10,11 @@ import java.util.Objects;
/** /**
* Redacted checked failure raised by encrypted keyring operations. * Redacted checked failure raised by encrypted keyring operations.
* *
* <p>The public message contains only the stable error code. Filesystem paths, * <p>
* The public message contains only the stable error code. Filesystem paths,
* aliases, key material, ciphertext, and provider-controlled messages are * aliases, key material, ciphertext, and provider-controlled messages are
* deliberately excluded.</p> * deliberately excluded.
* </p>
*/ */
public final class KeyringException extends IOException { public final class KeyringException extends IOException {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@@ -21,17 +23,9 @@ public final class KeyringException extends IOException {
* Stable keyring failure categories. * Stable keyring failure categories.
*/ */
public enum Code { public enum Code {
KEYRING_ALREADY_OPEN, KEYRING_ALREADY_OPEN, KEYRING_FILESYSTEM_UNSUPPORTED, KEYRING_FORMAT_INVALID, KEYRING_LIMIT_EXCEEDED,
KEYRING_FILESYSTEM_UNSUPPORTED, KEYRING_UNLOCK_FAILED, KEYRING_IO_FAILED, KEYRING_DURABILITY_UNCONFIRMED, KEYRING_CLOSED,
KEYRING_FORMAT_INVALID, KEYRING_NON_EXPORTABLE_KEY, KEYRING_IMPORT_MAPPING_INVALID, KEYRING_IMPORT_METADATA_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 KEYRING_KEY_NOT_CANONICALIZABLE
} }

View File

@@ -24,8 +24,7 @@ interface KeyringFileOperations {
/** Atomic persistence destination. */ /** Atomic persistence destination. */
enum Target { enum Target {
MAIN_IMAGE, MAIN_IMAGE, NONCE_RESERVATION
NONCE_RESERVATION
} }
/** Creates one owner-only temporary file beside its destination. */ /** Creates one owner-only temporary file beside its destination. */
@@ -57,10 +56,8 @@ final class NioKeyringFileOperations implements KeyringFileOperations {
} }
@Override @Override
public void writeTemporary(Target target, Path temporary, byte[] image) public void writeTemporary(Target target, Path temporary, byte[] image) throws IOException {
throws IOException { try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS)) {
try (FileChannel channel = FileChannel.open(temporary,
StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS)) {
ByteBuffer buffer = ByteBuffer.wrap(image); ByteBuffer buffer = ByteBuffer.wrap(image);
while (buffer.hasRemaining()) { while (buffer.hasRemaining()) {
channel.write(buffer); channel.write(buffer);
@@ -70,17 +67,14 @@ final class NioKeyringFileOperations implements KeyringFileOperations {
@Override @Override
public void forceTemporary(Target target, Path temporary) throws IOException { public void forceTemporary(Target target, Path temporary) throws IOException {
try (FileChannel channel = FileChannel.open(temporary, try (FileChannel channel = FileChannel.open(temporary, StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS)) {
StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS)) {
channel.force(true); channel.force(true);
} }
} }
@Override @Override
public void atomicReplace(Target target, Path temporary, Path destination) public void atomicReplace(Target target, Path temporary, Path destination) throws IOException {
throws IOException { Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING);
Files.move(temporary, destination, StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} }
@Override @Override

View File

@@ -71,18 +71,18 @@ import zeroecho.core.spi.SymmetricKeyImporter;
* Closed trusted mapping from persistent key identities to canonical registered * Closed trusted mapping from persistent key identities to canonical registered
* import operations. * import operations.
* *
* <p>Provider identity is deliberately absent. Standard encoded key material * <p>
* is reconstructed by the current runtime's canonical ZeroEcho importer. The * Provider identity is deliberately absent. Standard encoded key material is
* original JCA provider is neither persisted nor reproduced.</p> * reconstructed by the current runtime's canonical ZeroEcho importer. The
* original JCA provider is neither persisted nor reproduced.
* </p>
*/ */
final class KeyringImportRegistry { final class KeyringImportRegistry {
private static final String ALGORITHM_AES = "AES"; private static final String ALGORITHM_AES = "AES";
private static final String ALGORITHM_HMAC = "HMAC"; private static final String ALGORITHM_HMAC = "HMAC";
private static final String ALGORITHM_CHACHA20 = "CHACHA20"; private static final String ALGORITHM_CHACHA20 = "CHACHA20";
private static final String ALGORITHM_CHACHA20_POLY1305 = "CHACHA20-POLY1305"; private static final String ALGORITHM_CHACHA20_POLY1305 = "CHACHA20-POLY1305";
private static final Map<Class<? extends AlgorithmKeySpec>, private static final Map<Class<? extends AlgorithmKeySpec>, Function<byte[], ? extends AlgorithmKeySpec>> SPEC_FACTORIES = createSpecFactories();
Function<byte[], ? extends AlgorithmKeySpec>> SPEC_FACTORIES =
createSpecFactories();
private static final Map<Tuple, PersistentMapping> MAPPINGS = createMappings(); private static final Map<Tuple, PersistentMapping> MAPPINGS = createMappings();
private KeyringImportRegistry() { private KeyringImportRegistry() {
@@ -93,10 +93,7 @@ final class KeyringImportRegistry {
*/ */
/* default */ /* default */
enum HmacVariant { enum HmacVariant {
NONE(0, null), NONE(0, null), SHA256(1, "HmacSHA256"), SHA384(2, "HmacSHA384"), SHA512(3, "HmacSHA512");
SHA256(1, "HmacSHA256"),
SHA384(2, "HmacSHA384"),
SHA512(3, "HmacSHA512");
private final int code; private final int code;
private final String jcaName; private final String jcaName;
@@ -123,8 +120,7 @@ final class KeyringImportRegistry {
throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_METADATA_INVALID); throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_METADATA_INVALID);
} }
/* default */ static HmacVariant forStoredKey(String algorithmId, Key key) /* default */ static HmacVariant forStoredKey(String algorithmId, Key key) throws KeyringException {
throws KeyringException {
if (!ALGORITHM_HMAC.equals(algorithmId)) { if (!ALGORITHM_HMAC.equals(algorithmId)) {
return NONE; return NONE;
} }
@@ -142,21 +138,19 @@ final class KeyringImportRegistry {
* Immutable description used by the finite importer-matrix test. * Immutable description used by the finite importer-matrix test.
* *
* @param algorithmId canonical ZeroEcho algorithm identifier * @param algorithmId canonical ZeroEcho algorithm identifier
* @param kind key kind * @param kind key kind
* @param encoding standard encoding * @param encoding standard encoding
* @param hmacVariant closed HMAC variant, or {@link HmacVariant#NONE} * @param hmacVariant closed HMAC variant, or {@link HmacVariant#NONE}
* @param specType exact registered importer specification type * @param specType exact registered importer specification type
*/ */
/* default */ /* default */
record PersistentMapping(String algorithmId, KeyringStore.Kind kind, record PersistentMapping(String algorithmId, KeyringStore.Kind kind, KeyringStore.Encoding encoding,
KeyringStore.Encoding encoding, HmacVariant hmacVariant, HmacVariant hmacVariant, Class<? extends AlgorithmKeySpec> specType) {
Class<? extends AlgorithmKeySpec> specType) {
} }
@SuppressWarnings("PMD.AvoidCatchingGenericException") @SuppressWarnings("PMD.AvoidCatchingGenericException")
/* default */ static Key importKey(String algorithmId, KeyringStore.Kind kind, /* default */ static Key importKey(String algorithmId, KeyringStore.Kind kind, KeyringStore.Encoding encoding,
KeyringStore.Encoding encoding, HmacVariant hmacVariant, byte[] encoded) HmacVariant hmacVariant, byte[] encoded) throws GeneralSecurityException, KeyringException {
throws GeneralSecurityException, KeyringException {
PersistentMapping mapping = requireMapping(algorithmId, kind, encoding, hmacVariant); PersistentMapping mapping = requireMapping(algorithmId, kind, encoding, hmacVariant);
CryptoAlgorithm algorithm = requireAlgorithm(mapping.algorithmId); CryptoAlgorithm algorithm = requireAlgorithm(mapping.algorithmId);
AlgorithmKeySpec spec = createSpec(mapping, encoded); AlgorithmKeySpec spec = createSpec(mapping, encoded);
@@ -172,8 +166,7 @@ final class KeyringImportRegistry {
} }
/* default */ static void validateMapping(String algorithmId, KeyringStore.Kind kind, /* default */ static void validateMapping(String algorithmId, KeyringStore.Kind kind,
KeyringStore.Encoding encoding, HmacVariant hmacVariant) KeyringStore.Encoding encoding, HmacVariant hmacVariant) throws KeyringException {
throws KeyringException {
PersistentMapping mapping = requireMapping(algorithmId, kind, encoding, hmacVariant); PersistentMapping mapping = requireMapping(algorithmId, kind, encoding, hmacVariant);
CryptoAlgorithm algorithm = requireAlgorithm(mapping.algorithmId); CryptoAlgorithm algorithm = requireAlgorithm(mapping.algorithmId);
List<KeyOperationInfo> operations = matchingOperations(algorithm, kind); List<KeyOperationInfo> operations = matchingOperations(algorithm, kind);
@@ -184,27 +177,23 @@ final class KeyringImportRegistry {
@SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.AvoidCatchingGenericException" }) @SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.AvoidCatchingGenericException" })
/* default */ static void validateCanonical(String algorithmId, KeyringStore.Kind kind, /* default */ static void validateCanonical(String algorithmId, KeyringStore.Kind kind,
KeyringStore.Encoding encoding, HmacVariant hmacVariant, Key source, KeyringStore.Encoding encoding, HmacVariant hmacVariant, Key source, byte[] encoded)
byte[] encoded)
throws KeyringException { throws KeyringException {
Key imported = null; Key imported = null;
byte[] canonical = null; byte[] canonical = null;
try { try {
if (!matchesSourceAlgorithm(source, algorithmId, hmacVariant)) { if (!matchesSourceAlgorithm(source, algorithmId, hmacVariant)) {
throw new KeyringException( throw new KeyringException(KeyringException.Code.KEYRING_KEY_NOT_CANONICALIZABLE);
KeyringException.Code.KEYRING_KEY_NOT_CANONICALIZABLE);
} }
imported = importKey(algorithmId, kind, encoding, hmacVariant, encoded); imported = importKey(algorithmId, kind, encoding, hmacVariant, encoded);
canonical = imported.getEncoded(); canonical = imported.getEncoded();
if (canonical == null || !matchesFormat(imported.getFormat(), encoding) if (canonical == null || !matchesFormat(imported.getFormat(), encoding)
|| !MessageDigest.isEqual(encoded, canonical) || !MessageDigest.isEqual(encoded, canonical)
|| !matchesAlgorithm(imported, algorithmId, hmacVariant)) { || !matchesAlgorithm(imported, algorithmId, hmacVariant)) {
throw new KeyringException( throw new KeyringException(KeyringException.Code.KEYRING_KEY_NOT_CANONICALIZABLE);
KeyringException.Code.KEYRING_KEY_NOT_CANONICALIZABLE);
} }
} catch (GeneralSecurityException | RuntimeException exception) { } catch (GeneralSecurityException | RuntimeException exception) {
throw new KeyringException( throw new KeyringException(KeyringException.Code.KEYRING_KEY_NOT_CANONICALIZABLE);
KeyringException.Code.KEYRING_KEY_NOT_CANONICALIZABLE);
} finally { } finally {
if (canonical != null) { if (canonical != null) {
Arrays.fill(canonical, (byte) 0); Arrays.fill(canonical, (byte) 0);
@@ -217,11 +206,9 @@ final class KeyringImportRegistry {
return List.copyOf(MAPPINGS.values()); return List.copyOf(MAPPINGS.values());
} }
private static PersistentMapping requireMapping(String algorithmId, private static PersistentMapping requireMapping(String algorithmId, KeyringStore.Kind kind,
KeyringStore.Kind kind, KeyringStore.Encoding encoding, KeyringStore.Encoding encoding, HmacVariant hmacVariant) throws KeyringException {
HmacVariant hmacVariant) throws KeyringException { PersistentMapping mapping = MAPPINGS.get(new Tuple(algorithmId, kind, encoding, hmacVariant));
PersistentMapping mapping = MAPPINGS.get(
new Tuple(algorithmId, kind, encoding, hmacVariant));
if (mapping == null) { if (mapping == null) {
throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_MAPPING_INVALID); throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_MAPPING_INVALID);
} }
@@ -229,8 +216,7 @@ final class KeyringImportRegistry {
} }
@SuppressWarnings("PMD.PreserveStackTrace") @SuppressWarnings("PMD.PreserveStackTrace")
private static CryptoAlgorithm requireAlgorithm(String algorithmId) private static CryptoAlgorithm requireAlgorithm(String algorithmId) throws KeyringException {
throws KeyringException {
try { try {
return CryptoAlgorithms.require(algorithmId); return CryptoAlgorithms.require(algorithmId);
} catch (IllegalArgumentException exception) { } catch (IllegalArgumentException exception) {
@@ -238,11 +224,8 @@ final class KeyringImportRegistry {
} }
} }
private static List<KeyOperationInfo> matchingOperations(CryptoAlgorithm algorithm, private static List<KeyOperationInfo> matchingOperations(CryptoAlgorithm algorithm, KeyringStore.Kind kind) {
KeyringStore.Kind kind) { return algorithm.keyOperations().stream().filter(info -> info.operation() == operation(kind)).toList();
return algorithm.keyOperations().stream()
.filter(info -> info.operation() == operation(kind))
.toList();
} }
private static KeyOperation operation(KeyringStore.Kind kind) { private static KeyOperation operation(KeyringStore.Kind kind) {
@@ -254,25 +237,22 @@ final class KeyringImportRegistry {
} }
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
private static Key invokeImporter(CryptoAlgorithm algorithm, KeyringStore.Kind kind, private static Key invokeImporter(CryptoAlgorithm algorithm, KeyringStore.Kind kind, AlgorithmKeySpec spec)
AlgorithmKeySpec spec) throws GeneralSecurityException { throws GeneralSecurityException {
return switch (kind) { return switch (kind) {
case PUBLIC_KEY -> ((PublicKeyImporter) algorithm.publicKeyImporter(spec.getClass())) case PUBLIC_KEY -> ((PublicKeyImporter) algorithm.publicKeyImporter(spec.getClass())).importPublic(spec);
.importPublic(spec); case PRIVATE_KEY ->
case PRIVATE_KEY -> ((PrivateKeyImporter) algorithm.privateKeyImporter(spec.getClass())) ((PrivateKeyImporter) algorithm.privateKeyImporter(spec.getClass())).importPrivate(spec);
.importPrivate(spec); case SECRET_KEY ->
case SECRET_KEY -> ((SymmetricKeyImporter) algorithm.symmetricKeyImporter(spec.getClass())) ((SymmetricKeyImporter) algorithm.symmetricKeyImporter(spec.getClass())).importSecret(spec);
.importSecret(spec);
}; };
} }
private static AlgorithmKeySpec createSpec(PersistentMapping mapping, byte[] encoded) private static AlgorithmKeySpec createSpec(PersistentMapping mapping, byte[] encoded) throws KeyringException {
throws KeyringException {
if (mapping.specType == HmacKeyImportSpec.class) { if (mapping.specType == HmacKeyImportSpec.class) {
return new HmacKeyImportSpec(mapping.hmacVariant.jcaName, encoded); return new HmacKeyImportSpec(mapping.hmacVariant.jcaName, encoded);
} }
Function<byte[], ? extends AlgorithmKeySpec> factory = Function<byte[], ? extends AlgorithmKeySpec> factory = SPEC_FACTORIES.get(mapping.specType);
SPEC_FACTORIES.get(mapping.specType);
AlgorithmKeySpec result = factory == null ? null : factory.apply(encoded); AlgorithmKeySpec result = factory == null ? null : factory.apply(encoded);
if (result == null) { if (result == null) {
throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_MAPPING_INVALID); throw new KeyringException(KeyringException.Code.KEYRING_IMPORT_MAPPING_INVALID);
@@ -288,31 +268,27 @@ final class KeyringImportRegistry {
}; };
} }
private static boolean matchesAlgorithm(Key imported, String algorithmId, private static boolean matchesAlgorithm(Key imported, String algorithmId, HmacVariant hmacVariant) {
HmacVariant hmacVariant) {
if (ALGORITHM_HMAC.equals(algorithmId)) { if (ALGORITHM_HMAC.equals(algorithmId)) {
return hmacVariant.jcaName.equals(imported.getAlgorithm()); return hmacVariant.jcaName.equals(imported.getAlgorithm());
} }
if (ALGORITHM_AES.equals(algorithmId)) { if (ALGORITHM_AES.equals(algorithmId)) {
return ALGORITHM_AES.equals(imported.getAlgorithm()); return ALGORITHM_AES.equals(imported.getAlgorithm());
} }
if (ALGORITHM_CHACHA20.equals(algorithmId) if (ALGORITHM_CHACHA20.equals(algorithmId) || ALGORITHM_CHACHA20_POLY1305.equals(algorithmId)) {
|| ALGORITHM_CHACHA20_POLY1305.equals(algorithmId)) {
return "ChaCha20".equals(imported.getAlgorithm()); return "ChaCha20".equals(imported.getAlgorithm());
} }
return true; return true;
} }
private static boolean matchesSourceAlgorithm(Key source, String algorithmId, private static boolean matchesSourceAlgorithm(Key source, String algorithmId, HmacVariant hmacVariant) {
HmacVariant hmacVariant) {
if (ALGORITHM_HMAC.equals(algorithmId)) { if (ALGORITHM_HMAC.equals(algorithmId)) {
return hmacVariant.jcaName.equals(source.getAlgorithm()); return hmacVariant.jcaName.equals(source.getAlgorithm());
} }
if (ALGORITHM_AES.equals(algorithmId)) { if (ALGORITHM_AES.equals(algorithmId)) {
return ALGORITHM_AES.equals(source.getAlgorithm()); return ALGORITHM_AES.equals(source.getAlgorithm());
} }
if (ALGORITHM_CHACHA20.equals(algorithmId) if (ALGORITHM_CHACHA20.equals(algorithmId) || ALGORITHM_CHACHA20_POLY1305.equals(algorithmId)) {
|| ALGORITHM_CHACHA20_POLY1305.equals(algorithmId)) {
return "ChaCha20".equals(source.getAlgorithm()); return "ChaCha20".equals(source.getAlgorithm());
} }
return true; return true;
@@ -329,10 +305,8 @@ final class KeyringImportRegistry {
} }
} }
private static Map<Class<? extends AlgorithmKeySpec>, private static Map<Class<? extends AlgorithmKeySpec>, Function<byte[], ? extends AlgorithmKeySpec>> createSpecFactories() {
Function<byte[], ? extends AlgorithmKeySpec>> createSpecFactories() { return Map.ofEntries(Map.entry(AesKeyImportSpec.class, AesKeyImportSpec::fromRaw),
return Map.ofEntries(
Map.entry(AesKeyImportSpec.class, AesKeyImportSpec::fromRaw),
Map.entry(ChaChaKeyImportSpec.class, ChaChaKeyImportSpec::fromRaw), Map.entry(ChaChaKeyImportSpec.class, ChaChaKeyImportSpec::fromRaw),
Map.entry(BikePublicKeySpec.class, BikePublicKeySpec::new), Map.entry(BikePublicKeySpec.class, BikePublicKeySpec::new),
Map.entry(BikePrivateKeySpec.class, BikePrivateKeySpec::new), Map.entry(BikePrivateKeySpec.class, BikePrivateKeySpec::new),
@@ -380,73 +354,54 @@ final class KeyringImportRegistry {
addAsymmetric(mappings, "CMCE", CmcePublicKeySpec.class, CmcePrivateKeySpec.class); addAsymmetric(mappings, "CMCE", CmcePublicKeySpec.class, CmcePrivateKeySpec.class);
addAsymmetric(mappings, "DH", DhPublicKeySpec.class, DhPrivateKeySpec.class); addAsymmetric(mappings, "DH", DhPublicKeySpec.class, DhPrivateKeySpec.class);
addAsymmetric(mappings, "ECDSA", EcdsaPublicKeySpec.class, EcdsaPrivateKeySpec.class); addAsymmetric(mappings, "ECDSA", EcdsaPublicKeySpec.class, EcdsaPrivateKeySpec.class);
addAsymmetric(mappings, "Ed25519", Ed25519PublicKeySpec.class, addAsymmetric(mappings, "Ed25519", Ed25519PublicKeySpec.class, Ed25519PrivateKeySpec.class);
Ed25519PrivateKeySpec.class); addAsymmetric(mappings, "Ed448", Ed448PublicKeySpec.class, Ed448PrivateKeySpec.class);
addAsymmetric(mappings, "Ed448", Ed448PublicKeySpec.class, addAsymmetric(mappings, "ElGamal", ElgamalPublicKeySpec.class, ElgamalPrivateKeySpec.class);
Ed448PrivateKeySpec.class); addAsymmetric(mappings, "Frodo", FrodoPublicKeySpec.class, FrodoPrivateKeySpec.class);
addAsymmetric(mappings, "ElGamal", ElgamalPublicKeySpec.class,
ElgamalPrivateKeySpec.class);
addAsymmetric(mappings, "Frodo", FrodoPublicKeySpec.class,
FrodoPrivateKeySpec.class);
addAsymmetric(mappings, "HQC", HqcPublicKeySpec.class, HqcPrivateKeySpec.class); addAsymmetric(mappings, "HQC", HqcPublicKeySpec.class, HqcPrivateKeySpec.class);
addAsymmetric(mappings, "ML-KEM", KyberPublicKeySpec.class, addAsymmetric(mappings, "ML-KEM", KyberPublicKeySpec.class, KyberPrivateKeySpec.class);
KyberPrivateKeySpec.class); addAsymmetric(mappings, "ML-DSA", MldsaPublicKeySpec.class, MldsaPrivateKeySpec.class);
addAsymmetric(mappings, "ML-DSA", MldsaPublicKeySpec.class,
MldsaPrivateKeySpec.class);
addAsymmetric(mappings, "NTRU", NtruPublicKeySpec.class, NtruPrivateKeySpec.class); addAsymmetric(mappings, "NTRU", NtruPublicKeySpec.class, NtruPrivateKeySpec.class);
addAsymmetric(mappings, "NTRULPRime", NtrulPrimePublicKeySpec.class, addAsymmetric(mappings, "NTRULPRime", NtrulPrimePublicKeySpec.class, NtrulPrimePrivateKeySpec.class);
NtrulPrimePrivateKeySpec.class); addAsymmetric(mappings, "SNTRUPrime", SntruPrimePublicKeySpec.class, SntruPrimePrivateKeySpec.class);
addAsymmetric(mappings, "SNTRUPrime", SntruPrimePublicKeySpec.class,
SntruPrimePrivateKeySpec.class);
addAsymmetric(mappings, "RSA", RsaPublicKeySpec.class, RsaPrivateKeySpec.class); addAsymmetric(mappings, "RSA", RsaPublicKeySpec.class, RsaPrivateKeySpec.class);
addAsymmetric(mappings, "SABER", SaberPublicKeySpec.class, addAsymmetric(mappings, "SABER", SaberPublicKeySpec.class, SaberPrivateKeySpec.class);
SaberPrivateKeySpec.class); addAsymmetric(mappings, "SLH-DSA", SlhDsaPublicKeySpec.class, SlhDsaPrivateKeySpec.class);
addAsymmetric(mappings, "SLH-DSA", SlhDsaPublicKeySpec.class, addAsymmetric(mappings, "SPHINCS+", SphincsPlusPublicKeySpec.class, SphincsPlusPrivateKeySpec.class);
SlhDsaPrivateKeySpec.class);
addAsymmetric(mappings, "SPHINCS+", SphincsPlusPublicKeySpec.class,
SphincsPlusPrivateKeySpec.class);
addAsymmetric(mappings, "Xdh", XdhPublicKeySpec.class, XdhPrivateKeySpec.class); addAsymmetric(mappings, "Xdh", XdhPublicKeySpec.class, XdhPrivateKeySpec.class);
add(mappings, ALGORITHM_AES, KeyringStore.Kind.SECRET_KEY, add(mappings, ALGORITHM_AES, KeyringStore.Kind.SECRET_KEY, KeyringStore.Encoding.RAW, HmacVariant.NONE,
KeyringStore.Encoding.RAW, AesKeyImportSpec.class);
HmacVariant.NONE, AesKeyImportSpec.class); add(mappings, ALGORITHM_CHACHA20, KeyringStore.Kind.SECRET_KEY, KeyringStore.Encoding.RAW, HmacVariant.NONE,
add(mappings, ALGORITHM_CHACHA20, KeyringStore.Kind.SECRET_KEY, ChaChaKeyImportSpec.class);
KeyringStore.Encoding.RAW, add(mappings, ALGORITHM_CHACHA20_POLY1305, KeyringStore.Kind.SECRET_KEY, KeyringStore.Encoding.RAW,
HmacVariant.NONE, ChaChaKeyImportSpec.class); HmacVariant.NONE, ChaChaKeyImportSpec.class);
add(mappings, ALGORITHM_CHACHA20_POLY1305, KeyringStore.Kind.SECRET_KEY, add(mappings, ALGORITHM_HMAC, KeyringStore.Kind.SECRET_KEY, KeyringStore.Encoding.RAW, HmacVariant.SHA256,
KeyringStore.Encoding.RAW, HmacVariant.NONE, ChaChaKeyImportSpec.class); HmacKeyImportSpec.class);
add(mappings, ALGORITHM_HMAC, KeyringStore.Kind.SECRET_KEY, add(mappings, ALGORITHM_HMAC, KeyringStore.Kind.SECRET_KEY, KeyringStore.Encoding.RAW, HmacVariant.SHA384,
KeyringStore.Encoding.RAW, HmacKeyImportSpec.class);
HmacVariant.SHA256, HmacKeyImportSpec.class); add(mappings, ALGORITHM_HMAC, KeyringStore.Kind.SECRET_KEY, KeyringStore.Encoding.RAW, HmacVariant.SHA512,
add(mappings, ALGORITHM_HMAC, KeyringStore.Kind.SECRET_KEY, HmacKeyImportSpec.class);
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); return Collections.unmodifiableMap(mappings);
} }
private static void addAsymmetric(Map<Tuple, PersistentMapping> mappings, private static void addAsymmetric(Map<Tuple, PersistentMapping> mappings, String algorithmId,
String algorithmId, Class<? extends AlgorithmKeySpec> publicSpec, Class<? extends AlgorithmKeySpec> publicSpec, Class<? extends AlgorithmKeySpec> privateSpec) {
Class<? extends AlgorithmKeySpec> privateSpec) { add(mappings, algorithmId, KeyringStore.Kind.PUBLIC_KEY, KeyringStore.Encoding.X509, HmacVariant.NONE,
add(mappings, algorithmId, KeyringStore.Kind.PUBLIC_KEY, publicSpec);
KeyringStore.Encoding.X509, HmacVariant.NONE, publicSpec); add(mappings, algorithmId, KeyringStore.Kind.PRIVATE_KEY, KeyringStore.Encoding.PKCS8, HmacVariant.NONE,
add(mappings, algorithmId, KeyringStore.Kind.PRIVATE_KEY, privateSpec);
KeyringStore.Encoding.PKCS8, HmacVariant.NONE, privateSpec);
} }
private static void add(Map<Tuple, PersistentMapping> mappings, String algorithmId, private static void add(Map<Tuple, PersistentMapping> mappings, String algorithmId, KeyringStore.Kind kind,
KeyringStore.Kind kind, KeyringStore.Encoding encoding, KeyringStore.Encoding encoding, HmacVariant hmacVariant, Class<? extends AlgorithmKeySpec> specType) {
HmacVariant hmacVariant, Class<? extends AlgorithmKeySpec> specType) {
Tuple tuple = new Tuple(algorithmId, kind, encoding, hmacVariant); Tuple tuple = new Tuple(algorithmId, kind, encoding, hmacVariant);
PersistentMapping mapping = new PersistentMapping(algorithmId, kind, PersistentMapping mapping = new PersistentMapping(algorithmId, kind, encoding, hmacVariant, specType);
encoding, hmacVariant, specType);
if (mappings.put(tuple, mapping) != null) { if (mappings.put(tuple, mapping) != null) {
throw new IllegalStateException("Duplicate persistent key importer tuple"); throw new IllegalStateException("Duplicate persistent key importer tuple");
} }
} }
private record Tuple(String algorithmId, KeyringStore.Kind kind, private record Tuple(String algorithmId, KeyringStore.Kind kind, KeyringStore.Encoding encoding,
KeyringStore.Encoding encoding, HmacVariant hmacVariant) { HmacVariant hmacVariant) {
} }
} }

View File

@@ -19,8 +19,7 @@ final class KeyringNonceReservationKdf {
private static final int STORE_ID_BYTES = 16; private static final int STORE_ID_BYTES = 16;
private static final int OUTPUT_BYTES = 32; private static final int OUTPUT_BYTES = 32;
private static final String HMAC_SHA256 = "HmacSHA256"; private static final String HMAC_SHA256 = "HmacSHA256";
private static final String DOMAIN_LABEL = private static final String DOMAIN_LABEL = "zeroecho:keyring:nonce-reservation-mac:v1";
"zeroecho:keyring:nonce-reservation-mac:v1";
private KeyringNonceReservationKdf() { private KeyringNonceReservationKdf() {
} }
@@ -29,14 +28,13 @@ final class KeyringNonceReservationKdf {
* Derives the store-specific nonce-reservation MAC key. * Derives the store-specific nonce-reservation MAC key.
* *
* @param masterKey borrowed 256-bit store master key * @param masterKey borrowed 256-bit store master key
* @param storeId borrowed canonical 128-bit binary store UUID * @param storeId borrowed canonical 128-bit binary store UUID
* @return newly owned 256-bit derived key * @return newly owned 256-bit derived key
* @throws GeneralSecurityException if HMAC-SHA-256 is unavailable * @throws GeneralSecurityException if HMAC-SHA-256 is unavailable
*/ */
/* default */ static byte[] derive(byte[] masterKey, byte[] storeId) /* default */ static byte[] derive(byte[] masterKey, byte[] storeId) throws GeneralSecurityException {
throws GeneralSecurityException { if (masterKey == null || masterKey.length != MASTER_KEY_BYTES || storeId == null
if (masterKey == null || masterKey.length != MASTER_KEY_BYTES || storeId.length != STORE_ID_BYTES) {
|| storeId == null || storeId.length != STORE_ID_BYTES) {
throw new IllegalArgumentException("Invalid keyring derivation input"); throw new IllegalArgumentException("Invalid keyring derivation input");
} }
byte[] salt = storeId.clone(); byte[] salt = storeId.clone();
@@ -61,8 +59,7 @@ final class KeyringNonceReservationKdf {
} }
} }
private static byte[] hmac(byte[] key, byte[] input) private static byte[] hmac(byte[] key, byte[] input) throws GeneralSecurityException {
throws GeneralSecurityException {
Mac mac = Mac.getInstance(HMAC_SHA256); Mac mac = Mac.getInstance(HMAC_SHA256);
mac.init(new SecretKeySpec(key, HMAC_SHA256)); mac.init(new SecretKeySpec(key, HMAC_SHA256));
return mac.doFinal(input); return mac.doFinal(input);

View File

@@ -14,14 +14,18 @@ import javax.security.auth.Destroyable;
/** /**
* Destroyable owner of a keyring password. * Destroyable owner of a keyring password.
* *
* <p>The constructor and {@link #copy()} use defensive copies. Callers retain * <p>
* The constructor and {@link #copy()} use defensive copies. Callers retain
* ownership of the array supplied to the constructor and must clear it. The * 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 * returned copy belongs to the receiver and must be cleared immediately after
* key derivation. This object never creates an immutable password * key derivation. This object never creates an immutable password
* {@link String}.</p> * {@link String}.
* </p>
* *
* <p>Instances are thread-safe. Destruction is idempotent and makes subsequent * <p>
* access fail deterministically.</p> * Instances are thread-safe. Destruction is idempotent and makes subsequent
* access fail deterministically.
* </p>
*/ */
public final class KeyringPassword implements Destroyable, AutoCloseable { public final class KeyringPassword implements Destroyable, AutoCloseable {
private final ReentrantLock lifecycleLock = new ReentrantLock(); private final ReentrantLock lifecycleLock = new ReentrantLock();
@@ -32,7 +36,7 @@ public final class KeyringPassword implements Destroyable, AutoCloseable {
* Creates a password owner. * Creates a password owner.
* *
* @param password password characters, which are defensively copied * @param password password characters, which are defensively copied
* @throws NullPointerException if {@code password} is {@code null} * @throws NullPointerException if {@code password} is {@code null}
* @throws IllegalArgumentException if {@code password} is empty * @throws IllegalArgumentException if {@code password} is empty
*/ */
@SuppressWarnings("PMD.UseVarargs") @SuppressWarnings("PMD.UseVarargs")

View File

@@ -8,7 +8,8 @@ package zeroecho.core.storage;
* Operational limits applied while opening an encrypted software keyring. * Operational limits applied while opening an encrypted software keyring.
* *
* @param operationalIterationMaximum maximum accepted PBKDF2 iteration count; * @param operationalIterationMaximum maximum accepted PBKDF2 iteration count;
* it may restrict but never exceed the absolute decoded maximum * it may restrict but never exceed the
* absolute decoded maximum
*/ */
public record KeyringProtection(int operationalIterationMaximum) { public record KeyringProtection(int operationalIterationMaximum) {
/** Iterations used when a new keyring is created. */ /** Iterations used when a new keyring is created. */
@@ -21,8 +22,8 @@ public record KeyringProtection(int operationalIterationMaximum) {
/** /**
* Validates the operational limit. * Validates the operational limit.
* *
* @throws IllegalArgumentException if the limit is below the creation * @throws IllegalArgumentException if the limit is below the creation setting
* setting or above the absolute operational maximum * or above the absolute operational maximum
*/ */
public KeyringProtection { public KeyringProtection {
if (operationalIterationMaximum < CREATION_ITERATIONS if (operationalIterationMaximum < CREATION_ITERATIONS

View File

@@ -7,8 +7,10 @@ package zeroecho.core.storage;
/** /**
* Fills keyring randomness buffers. * Fills keyring randomness buffers.
* *
* <p>This package-private seam supports deterministic format tests; production * <p>
* creation uses the authoritative shared secure random source.</p> * This package-private seam supports deterministic format tests; production
* creation uses the authoritative shared secure random source.
* </p>
*/ */
@FunctionalInterface @FunctionalInterface
interface KeyringRandomBytes { interface KeyringRandomBytes {

File diff suppressed because it is too large Load Diff

View File

@@ -53,15 +53,15 @@
* </p> * </p>
* *
* <p> * <p>
* Unlock passwords are destroyable, transfer ownership to the receiver, and * Unlock passwords are destroyable, transfer ownership to the receiver, and are
* are destroyed immediately after the master key is unwrapped. The unlocked * destroyed immediately after the master key is unwrapped. The unlocked store
* store retains the master key, its domain-separated nonce-reservation MAC * retains the master key, its domain-separated nonce-reservation MAC key, and
* key, and encrypted entry records; closing the store clears this material. * encrypted entry records; closing the store clears this material. The store
* The store requires a POSIX filesystem on which owner-only permissions can be * requires a POSIX filesystem on which owner-only permissions can be verified.
* verified. A directory-force failure after atomic replacement makes the open * A directory-force failure after atomic replacement makes the open instance
* instance unusable until close and authenticated reopen resolves which * unusable until close and authenticated reopen resolves which complete image
* complete image is current. Non-exportable keys must remain behind an * is current. Non-exportable keys must remain behind an external provider
* external provider reference. * reference.
* </p> * </p>
* *
* <p> * <p>

View File

@@ -124,8 +124,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
public static TagEngineBuilder<byte[]> digest(final ZeroEchoSession session, final DigestSpec spec) { public static TagEngineBuilder<byte[]> digest(final ZeroEchoSession session, final DigestSpec spec) {
Objects.requireNonNull(session, "session"); Objects.requireNonNull(session, "session");
final DigestSpec s = spec == null ? DigestSpec.sha256() : spec; final DigestSpec s = spec == null ? DigestSpec.sha256() : spec;
return new TagEngineBuilder<>( return new TagEngineBuilder<>(() -> session.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, s));
() -> session.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, s));
} }
/** /**
@@ -193,8 +192,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
* @return a builder that produces Ed25519 signature engines in SIGN mode * @return a builder that produces Ed25519 signature engines in SIGN mode
* @throws NullPointerException if {@code privateKey} is {@code null} * @throws NullPointerException if {@code privateKey} is {@code null}
*/ */
public static TagEngineBuilder<Signature> ed25519Sign(final ZeroEchoSession session, public static TagEngineBuilder<Signature> ed25519Sign(final ZeroEchoSession session, final PrivateKey privateKey) {
final PrivateKey privateKey) {
Objects.requireNonNull(privateKey, PRIVATE_KEY); Objects.requireNonNull(privateKey, PRIVATE_KEY);
return signature(session, "Ed25519", privateKey, VoidSpec.INSTANCE); return signature(session, "Ed25519", privateKey, VoidSpec.INSTANCE);
} }
@@ -206,8 +204,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
* @return a builder that produces Ed25519 signature engines in VERIFY mode * @return a builder that produces Ed25519 signature engines in VERIFY mode
* @throws NullPointerException if {@code publicKey} is {@code null} * @throws NullPointerException if {@code publicKey} is {@code null}
*/ */
public static TagEngineBuilder<Signature> ed25519Verify(final ZeroEchoSession session, public static TagEngineBuilder<Signature> ed25519Verify(final ZeroEchoSession session, final PublicKey publicKey) {
final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY); Objects.requireNonNull(publicKey, PUBLIC_KEY);
return signature(session, "Ed25519", publicKey, VoidSpec.INSTANCE); return signature(session, "Ed25519", publicKey, VoidSpec.INSTANCE);
} }
@@ -229,8 +226,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
public static TagEngineBuilder<Signature> rsaSign(final ZeroEchoSession session, final PrivateKey privateKey, public static TagEngineBuilder<Signature> rsaSign(final ZeroEchoSession session, final PrivateKey privateKey,
final RsaSigSpec spec) { final RsaSigSpec spec) {
Objects.requireNonNull(privateKey, PRIVATE_KEY); Objects.requireNonNull(privateKey, PRIVATE_KEY);
return signature(session, "RSA", privateKey, return signature(session, "RSA", privateKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
} }
/** /**
@@ -250,8 +246,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
public static TagEngineBuilder<Signature> rsaVerify(final ZeroEchoSession session, final PublicKey publicKey, public static TagEngineBuilder<Signature> rsaVerify(final ZeroEchoSession session, final PublicKey publicKey,
final RsaSigSpec spec) { final RsaSigSpec spec) {
Objects.requireNonNull(publicKey, PUBLIC_KEY); Objects.requireNonNull(publicKey, PUBLIC_KEY);
return signature(session, "RSA", publicKey, return signature(session, "RSA", publicKey, spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
} }
/** /**
@@ -369,8 +364,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
* @return a builder that produces SLH-DSA signature engines in SIGN mode * @return a builder that produces SLH-DSA signature engines in SIGN mode
* @throws NullPointerException if {@code privateKey} is {@code null} * @throws NullPointerException if {@code privateKey} is {@code null}
*/ */
public static TagEngineBuilder<Signature> slhDsaSign(final ZeroEchoSession session, public static TagEngineBuilder<Signature> slhDsaSign(final ZeroEchoSession session, final PrivateKey privateKey) {
final PrivateKey privateKey) {
Objects.requireNonNull(privateKey, PRIVATE_KEY); Objects.requireNonNull(privateKey, PRIVATE_KEY);
return signature(session, "SLH-DSA", privateKey, VoidSpec.INSTANCE); return signature(session, "SLH-DSA", privateKey, VoidSpec.INSTANCE);
} }
@@ -388,8 +382,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
* @return a builder that produces SLH-DSA signature engines in VERIFY mode * @return a builder that produces SLH-DSA signature engines in VERIFY mode
* @throws NullPointerException if {@code publicKey} is {@code null} * @throws NullPointerException if {@code publicKey} is {@code null}
*/ */
public static TagEngineBuilder<Signature> slhDsaVerify(final ZeroEchoSession session, public static TagEngineBuilder<Signature> slhDsaVerify(final ZeroEchoSession session, final PublicKey publicKey) {
final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY); Objects.requireNonNull(publicKey, PUBLIC_KEY);
return signature(session, "SLH-DSA", publicKey, VoidSpec.INSTANCE); return signature(session, "SLH-DSA", publicKey, VoidSpec.INSTANCE);
} }
@@ -408,8 +401,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
* @return a builder that produces ML-DSA signature engines in SIGN mode * @return a builder that produces ML-DSA signature engines in SIGN mode
* @throws NullPointerException if {@code privateKey} is {@code null} * @throws NullPointerException if {@code privateKey} is {@code null}
*/ */
public static TagEngineBuilder<Signature> mldsaSign(final ZeroEchoSession session, public static TagEngineBuilder<Signature> mldsaSign(final ZeroEchoSession session, final PrivateKey privateKey) {
final PrivateKey privateKey) {
Objects.requireNonNull(privateKey, PRIVATE_KEY); Objects.requireNonNull(privateKey, PRIVATE_KEY);
return signature(session, "ML-DSA", privateKey, VoidSpec.INSTANCE); return signature(session, "ML-DSA", privateKey, VoidSpec.INSTANCE);
} }
@@ -428,8 +420,7 @@ public final class TagEngineBuilder<T> implements Supplier<TagEngine<T>> {
* @return a builder that produces ML-DSA signature engines in VERIFY mode * @return a builder that produces ML-DSA signature engines in VERIFY mode
* @throws NullPointerException if {@code publicKey} is {@code null} * @throws NullPointerException if {@code publicKey} is {@code null}
*/ */
public static TagEngineBuilder<Signature> mldsaVerify(final ZeroEchoSession session, public static TagEngineBuilder<Signature> mldsaVerify(final ZeroEchoSession session, final PublicKey publicKey) {
final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY); Objects.requireNonNull(publicKey, PUBLIC_KEY);
return signature(session, "ML-DSA", publicKey, VoidSpec.INSTANCE); return signature(session, "ML-DSA", publicKey, VoidSpec.INSTANCE);
} }

View File

@@ -25,9 +25,11 @@ import zeroecho.core.spi.SymmetricKeyImporter;
/** /**
* Session-bound entry point for exact key-material operations. * Session-bound entry point for exact key-material operations.
* *
* <p>Capability lookup fails before an operation object is returned. Returned * <p>
* Capability lookup fails before an operation object is returned. Returned
* objects guarantee the requested operation and report successful execution to * objects guarantee the requested operation and report successful execution to
* the owning session's audit listener on a best-effort basis.</p> * the owning session's audit listener on a best-effort basis.
* </p>
* *
* @since 1.0 * @since 1.0
*/ */
@@ -69,13 +71,12 @@ public final class KeyBuilders {
* Resolves an exact symmetric generator. * Resolves an exact symmetric generator.
* *
* @param algorithmId canonical algorithm identifier * @param algorithmId canonical algorithm identifier
* @param specType exact specification class * @param specType exact specification class
* @param <S> specification type * @param <S> specification type
* @return guaranteed generator * @return guaranteed generator
* @throws IllegalArgumentException if the capability is absent * @throws IllegalArgumentException if the capability is absent
*/ */
public <S extends AlgorithmKeySpec> SymmetricKeyGenerator<S> generator(String algorithmId, public <S extends AlgorithmKeySpec> SymmetricKeyGenerator<S> generator(String algorithmId, Class<S> specType) {
Class<S> specType) {
CryptoAlgorithm algorithm = session.require(algorithmId); CryptoAlgorithm algorithm = session.require(algorithmId);
SymmetricKeyGenerator<S> delegate = algorithm.symmetricKeyGenerator(specType); SymmetricKeyGenerator<S> delegate = algorithm.symmetricKeyGenerator(specType);
return spec -> { return spec -> {
@@ -89,13 +90,12 @@ public final class KeyBuilders {
* Resolves an exact symmetric importer. * Resolves an exact symmetric importer.
* *
* @param algorithmId canonical algorithm identifier * @param algorithmId canonical algorithm identifier
* @param specType exact specification class * @param specType exact specification class
* @param <S> specification type * @param <S> specification type
* @return guaranteed importer * @return guaranteed importer
* @throws IllegalArgumentException if the capability is absent * @throws IllegalArgumentException if the capability is absent
*/ */
public <S extends AlgorithmKeySpec> SymmetricKeyImporter<S> importer(String algorithmId, public <S extends AlgorithmKeySpec> SymmetricKeyImporter<S> importer(String algorithmId, Class<S> specType) {
Class<S> specType) {
CryptoAlgorithm algorithm = session.require(algorithmId); CryptoAlgorithm algorithm = session.require(algorithmId);
SymmetricKeyImporter<S> delegate = algorithm.symmetricKeyImporter(specType); SymmetricKeyImporter<S> delegate = algorithm.symmetricKeyImporter(specType);
return spec -> { return spec -> {
@@ -109,12 +109,13 @@ public final class KeyBuilders {
* Generates a symmetric key using the exact runtime specification type. * Generates a symmetric key using the exact runtime specification type.
* *
* @param algorithmId canonical algorithm identifier * @param algorithmId canonical algorithm identifier
* @param spec generation specification * @param spec generation specification
* @param <S> specification type * @param <S> specification type
* @return generated secret key * @return generated secret key
* @throws java.security.GeneralSecurityException if generation fails * @throws java.security.GeneralSecurityException if generation fails
* @throws IllegalArgumentException if the capability is absent * @throws IllegalArgumentException if the capability is absent
* @throws NullPointerException if {@code spec} is {@code null} * @throws NullPointerException if {@code spec} is
* {@code null}
*/ */
public <S extends AlgorithmKeySpec> SecretKey generate(String algorithmId, S spec) public <S extends AlgorithmKeySpec> SecretKey generate(String algorithmId, S spec)
throws java.security.GeneralSecurityException { throws java.security.GeneralSecurityException {
@@ -128,12 +129,13 @@ public final class KeyBuilders {
* Imports a symmetric key using the exact runtime specification type. * Imports a symmetric key using the exact runtime specification type.
* *
* @param algorithmId canonical algorithm identifier * @param algorithmId canonical algorithm identifier
* @param spec import specification * @param spec import specification
* @param <S> specification type * @param <S> specification type
* @return imported secret key * @return imported secret key
* @throws java.security.GeneralSecurityException if import fails * @throws java.security.GeneralSecurityException if import fails
* @throws IllegalArgumentException if the capability is absent * @throws IllegalArgumentException if the capability is absent
* @throws NullPointerException if {@code spec} is {@code null} * @throws NullPointerException if {@code spec} is
* {@code null}
*/ */
public <S extends AlgorithmKeySpec> SecretKey importKey(String algorithmId, S spec) public <S extends AlgorithmKeySpec> SecretKey importKey(String algorithmId, S spec)
throws java.security.GeneralSecurityException { throws java.security.GeneralSecurityException {
@@ -155,8 +157,8 @@ public final class KeyBuilders {
* Resolves an exact key-pair generator. * Resolves an exact key-pair generator.
* *
* @param algorithmId canonical algorithm identifier * @param algorithmId canonical algorithm identifier
* @param specType exact specification class * @param specType exact specification class
* @param <S> specification type * @param <S> specification type
* @return guaranteed generator * @return guaranteed generator
* @throws IllegalArgumentException if the capability is absent * @throws IllegalArgumentException if the capability is absent
*/ */
@@ -175,13 +177,12 @@ public final class KeyBuilders {
* Resolves an exact public-key importer. * Resolves an exact public-key importer.
* *
* @param algorithmId canonical algorithm identifier * @param algorithmId canonical algorithm identifier
* @param specType exact specification class * @param specType exact specification class
* @param <S> specification type * @param <S> specification type
* @return guaranteed importer * @return guaranteed importer
* @throws IllegalArgumentException if the capability is absent * @throws IllegalArgumentException if the capability is absent
*/ */
public <S extends AlgorithmKeySpec> PublicKeyImporter<S> publicImporter(String algorithmId, public <S extends AlgorithmKeySpec> PublicKeyImporter<S> publicImporter(String algorithmId, Class<S> specType) {
Class<S> specType) {
CryptoAlgorithm algorithm = session.require(algorithmId); CryptoAlgorithm algorithm = session.require(algorithmId);
PublicKeyImporter<S> delegate = algorithm.publicKeyImporter(specType); PublicKeyImporter<S> delegate = algorithm.publicKeyImporter(specType);
return spec -> { return spec -> {
@@ -195,8 +196,8 @@ public final class KeyBuilders {
* Resolves an exact private-key importer. * Resolves an exact private-key importer.
* *
* @param algorithmId canonical algorithm identifier * @param algorithmId canonical algorithm identifier
* @param specType exact specification class * @param specType exact specification class
* @param <S> specification type * @param <S> specification type
* @return guaranteed importer * @return guaranteed importer
* @throws IllegalArgumentException if the capability is absent * @throws IllegalArgumentException if the capability is absent
*/ */
@@ -215,12 +216,13 @@ public final class KeyBuilders {
* Generates a key pair using the exact runtime specification type. * Generates a key pair using the exact runtime specification type.
* *
* @param algorithmId canonical algorithm identifier * @param algorithmId canonical algorithm identifier
* @param spec generation specification * @param spec generation specification
* @param <S> specification type * @param <S> specification type
* @return generated key pair * @return generated key pair
* @throws java.security.GeneralSecurityException if generation fails * @throws java.security.GeneralSecurityException if generation fails
* @throws IllegalArgumentException if the capability is absent * @throws IllegalArgumentException if the capability is absent
* @throws NullPointerException if {@code spec} is {@code null} * @throws NullPointerException if {@code spec} is
* {@code null}
*/ */
public <S extends AlgorithmKeySpec> KeyPair generateKeyPair(String algorithmId, S spec) public <S extends AlgorithmKeySpec> KeyPair generateKeyPair(String algorithmId, S spec)
throws java.security.GeneralSecurityException { throws java.security.GeneralSecurityException {
@@ -234,12 +236,13 @@ public final class KeyBuilders {
* Imports a public key using the exact runtime specification type. * Imports a public key using the exact runtime specification type.
* *
* @param algorithmId canonical algorithm identifier * @param algorithmId canonical algorithm identifier
* @param spec public-key import specification * @param spec public-key import specification
* @param <S> specification type * @param <S> specification type
* @return imported public key * @return imported public key
* @throws java.security.GeneralSecurityException if import fails * @throws java.security.GeneralSecurityException if import fails
* @throws IllegalArgumentException if the capability is absent * @throws IllegalArgumentException if the capability is absent
* @throws NullPointerException if {@code spec} is {@code null} * @throws NullPointerException if {@code spec} is
* {@code null}
*/ */
public <S extends AlgorithmKeySpec> PublicKey importPublic(String algorithmId, S spec) public <S extends AlgorithmKeySpec> PublicKey importPublic(String algorithmId, S spec)
throws java.security.GeneralSecurityException { throws java.security.GeneralSecurityException {
@@ -253,12 +256,13 @@ public final class KeyBuilders {
* Imports a private key using the exact runtime specification type. * Imports a private key using the exact runtime specification type.
* *
* @param algorithmId canonical algorithm identifier * @param algorithmId canonical algorithm identifier
* @param spec private-key import specification * @param spec private-key import specification
* @param <S> specification type * @param <S> specification type
* @return imported private key * @return imported private key
* @throws java.security.GeneralSecurityException if import fails * @throws java.security.GeneralSecurityException if import fails
* @throws IllegalArgumentException if the capability is absent * @throws IllegalArgumentException if the capability is absent
* @throws NullPointerException if {@code spec} is {@code null} * @throws NullPointerException if {@code spec} is
* {@code null}
*/ */
public <S extends AlgorithmKeySpec> PrivateKey importPrivate(String algorithmId, S spec) public <S extends AlgorithmKeySpec> PrivateKey importPrivate(String algorithmId, S spec)
throws java.security.GeneralSecurityException { throws java.security.GeneralSecurityException {

View File

@@ -8,10 +8,11 @@
package zeroecho.sdk; package zeroecho.sdk;
/** /**
* Explicit PBKDF2 work-factor limits for trusted configuration and decoded data. * Explicit PBKDF2 work-factor limits for trusted configuration and decoded
* data.
* *
* @param operationalMaximum largest iteration count accepted from trusted local * @param operationalMaximum largest iteration count accepted from trusted
* configuration * local configuration
* @param absoluteDecodedMaximum hard safety ceiling for untrusted decoded data * @param absoluteDecodedMaximum hard safety ceiling for untrusted decoded data
* @since 1.0 * @since 1.0
*/ */
@@ -41,8 +42,8 @@ public record Pbkdf2Limits(int operationalMaximum, int absoluteDecodedMaximum) {
*/ */
public void validateTrusted(int iterations) { public void validateTrusted(int iterations) {
if (iterations < MINIMUM || iterations > operationalMaximum) { if (iterations < MINIMUM || iterations > operationalMaximum) {
throw new IllegalArgumentException("PBKDF2 iterations must be in range " + MINIMUM + ".." throw new IllegalArgumentException(
+ operationalMaximum + ": " + iterations); "PBKDF2 iterations must be in range " + MINIMUM + ".." + operationalMaximum + ": " + iterations);
} }
} }

View File

@@ -108,8 +108,8 @@ public final class ZeroEchoSession {
this(CryptoPolicy.permissive(), AuditListener.noop(), AuditMode.OFF, null); this(CryptoPolicy.permissive(), AuditListener.noop(), AuditMode.OFF, null);
} }
private ZeroEchoSession(CryptoPolicy<ContextSpec, Key> policy, AuditListener auditListener, private ZeroEchoSession(CryptoPolicy<ContextSpec, Key> policy, AuditListener auditListener, AuditMode auditMode,
AuditMode auditMode, Pbkdf2Limits pbkdf2Limits) { Pbkdf2Limits pbkdf2Limits) {
this.policy = Objects.requireNonNull(policy, "policy must not be null"); this.policy = Objects.requireNonNull(policy, "policy must not be null");
this.auditListener = Objects.requireNonNull(auditListener, "auditListener must not be null"); this.auditListener = Objects.requireNonNull(auditListener, "auditListener must not be null");
this.auditSink = AuditListeners.bestEffort(auditListener); this.auditSink = AuditListeners.bestEffort(auditListener);
@@ -143,8 +143,7 @@ public final class ZeroEchoSession {
*/ */
public ZeroEchoSession withAuditListener(AuditListener newAuditListener) { public ZeroEchoSession withAuditListener(AuditListener newAuditListener) {
return new ZeroEchoSession(policy, return new ZeroEchoSession(policy,
Objects.requireNonNull(newAuditListener, "newAuditListener must not be null"), auditMode, Objects.requireNonNull(newAuditListener, "newAuditListener must not be null"), auditMode, pbkdf2Limits);
pbkdf2Limits);
} }
/** /**
@@ -198,8 +197,8 @@ public final class ZeroEchoSession {
* Returns the audit listener owned by this session. * Returns the audit listener owned by this session.
* *
* <p> * <p>
* The returned listener is the configured strategy, not mutable session * The returned listener is the configured strategy, not mutable session state.
* state. It is exposed to support manual audit mode. * It is exposed to support manual audit mode.
* </p> * </p>
* *
* @return the non-null audit listener * @return the non-null audit listener
@@ -254,8 +253,8 @@ public final class ZeroEchoSession {
* @param id canonical algorithm identifier * @param id canonical algorithm identifier
* @param role intended key usage * @param role intended key usage
* @param key key compatible with the selected algorithm and role * @param key key compatible with the selected algorithm and role
* @param spec optional context specification, or {@code null} for the * @param spec optional context specification, or {@code null} for the algorithm
* algorithm default * default
* @param <C> context type * @param <C> context type
* @param <K> key type * @param <K> key type
* @param <S> context specification type * @param <S> context specification type
@@ -275,8 +274,8 @@ public final class ZeroEchoSession {
return finishContext(algorithm, context, role, spec); return finishContext(algorithm, context, role, spec);
} }
private <C extends CryptoContext, S extends ContextSpec> C finishContext(CryptoAlgorithm algorithm, private <C extends CryptoContext, S extends ContextSpec> C finishContext(CryptoAlgorithm algorithm, C context,
C context, KeyUsage role, S spec) { KeyUsage role, S spec) {
if (auditMode == AuditMode.OFF) { if (auditMode == AuditMode.OFF) {
notifyContextCreated(algorithm, role, spec); notifyContextCreated(algorithm, role, spec);
return context; return context;
@@ -321,14 +320,15 @@ public final class ZeroEchoSession {
* Destroys a key and verifies that it entered the destroyed state. * Destroys a key and verifies that it entered the destroyed state.
* *
* @param algorithmId algorithm identifier used as audit metadata * @param algorithmId algorithm identifier used as audit metadata
* @param provider provider name used as audit metadata * @param provider provider name used as audit metadata
* @param key key to destroy; must not be {@code null} * @param key key to destroy; must not be {@code null}
* @return {@code true} only when this call transitions the key to destroyed; * @return {@code true} only when this call transitions the key to destroyed;
* {@code false} for a non-destroyable or already destroyed key * {@code false} for a non-destroyable or already destroyed key
* @throws NullPointerException if {@code key} is {@code null} * @throws NullPointerException if {@code key} is {@code null}
* @throws DestroyFailedException if destruction fails or the key does not * @throws DestroyFailedException if destruction fails or the key does not
* report itself destroyed afterward * report itself destroyed afterward
* @throws RuntimeException if the key's lifecycle implementation throws one * @throws RuntimeException if the key's lifecycle implementation throws
* one
*/ */
public boolean destroyKey(String algorithmId, String provider, Key key) throws DestroyFailedException { public boolean destroyKey(String algorithmId, String provider, Key key) throws DestroyFailedException {
Objects.requireNonNull(key, "key must not be null"); Objects.requireNonNull(key, "key must not be null");
@@ -352,12 +352,10 @@ public final class ZeroEchoSession {
return true; return true;
} }
private <S extends ContextSpec> void notifyContextCreated(CryptoAlgorithm algorithm, private <S extends ContextSpec> void notifyContextCreated(CryptoAlgorithm algorithm, KeyUsage role, S spec) {
KeyUsage role, S spec) { Map<String, Object> metadata = spec == null ? Map.of() : Map.of("specType", spec.getClass().getName());
Map<String, Object> metadata = spec == null ? Map.of() auditSink.onContextCreatedMeta(UUID.randomUUID().toString(), algorithm.id(), algorithm.providerName(), role,
: Map.of("specType", spec.getClass().getName()); "n/a", metadata);
auditSink.onContextCreatedMeta(UUID.randomUUID().toString(), algorithm.id(), algorithm.providerName(),
role, "n/a", metadata);
} }
/* default */ void notifyKeyPairGenerated(CryptoAlgorithm algorithm, AlgorithmKeySpec spec, KeyPair keyPair) { /* default */ void notifyKeyPairGenerated(CryptoAlgorithm algorithm, AlgorithmKeySpec spec, KeyPair keyPair) {

View File

@@ -342,10 +342,8 @@ public final class HybridKexBuilder {
if (pqcAlgId == null) { if (pqcAlgId == null) {
throw new IllegalStateException("pqc algorithm id must be set"); throw new IllegalStateException("pqc algorithm id must be set");
} }
if (classicMode == ClassicMode.CLASSIC_AGREEMENT if (classicMode == ClassicMode.CLASSIC_AGREEMENT && (classicPrivate == null || classicPeerPublic == null)) {
&& (classicPrivate == null || classicPeerPublic == null)) { throw new IllegalStateException("classic private key and peer public must be set for CLASSIC_AGREEMENT");
throw new IllegalStateException(
"classic private key and peer public must be set for CLASSIC_AGREEMENT");
} }
if (classicMode == ClassicMode.PAIR_MESSAGE && classicKeyPair == null) { if (classicMode == ClassicMode.PAIR_MESSAGE && classicKeyPair == null) {
throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE"); throw new IllegalStateException("classic key pair must be set for PAIR_MESSAGE");

View File

@@ -77,12 +77,13 @@ import zeroecho.sdk.hybrid.signature.HybridSignatureProfile;
* <li>{@link #single(ZeroEchoSession)}: constructs a non-hybrid * <li>{@link #single(ZeroEchoSession)}: constructs a non-hybrid
* {@code SignatureContext}.</li> * {@code SignatureContext}.</li>
* <li>{@link #hybrid(ZeroEchoSession)}: constructs a hybrid * <li>{@link #hybrid(ZeroEchoSession)}: constructs a hybrid
* {@code SignatureContext} via * {@code SignatureContext} via {@link HybridSignatureContexts}.</li>
* {@link HybridSignatureContexts}.</li>
* </ul> * </ul>
* *
* <p>Context construction is in-memory. Checked I/O failures arise only when a * <p>
* built stream is attached or processed.</p> * Context construction is in-memory. Checked I/O failures arise only when a
* built stream is attached or processed.
* </p>
* *
* @since 1.0 * @since 1.0
*/ */
@@ -253,8 +254,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
Objects.requireNonNull(algorithmId, "algorithmId"); Objects.requireNonNull(algorithmId, "algorithmId");
Objects.requireNonNull(privateKey, "privateKey"); Objects.requireNonNull(privateKey, "privateKey");
Supplier<TagEngine<Signature>> factory = () -> session.createContext(algorithmId, KeyUsage.SIGN, Supplier<TagEngine<Signature>> factory = () -> session.createContext(algorithmId, KeyUsage.SIGN, privateKey,
privateKey, spec); spec);
return core(factory); return core(factory);
} }
@@ -293,8 +294,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
Objects.requireNonNull(algorithmId, "algorithmId"); Objects.requireNonNull(algorithmId, "algorithmId");
Objects.requireNonNull(publicKey, "publicKey"); Objects.requireNonNull(publicKey, "publicKey");
Supplier<TagEngine<Signature>> factory = () -> session.createContext(algorithmId, Supplier<TagEngine<Signature>> factory = () -> session.createContext(algorithmId, KeyUsage.VERIFY,
KeyUsage.VERIFY, publicKey, spec); publicKey, spec);
return core(factory); return core(factory);
} }

View File

@@ -567,8 +567,7 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder<DataCo
* <p> * <p>
* The actual cipher work is delegated to an * The actual cipher work is delegated to an
* {@link zeroecho.core.context.EncryptionContext} created through * {@link zeroecho.core.context.EncryptionContext} created through
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String, * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}.
* zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}.
* If the created context implements {@code ContextAware}, the configured * If the created context implements {@code ContextAware}, the configured
* context is injected before the stream is attached. * context is injected before the stream is attached.
* </p> * </p>
@@ -638,8 +637,7 @@ public final class ChaChaDataContentBuilder implements DataContentBuilder<DataCo
* <p> * <p>
* The actual cipher work is delegated to an * The actual cipher work is delegated to an
* {@link zeroecho.core.context.EncryptionContext} created through * {@link zeroecho.core.context.EncryptionContext} created through
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String, * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}.
* zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}.
* If the created context implements {@code ContextAware}, the configured * If the created context implements {@code ContextAware}, the configured
* context is injected before the stream is attached. * context is injected before the stream is attached.
* </p> * </p>

View File

@@ -113,6 +113,7 @@ import zeroecho.sdk.content.api.PlainContent;
*/ */
public final class DigestDataContentBuilder implements DataContentBuilder<PlainContent> { public final class DigestDataContentBuilder implements DataContentBuilder<PlainContent> {
private final ZeroEchoSession session; private final ZeroEchoSession session;
/** /**
* OutputMode selects how the digest-computing pipeline presents its result to * OutputMode selects how the digest-computing pipeline presents its result to
* callers. * callers.

View File

@@ -361,8 +361,7 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilder<Da
* {@link ElgamalEncSpec}. When {@link #getStream()} is called, it creates an * {@link ElgamalEncSpec}. When {@link #getStream()} is called, it creates an
* {@link zeroecho.core.context.EncryptionContext} for the "ElGamal" algorithm * {@link zeroecho.core.context.EncryptionContext} for the "ElGamal" algorithm
* in {@link zeroecho.core.KeyUsage#ENCRYPT} role via * in {@link zeroecho.core.KeyUsage#ENCRYPT} role via
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String, * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* attaches the upstream stream, and returns a pull-based stream that encrypts * attaches the upstream stream, and returns a pull-based stream that encrypts
* on the fly. * on the fly.
* </p> * </p>
@@ -426,8 +425,7 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilder<Da
* {@link ElgamalEncSpec}. When {@link #getStream()} is called, it creates an * {@link ElgamalEncSpec}. When {@link #getStream()} is called, it creates an
* {@link zeroecho.core.context.EncryptionContext} for the "ElGamal" algorithm * {@link zeroecho.core.context.EncryptionContext} for the "ElGamal" algorithm
* in {@link zeroecho.core.KeyUsage#DECRYPT} role via * in {@link zeroecho.core.KeyUsage#DECRYPT} role via
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String, * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* attaches the upstream stream, and returns a pull-based stream that decrypts * attaches the upstream stream, and returns a pull-based stream that decrypts
* on the fly. * on the fly.
* </p> * </p>

View File

@@ -91,6 +91,7 @@ import zeroecho.sdk.content.api.PlainContent;
public final class HmacDataContentBuilder implements DataContentBuilder<PlainContent> { public final class HmacDataContentBuilder implements DataContentBuilder<PlainContent> {
private static final String ALGORITHM_ID = "HMAC"; private static final String ALGORITHM_ID = "HMAC";
private final ZeroEchoSession session; private final ZeroEchoSession session;
/** /**
* Mode selects whether the pipeline computes an HMAC tag or verifies one. * Mode selects whether the pipeline computes an HMAC tag or verifies one.
* *
@@ -622,8 +623,7 @@ public final class HmacDataContentBuilder implements DataContentBuilder<PlainCon
final String mac = spec.macName(); // e.g., "HmacSHA256" final String mac = spec.macName(); // e.g., "HmacSHA256"
try { try {
if (genKeyBits != null) { if (genKeyBits != null) {
return session.keyBuilders().symmetric().generate(ALGORITHM_ID, return session.keyBuilders().symmetric().generate(ALGORITHM_ID, new HmacKeyGenSpec(mac, genKeyBits));
new HmacKeyGenSpec(mac, genKeyBits));
} }
if (importRaw != null || importHex != null || importBase64 != null) { if (importRaw != null || importHex != null || importBase64 != null) {
HmacKeyImportSpec ispec; HmacKeyImportSpec ispec;

View File

@@ -345,8 +345,7 @@ public final class RsaEncDataContentBuilder implements DataContentBuilder<DataCo
* When {@link #getStream()} is invoked, this class creates an * When {@link #getStream()} is invoked, this class creates an
* {@link zeroecho.core.context.EncryptionContext} for the "RSA" algorithm in * {@link zeroecho.core.context.EncryptionContext} for the "RSA" algorithm in
* {@link zeroecho.core.KeyUsage#ENCRYPT} role via * {@link zeroecho.core.KeyUsage#ENCRYPT} role via
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String, * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* attaches the upstream stream, and returns a pull-based stream that encrypts * attaches the upstream stream, and returns a pull-based stream that encrypts
* on-the-fly using the configured {@link RsaEncSpec}. * on-the-fly using the configured {@link RsaEncSpec}.
* </p> * </p>
@@ -407,8 +406,7 @@ public final class RsaEncDataContentBuilder implements DataContentBuilder<DataCo
* When {@link #getStream()} is invoked, this class creates an * When {@link #getStream()} is invoked, this class creates an
* {@link zeroecho.core.context.EncryptionContext} for the "RSA" algorithm in * {@link zeroecho.core.context.EncryptionContext} for the "RSA" algorithm in
* {@link zeroecho.core.KeyUsage#DECRYPT} role via * {@link zeroecho.core.KeyUsage#DECRYPT} role via
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String, * {@link zeroecho.sdk.ZeroEchoSession#createContext(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* attaches the upstream stream, and returns a pull-based stream that decrypts * attaches the upstream stream, and returns a pull-based stream that decrypts
* on-the-fly using the configured {@link RsaEncSpec}. * on-the-fly using the configured {@link RsaEncSpec}.
* </p> * </p>

View File

@@ -109,6 +109,7 @@ import zeroecho.sdk.content.api.PlainContent;
public final class RsaSigDataContentBuilder implements DataContentBuilder<PlainContent> { public final class RsaSigDataContentBuilder implements DataContentBuilder<PlainContent> {
private static final String ALGORITHM_ID = "RSA"; private static final String ALGORITHM_ID = "RSA";
private final ZeroEchoSession session; private final ZeroEchoSession session;
/** /**
* Mode selects whether the builder signs or verifies. * Mode selects whether the builder signs or verifies.
*/ */

View File

@@ -70,7 +70,8 @@
* {@link zeroecho.sdk.builders.alg.ElgamalEncDataContentBuilder}.</li> * {@link zeroecho.sdk.builders.alg.ElgamalEncDataContentBuilder}.</li>
* <li>RSA signatures: * <li>RSA signatures:
* {@link zeroecho.sdk.builders.alg.RsaSigDataContentBuilder}; generic signature * {@link zeroecho.sdk.builders.alg.RsaSigDataContentBuilder}; generic signature
* trailers use {@link zeroecho.sdk.builders.SignatureTrailerDataContentBuilder}.</li> * trailers use
* {@link zeroecho.sdk.builders.SignatureTrailerDataContentBuilder}.</li>
* <li>MAC and digest: {@link zeroecho.sdk.builders.alg.HmacDataContentBuilder}, * <li>MAC and digest: {@link zeroecho.sdk.builders.alg.HmacDataContentBuilder},
* {@link zeroecho.sdk.builders.alg.DigestDataContentBuilder}.</li> * {@link zeroecho.sdk.builders.alg.DigestDataContentBuilder}.</li>
* <li>KEM envelopes: {@link zeroecho.sdk.builders.alg.KemDataContentBuilder} * <li>KEM envelopes: {@link zeroecho.sdk.builders.alg.KemDataContentBuilder}

View File

@@ -85,8 +85,8 @@ public final class SecretPassword implements SecretContent, Destroyable {
} }
/** /**
* Constructs a password from a caller-owned character array. The supplied * Constructs a password from a caller-owned character array. The supplied array
* array is cloned and remains owned by the caller. * is cloned and remains owned by the caller.
* *
* @param password password characters; must not be {@code null} * @param password password characters; must not be {@code null}
* @throws NullPointerException if {@code password} is {@code null} * @throws NullPointerException if {@code password} is {@code null}

View File

@@ -148,8 +148,7 @@ final class Decryptor implements PlainContent, MultiRecipientContent {
} catch (AEADBadTagException ex) { } catch (AEADBadTagException ex) {
// wrong key/password for that entry, continue scanning // wrong key/password for that entry, continue scanning
if (LOG.isLoggable(Level.FINE)) { if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "recipient authentication failed: {0}", LOG.log(Level.FINE, "recipient authentication failed: {0}", ex.getClass().getSimpleName());
ex.getClass().getSimpleName());
} }
} catch (GeneralSecurityException | IOException | IllegalArgumentException ex) { } catch (GeneralSecurityException | IOException | IllegalArgumentException ex) {
// entry not applicable to this opener/material; ignore and continue // entry not applicable to this opener/material; ignore and continue
@@ -198,8 +197,7 @@ final class Decryptor implements PlainContent, MultiRecipientContent {
} }
} }
private void closeAfterAttempt(InputStream input, boolean transferred, Throwable primary) private void closeAfterAttempt(InputStream input, boolean transferred, Throwable primary) throws IOException {
throws IOException {
IOException cleanupFailure = null; IOException cleanupFailure = null;
if (!transferred) { if (!transferred) {
try { try {
@@ -282,8 +280,7 @@ final class Decryptor implements PlainContent, MultiRecipientContent {
closeOpeners(openers, primary); closeOpeners(openers, primary);
} }
/* default */ static void closeOpeners(List<RecipientOpener> ownedOpeners, Throwable primary) /* default */ static void closeOpeners(List<RecipientOpener> ownedOpeners, Throwable primary) throws IOException {
throws IOException {
IOException cleanupFailure = null; IOException cleanupFailure = null;
for (RecipientOpener opener : ownedOpeners) { // NOPMD - each opener is closed in this loop for (RecipientOpener opener : ownedOpeners) { // NOPMD - each opener is closed in this loop
try { try {
@@ -309,14 +306,13 @@ final class Decryptor implements PlainContent, MultiRecipientContent {
} }
} }
private void rejectOversizedCek(byte[] candidate, int fieldIndex, String recipientId, private void rejectOversizedCek(byte[] candidate, int fieldIndex, String recipientId, RecipientOpener opener) {
RecipientOpener opener) {
try { try {
if (LOG.isLoggable(Level.WARNING)) { if (LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING, LOG.log(Level.WARNING,
"Suspicious material in field {0}: {1}/{2} returned length {3}, while {4} is the limit. Ignoring.", "Suspicious material in field {0}: {1}/{2} returned length {3}, while {4} is the limit. Ignoring.",
new Object[] { fieldIndex, recipientId, opener.getClass().getName(), new Object[] { fieldIndex, recipientId, opener.getClass().getName(), candidate.length,
candidate.length, keyBytes }); keyBytes });
} }
} finally { } finally {
Arrays.fill(candidate, (byte) 0); Arrays.fill(candidate, (byte) 0);

View File

@@ -84,8 +84,7 @@ final class Encryptor implements EncryptedContent, MultiRecipientContent {
this.keyBytes = keyBytes; this.keyBytes = keyBytes;
this.maxRecipients = maxRecipients; this.maxRecipients = maxRecipients;
this.maxEntryLen = maxEntryLen; this.maxEntryLen = maxEntryLen;
this.randomBytesFactory = Objects.requireNonNull(randomBytesFactory, this.randomBytesFactory = Objects.requireNonNull(randomBytesFactory, "randomBytesFactory must not be null");
"randomBytesFactory must not be null");
} }
/** /**
@@ -232,8 +231,7 @@ final class Encryptor implements EncryptedContent, MultiRecipientContent {
return key; return key;
} }
/* default */ static void closeRecipients(List<Recipient> ownedRecipients, Throwable primary) /* default */ static void closeRecipients(List<Recipient> ownedRecipients, Throwable primary) throws IOException {
throws IOException {
IOException cleanupFailure = null; IOException cleanupFailure = null;
for (Recipient recipient : ownedRecipients) { for (Recipient recipient : ownedRecipients) {
try { try {

View File

@@ -82,8 +82,7 @@ public final class KemCtxRecipient implements Recipient, AutoCloseable {
* @param kekBytes KEK length; exactly 16 or 32 bytes * @param kekBytes KEK length; exactly 16 or 32 bytes
* @param saltLen length of the random salt to apply during HKDF * @param saltLen length of the random salt to apply during HKDF
* @throws NullPointerException if {@code ctx} is {@code null} * @throws NullPointerException if {@code ctx} is {@code null}
* @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or * @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or 32
* 32
*/ */
public KemCtxRecipient(KemContext ctx, int kekBytes, int saltLen) { public KemCtxRecipient(KemContext ctx, int kekBytes, int saltLen) {
this(ctx, kekBytes, saltLen, false); this(ctx, kekBytes, saltLen, false);
@@ -107,8 +106,7 @@ public final class KemCtxRecipient implements Recipient, AutoCloseable {
* @param decoy {@code true} if this recipient is a decoy (fake entry that * @param decoy {@code true} if this recipient is a decoy (fake entry that
* cannot unwrap a CEK); {@code false} if it is a real recipient * cannot unwrap a CEK); {@code false} if it is a real recipient
* @throws NullPointerException if {@code ctx} is {@code null} * @throws NullPointerException if {@code ctx} is {@code null}
* @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or * @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or 32
* 32
*/ */
public KemCtxRecipient(KemContext ctx, int kekBytes, int saltLen, boolean decoy) { public KemCtxRecipient(KemContext ctx, int kekBytes, int saltLen, boolean decoy) {
int validatedKekBytes = RecipientKekSizes.requireSupported(kekBytes); int validatedKekBytes = RecipientKekSizes.requireSupported(kekBytes);

View File

@@ -23,9 +23,9 @@ final class KemKeyDerivation {
* Derives a KEK bound to the KEM algorithm identifier. * Derives a KEK bound to the KEM algorithm identifier.
* *
* @param sharedSecret KEM shared secret * @param sharedSecret KEM shared secret
* @param salt HKDF salt * @param salt HKDF salt
* @param algorithmId canonical KEM algorithm identifier * @param algorithmId canonical KEM algorithm identifier
* @param outputBytes requested KEK size * @param outputBytes requested KEK size
* @return newly allocated KEK bytes * @return newly allocated KEK bytes
* @throws GeneralSecurityException if HKDF fails * @throws GeneralSecurityException if HKDF fails
*/ */

View File

@@ -46,17 +46,17 @@ import zeroecho.sdk.content.api.DataContent;
* resources until processing or explicit cleanup. * resources until processing or explicit cleanup.
* *
* <p> * <p>
* Callers must invoke {@link #close()} when a built instance is abandoned before * Callers must invoke {@link #close()} when a built instance is abandoned
* {@link #getStream()} is called. Successful or failed stream construction also * before {@link #getStream()} is called. Successful or failed stream
* releases the owned recipient resources. Unlocking keys and password material * construction also releases the owned recipient resources. Unlocking keys and
* supplied separately remain caller-owned and are never destroyed by this * password material supplied separately remain caller-owned and are never
* content. * destroyed by this content.
* </p> * </p>
* *
* <p> * <p>
* Implementations are not thread-safe. Cleanup is idempotent, and content cannot * Implementations are not thread-safe. Cleanup is idempotent, and content
* be used after cleanup. Calling {@link #getStream()} is terminal even when stream * cannot be used after cleanup. Calling {@link #getStream()} is terminal even
* construction fails. * when stream construction fails.
* </p> * </p>
*/ */
public interface MultiRecipientContent extends DataContent, Destroyable, AutoCloseable { public interface MultiRecipientContent extends DataContent, Destroyable, AutoCloseable {

View File

@@ -215,8 +215,8 @@ public final class MultiRecipientDataSourceBuilder
ensureOpen(); ensureOpen();
RecipientKekSizes.requireSupported(kekBytes); RecipientKekSizes.requireSupported(kekBytes);
session.pbkdf2Limits().validateTrusted(iterations); session.pbkdf2Limits().validateTrusted(iterations);
this.recipients.add(new PasswordRecipient(password, iterations, saltLen, kekBytes, false, this.recipients
session.pbkdf2Limits())); .add(new PasswordRecipient(password, iterations, saltLen, kekBytes, false, session.pbkdf2Limits()));
return this; return this;
} }
@@ -236,8 +236,7 @@ public final class MultiRecipientDataSourceBuilder
* @param saltLen HKDF salt length in bytes * @param saltLen HKDF salt length in bytes
* @return this builder * @return this builder
* @throws NullPointerException if {@code kem} is {@code null} * @throws NullPointerException if {@code kem} is {@code null}
* @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or * @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or 32
* 32
*/ */
public MultiRecipientDataSourceBuilder addRecipient(KemContext kem, int kekBytes, int saltLen) { public MultiRecipientDataSourceBuilder addRecipient(KemContext kem, int kekBytes, int saltLen) {
ensureOpen(); ensureOpen();
@@ -292,8 +291,8 @@ public final class MultiRecipientDataSourceBuilder
ensureOpen(); ensureOpen();
RecipientKekSizes.requireSupported(kekBytes); RecipientKekSizes.requireSupported(kekBytes);
session.pbkdf2Limits().validateTrusted(iterations); session.pbkdf2Limits().validateTrusted(iterations);
this.recipients.add(new PasswordRecipient(password, iterations, saltLen, kekBytes, true, this.recipients
session.pbkdf2Limits())); .add(new PasswordRecipient(password, iterations, saltLen, kekBytes, true, session.pbkdf2Limits()));
return this; return this;
} }
@@ -314,8 +313,7 @@ public final class MultiRecipientDataSourceBuilder
* @param saltLen HKDF salt length in bytes * @param saltLen HKDF salt length in bytes
* @return this builder * @return this builder
* @throws NullPointerException if {@code kem} is {@code null} * @throws NullPointerException if {@code kem} is {@code null}
* @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or * @throws IllegalArgumentException if {@code kekBytes} is not exactly 16 or 32
* 32
*/ */
public MultiRecipientDataSourceBuilder addRecipientDecoy(KemContext kem, int kekBytes, int saltLen) { public MultiRecipientDataSourceBuilder addRecipientDecoy(KemContext kem, int kekBytes, int saltLen) {
ensureOpen(); ensureOpen();
@@ -384,9 +382,9 @@ public final class MultiRecipientDataSourceBuilder
* </p> * </p>
* *
* <p> * <p>
* The builder takes ownership of the opener. The opener must be reusable * The builder takes ownership of the opener. The opener must be reusable across
* across all recipient entries and is closed after scanning or when the built * all recipient entries and is closed after scanning or when the built content
* content is abandoned. * is abandoned.
* </p> * </p>
* *
* @param opener reusable opener to add * @param opener reusable opener to add
@@ -479,8 +477,10 @@ public final class MultiRecipientDataSourceBuilder
/** /**
* Destroys recipient secrets still owned by this builder. * Destroys recipient secrets still owned by this builder.
* *
* <p>Recipients transferred to a successfully built encrypting content object * <p>
* are owned and destroyed by that object instead.</p> * Recipients transferred to a successfully built encrypting content object are
* owned and destroyed by that object instead.
* </p>
* *
* @throws DestroyFailedException if recipient cleanup fails * @throws DestroyFailedException if recipient cleanup fails
*/ */

View File

@@ -58,6 +58,7 @@ public final class PasswordOpener implements RecipientOpener {
public PasswordOpener(Pbkdf2Limits limits) { public PasswordOpener(Pbkdf2Limits limits) {
this.limits = java.util.Objects.requireNonNull(limits, "limits must not be null"); this.limits = java.util.Objects.requireNonNull(limits, "limits must not be null");
} }
/** /**
* Attempts to open a password-based recipient entry using a password unlock * Attempts to open a password-based recipient entry using a password unlock
* material. * material.

View File

@@ -76,8 +76,8 @@ public final class PasswordRecipient implements Recipient, Destroyable, AutoClos
* <li>The caller should clear the {@code password} array after constructing the * <li>The caller should clear the {@code password} array after constructing the
* recipient to minimize exposure in memory.</li> * recipient to minimize exposure in memory.</li>
* <li>Choose an iteration count appropriate to the target platform to balance * <li>Choose an iteration count appropriate to the target platform to balance
* password-guessing resistance against recipient creation and opening * password-guessing resistance against recipient creation and opening latency.
* latency. Counts below {@value Pbkdf2Limits#MINIMUM} are rejected.</li> * Counts below {@value Pbkdf2Limits#MINIMUM} are rejected.</li>
* <li>Decoy recipients increase confidentiality by hiding the number of real * <li>Decoy recipients increase confidentiality by hiding the number of real
* recipients but cannot successfully unwrap the CEK.</li> * recipients but cannot successfully unwrap the CEK.</li>
* </ul> * </ul>

View File

@@ -8,9 +8,11 @@ package zeroecho.sdk.guard;
* Defines the KEK sizes supported by recipient entries without an encoded size * Defines the KEK sizes supported by recipient entries without an encoded size
* discriminator. * discriminator.
* *
* <p>The current recipient format permits AES-128 and AES-256 wrapping only. * <p>
* The current recipient format permits AES-128 and AES-256 wrapping only.
* Validation must occur before an entry is registered so every emitted entry * Validation must occur before an entry is registered so every emitted entry
* remains openable by the corresponding recipient opener.</p> * remains openable by the corresponding recipient opener.
* </p>
* *
* @since 1.0 * @since 1.0
*/ */

View File

@@ -18,9 +18,11 @@ import zeroecho.core.annotation.Describable;
/** /**
* Caller-owned session-operation input used to unlock a recipient entry. * Caller-owned session-operation input used to unlock a recipient entry.
* *
* <p>Components accepting an {@code UnlockMaterial} borrow it and do not destroy * <p>
* Components accepting an {@code UnlockMaterial} borrow it and do not destroy
* it. The caller must keep it usable until the operation completes and destroy * it. The caller must keep it usable until the operation completes and destroy
* password material afterwards.</p> * password material afterwards.
* </p>
*/ */
public sealed interface UnlockMaterial extends Describable { public sealed interface UnlockMaterial extends Describable {
/** /**
@@ -44,8 +46,10 @@ public sealed interface UnlockMaterial extends Describable {
/** /**
* Destroyable password unlocking material backed by an owned character array. * Destroyable password unlocking material backed by an owned character array.
* *
* <p>Construction and access use defensive copies. Destruction is idempotent * <p>
* and prevents subsequent access.</p> * Construction and access use defensive copies. Destruction is idempotent and
* prevents subsequent access.
* </p>
*/ */
final class Password implements UnlockMaterial, Destroyable { final class Password implements UnlockMaterial, Destroyable {
private final char[] characters; private final char[] characters;

View File

@@ -108,8 +108,8 @@
* generation and recipient entries; the symmetric builder manages algorithm * generation and recipient entries; the symmetric builder manages algorithm
* parameters and payload framing.</li> * parameters and payload framing.</li>
* <li><strong>Reusable opener strategies:</strong> recipients encode entries; * <li><strong>Reusable opener strategies:</strong> recipients encode entries;
* openers attempt every applicable entry and create fresh cryptographic contexts * openers attempt every applicable entry and create fresh cryptographic
* per attempt. Neither carries long-lived secret state.</li> * contexts per attempt. Neither carries long-lived secret state.</li>
* <li><strong>Defensive parsing:</strong> the builder applies limits to the * <li><strong>Defensive parsing:</strong> the builder applies limits to the
* number of recipients and the size of each entry blob; the symmetric stage * number of recipients and the size of each entry blob; the symmetric stage
* applies its own limits to its header and payload.</li> * applies its own limits to its header and payload.</li>

View File

@@ -196,8 +196,8 @@ public final class HybridDerived {
* construction. * construction.
* </p> * </p>
* *
* @param aes AES builder to configure (must not be null) * @param aes AES builder to configure (must not be null)
* @param keyBits AES key size in bits (128/192/256) * @param keyBits AES key size in bits (128/192/256)
* @return the provided builder instance * @return the provided builder instance
* @throws NullPointerException if aes is null * @throws NullPointerException if aes is null
* @throws IllegalArgumentException if keyBits is invalid * @throws IllegalArgumentException if keyBits is invalid
@@ -225,16 +225,16 @@ public final class HybridDerived {
} }
/** /**
* Derives a ChaCha key and applies it with optional AAD to the provided * Derives a ChaCha key and applies it with optional AAD to the provided ChaCha
* ChaCha builder. * builder.
* *
* <p> * <p>
* The returned value is the same builder instance to preserve fluent pipeline * The returned value is the same builder instance to preserve fluent pipeline
* construction. * construction.
* </p> * </p>
* *
* @param chacha ChaCha builder to configure (must not be null) * @param chacha ChaCha builder to configure (must not be null)
* @param keyBits key size in bits (typically 256) * @param keyBits key size in bits (typically 256)
* @return the provided builder instance * @return the provided builder instance
* @throws NullPointerException if chacha is null * @throws NullPointerException if chacha is null
* @throws IllegalArgumentException if keyBits is invalid * @throws IllegalArgumentException if keyBits is invalid

Some files were not shown because too many files have changed in this diff Show More