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

@@ -146,10 +146,8 @@ public final class Guard {
* @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

@@ -214,8 +214,7 @@ public final class Kem { // NOPMD
* @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());
} }
@@ -230,10 +229,8 @@ public final class Kem { // NOPMD
* @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);

View File

@@ -200,8 +200,7 @@ public final class KeyStoreManagement {
* @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,8 +158,7 @@ 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());
} }
@@ -174,8 +173,7 @@ public final class Tag { // NOPMD
* @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,9 +16,11 @@ 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
@@ -30,15 +32,15 @@ import zeroecho.core.spec.ContextSpec;
* @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,
@@ -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,
@@ -515,7 +513,9 @@ 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
@@ -537,7 +537,9 @@ 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
@@ -556,10 +558,11 @@ 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
@@ -581,7 +584,9 @@ 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
@@ -603,7 +608,9 @@ 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
@@ -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

@@ -20,8 +20,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* 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.
@@ -38,8 +38,8 @@ 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

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;
} }
@@ -148,15 +144,13 @@ final class KeyringImportRegistry {
* @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() {
} }
@@ -33,10 +32,9 @@ final class KeyringNonceReservationKdf {
* @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();

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 {

View File

@@ -65,35 +65,44 @@ import zeroecho.core.util.RandomSupport;
/** /**
* Path-bound encrypted local software keystore. * Path-bound encrypted local software keystore.
* *
* <p>A keyring uses PBKDF2-HMAC-SHA-256 to unwrap one random 256-bit store * <p>
* master key. Every entry and the ordered manifest are independently protected * A keyring uses PBKDF2-HMAC-SHA-256 to unwrap one random 256-bit store master
* by AES-256-GCM. Java class names are neither persisted nor resolved. Import * key. Every entry and the ordered manifest are independently protected by
* AES-256-GCM. Java class names are neither persisted nor resolved. Import
* mappings come exclusively from a closed mapping to the immutable algorithm * mappings come exclusively from a closed mapping to the immutable algorithm
* registry. Provider names are not persisted: standard key encodings are * registry. Provider names are not persisted: standard key encodings are
* reconstructed by the current runtime's canonical importer. HMAC persistence * reconstructed by the current runtime's canonical importer. HMAC persistence
* admits only HmacSHA256, HmacSHA384, and HmacSHA512.</p> * admits only HmacSHA256, HmacSHA384, and HmacSHA512.
* </p>
* *
* <p>The keyring exclusively owns its filesystem path from successful * <p>
* The keyring exclusively owns its filesystem path from successful
* {@link #create(Path, KeyringPassword)} or * {@link #create(Path, KeyringPassword)} or
* {@link #open(Path, KeyringPassword)} until {@link #close()}. Only POSIX * {@link #open(Path, KeyringPassword)} until {@link #close()}. Only POSIX
* filesystems on which owner-only permissions and ownership can be verified are * filesystems on which owner-only permissions and ownership can be verified are
* supported. Mutations replace a complete encrypted image atomically.</p> * supported. Mutations replace a complete encrypted image atomically.
* </p>
* *
* <p>The nonce-reservation sidecar is authenticated independently from AES * <p>
* entry and manifest encryption. Its 256-bit MAC key is derived from the store * The nonce-reservation sidecar is authenticated independently from AES entry
* master key and binary store UUID with HKDF-HMAC-SHA-256 and the fixed domain * and manifest encryption. Its 256-bit MAC key is derived from the store master
* label {@code zeroecho:keyring:nonce-reservation-mac:v1}. The derived key is * key and binary store UUID with HKDF-HMAC-SHA-256 and the fixed domain label
* {@code zeroecho:keyring:nonce-reservation-mac:v1}. The derived key is
* retained only while the store is open and is cleared on close. Previous * retained only while the store is open and is cleared on close. Previous
* sidecar versions are rejected.</p> * sidecar versions are rejected.
* </p>
* *
* <p>Instances are thread-safe. Reads may proceed concurrently; mutations and * <p>
* Instances are thread-safe. Reads may proceed concurrently; mutations and
* close are exclusive. Closing clears the master and sidecar MAC keys and makes * close are exclusive. Closing clears the master and sidecar MAC keys and makes
* all subsequent operations fail. The pre-release plaintext format is rejected * all subsequent operations fail. The pre-release plaintext format is rejected
* and is not migrated.</p> * and is not migrated.
* </p>
*/ */
// The store intentionally centralizes its closed format, crypto, and lifecycle types. // The store intentionally centralizes its closed format, crypto, and lifecycle types.
@SuppressWarnings("PMD.CouplingBetweenObjects") @SuppressWarnings("PMD.CouplingBetweenObjects")
public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD public final class KeyringStore implements AutoCloseable, Destroyable {
// NOPMD
/* default */ static final byte[] MAGIC = { 'Z', 'E', 'K', 'R', 'I', 'N', 'G', '2' }; /* default */ static final byte[] MAGIC = { 'Z', 'E', 'K', 'R', 'I', 'N', 'G', '2' };
/* default */ static final int FORMAT_VERSION = 2; /* default */ static final int FORMAT_VERSION = 2;
/* default */ static final int ENTRY_FORMAT_VERSION = 1; /* default */ static final int ENTRY_FORMAT_VERSION = 1;
@@ -113,8 +122,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
/* default */ static final int SHA256_BYTES = 32; /* default */ static final int SHA256_BYTES = 32;
/* default */ static final byte KDF_PBKDF2_SHA256 = 1; /* default */ static final byte KDF_PBKDF2_SHA256 = 1;
/* default */ static final byte AEAD_AES_256_GCM = 1; /* default */ static final byte AEAD_AES_256_GCM = 1;
private static final byte[] NONCE_RESERVATION_MAGIC = private static final byte[] NONCE_RESERVATION_MAGIC = { 'Z', 'E', 'K', 'N', 'O', 'N', 'C', '2' };
{ 'Z', 'E', 'K', 'N', 'O', 'N', 'C', '2' };
private static final int NONCE_RESERVATION_VERSION = 2; private static final int NONCE_RESERVATION_VERSION = 2;
private static final int NONCE_RESERVATION_TAG_BYTES = 32; private static final int NONCE_RESERVATION_TAG_BYTES = 32;
private static final byte MASTER_WRAP_NONCE_DOMAIN = 1; private static final byte MASTER_WRAP_NONCE_DOMAIN = 1;
@@ -123,17 +131,16 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
private static final String SUFFIX_PUBLIC = ".pub"; private static final String SUFFIX_PUBLIC = ".pub";
private static final String SUFFIX_PRIVATE = ".priv"; private static final String SUFFIX_PRIVATE = ".priv";
private static final long MIN_NONCE_HIGH_WATER = 1L; private static final long MIN_NONCE_HIGH_WATER = 1L;
private static final Set<PosixFilePermission> DIRECTORY_PERMISSIONS = private static final Set<PosixFilePermission> DIRECTORY_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ,
EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE);
PosixFilePermission.OWNER_EXECUTE); private static final Set<PosixFilePermission> FILE_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ,
private static final Set<PosixFilePermission> FILE_PERMISSIONS = PosixFilePermission.OWNER_WRITE);
EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE); private static final FileAttribute<Set<PosixFilePermission>> DIRECTORY_ATTRIBUTE = PosixFilePermissions
private static final FileAttribute<Set<PosixFilePermission>> DIRECTORY_ATTRIBUTE = .asFileAttribute(DIRECTORY_PERMISSIONS);
PosixFilePermissions.asFileAttribute(DIRECTORY_PERMISSIONS); private static final FileAttribute<Set<PosixFilePermission>> FILE_ATTRIBUTE = PosixFilePermissions
private static final FileAttribute<Set<PosixFilePermission>> FILE_ATTRIBUTE = .asFileAttribute(FILE_PERMISSIONS);
PosixFilePermissions.asFileAttribute(FILE_PERMISSIONS); private static final KeyringRandomBytes SYSTEM_RANDOM = destination -> RandomSupport.getRandom()
private static final KeyringRandomBytes SYSTEM_RANDOM = destination -> .nextBytes(destination);
RandomSupport.getRandom().nextBytes(destination);
private final Path path; private final Path path;
private final Path nonceReservationPath; private final Path nonceReservationPath;
private final Ownership ownership; private final Ownership ownership;
@@ -157,9 +164,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
* Standard encoded key forms supported by the encrypted store. * Standard encoded key forms supported by the encrypted store.
*/ */
public enum Encoding { public enum Encoding {
X509(1), X509(1), PKCS8(2), RAW(3);
PKCS8(2),
RAW(3);
private final int code; private final int code;
@@ -181,9 +186,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
* Key classifications supported by the encrypted store. * Key classifications supported by the encrypted store.
*/ */
public enum Kind { public enum Kind {
PUBLIC_KEY(1), PUBLIC_KEY(1), PRIVATE_KEY(2), SECRET_KEY(3);
PRIVATE_KEY(2),
SECRET_KEY(3);
private final int code; private final int code;
@@ -201,11 +204,9 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private KeyringStore(Path path, Ownership ownership, private KeyringStore(Path path, Ownership ownership, KeyringRandomBytes random,
KeyringRandomBytes random, KeyringFileOperations fileOperations, KeyringFileOperations fileOperations, Header header, byte[] masterKey, byte[] nonceReservationMacKey,
Header header, byte[] masterKey, Manifest manifest, Map<String, EncryptedEntry> entries) {
byte[] nonceReservationMacKey, Manifest manifest,
Map<String, EncryptedEntry> entries) {
this.path = path; this.path = path;
this.nonceReservationPath = nonceReservationPath(path); this.nonceReservationPath = nonceReservationPath(path);
this.ownership = ownership; this.ownership = ownership;
@@ -227,10 +228,11 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
* Creates a new encrypted keyring with the standard protection policy. * Creates a new encrypted keyring with the standard protection policy.
* *
* @param path new keyring file * @param path new keyring file
* @param password borrowed unlock password; the caller remains responsible * @param password borrowed unlock password; the caller remains responsible for
* for destroying it * destroying it
* @return open encrypted keyring * @return open encrypted keyring
* @throws IOException if secure creation or durable persistence fails * @throws IOException if secure creation or durable persistence
* fails
* @throws GeneralSecurityException if cryptographic initialization fails * @throws GeneralSecurityException if cryptographic initialization fails
*/ */
public static KeyringStore create(Path path, KeyringPassword password) public static KeyringStore create(Path path, KeyringPassword password)
@@ -245,24 +247,23 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
* @param password borrowed unlock password * @param password borrowed unlock password
* @param protection operational protection policy * @param protection operational protection policy
* @return open encrypted keyring * @return open encrypted keyring
* @throws IOException if secure creation or durable persistence fails * @throws IOException if secure creation or durable persistence
* fails
* @throws GeneralSecurityException if cryptographic initialization fails * @throws GeneralSecurityException if cryptographic initialization fails
*/ */
public static KeyringStore create(Path path, KeyringPassword password, public static KeyringStore create(Path path, KeyringPassword password, KeyringProtection protection)
KeyringProtection protection) throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
return create(path, password, protection, SYSTEM_RANDOM); return create(path, password, protection, SYSTEM_RANDOM);
} }
/* default */ static KeyringStore create(Path path, KeyringPassword password, /* default */ static KeyringStore create(Path path, KeyringPassword password, KeyringProtection protection,
KeyringProtection protection, KeyringRandomBytes random) KeyringRandomBytes random) throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
return create(path, password, protection, random, KeyringFileOperations.NIO); return create(path, password, protection, random, KeyringFileOperations.NIO);
} }
@SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" }) @SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" })
/* default */ static KeyringStore create(Path path, KeyringPassword password, /* default */ static KeyringStore create(Path path, KeyringPassword password, KeyringProtection protection,
KeyringProtection protection, KeyringRandomBytes random, KeyringRandomBytes random, KeyringFileOperations fileOperations)
KeyringFileOperations fileOperations)
throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
Objects.requireNonNull(password, "password must not be null"); Objects.requireNonNull(password, "password must not be null");
Objects.requireNonNull(protection, "protection must not be null"); Objects.requireNonNull(protection, "protection must not be null");
@@ -287,19 +288,18 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
random.nextBytes(masterKey); random.nextBytes(masterKey);
random.nextBytes(salt); random.nextBytes(salt);
random.nextBytes(storeId); random.nextBytes(storeId);
nonceReservationMacKey = nonceReservationMacKey = KeyringNonceReservationKdf.derive(masterKey, storeId);
KeyringNonceReservationKdf.derive(masterKey, storeId);
fillMasterWrapNonce(random, wrapNonce); fillMasterWrapNonce(random, wrapNonce);
int prefix = randomPrefix(random); int prefix = randomPrefix(random);
Header provisional = new Header(storeId, KeyringProtection.CREATION_ITERATIONS, Header provisional = new Header(storeId, KeyringProtection.CREATION_ITERATIONS, salt, wrapNonce,
salt, wrapNonce, new byte[0]); new byte[0]);
aad = masterWrapAad(provisional); aad = masterWrapAad(provisional);
kek = deriveKek(password, salt, provisional.iterations); kek = deriveKek(password, salt, provisional.iterations);
wrapped = crypt(Cipher.ENCRYPT_MODE, kek, wrapNonce, aad, masterKey); wrapped = crypt(Cipher.ENCRYPT_MODE, kek, wrapNonce, aad, masterKey);
Header header = new Header(storeId, provisional.iterations, salt, wrapNonce, wrapped); Header header = new Header(storeId, provisional.iterations, salt, wrapNonce, wrapped);
Manifest initial = new Manifest(prefix, 0, List.of()); Manifest initial = new Manifest(prefix, 0, List.of());
store = new KeyringStore(normalized, ownership, random, fileOperations, header, store = new KeyringStore(normalized, ownership, random, fileOperations, header, masterKey,
masterKey, nonceReservationMacKey, initial, new LinkedHashMap<>()); nonceReservationMacKey, initial, new LinkedHashMap<>());
store.persistSnapshot(new LinkedHashMap<>()); store.persistSnapshot(new LinkedHashMap<>());
success = true; success = true;
return store; return store;
@@ -328,12 +328,11 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
* @param path keyring file * @param path keyring file
* @param password borrowed unlock password * @param password borrowed unlock password
* @return open encrypted keyring * @return open encrypted keyring
* @throws IOException if the format, filesystem, or authentication check * @throws IOException if the format, filesystem, or authentication
* fails * check fails
* @throws GeneralSecurityException if cryptographic initialization fails * @throws GeneralSecurityException if cryptographic initialization fails
*/ */
public static KeyringStore open(Path path, KeyringPassword password) public static KeyringStore open(Path path, KeyringPassword password) throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
return open(path, password, KeyringProtection.standard()); return open(path, password, KeyringProtection.standard());
} }
@@ -344,25 +343,23 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
* @param password borrowed unlock password * @param password borrowed unlock password
* @param protection operational protection policy * @param protection operational protection policy
* @return open encrypted keyring * @return open encrypted keyring
* @throws IOException if the format, filesystem, or authentication check * @throws IOException if the format, filesystem, or authentication
* fails * check fails
* @throws GeneralSecurityException if cryptographic initialization fails * @throws GeneralSecurityException if cryptographic initialization fails
*/ */
public static KeyringStore open(Path path, KeyringPassword password, public static KeyringStore open(Path path, KeyringPassword password, KeyringProtection protection)
KeyringProtection protection) throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
return open(path, password, protection, SYSTEM_RANDOM); return open(path, password, protection, SYSTEM_RANDOM);
} }
/* default */ static KeyringStore open(Path path, KeyringPassword password, /* default */ static KeyringStore open(Path path, KeyringPassword password, KeyringProtection protection,
KeyringProtection protection, KeyringRandomBytes random) KeyringRandomBytes random) throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
return open(path, password, protection, random, KeyringFileOperations.NIO); return open(path, password, protection, random, KeyringFileOperations.NIO);
} }
@SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" }) @SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" })
/* default */ static KeyringStore open(Path path, KeyringPassword password, /* default */ static KeyringStore open(Path path, KeyringPassword password, KeyringProtection protection,
KeyringProtection protection, KeyringRandomBytes random, KeyringRandomBytes random, KeyringFileOperations fileOperations)
KeyringFileOperations fileOperations)
throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
Objects.requireNonNull(password, "password must not be null"); Objects.requireNonNull(password, "password must not be null");
Objects.requireNonNull(protection, "protection must not be null"); Objects.requireNonNull(protection, "protection must not be null");
@@ -379,19 +376,16 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
image = readStoreImage(normalized); image = readStoreImage(normalized);
Decoded decoded = decodeImage(image, password, protection); Decoded decoded = decodeImage(image, password, protection);
masterKey = decoded.masterKey; masterKey = decoded.masterKey;
nonceReservationMacKey = KeyringNonceReservationKdf.derive( nonceReservationMacKey = KeyringNonceReservationKdf.derive(masterKey, decoded.header.storeId);
masterKey, decoded.header.storeId); long reservedHighWater = readNonceReservation(normalized, ownership.owner(), decoded.header.storeId,
long reservedHighWater = readNonceReservation(normalized, ownership.owner(), decoded.manifest.noncePrefix, nonceReservationMacKey);
decoded.header.storeId, decoded.manifest.noncePrefix,
nonceReservationMacKey);
if (reservedHighWater < decoded.manifest.highWater) { if (reservedHighWater < decoded.manifest.highWater) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
} }
Manifest effectiveManifest = new Manifest(decoded.manifest.noncePrefix, Manifest effectiveManifest = new Manifest(decoded.manifest.noncePrefix, reservedHighWater,
reservedHighWater, decoded.manifest.entries); decoded.manifest.entries);
KeyringStore store = new KeyringStore(normalized, ownership, KeyringStore store = new KeyringStore(normalized, ownership, random, fileOperations, decoded.header,
random, fileOperations, decoded.header, masterKey, nonceReservationMacKey, masterKey, nonceReservationMacKey, effectiveManifest, decoded.entries);
effectiveManifest, decoded.entries);
success = true; success = true;
return store; return store;
} finally { } finally {
@@ -446,8 +440,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
public void putSecret(String alias, String algorithmId, SecretKey key) public void putSecret(String alias, String algorithmId, SecretKey key)
throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
Objects.requireNonNull(key, "key must not be null"); Objects.requireNonNull(key, "key must not be null");
KeyringImportRegistry.HmacVariant hmacVariant = KeyringImportRegistry.HmacVariant hmacVariant = KeyringImportRegistry.HmacVariant.forStoredKey(algorithmId,
KeyringImportRegistry.HmacVariant.forStoredKey(algorithmId, key); key);
put(alias, algorithmId, Kind.SECRET_KEY, Encoding.RAW, key, hmacVariant); put(alias, algorithmId, Kind.SECRET_KEY, Encoding.RAW, key, hmacVariant);
} }
@@ -598,7 +592,9 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
* Clears the master and nonce-reservation MAC keys and releases filesystem * Clears the master and nonce-reservation MAC keys and releases filesystem
* ownership. * ownership.
* *
* <p>Concurrent calls are safe and cleanup occurs exactly once.</p> * <p>
* Concurrent calls are safe and cleanup occurs exactly once.
* </p>
* *
* @throws DestroyFailedException if resource release fails * @throws DestroyFailedException if resource release fails
*/ */
@@ -647,9 +643,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private void put(String alias, String algorithmId, Kind kind, Encoding encoding, private void put(String alias, String algorithmId, Kind kind, Encoding encoding, Key key,
Key key, KeyringImportRegistry.HmacVariant hmacVariant) KeyringImportRegistry.HmacVariant hmacVariant) throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
Objects.requireNonNull(key, "key must not be null"); Objects.requireNonNull(key, "key must not be null");
validateString(alias, MAX_ALIAS_BYTES); validateString(alias, MAX_ALIAS_BYTES);
validateString(algorithmId, MAX_METADATA_BYTES); validateString(algorithmId, MAX_METADATA_BYTES);
@@ -663,8 +658,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
throw new KeyringException(Code.KEYRING_NON_EXPORTABLE_KEY); throw new KeyringException(Code.KEYRING_NON_EXPORTABLE_KEY);
} }
try { try {
KeyringImportRegistry.validateCanonical(algorithmId, kind, encoding, KeyringImportRegistry.validateCanonical(algorithmId, kind, encoding, hmacVariant, key, encoded);
hmacVariant, key, encoded);
lifecycleLock.writeLock().lock(); lifecycleLock.writeLock().lock();
try { try {
ensureOpen(); ensureOpen();
@@ -680,16 +674,14 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
byte[] aad = null; byte[] aad = null;
byte[] ciphertext = null; byte[] ciphertext = null;
try { try {
plaintext = encodeEntryPlaintext(alias, algorithmId, kind, encoding, plaintext = encodeEntryPlaintext(alias, algorithmId, kind, encoding, hmacVariant, encoded);
hmacVariant, encoded);
aad = entryAad(entryId, position); aad = entryAad(entryId, position);
ciphertext = crypt(Cipher.ENCRYPT_MODE, masterKey, nonce, aad, plaintext); ciphertext = crypt(Cipher.ENCRYPT_MODE, masterKey, nonce, aad, plaintext);
if (ciphertext.length > MAX_ENTRY_CIPHERTEXT_BYTES) { if (ciphertext.length > MAX_ENTRY_CIPHERTEXT_BYTES) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED); throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
} }
EncryptedEntry entry = new EncryptedEntry(entryId, position, alias, EncryptedEntry entry = new EncryptedEntry(entryId, position, alias, algorithmId, kind, encoding,
algorithmId, kind, encoding, hmacVariant, nonce, ciphertext, hmacVariant, nonce, ciphertext, digest(ciphertext));
digest(ciphertext));
candidate.put(alias, entry); candidate.put(alias, entry);
persistSnapshot(candidate); persistSnapshot(candidate);
entries = candidate; entries = candidate;
@@ -709,8 +701,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
@SuppressWarnings("PMD.PreserveStackTrace") @SuppressWarnings("PMD.PreserveStackTrace")
private Imported importEntry(String alias, Kind expected) private Imported importEntry(String alias, Kind expected) throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
lifecycleLock.readLock().lock(); lifecycleLock.readLock().lock();
try { try {
ensureOpen(); ensureOpen();
@@ -726,8 +717,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
plaintext = crypt(Cipher.DECRYPT_MODE, masterKey, entry.nonce, aad, entry.ciphertext); plaintext = crypt(Cipher.DECRYPT_MODE, masterKey, entry.nonce, aad, entry.ciphertext);
decoded = decodeEntryPlaintext(plaintext); decoded = decodeEntryPlaintext(plaintext);
validateEntryBinding(entry, decoded); validateEntryBinding(entry, decoded);
Key key = KeyringImportRegistry.importKey(decoded.algorithm, decoded.kind, Key key = KeyringImportRegistry.importKey(decoded.algorithm, decoded.kind, decoded.encoding,
decoded.encoding, decoded.hmacVariant, decoded.encoded); decoded.hmacVariant, decoded.encoded);
return new Imported(decoded.algorithm, key); return new Imported(decoded.algorithm, key);
} catch (AEADBadTagException exception) { } catch (AEADBadTagException exception) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
@@ -743,8 +734,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private void persistSnapshot(Map<String, EncryptedEntry> candidate) private void persistSnapshot(Map<String, EncryptedEntry> candidate) throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
byte[] manifestNonce = null; byte[] manifestNonce = null;
byte[] manifestPlain = null; byte[] manifestPlain = null;
byte[] manifestAad = null; byte[] manifestAad = null;
@@ -754,8 +744,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
manifestNonce = nextNonce(); manifestNonce = nextNonce();
manifestPlain = encodeManifest(candidate); manifestPlain = encodeManifest(candidate);
manifestAad = manifestAad(candidate.size()); manifestAad = manifestAad(candidate.size());
manifestCipher = crypt(Cipher.ENCRYPT_MODE, masterKey, manifestNonce, manifestCipher = crypt(Cipher.ENCRYPT_MODE, masterKey, manifestNonce, manifestAad, manifestPlain);
manifestAad, manifestPlain);
image = encodeImage(candidate, manifestNonce, manifestCipher); image = encodeImage(candidate, manifestNonce, manifestCipher);
writeAtomically(image); writeAtomically(image);
} catch (IOException | GeneralSecurityException exception) { } catch (IOException | GeneralSecurityException exception) {
@@ -780,15 +769,14 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
@SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.ExceptionAsFlowControl" }) @SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.ExceptionAsFlowControl" })
private void writeAtomically(Target target, Path destination, byte[] image) private void writeAtomically(Target target, Path destination, byte[] image) throws IOException {
throws IOException {
Path parent = destination.getParent(); Path parent = destination.getParent();
Path temporary = null; Path temporary = null;
boolean moved = false; boolean moved = false;
IOException primary = null; IOException primary = null;
try { try {
temporary = fileOperations.createTemporary(target, parent, temporary = fileOperations.createTemporary(target, parent, "." + destination.getFileName() + ".", ".tmp",
"." + destination.getFileName() + ".", ".tmp", FILE_ATTRIBUTE); FILE_ATTRIBUTE);
validateOwnerOnly(temporary, false); validateOwnerOnly(temporary, false);
fileOperations.writeTemporary(target, temporary, image); fileOperations.writeTemporary(target, temporary, image);
fileOperations.forceTemporary(target, temporary); fileOperations.forceTemporary(target, temporary);
@@ -844,15 +832,13 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private void writeNonceReservation(long highWater) private void writeNonceReservation(long highWater) throws IOException, GeneralSecurityException {
throws IOException, GeneralSecurityException {
byte[] authenticated = encodeNonceReservation(storeId, noncePrefix, highWater); byte[] authenticated = encodeNonceReservation(storeId, noncePrefix, highWater);
byte[] tag = null; byte[] tag = null;
byte[] image = null; byte[] image = null;
try { try {
tag = hmac(nonceReservationMacKey, authenticated); tag = hmac(nonceReservationMacKey, authenticated);
image = ByteBuffer.allocate(authenticated.length + tag.length) image = ByteBuffer.allocate(authenticated.length + tag.length).put(authenticated).put(tag).array();
.put(authenticated).put(tag).array();
writeSidecarAtomically(nonceReservationPath, image); writeSidecarAtomically(nonceReservationPath, image);
} finally { } finally {
wipe(authenticated); wipe(authenticated);
@@ -861,19 +847,14 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private static long readNonceReservation(Path keyring, UserPrincipal owner, private static long readNonceReservation(Path keyring, UserPrincipal owner, byte[] expectedStoreId,
byte[] expectedStoreId, int expectedPrefix, int expectedPrefix, byte[] nonceReservationMacKey) throws IOException, GeneralSecurityException {
byte[] nonceReservationMacKey)
throws IOException, GeneralSecurityException {
Path reservation = nonceReservationPath(keyring); Path reservation = nonceReservationPath(keyring);
validateExistingFile(reservation, owner); validateExistingFile(reservation, owner);
byte[] image = readFixedImage(reservation, byte[] image = readFixedImage(reservation, NONCE_RESERVATION_MAGIC.length + Integer.BYTES + UUID_BYTES
NONCE_RESERVATION_MAGIC.length + Integer.BYTES + UUID_BYTES
+ Integer.BYTES + Long.BYTES + NONCE_RESERVATION_TAG_BYTES); + Integer.BYTES + Long.BYTES + NONCE_RESERVATION_TAG_BYTES);
byte[] authenticated = Arrays.copyOf(image, byte[] authenticated = Arrays.copyOf(image, image.length - NONCE_RESERVATION_TAG_BYTES);
image.length - NONCE_RESERVATION_TAG_BYTES); byte[] actualTag = Arrays.copyOfRange(image, authenticated.length, image.length);
byte[] actualTag = Arrays.copyOfRange(image,
authenticated.length, image.length);
byte[] expectedTag = null; byte[] expectedTag = null;
try { try {
expectedTag = hmac(nonceReservationMacKey, authenticated); expectedTag = hmac(nonceReservationMacKey, authenticated);
@@ -889,11 +870,10 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
int prefix = buffer.getInt(); int prefix = buffer.getInt();
long highWater = buffer.getLong(); long highWater = buffer.getLong();
if (!MessageDigest.isEqual(magic, NONCE_RESERVATION_MAGIC) if (!MessageDigest.isEqual(magic, NONCE_RESERVATION_MAGIC)
|| ByteBuffer.wrap(authenticated, || ByteBuffer.wrap(authenticated, NONCE_RESERVATION_MAGIC.length, Integer.BYTES)
NONCE_RESERVATION_MAGIC.length, Integer.BYTES).getInt() .getInt() != NONCE_RESERVATION_VERSION
!= NONCE_RESERVATION_VERSION || !MessageDigest.isEqual(storeId, expectedStoreId) || prefix != expectedPrefix
|| !MessageDigest.isEqual(storeId, expectedStoreId) || highWater < MIN_NONCE_HIGH_WATER) {
|| prefix != expectedPrefix || highWater < MIN_NONCE_HIGH_WATER) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
} }
return highWater; return highWater;
@@ -905,20 +885,14 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private static byte[] encodeNonceReservation(byte[] storeId, int prefix, private static byte[] encodeNonceReservation(byte[] storeId, int prefix, long highWater) {
long highWater) { return ByteBuffer
return ByteBuffer.allocate(NONCE_RESERVATION_MAGIC.length + Integer.BYTES .allocate(NONCE_RESERVATION_MAGIC.length + Integer.BYTES + UUID_BYTES + Integer.BYTES + Long.BYTES)
+ UUID_BYTES + Integer.BYTES + Long.BYTES) .put(NONCE_RESERVATION_MAGIC).putInt(NONCE_RESERVATION_VERSION).put(storeId).putInt(prefix)
.put(NONCE_RESERVATION_MAGIC) .putLong(highWater).array();
.putInt(NONCE_RESERVATION_VERSION)
.put(storeId)
.putInt(prefix)
.putLong(highWater)
.array();
} }
private static byte[] hmac(byte[] key, byte[] input) private static byte[] hmac(byte[] key, byte[] input) throws GeneralSecurityException {
throws GeneralSecurityException {
Mac mac = Mac.getInstance("HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256")); mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(input); return mac.doFinal(input);
@@ -928,8 +902,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
writeAtomically(Target.NONCE_RESERVATION, target, image); writeAtomically(Target.NONCE_RESERVATION, target, image);
} }
private byte[] encodeImage(Map<String, EncryptedEntry> candidate, private byte[] encodeImage(Map<String, EncryptedEntry> candidate, byte[] manifestNonce, byte[] manifestCipher)
byte[] manifestNonce, byte[] manifestCipher) throws IOException, KeyringException { throws IOException, KeyringException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(); ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(bytes)) { try (DataOutputStream out = new DataOutputStream(bytes)) {
out.write(MAGIC); out.write(MAGIC);
@@ -963,8 +937,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
return result; return result;
} }
private byte[] encodeManifest(Map<String, EncryptedEntry> candidate) private byte[] encodeManifest(Map<String, EncryptedEntry> candidate) throws IOException {
throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream(); ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(bytes)) { try (DataOutputStream out = new DataOutputStream(bytes)) {
out.writeInt(MANIFEST_FORMAT_VERSION); out.writeInt(MANIFEST_FORMAT_VERSION);
@@ -987,11 +960,10 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
return bytes.toByteArray(); return bytes.toByteArray();
} }
@SuppressWarnings({ "PMD.NcssCount", "PMD.CyclomaticComplexity", @SuppressWarnings({ "PMD.NcssCount", "PMD.CyclomaticComplexity", "PMD.PreserveStackTrace",
"PMD.PreserveStackTrace", "PMD.ExceptionAsFlowControl", "PMD.ExceptionAsFlowControl", "PMD.AvoidCatchingGenericException" })
"PMD.AvoidCatchingGenericException" }) private static Decoded decodeImage(byte[] image, KeyringPassword password, KeyringProtection protection)
private static Decoded decodeImage(byte[] image, KeyringPassword password, throws IOException, GeneralSecurityException {
KeyringProtection protection) throws IOException, GeneralSecurityException {
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(image))) { try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(image))) {
byte[] magic = readFixed(in, MAGIC.length); byte[] magic = readFixed(in, MAGIC.length);
if (!MessageDigest.isEqual(MAGIC, magic)) { if (!MessageDigest.isEqual(MAGIC, magic)) {
@@ -1014,8 +986,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
if (wrapNonce[0] != MASTER_WRAP_NONCE_DOMAIN) { if (wrapNonce[0] != MASTER_WRAP_NONCE_DOMAIN) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
} }
byte[] wrapped = readBoundedBytes(in, MASTER_KEY_BYTES + GCM_TAG_BYTES, byte[] wrapped = readBoundedBytes(in, MASTER_KEY_BYTES + GCM_TAG_BYTES, MASTER_KEY_BYTES + GCM_TAG_BYTES);
MASTER_KEY_BYTES + GCM_TAG_BYTES);
if (wrapped.length != MASTER_KEY_BYTES + GCM_TAG_BYTES) { if (wrapped.length != MASTER_KEY_BYTES + GCM_TAG_BYTES) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
} }
@@ -1047,8 +1018,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
for (int index = 0; index < count; index++) { for (int index = 0; index < count; index++) {
byte[] entryId = readFixed(in, UUID_BYTES); byte[] entryId = readFixed(in, UUID_BYTES);
byte[] nonce = readFixed(in, NONCE_BYTES); byte[] nonce = readFixed(in, NONCE_BYTES);
byte[] ciphertext = readBoundedBytes(in, MAX_ENTRY_CIPHERTEXT_BYTES, byte[] ciphertext = readBoundedBytes(in, MAX_ENTRY_CIPHERTEXT_BYTES, MAX_ENTRY_CIPHERTEXT_BYTES);
MAX_ENTRY_CIPHERTEXT_BYTES);
wireEntries.add(new EncryptedWireEntry(entryId, nonce, ciphertext)); wireEntries.add(new EncryptedWireEntry(entryId, nonce, ciphertext));
} }
byte[] manifestNonce = readFixed(in, NONCE_BYTES); byte[] manifestNonce = readFixed(in, NONCE_BYTES);
@@ -1061,14 +1031,12 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
try { try {
aad = manifestAad(storeId, count); aad = manifestAad(storeId, count);
try { try {
manifestPlain = crypt(Cipher.DECRYPT_MODE, masterKey, manifestPlain = crypt(Cipher.DECRYPT_MODE, masterKey, manifestNonce, aad, manifestCipher);
manifestNonce, aad, manifestCipher);
} catch (AEADBadTagException exception) { } catch (AEADBadTagException exception) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
} }
Manifest manifest = decodeManifest(manifestPlain, count); Manifest manifest = decodeManifest(manifestPlain, count);
Map<String, EncryptedEntry> entries = Map<String, EncryptedEntry> entries = bindManifest(manifest, wireEntries, manifestNonce);
bindManifest(manifest, wireEntries, manifestNonce);
return new Decoded(header, masterKey, manifest, entries); return new Decoded(header, masterKey, manifest, entries);
} finally { } finally {
wipe(aad); wipe(aad);
@@ -1086,8 +1054,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
@SuppressWarnings("PMD.PreserveStackTrace") @SuppressWarnings("PMD.PreserveStackTrace")
private static Manifest decodeManifest(byte[] plaintext, int expectedCount) private static Manifest decodeManifest(byte[] plaintext, int expectedCount) throws IOException {
throws IOException {
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(plaintext))) { try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(plaintext))) {
if (in.readInt() != MANIFEST_FORMAT_VERSION) { if (in.readInt() != MANIFEST_FORMAT_VERSION) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
@@ -1109,16 +1076,16 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
String algorithm = readString(in, MAX_METADATA_BYTES); String algorithm = readString(in, MAX_METADATA_BYTES);
Kind kind = Kind.fromCode(in.readUnsignedByte()); Kind kind = Kind.fromCode(in.readUnsignedByte());
Encoding encoding = Encoding.fromCode(in.readUnsignedByte()); Encoding encoding = Encoding.fromCode(in.readUnsignedByte());
KeyringImportRegistry.HmacVariant hmacVariant = KeyringImportRegistry.HmacVariant hmacVariant = KeyringImportRegistry.HmacVariant
KeyringImportRegistry.HmacVariant.fromCode(in.readUnsignedByte()); .fromCode(in.readUnsignedByte());
byte[] nonce = readFixed(in, NONCE_BYTES); byte[] nonce = readFixed(in, NONCE_BYTES);
int length = in.readInt(); int length = in.readInt();
if (length < GCM_TAG_BYTES || length > MAX_ENTRY_CIPHERTEXT_BYTES) { if (length < GCM_TAG_BYTES || length > MAX_ENTRY_CIPHERTEXT_BYTES) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED); throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
} }
byte[] digest = readFixed(in, SHA256_BYTES); byte[] digest = readFixed(in, SHA256_BYTES);
descriptors.add(new ManifestEntry(entryId, position, alias, algorithm, descriptors.add(new ManifestEntry(entryId, position, alias, algorithm, kind, encoding, hmacVariant,
kind, encoding, hmacVariant, nonce, length, digest)); nonce, length, digest));
} }
if (in.read() != -1) { if (in.read() != -1) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
@@ -1130,9 +1097,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops") @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
private static Map<String, EncryptedEntry> bindManifest(Manifest manifest, private static Map<String, EncryptedEntry> bindManifest(Manifest manifest, List<EncryptedWireEntry> wireEntries,
List<EncryptedWireEntry> wireEntries, byte[] manifestNonce) byte[] manifestNonce) throws KeyringException {
throws KeyringException {
if (manifest.entries.size() != wireEntries.size()) { if (manifest.entries.size() != wireEntries.size()) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
} }
@@ -1145,44 +1111,38 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
for (int index = 0; index < wireEntries.size(); index++) { for (int index = 0; index < wireEntries.size(); index++) {
ManifestEntry descriptor = manifest.entries.get(index); ManifestEntry descriptor = manifest.entries.get(index);
EncryptedWireEntry wire = wireEntries.get(index); EncryptedWireEntry wire = wireEntries.get(index);
if (descriptor.position != index if (descriptor.position != index || !MessageDigest.isEqual(descriptor.entryId, wire.entryId)
|| !MessageDigest.isEqual(descriptor.entryId, wire.entryId)
|| !MessageDigest.isEqual(descriptor.nonce, wire.nonce) || !MessageDigest.isEqual(descriptor.nonce, wire.nonce)
|| descriptor.ciphertextLength != wire.ciphertext.length || descriptor.ciphertextLength != wire.ciphertext.length
|| !MessageDigest.isEqual(descriptor.digest, digest(wire.ciphertext))) { || !MessageDigest.isEqual(descriptor.digest, digest(wire.ciphertext))) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
} }
validateNonce(wire.nonce, manifest.noncePrefix, manifest.highWater); validateNonce(wire.nonce, manifest.noncePrefix, manifest.highWater);
if (!aliases.add(descriptor.alias) if (!aliases.add(descriptor.alias) || !ids.add(uuid(descriptor.entryId))
|| !ids.add(uuid(descriptor.entryId))
|| !nonces.add(new Nonce(wire.nonce))) { || !nonces.add(new Nonce(wire.nonce))) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
} }
KeyringImportRegistry.validateMapping(descriptor.algorithm, KeyringImportRegistry.validateMapping(descriptor.algorithm, descriptor.kind, descriptor.encoding,
descriptor.kind, descriptor.encoding, descriptor.hmacVariant); descriptor.hmacVariant);
result.put(descriptor.alias, new EncryptedEntry(wire.entryId, index, result.put(descriptor.alias,
descriptor.alias, descriptor.algorithm, descriptor.kind, new EncryptedEntry(wire.entryId, index, descriptor.alias, descriptor.algorithm, descriptor.kind,
descriptor.encoding, descriptor.hmacVariant, wire.nonce, descriptor.encoding, descriptor.hmacVariant, wire.nonce, wire.ciphertext,
wire.ciphertext, descriptor.digest)); descriptor.digest));
} }
return result; return result;
} }
@SuppressWarnings("PMD.PreserveStackTrace") @SuppressWarnings("PMD.PreserveStackTrace")
private static byte[] encodeEntryPlaintext(String alias, String algorithm, private static byte[] encodeEntryPlaintext(String alias, String algorithm, Kind kind, Encoding encoding,
Kind kind, Encoding encoding, KeyringImportRegistry.HmacVariant hmacVariant, KeyringImportRegistry.HmacVariant hmacVariant, byte[] encoded) throws KeyringException {
byte[] encoded)
throws KeyringException {
if (encoded.length <= 0 || encoded.length > MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES) { if (encoded.length <= 0 || encoded.length > MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED); throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
} }
byte[] aliasBytes = alias.getBytes(StandardCharsets.UTF_8); byte[] aliasBytes = alias.getBytes(StandardCharsets.UTF_8);
byte[] algorithmBytes = algorithm.getBytes(StandardCharsets.UTF_8); byte[] algorithmBytes = algorithm.getBytes(StandardCharsets.UTF_8);
try { try {
int size = Math.addExact(Integer.BYTES + 3, int size = Math.addExact(Integer.BYTES + 3, Math.addExact(lengthPrefixedSize(aliasBytes),
Math.addExact(lengthPrefixedSize(aliasBytes), Math.addExact(lengthPrefixedSize(algorithmBytes), lengthPrefixedSize(encoded))));
Math.addExact(lengthPrefixedSize(algorithmBytes),
lengthPrefixedSize(encoded))));
if (size > MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES) { if (size > MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED); throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
} }
@@ -1222,10 +1182,9 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
String algorithm = readString(in, MAX_METADATA_BYTES); String algorithm = readString(in, MAX_METADATA_BYTES);
Kind kind = Kind.fromCode(in.readUnsignedByte()); Kind kind = Kind.fromCode(in.readUnsignedByte());
Encoding encoding = Encoding.fromCode(in.readUnsignedByte()); Encoding encoding = Encoding.fromCode(in.readUnsignedByte());
KeyringImportRegistry.HmacVariant hmacVariant = KeyringImportRegistry.HmacVariant hmacVariant = KeyringImportRegistry.HmacVariant
KeyringImportRegistry.HmacVariant.fromCode(in.readUnsignedByte()); .fromCode(in.readUnsignedByte());
byte[] encoded = readBoundedBytes(in, byte[] encoded = readBoundedBytes(in, MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES,
MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES,
MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES); MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES);
if (in.read() != -1) { if (in.read() != -1) {
wipe(encoded); wipe(encoded);
@@ -1237,8 +1196,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private static void validateEntryBinding(EncryptedEntry entry, EntryPlain decoded) private static void validateEntryBinding(EncryptedEntry entry, EntryPlain decoded) throws KeyringException {
throws KeyringException {
if (!entry.alias.equals(decoded.alias) || !entry.algorithm.equals(decoded.algorithm) if (!entry.alias.equals(decoded.alias) || !entry.algorithm.equals(decoded.algorithm)
|| entry.kind != decoded.kind || entry.encoding != decoded.encoding || entry.kind != decoded.kind || entry.encoding != decoded.encoding
|| entry.hmacVariant != decoded.hmacVariant) { || entry.hmacVariant != decoded.hmacVariant) {
@@ -1305,8 +1263,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
return buffer.array(); return buffer.array();
} }
private static void validateNonce(byte[] nonce, int prefix, long highWater) private static void validateNonce(byte[] nonce, int prefix, long highWater) throws KeyringException {
throws KeyringException {
if (nonce.length != NONCE_BYTES) { if (nonce.length != NONCE_BYTES) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
} }
@@ -1325,8 +1282,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
byte[] prefix = new byte[3]; byte[] prefix = new byte[3];
try { try {
random.nextBytes(prefix); random.nextBytes(prefix);
return (Byte.toUnsignedInt(prefix[0]) << 16) return (Byte.toUnsignedInt(prefix[0]) << 16) | (Byte.toUnsignedInt(prefix[1]) << 8)
| (Byte.toUnsignedInt(prefix[1]) << 8)
| Byte.toUnsignedInt(prefix[2]); | Byte.toUnsignedInt(prefix[2]);
} finally { } finally {
wipe(prefix); wipe(prefix);
@@ -1351,8 +1307,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
private static int readThreeBytePrefix(ByteBuffer buffer) { private static int readThreeBytePrefix(ByteBuffer buffer) {
return (Byte.toUnsignedInt(buffer.get()) << 16) return (Byte.toUnsignedInt(buffer.get()) << 16) | (Byte.toUnsignedInt(buffer.get()) << 8)
| (Byte.toUnsignedInt(buffer.get()) << 8)
| Byte.toUnsignedInt(buffer.get()); | Byte.toUnsignedInt(buffer.get());
} }
@@ -1362,18 +1317,16 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, KEK_BYTES * Byte.SIZE); PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, KEK_BYTES * Byte.SIZE);
Arrays.fill(chars, '\0'); Arrays.fill(chars, '\0');
try { try {
return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
.generateSecret(spec).getEncoded();
} finally { } finally {
spec.clearPassword(); spec.clearPassword();
} }
} }
private static byte[] crypt(int mode, byte[] key, byte[] nonce, byte[] aad, private static byte[] crypt(int mode, byte[] key, byte[] nonce, byte[] aad, byte[] input)
byte[] input) throws GeneralSecurityException { throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(mode, new SecretKeySpec(key, "AES"), cipher.init(mode, new SecretKeySpec(key, "AES"), new GCMParameterSpec(GCM_TAG_BITS, nonce));
new GCMParameterSpec(GCM_TAG_BITS, nonce));
cipher.updateAAD(aad); cipher.updateAAD(aad);
return cipher.doFinal(input); return cipher.doFinal(input);
} }
@@ -1387,10 +1340,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private static void validateIterations(int iterations, KeyringProtection protection) private static void validateIterations(int iterations, KeyringProtection protection) throws KeyringException {
throws KeyringException { if (iterations < KeyringProtection.CREATION_ITERATIONS || iterations > KeyringProtection.MAX_DECODED_ITERATIONS
if (iterations < KeyringProtection.CREATION_ITERATIONS
|| iterations > KeyringProtection.MAX_DECODED_ITERATIONS
|| iterations > protection.operationalIterationMaximum()) { || iterations > protection.operationalIterationMaximum()) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED); throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
} }
@@ -1404,8 +1355,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}; };
} }
private static int positionFor(Map<String, EncryptedEntry> values, private static int positionFor(Map<String, EncryptedEntry> values, String alias) {
String alias) {
int index = 0; int index = 0;
for (String current : values.keySet()) { for (String current : values.keySet()) {
if (current.equals(alias)) { if (current.equals(alias)) {
@@ -1442,10 +1392,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private static void validateString(String value, int maximumBytes) private static void validateString(String value, int maximumBytes) throws KeyringException {
throws KeyringException { if (value == null || value.isBlank() || value.getBytes(StandardCharsets.UTF_8).length > maximumBytes) {
if (value == null || value.isBlank()
|| value.getBytes(StandardCharsets.UTF_8).length > maximumBytes) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED); throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
} }
} }
@@ -1485,11 +1433,9 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
return value; return value;
} }
private static byte[] readBoundedBytes(DataInputStream in, int maximum, private static byte[] readBoundedBytes(DataInputStream in, int maximum, int exactOrMaximum) throws IOException {
int exactOrMaximum) throws IOException {
int length = in.readInt(); int length = in.readInt();
if (length < 0 || length > maximum if (length < 0 || length > maximum || exactOrMaximum < maximum && length != exactOrMaximum) {
|| exactOrMaximum < maximum && length != exactOrMaximum) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED); throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
} }
byte[] value = new byte[length]; byte[] value = new byte[length];
@@ -1512,10 +1458,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
byte[] encoded = readBoundedBytes(in, maximum, maximum); byte[] encoded = readBoundedBytes(in, maximum, maximum);
try { try {
try { try {
return StandardCharsets.UTF_8.newDecoder() return StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT)
.onMalformedInput(CodingErrorAction.REPORT) .onUnmappableCharacter(CodingErrorAction.REPORT).decode(ByteBuffer.wrap(encoded)).toString();
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(encoded)).toString();
} catch (CharacterCodingException exception) { } catch (CharacterCodingException exception) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID); throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
} }
@@ -1541,8 +1485,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
@SuppressWarnings("PMD.PreserveStackTrace") @SuppressWarnings("PMD.PreserveStackTrace")
private static byte[] readStoreImage(Path source) throws IOException { private static byte[] readStoreImage(Path source) throws IOException {
try (FileChannel channel = FileChannel.open(source, try (FileChannel channel = FileChannel.open(source, StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS)) {
StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS)) {
long size = channel.size(); long size = channel.size();
if (size <= 0 || size > MAX_FILE_BYTES) { if (size <= 0 || size > MAX_FILE_BYTES) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED); throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
@@ -1576,8 +1519,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
@SuppressWarnings("PMD.PreserveStackTrace") @SuppressWarnings("PMD.PreserveStackTrace")
private static Path securePath(Path requested, boolean createParent) private static Path securePath(Path requested, boolean createParent) throws IOException {
throws IOException {
Objects.requireNonNull(requested, "path must not be null"); Objects.requireNonNull(requested, "path must not be null");
Path absolute = requested.toAbsolutePath().normalize(); Path absolute = requested.toAbsolutePath().normalize();
Path parent = absolute.getParent(); Path parent = absolute.getParent();
@@ -1609,15 +1551,13 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
@SuppressWarnings("PMD.PreserveStackTrace") @SuppressWarnings("PMD.PreserveStackTrace")
private static void validateExistingFile(Path file, UserPrincipal expectedOwner) private static void validateExistingFile(Path file, UserPrincipal expectedOwner) throws IOException {
throws IOException { if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(file)) {
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)
|| Files.isSymbolicLink(file)) {
throw new KeyringException(Code.KEYRING_FILESYSTEM_UNSUPPORTED); throw new KeyringException(Code.KEYRING_FILESYSTEM_UNSUPPORTED);
} }
validateOwnerOnly(file, false); validateOwnerOnly(file, false);
PosixFileAttributes attributes = Files.readAttributes(file, PosixFileAttributes attributes = Files.readAttributes(file, PosixFileAttributes.class,
PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS); LinkOption.NOFOLLOW_LINKS);
if (!attributes.owner().equals(expectedOwner)) { if (!attributes.owner().equals(expectedOwner)) {
throw new KeyringException(Code.KEYRING_FILESYSTEM_UNSUPPORTED); throw new KeyringException(Code.KEYRING_FILESYSTEM_UNSUPPORTED);
} }
@@ -1631,10 +1571,9 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private static void validateOwnerOnly(Path path, boolean directory) private static void validateOwnerOnly(Path path, boolean directory) throws IOException {
throws IOException { PosixFileAttributes attributes = Files.readAttributes(path, PosixFileAttributes.class,
PosixFileAttributes attributes = Files.readAttributes(path, LinkOption.NOFOLLOW_LINKS);
PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
Set<PosixFilePermission> expected = directory ? DIRECTORY_PERMISSIONS : FILE_PERMISSIONS; Set<PosixFilePermission> expected = directory ? DIRECTORY_PERMISSIONS : FILE_PERMISSIONS;
if (!attributes.permissions().equals(expected)) { if (!attributes.permissions().equals(expected)) {
throw new KeyringException(Code.KEYRING_FILESYSTEM_UNSUPPORTED); throw new KeyringException(Code.KEYRING_FILESYSTEM_UNSUPPORTED);
@@ -1642,8 +1581,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
@SuppressWarnings("PMD.PreserveStackTrace") @SuppressWarnings("PMD.PreserveStackTrace")
/* default */ static void destroyTemporarySpec( /* default */ static void destroyTemporarySpec(zeroecho.core.spec.AlgorithmKeySpec spec, Throwable primary)
zeroecho.core.spec.AlgorithmKeySpec spec, Throwable primary)
throws GeneralSecurityException { throws GeneralSecurityException {
if (!(spec instanceof Destroyable destroyable)) { if (!(spec instanceof Destroyable destroyable)) {
return; return;
@@ -1667,22 +1605,19 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private record Header(byte[] storeId, int iterations, byte[] salt, private record Header(byte[] storeId, int iterations, byte[] salt, byte[] masterWrapNonce,
byte[] masterWrapNonce, byte[] wrappedMasterKey) { byte[] wrappedMasterKey) {
} }
private record Manifest(int noncePrefix, long highWater, private record Manifest(int noncePrefix, long highWater, List<ManifestEntry> entries) {
List<ManifestEntry> entries) {
} }
private record ManifestEntry(byte[] entryId, int position, String alias, private record ManifestEntry(byte[] entryId, int position, String alias, String algorithm, Kind kind,
String algorithm, Kind kind, Encoding encoding, Encoding encoding, KeyringImportRegistry.HmacVariant hmacVariant, byte[] nonce, int ciphertextLength,
KeyringImportRegistry.HmacVariant hmacVariant, byte[] digest) {
byte[] nonce, int ciphertextLength, byte[] digest) {
} }
private record EncryptedWireEntry(byte[] entryId, byte[] nonce, private record EncryptedWireEntry(byte[] entryId, byte[] nonce, byte[] ciphertext) {
byte[] ciphertext) {
} }
/** Immutable encrypted entry retained while the store is open. */ /** Immutable encrypted entry retained while the store is open. */
@@ -1698,10 +1633,9 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
private final byte[] ciphertext; private final byte[] ciphertext;
private final byte[] digest; private final byte[] digest;
private EncryptedEntry(byte[] entryId, int position, String alias, private EncryptedEntry(byte[] entryId, int position, String alias, String algorithm, Kind kind,
String algorithm, Kind kind, Encoding encoding, Encoding encoding, KeyringImportRegistry.HmacVariant hmacVariant, byte[] nonce, byte[] ciphertext,
KeyringImportRegistry.HmacVariant hmacVariant, byte[] digest) {
byte[] nonce, byte[] ciphertext, byte[] digest) {
this.entryId = entryId.clone(); this.entryId = entryId.clone();
this.position = position; this.position = position;
this.alias = alias; this.alias = alias;
@@ -1715,16 +1649,14 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
private record EntryPlain(String alias, String algorithm, Kind kind, private record EntryPlain(String alias, String algorithm, Kind kind, Encoding encoding,
Encoding encoding, KeyringImportRegistry.HmacVariant hmacVariant, KeyringImportRegistry.HmacVariant hmacVariant, byte[] encoded) {
byte[] encoded) {
} }
private record Imported(String algorithm, Key key) { private record Imported(String algorithm, Key key) {
} }
private record Decoded(Header header, byte[] masterKey, Manifest manifest, private record Decoded(Header header, byte[] masterKey, Manifest manifest, Map<String, EncryptedEntry> entries) {
Map<String, EncryptedEntry> entries) {
} }
/** Value-semantic nonce used only for duplicate detection. */ /** Value-semantic nonce used only for duplicate detection. */
@@ -1764,8 +1696,9 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
/* default */ static Ownership acquire(Path keyring) throws IOException { /* default */ static Ownership acquire(Path keyring) throws IOException {
Path lockPath = keyring.resolveSibling(keyring.getFileName() + ".lock"); Path lockPath = keyring.resolveSibling(keyring.getFileName() + ".lock");
try { try {
UserPrincipal expectedOwner = Files.readAttributes(lockPath.getParent(), UserPrincipal expectedOwner = Files
PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS).owner(); .readAttributes(lockPath.getParent(), PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS)
.owner();
if (!Files.exists(lockPath, LinkOption.NOFOLLOW_LINKS)) { if (!Files.exists(lockPath, LinkOption.NOFOLLOW_LINKS)) {
try { try {
Files.createFile(lockPath, FILE_ATTRIBUTE); Files.createFile(lockPath, FILE_ATTRIBUTE);
@@ -1774,8 +1707,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
} }
} }
validateExistingFile(lockPath, expectedOwner); validateExistingFile(lockPath, expectedOwner);
FileChannel channel = FileChannel.open(lockPath, FileChannel channel = FileChannel.open(lockPath, StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS);
StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS);
try { try {
FileLock lock; FileLock lock;
try { try {

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
*/ */
@@ -74,8 +76,7 @@ public final class KeyBuilders {
* @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 -> {
@@ -94,8 +95,7 @@ public final class KeyBuilders {
* @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 -> {
@@ -114,7 +114,8 @@ public final class KeyBuilders {
* @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 {
@@ -133,7 +134,8 @@ public final class KeyBuilders {
* @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 {
@@ -180,8 +182,7 @@ public final class KeyBuilders {
* @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 -> {
@@ -220,7 +221,8 @@ public final class KeyBuilders {
* @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 {
@@ -239,7 +241,8 @@ public final class KeyBuilders {
* @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 {
@@ -258,7 +261,8 @@ public final class KeyBuilders {
* @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;
@@ -328,7 +327,8 @@ public final class ZeroEchoSession {
* @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

@@ -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

@@ -225,8 +225,8 @@ 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

View File

@@ -150,8 +150,8 @@ public final class HybridKexContext implements MessageAgreementContext {
this(profile, classic, pqc, Kdf::hkdfSha256); this(profile, classic, pqc, Kdf::hkdfSha256);
} }
/* default */ HybridKexContext(HybridKexProfile profile, AgreementContext classic, /* default */ HybridKexContext(HybridKexProfile profile, AgreementContext classic, MessageAgreementContext pqc,
MessageAgreementContext pqc, SecretDeriver secretDeriver) { SecretDeriver secretDeriver) {
this.profile = Objects.requireNonNull(profile, "profile"); this.profile = Objects.requireNonNull(profile, "profile");
this.classic = Objects.requireNonNull(classic, "classic"); this.classic = Objects.requireNonNull(classic, "classic");
this.pqc = Objects.requireNonNull(pqc, "pqc"); this.pqc = Objects.requireNonNull(pqc, "pqc");
@@ -365,8 +365,7 @@ public final class HybridKexContext implements MessageAgreementContext {
* @return derived output transferred to the caller * @return derived output transferred to the caller
* @throws GeneralSecurityException if derivation fails * @throws GeneralSecurityException if derivation fails
*/ */
byte[] derive(byte[] ikm, byte[] salt, byte[] info, int outputLength) byte[] derive(byte[] ikm, byte[] salt, byte[] info, int outputLength) throws GeneralSecurityException;
throws GeneralSecurityException;
} }
/** /**

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