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

View File

@@ -200,8 +200,7 @@ public final class KeyStoreManagement {
* @throws IOException if keyring I/O fails
* @throws GeneralSecurityException if cryptographic processing fails
*/
public static int main(final String[] args, final Options dispatcherOptions,
KeyringUnlockProvider unlockProvider)
public static int main(final String[] args, final Options dispatcherOptions, KeyringUnlockProvider unlockProvider)
throws ParseException, IOException, GeneralSecurityException {
ZeroEchoSession session = new ZeroEchoSession();
defineOptions(dispatcherOptions);
@@ -218,8 +217,7 @@ public final class KeyStoreManagement {
}
Path keyringPath = Path.of(cmd.getOptionValue(KEYSTORE_OPTION.getLongOpt()));
try (KeyringStore store = Files.exists(keyringPath)
? KeyringUnlocks.open(keyringPath, unlockProvider)
try (KeyringStore store = Files.exists(keyringPath) ? KeyringUnlocks.open(keyringPath, unlockProvider)
: KeyringUnlocks.create(keyringPath, unlockProvider)) {
if (cmd.hasOption(LIST_ALIASES_OPTION.getLongOpt())) {
listAliases(store);
@@ -310,8 +308,8 @@ public final class KeyStoreManagement {
* @param store keyring store to mutate
* @param cmd parsed command line
*/
public static void doGenerate(final ZeroEchoSession session, final KeyringStore store,
final CommandLine cmd) throws IOException, GeneralSecurityException {
public static void doGenerate(final ZeroEchoSession session, final KeyringStore store, final CommandLine cmd)
throws IOException, GeneralSecurityException {
String algId = required(cmd, ALG_OPTION, "--alg is required for --generate");
String aliasBase = required(cmd, ALIAS_OPTION, "--alias is required for --generate");
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,
String algorithmId, String alias, boolean overwrite)
throws IOException, GeneralSecurityException {
String algorithmId, String alias, boolean overwrite) throws IOException, GeneralSecurityException {
GeneratedSecret generated = firstGeneratedSecret(session, algorithm, algorithmId);
ensureWritable(store, alias, overwrite);
store.putSecret(alias, algorithmId, generated.key());
@@ -430,9 +427,7 @@ public final class KeyStoreManagement {
/** Selects the exact key-generation operation requested by the command. */
private enum GenerationKind {
ASYMMETRIC,
SYMMETRIC,
NONE
ASYMMETRIC, SYMMETRIC, NONE
}
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)
throws IOException {
private static KeyringPassword acquire(KeyringUnlockProvider provider) throws IOException {
KeyringPassword password = provider.acquire();
if (password == null) {
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
* signature or digest processing
*/
public static int main(String[] args, Options root)
throws ParseException, IOException, GeneralSecurityException {
public static int main(String[] args, Options root) throws ParseException, IOException, GeneralSecurityException {
return main(args, root, KeyringUnlocks.console());
}
@@ -174,8 +173,7 @@ public final class Tag { // NOPMD
* @throws IOException if I/O fails
* @throws GeneralSecurityException if cryptographic processing fails
*/
public static int main(String[] args, Options root,
KeyringUnlockProvider unlockProvider)
public static int main(String[] args, Options root, KeyringUnlockProvider unlockProvider)
throws ParseException, IOException, GeneralSecurityException {
ZeroEchoSession session = new ZeroEchoSession();
Options opts = root;
@@ -222,13 +220,13 @@ public final class Tag { // NOPMD
if (produce) {
String privAlias = require(cli, PRIV_OPT, "signature produce requires --priv <alias>");
PrivateKey priv = keyring.getPrivate(privAlias);
tail = new TagTrailerDataContentBuilder<>(
TagEngineBuilder.signature(session, alg, priv, spec)).build(true);
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, priv, spec))
.build(true);
} else {
String pubAlias = require(cli, PUB_OPT, "signature verify requires --pub <alias>");
PublicKey pub = keyring.getPublic(pubAlias);
tail = new TagTrailerDataContentBuilder<>(
TagEngineBuilder.signature(session, alg, pub, spec)).build(false);
tail = new TagTrailerDataContentBuilder<>(TagEngineBuilder.signature(session, alg, pub, spec))
.build(false);
}
}
} else { // digest

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -91,17 +91,17 @@ class JpegExifIntegrationTest {
// AES encryption setup
/*
* SecretKey key = zeroEchoSession.keyBuilders().symmetric()
* .generate("AES", AesKeyGenSpec.aes256()); AesSpec spec =
* SecretKey key = zeroEchoSession.keyBuilders().symmetric() .generate("AES",
* AesKeyGenSpec.aes256()); AesSpec spec =
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
* EncryptionContext enc = zeroEchoSession.createContext("AES", KeyUsage.ENCRYPT, key,
* spec); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" +
* System.nanoTime()); session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware)
* EncryptionContext enc = zeroEchoSession.createContext("AES",
* KeyUsage.ENCRYPT, key, spec); CtxInterface session =
* Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
* session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware)
* enc).setContext(session);
*/
ZeroEchoSession zeroEchoSession = new ZeroEchoSession();
SecretKey key = zeroEchoSession.keyBuilders().symmetric()
.generate("AES", AesKeyGenSpec.aes256());
SecretKey key = zeroEchoSession.keyBuilders().symmetric().generate("AES", AesKeyGenSpec.aes256());
CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
byte[] encryptedBytes;
@@ -152,7 +152,8 @@ class JpegExifIntegrationTest {
// input
.add(PlainBytesBuilder.builder().bytes(extractedEncryptedBytes))
// 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
.withHeader().withAad(aad).context(session))
// and create the pipeline
@@ -164,9 +165,9 @@ class JpegExifIntegrationTest {
/*
* AesSpec spec =
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
* EncryptionContext dec1 = zeroEchoSession.createContext("AES", KeyUsage.DECRYPT,
* key, spec); ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx
* byte[] pt1 = readAll(dec1.attach(new
* EncryptionContext dec1 = zeroEchoSession.createContext("AES",
* KeyUsage.DECRYPT, key, spec); ((ContextAware) dec1).setContext(session); //
* same IV/AAD in ctx byte[] pt1 = readAll(dec1.attach(new
* ByteArrayInputStream(extractedEncryptedBytes))); dec1.close();
*/
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.
*
* <p>The default specification is resolved once during provider construction.
* All components therefore have stable value semantics and are safe for
* concurrent reads.</p>
* <p>
* The default specification is resolved once during provider construction. All
* components therefore have stable value semantics and are safe for concurrent
* reads.
* </p>
*
* @param algorithmId canonical algorithm identifier
* @param family algorithm family
@@ -30,15 +32,15 @@ import zeroecho.core.spec.ContextSpec;
* @since 1.0
*/
public record Capability(String algorithmId, AlgorithmFamily family, KeyUsage role,
Class<? extends CryptoContext> contextType, Class<? extends Key> keyType,
Class<? extends ContextSpec> specType, ContextSpec defaultSpec) {
Class<? extends CryptoContext> contextType, Class<? extends Key> keyType, Class<? extends ContextSpec> specType,
ContextSpec defaultSpec) {
/**
* Validates the capability metadata.
*
* @throws NullPointerException if a component is {@code null}
* @throws IllegalArgumentException if {@code defaultSpec} is incompatible
* with {@code specType}
* @throws IllegalArgumentException if {@code defaultSpec} is incompatible with
* {@code specType}
*/
public Capability {
Objects.requireNonNull(algorithmId, "algorithmId must not be null");

View File

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

View File

@@ -17,10 +17,12 @@ import java.util.TreeMap;
/**
* 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.
* Runtime policy and auditing belong exclusively to explicitly created
* {@link zeroecho.sdk.ZeroEchoSession} instances.</p>
* {@link zeroecho.sdk.ZeroEchoSession} instances.
* </p>
*
* @since 1.0
*/

View File

@@ -93,9 +93,9 @@ public final class CryptoCatalog {
* {@link CryptoAlgorithms}.
*
* <p>
* Provider discovery, deterministic ordering, and duplicate checking occur
* once in {@code CryptoAlgorithms}. This method neither scans providers nor
* copies their collection.
* Provider discovery, deterministic ordering, and duplicate checking occur once
* in {@code CryptoAlgorithms}. This method neither scans providers nor copies
* their collection.
* </p>
*
* @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.
*
* <p>
* The schema is:
* </p>
* <pre>{@code
* {
* "algorithms": [
* {
* "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> The schema is: </p> <pre>{@code { "algorithms": [ { "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>
* String values are escaped for quotes and backslashes. The method does not
* attempt to pretty-print; callers can format the output if needed.
* </p>
* <p> String values are escaped for quotes and backslashes. The method does not
* attempt to pretty-print; callers can format the output if needed. </p>
*
* @return a JSON string describing algorithms, capabilities, and key builders
*/
@@ -230,8 +206,7 @@ public final class CryptoCatalog {
}
firstOperation = false;
sb.append('{').append(jsonField("operation", operation.operation().name())).append(',')
.append(jsonField("specType", operation.specType().getSimpleName()))
.append(",\"defaultSpec\":")
.append(jsonField("specType", operation.specType().getSimpleName())).append(",\"defaultSpec\":")
.append(operation.defaultSpec() == null ? "null" : jsonString(labelOf(operation.defaultSpec())))
.append('}');
}
@@ -273,9 +248,8 @@ public final class CryptoCatalog {
}
sb.append("</capabilities><keyOperations>");
for (KeyOperationInfo operation : a.keyOperations()) {
sb.append("<keyOperation operation=\"").append(operation.operation().name())
.append("\" specType=\"").append(esc(operation.specType().getSimpleName()))
.append("\"><defaultSpec>")
sb.append("<keyOperation operation=\"").append(operation.operation().name()).append("\" specType=\"")
.append(esc(operation.specType().getSimpleName())).append("\"><defaultSpec>")
.append(operation.defaultSpec() == null ? "" : esc(labelOf(operation.defaultSpec())))
.append("</defaultSpec></keyOperation>");
}

View File

@@ -20,8 +20,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* operations and generators without a default
* @since 1.0
*/
public record KeyOperationInfo(KeyOperation operation,
Class<? extends AlgorithmKeySpec> specType, AlgorithmKeySpec defaultSpec) {
public record KeyOperationInfo(KeyOperation operation, Class<? extends AlgorithmKeySpec> specType,
AlgorithmKeySpec defaultSpec) {
/**
* Validates the metadata invariant.
@@ -38,8 +38,8 @@ public record KeyOperationInfo(KeyOperation operation,
if (defaultSpec != null && !specType.isInstance(defaultSpec)) {
throw new IllegalArgumentException("defaultSpec must be an instance of " + specType.getName());
}
if (defaultSpec != null && (operation == KeyOperation.SYMMETRIC_IMPORT
|| operation == KeyOperation.ASYMMETRIC_PUBLIC_IMPORT
if (defaultSpec != null
&& (operation == KeyOperation.SYMMETRIC_IMPORT || operation == KeyOperation.ASYMMETRIC_PUBLIC_IMPORT
|| operation == KeyOperation.ASYMMETRIC_PRIVATE_IMPORT)) {
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})
* and again when
* {@link CryptoAlgorithm#createContext(KeyUsage, Key, ContextSpec)} is
* called. If a factory returns a context not assignable to {@code ctxType}, an
* {@link CryptoAlgorithm#createContext(KeyUsage, Key, ContextSpec)} is called.
* If a factory returns a context not assignable to {@code ctxType}, an
* {@link IllegalStateException} will be thrown.
*
* @param family high-level algorithm family classification

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,9 +44,9 @@ import zeroecho.core.context.AgreementContext;
* <h2>Generic JCA-based Key Agreement Context</h2>
*
* An {@link AgreementContext} backed by the standard JCA key-agreement API.
* This class supports elliptic-curve and modern Diffie-Hellman variants
* such as ECDH, XDH (X25519, X448), and others provided by the runtime or
* configured provider.
* This class supports elliptic-curve and modern Diffie-Hellman variants such as
* ECDH, XDH (X25519, X448), and others provided by the runtime or configured
* provider.
*
* <p>
* 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
* {@link java.security.KeyFactory}.</li>
* <li><b>Signature contexts:</b>
* {@link zeroecho.core.alg.common.eddsa.CommonEdDSASignatureContext}
* delegates all operations to a generic JCA-backed signature adapter, enforcing
* a fixed tag length for the selected EdDSA variant.</li>
* {@link zeroecho.core.alg.common.eddsa.CommonEdDSASignatureContext} delegates
* all operations to a generic JCA-backed signature adapter, enforcing a fixed
* tag length for the selected EdDSA variant.</li>
* </ul>
*
* <h2>Design notes</h2>

View File

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

View File

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

View File

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

View File

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

View File

@@ -45,8 +45,8 @@ import zeroecho.core.spi.AsymmetricKeyPairGenerator;
/**
* <h2>ECDH Key Pair Generator</h2>
*
* Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for elliptic curve
* Diffie-Hellman (ECDH) key pairs.
* Implementation of {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} for
* elliptic curve Diffie-Hellman (ECDH) key pairs.
*
* <p>
* 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>
* On construction, the algorithm declares its supported roles and registers
* builders with the {@link CryptoAlgorithm} infrastructure so they can be
* discovered by the {@link CryptoCatalog} or invoked through the
* session-bound {@link zeroecho.sdk.KeyBuilders} entry point.
* discovered by the {@link CryptoCatalog} or invoked through the session-bound
* {@link zeroecho.sdk.KeyBuilders} entry point.
* </p>
*/
public EcdsaAlgorithm() {
@@ -134,8 +134,7 @@ public final class EcdsaAlgorithm extends AbstractCryptoAlgorithm {
}
}, () -> EcdsaCurveSpec.P256);
registerAsymmetricKeyPairGenerator(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(),
() -> EcdsaCurveSpec.P256);
registerAsymmetricKeyPairGenerator(EcdsaCurveSpec.class, new EcdsaKeyGenBuilder(), () -> EcdsaCurveSpec.P256);
registerPublicKeyImporter(EcdsaPublicKeySpec.class, new EcdsaPublicKeyBuilder());
registerPrivateKeyImporter(EcdsaPrivateKeySpec.class, new EcdsaPrivateKeyBuilder());
}

View File

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

View File

@@ -49,9 +49,11 @@ import zeroecho.core.spi.PrivateKeyImporter;
* {@link EcdsaPrivateKeySpec}. This builder is responsible for importing ECDSA
* private keys from encoded representations.
*
* <p>The exact supported operation is
* {@link #importPrivate(EcdsaPrivateKeySpec)}. Generation and public import are
* registered through their own operation-specific implementations.</p>
* <p>
* The exact supported operation is {@link #importPrivate(EcdsaPrivateKeySpec)}.
* Generation and public import are registered through their own
* operation-specific implementations.
* </p>
*
* <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

View File

@@ -48,9 +48,11 @@ import zeroecho.core.spi.PublicKeyImporter;
* {@link EcdsaPublicKeySpec}. This builder is responsible for importing ECDSA
* public keys from X.509 SubjectPublicKeyInfo encodings.
*
* <p>The exact supported operation is
* {@link #importPublic(EcdsaPublicKeySpec)}. Generation and private import are
* registered through their own operation-specific implementations.</p>
* <p>
* The exact supported operation is {@link #importPublic(EcdsaPublicKeySpec)}.
* Generation and private import are registered through their own
* operation-specific implementations.
* </p>
*
* <h2>Encoding</h2> The {@link EcdsaPublicKeySpec} stores the public key in
* 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>
*
* Concrete {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} implementation for
* generating Ed25519 key pairs.
* Concrete {@link zeroecho.core.spi.AsymmetricKeyPairGenerator} implementation
* for generating Ed25519 key pairs.
*
* <p>
* 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>
*
* Concrete {@link zeroecho.core.spi.PrivateKeyImporter} for importing
* wrapping Ed25519 private keys.
* Concrete {@link zeroecho.core.spi.PrivateKeyImporter} for importing wrapping
* Ed25519 private keys.
*
* <p>
* 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>
*
* Concrete {@link zeroecho.core.spi.PublicKeyImporter} for importing
* wrapping Ed25519 public keys.
* Concrete {@link zeroecho.core.spi.PublicKeyImporter} for importing wrapping
* Ed25519 public keys.
*
* <p>
* 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
* {@link zeroecho.core.spi.PrivateKeyImporter#importPrivate(AlgorithmKeySpec)}
* creates a new
* {@link java.security.KeyFactory}.
* creates a new {@link java.security.KeyFactory}.
*
* @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
* {@link zeroecho.core.spi.PublicKeyImporter#importPublic(AlgorithmKeySpec)}
* creates a new
* {@link java.security.KeyFactory}.
* creates a new {@link java.security.KeyFactory}.
*
* @since 1.0
*/

View File

@@ -63,7 +63,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* </p>
*
* @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 {
/**

View File

@@ -47,9 +47,9 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* Specification wrapper for a Kyber (ML-KEM) private key encoded in PKCS#8.
*
* <p>
* Instances of this class carry an owned copy of the PKCS#8-encoded private
* key bytes. They are used with {@link zeroecho.core.CryptoAlgorithm} key
* builders to import keys into the providers native representation.
* Instances of this class carry an owned copy of the PKCS#8-encoded private key
* bytes. They are used with {@link zeroecho.core.CryptoAlgorithm} key builders
* to import keys into the providers native representation.
* </p>
*
* <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);
}
if (finalizationOutputChunks != 0) {
throw new IllegalArgumentException(
"finalizationOutputChunks must be zero: " + finalizationOutputChunks);
throw new IllegalArgumentException("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
* standardized in round-3 submissions. Each variant balances performance,
* 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>
*
* <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.
* </p>
*
* <p>The exact supported operation is
* {@link #generateKeyPair(SphincsPlusKeyGenSpec)}. Public and private import are
* registered separately for {@link SphincsPlusPublicKeySpec} and
* {@link SphincsPlusPrivateKeySpec}.</p>
* <p>
* The exact supported operation is
* {@link #generateKeyPair(SphincsPlusKeyGenSpec)}. Public and private import
* are registered separately for {@link SphincsPlusPublicKeySpec} and
* {@link SphincsPlusPrivateKeySpec}.
* </p>
*
* <h2>Example</h2> <pre>{@code
* SphincsPlusKeyGenSpec spec =

View File

@@ -51,9 +51,11 @@ import zeroecho.core.spi.PrivateKeyImporter;
* pairs but focuses solely on importing private key material.
* </p>
*
* <p>The exact supported operation is
* <p>
* The exact supported operation is
* {@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
* // Assuming bytes contain a PKCS#8-encoded SPHINCS+ private key:

View File

@@ -48,7 +48,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
* <p>
* {@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
* destroyable holder designed for use with {@link SphincsPlusPrivateKeyBuilder}.
* destroyable holder designed for use with
* {@link SphincsPlusPrivateKeyBuilder}.
* </p>
*
* <h2>Encoding</h2>

View File

@@ -50,9 +50,11 @@ import zeroecho.core.spi.PublicKeyImporter;
* pairs, but focuses solely on importing public key material.
* </p>
*
* <p>The exact supported operation is
* <p>
* The exact supported operation is
* {@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
* // 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>
* <ul>
* <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.
* If a specific provider is required, supply or register one that exposes the
* requested XDH algorithm name.</li>

View File

@@ -13,10 +13,12 @@ import java.util.Objects;
/**
* 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
* objects. Cryptographic operation outcomes therefore never depend on an audit
* sink's availability.</p>
* sink's availability.
* </p>
*
* @since 1.0
*/
@@ -36,8 +38,8 @@ public final class AuditListeners {
AuditListener target = Objects.requireNonNull(listener, "listener");
ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
ClassLoader loader = contextLoader == null ? ClassLoader.getSystemClassLoader() : contextLoader;
return (AuditListener) Proxy.newProxyInstance(loader,
new Class<?>[] { AuditListener.class }, (proxy, method, arguments) -> {
return (AuditListener) Proxy.newProxyInstance(loader, new Class<?>[] { AuditListener.class },
(proxy, method, arguments) -> {
if (method.getDeclaringClass() == Object.class) {
return method.invoke(target, arguments);
}

View File

@@ -10,8 +10,10 @@ package zeroecho.core.audit;
/**
* Defines the session-owned automatic auditing strategy.
*
* <p>Audit listener failures are best-effort diagnostics and never change the
* outcome of a cryptographic operation.</p>
* <p>
* Audit listener failures are best-effort diagnostics and never change the
* outcome of a cryptographic operation.
* </p>
*
* @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
* buffering beyond normal {@code FilterInputStream} forwarding is
* introduced.</li>
* <li>Idempotent wrapping: contexts already wrapped by this utility are returned
* unchanged. Unrelated JDK proxies are wrapped normally.</li>
* <li>Idempotent wrapping: contexts already wrapped by this utility are
* returned unchanged. Unrelated JDK proxies are wrapped normally.</li>
* </ul>
*
* <h2>Usage example</h2> <pre>{@code
@@ -190,8 +190,7 @@ public final class AuditedContexts {
}
private static boolean isAuditedProxy(CryptoContext context) {
return Proxy.isProxyClass(context.getClass())
&& Proxy.getInvocationHandler(context) instanceof AuditingHandler;
return Proxy.isProxyClass(context.getClass()) && Proxy.getInvocationHandler(context) instanceof AuditingHandler;
}
@SuppressWarnings("unchecked")
@@ -242,8 +241,7 @@ public final class AuditedContexts {
}
safeAudit.onContextCreatedMeta(ctxId, algoId == null ? UNKNOWN : algoId,
provider == null ? UNKNOWN : provider,
role, keyFp, specMeta);
provider == null ? UNKNOWN : provider, role, keyFp, specMeta);
}
ClassLoader cl = ctx.getClass().getClassLoader(); // NOPMD
@@ -720,8 +718,7 @@ public final class AuditedContexts {
StringBuilder fingerprint = new StringBuilder(16);
for (int index = 0; index < Math.min(8, digest.length); index++) {
int value = digest[index] & 0xff;
fingerprint.append(Character.forDigit(value >>> 4, 16))
.append(Character.forDigit(value & 0x0f, 16));
fingerprint.append(Character.forDigit(value >>> 4, 16)).append(Character.forDigit(value & 0x0f, 16));
}
return key.getAlgorithm() + ":" + fingerprint;
} 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.
*
* <p>
* Stack traces may contain provider exception messages or application
* values. Enabling them is an explicit diagnostic opt-in and requires a
* suitably protected log destination.
* Stack traces may contain provider exception messages or application values.
* Enabling them is an explicit diagnostic opt-in and requires a suitably
* protected log destination.
* </p>
*
* @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(':');
for (int i = 0; i < Math.min(8, digest.length); i++) {
int value = digest[i] & 0xff;
sb.append(Character.forDigit(value >>> 4, 16))
.append(Character.forDigit(value & 0x0f, 16));
sb.append(Character.forDigit(value >>> 4, 16)).append(Character.forDigit(value & 0x0f, 16));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {

View File

@@ -46,13 +46,11 @@ package zeroecho.core.err;
* <h2>When it is thrown</h2>
* <ul>
* <li>During
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String,
* zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
* after policy validation, if the resolved algorithm exposes no bindings for
* the given role.</li>
* <li>Directly from
* {@link zeroecho.core.CryptoAlgorithm#createContext(zeroecho.core.KeyUsage,
* java.security.Key, zeroecho.core.spec.ContextSpec)}
* {@link zeroecho.core.CryptoAlgorithm#createContext(zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)}
* when no binding exists for the role.</li>
* </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
* > 0)
* @param chunks number of chunks buffered at once (must be > 0)
* @throws IllegalArgumentException if a size is outside its documented range
* or a buffer size overflows
* @throws IllegalArgumentException if a size is outside its documented range or
* a buffer size overflows
*/
protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks) {
super(upstream);
@@ -190,8 +190,8 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
* steady-state (must be > 0)
* @param finalizationOutputChunks number of extra output chunks reserved for
* finalization (must be >= 0)
* @throws IllegalArgumentException if a size is outside its documented range
* or a buffer size overflows
* @throws IllegalArgumentException if a size is outside its documented range or
* a buffer size overflows
*/
protected AbstractChunkTransformInputStream(InputStream upstream, int inChunkSize, int outChunkSize, int chunks,
int finalizationOutputChunks) {
@@ -261,8 +261,7 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
// EOF: run finalization exactly once, even if there's no remainder,
// and surface any produced bytes (e.g., padding block, GCM tag).
if (!eofSeen) {
int finalOut = validateOutputCount(doFinal(inBuf, 0, 0, outBuf, 0), outBuf.length,
"finalization");
int finalOut = validateOutputCount(doFinal(inBuf, 0, 0, outBuf, 0), outBuf.length, "finalization");
outPtr = 0;
outLen = finalOut;
eofSeen = true;
@@ -273,8 +272,7 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
// all chunks are aligned to the specified boundary (inChunkSize) -> transform
// can be simply invoked
outLen = validateOutputCount(transform(inBuf, 0, inLen / inChunkSize, outBuf), outBuf.length,
"transformation");
outLen = validateOutputCount(transform(inBuf, 0, inLen / inChunkSize, outBuf), outBuf.length, "transformation");
outPtr = 0;
int left = inLen % inChunkSize;
@@ -291,8 +289,7 @@ public abstract class AbstractChunkTransformInputStream extends FilterInputStrea
return true;
}
private static void validateGeometry(int inChunkSize, int outChunkSize, int chunks,
int finalizationOutputChunks) {
private static void validateGeometry(int inChunkSize, int outChunkSize, int chunks, int finalizationOutputChunks) {
if (inChunkSize < MIN_INPUT_CHUNK_SIZE) {
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
* {@code doFinal}. This mode is restricted to RSA and ElGamal.</li>
* <li><b>Left-padded independent block stream</b> - like the independent block
* stream, but
* left-pads each transformed output block with zeros up to
* stream, but left-pads each transformed output block with zeros up to
* {@code outChunkSize}. Final blocks must be complete; otherwise an
* {@link IllegalStateException} is thrown.</li>
* <li><b>Continuous stream</b> - uses
* {@code Cipher.update(...)} for bulk bytes and a single {@code doFinal()} at
* end of stream. This is suitable for CTR/CFB/OFB/GCM and padding modes.</li>
* <li><b>Continuous stream</b> - uses {@code Cipher.update(...)} for bulk bytes
* and a single {@code doFinal()} at end of stream. This is suitable for
* CTR/CFB/OFB/GCM and padding modes.</li>
* </ul>
*
* <h2>Block sizing</h2>
@@ -344,7 +343,8 @@ public final class CipherTransformInputStreamBuilder {
* </p>
*
* @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
* for an unsupported algorithm or buffer
* 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 final Cipher cipher;
/* package */ SmartBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize,
int 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 final Cipher cipher;
/* package */ SmartPaddedBlockStream(InputStream upstream, Cipher cipher, int inChunkSize, int outChunkSize,
int bufferedBlocks) {
super(upstream, inChunkSize, outChunkSize, bufferedBlocks);

View File

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

View File

@@ -61,8 +61,8 @@ import java.util.List;
*
* <h2>Serialization</h2>
* <ul>
* <li>{@link #writeTo(Appendable)} outputs each pair as {@code k=v\n}
* lines without escaping and reports checked I/O failures.</li>
* <li>{@link #writeTo(Appendable)} outputs each pair as {@code k=v\n} lines
* without escaping and reports checked I/O failures.</li>
* <li>{@link #readFrom(java.io.Reader)} parses lines in the same format,
* ignoring blank lines and comments starting with {@code #}.</li>
* </ul>
@@ -99,8 +99,7 @@ public final class PairSeq {
if (kv[elementIndex] == null) {
int pairIndex = elementIndex >>> 1;
String role = (elementIndex & 1) == 0 ? "key" : "value";
throw new IllegalArgumentException(
"pair " + pairIndex + " " + role + " must not be null");
throw new IllegalArgumentException("pair " + pairIndex + " " + role + " must not be null");
}
}
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
* checked I/O failures directly.
* Appends all pairs to the target as {@code key=value} lines, reporting checked
* I/O failures directly.
*
* <p>
* 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>
*
* <h2>Thread-safety</h2> Instances are immutable and thread-safe. Public
* accessors are resolved once per runtime class and operation type, then invoked
* through cached method handles. The unload-safe {@link ClassValue} caches do
* not retain otherwise unreachable class loaders.
* accessors are resolved once per runtime class and operation type, then
* invoked through cached method handles. The unload-safe {@link ClassValue}
* caches do not retain otherwise unreachable class loaders.
*
* @param <T> domain type that follows the marshalling and unmarshalling
* conventions
@@ -274,8 +274,7 @@ public final class PairSeqCodec<T> implements Codec<T, PairSeq> {
try {
Method method = runtimeType.getMethod("marshal");
if (!PairSeq.class.isAssignableFrom(method.getReturnType())) {
return new MarshalPlan(null,
"marshal() must return PairSeq in " + runtimeType.getName(), null);
return new MarshalPlan(null, "marshal() must return PairSeq in " + runtimeType.getName(), null);
}
MethodHandle handle = MethodHandles.lookup().unreflect(method);
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);
}
MethodHandle handle = MethodHandles.lookup().unreflect(method);
return new UnmarshalPlan(handle,
"static unmarshal(PairSeq) failed for " + runtimeType.getName(), null, null);
return new UnmarshalPlan(handle, "static unmarshal(PairSeq) failed for " + runtimeType.getName(),
null, null);
}
} catch (NoSuchMethodException ignored) {
// Resolve the constructor fallback below.
} catch (IllegalAccessException exception) {
return new UnmarshalPlan(null, null,
"static unmarshal(PairSeq) failed for " + runtimeType.getName(), exception);
return new UnmarshalPlan(null, null, "static unmarshal(PairSeq) failed for " + runtimeType.getName(),
exception);
}
try {

View File

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

View File

@@ -11,7 +11,8 @@ import zeroecho.core.spec.AlgorithmKeySpec;
/**
* 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
* @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.
*
* <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
* expose an I/O failure contract. Factories must be stateless or otherwise safe
* for concurrent invocation; returned contexts retain their own documented
* thread-safety contracts.</p>
* thread-safety contracts.
* </p>
*
* @param <C> context type produced
* @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.
*
* <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
* opened. Implementations must not source passwords from immutable strings,
* process arguments, system properties, environment fallbacks, persistent
* files, or global mutable state.</p>
* files, or global mutable state.
* </p>
*/
@FunctionalInterface
public interface KeyringUnlockProvider {

View File

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

View File

@@ -34,20 +34,26 @@
/**
* 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
* 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 AsymmetricKeyPairGenerator}, {@link PublicKeyImporter}, and
* {@link PrivateKeyImporter}. A provider registers only the operations it
* 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
* and are not necessarily thread-safe.</p>
* and are not necessarily thread-safe.
* </p>
*
* @since 1.0
*/

View File

@@ -10,9 +10,11 @@ import java.util.Objects;
/**
* 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
* deliberately excluded.</p>
* deliberately excluded.
* </p>
*/
public final class KeyringException extends IOException {
private static final long serialVersionUID = 1L;
@@ -21,17 +23,9 @@ public final class KeyringException extends IOException {
* Stable keyring failure categories.
*/
public enum Code {
KEYRING_ALREADY_OPEN,
KEYRING_FILESYSTEM_UNSUPPORTED,
KEYRING_FORMAT_INVALID,
KEYRING_LIMIT_EXCEEDED,
KEYRING_UNLOCK_FAILED,
KEYRING_IO_FAILED,
KEYRING_DURABILITY_UNCONFIRMED,
KEYRING_CLOSED,
KEYRING_NON_EXPORTABLE_KEY,
KEYRING_IMPORT_MAPPING_INVALID,
KEYRING_IMPORT_METADATA_INVALID,
KEYRING_ALREADY_OPEN, KEYRING_FILESYSTEM_UNSUPPORTED, KEYRING_FORMAT_INVALID, KEYRING_LIMIT_EXCEEDED,
KEYRING_UNLOCK_FAILED, KEYRING_IO_FAILED, KEYRING_DURABILITY_UNCONFIRMED, KEYRING_CLOSED,
KEYRING_NON_EXPORTABLE_KEY, KEYRING_IMPORT_MAPPING_INVALID, KEYRING_IMPORT_METADATA_INVALID,
KEYRING_KEY_NOT_CANONICALIZABLE
}

View File

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

View File

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

View File

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

View File

@@ -14,14 +14,18 @@ import javax.security.auth.Destroyable;
/**
* 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
* returned copy belongs to the receiver and must be cleared immediately after
* 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
* access fail deterministically.</p>
* <p>
* Instances are thread-safe. Destruction is idempotent and makes subsequent
* access fail deterministically.
* </p>
*/
public final class KeyringPassword implements Destroyable, AutoCloseable {
private final ReentrantLock lifecycleLock = new ReentrantLock();

View File

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

View File

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

View File

@@ -65,35 +65,44 @@ import zeroecho.core.util.RandomSupport;
/**
* Path-bound encrypted local software keystore.
*
* <p>A keyring uses PBKDF2-HMAC-SHA-256 to unwrap one random 256-bit store
* master key. Every entry and the ordered manifest are independently protected
* by AES-256-GCM. Java class names are neither persisted nor resolved. Import
* <p>
* A keyring uses PBKDF2-HMAC-SHA-256 to unwrap one random 256-bit store master
* 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
* registry. Provider names are not persisted: standard key encodings are
* 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 #open(Path, KeyringPassword)} until {@link #close()}. Only POSIX
* 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
* entry and manifest encryption. Its 256-bit MAC key is derived from the store
* master 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
* <p>
* The nonce-reservation sidecar is authenticated independently from AES entry
* and manifest encryption. Its 256-bit MAC key is derived from the store master
* 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
* 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
* 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.
@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 int FORMAT_VERSION = 2;
/* 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 byte KDF_PBKDF2_SHA256 = 1;
/* default */ static final byte AEAD_AES_256_GCM = 1;
private static final byte[] NONCE_RESERVATION_MAGIC =
{ 'Z', 'E', 'K', 'N', 'O', 'N', 'C', '2' };
private static final byte[] NONCE_RESERVATION_MAGIC = { 'Z', 'E', 'K', 'N', 'O', 'N', 'C', '2' };
private static final int NONCE_RESERVATION_VERSION = 2;
private static final int NONCE_RESERVATION_TAG_BYTES = 32;
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_PRIVATE = ".priv";
private static final long MIN_NONCE_HIGH_WATER = 1L;
private static final Set<PosixFilePermission> DIRECTORY_PERMISSIONS =
EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
PosixFilePermission.OWNER_EXECUTE);
private static final Set<PosixFilePermission> FILE_PERMISSIONS =
EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE);
private static final FileAttribute<Set<PosixFilePermission>> DIRECTORY_ATTRIBUTE =
PosixFilePermissions.asFileAttribute(DIRECTORY_PERMISSIONS);
private static final FileAttribute<Set<PosixFilePermission>> FILE_ATTRIBUTE =
PosixFilePermissions.asFileAttribute(FILE_PERMISSIONS);
private static final KeyringRandomBytes SYSTEM_RANDOM = destination ->
RandomSupport.getRandom().nextBytes(destination);
private static final Set<PosixFilePermission> DIRECTORY_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE);
private static final Set<PosixFilePermission> FILE_PERMISSIONS = EnumSet.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE);
private static final FileAttribute<Set<PosixFilePermission>> DIRECTORY_ATTRIBUTE = PosixFilePermissions
.asFileAttribute(DIRECTORY_PERMISSIONS);
private static final FileAttribute<Set<PosixFilePermission>> FILE_ATTRIBUTE = PosixFilePermissions
.asFileAttribute(FILE_PERMISSIONS);
private static final KeyringRandomBytes SYSTEM_RANDOM = destination -> RandomSupport.getRandom()
.nextBytes(destination);
private final Path path;
private final Path nonceReservationPath;
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.
*/
public enum Encoding {
X509(1),
PKCS8(2),
RAW(3);
X509(1), PKCS8(2), RAW(3);
private final int code;
@@ -181,9 +186,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
* Key classifications supported by the encrypted store.
*/
public enum Kind {
PUBLIC_KEY(1),
PRIVATE_KEY(2),
SECRET_KEY(3);
PUBLIC_KEY(1), PRIVATE_KEY(2), SECRET_KEY(3);
private final int code;
@@ -201,11 +204,9 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
}
private KeyringStore(Path path, Ownership ownership,
KeyringRandomBytes random, KeyringFileOperations fileOperations,
Header header, byte[] masterKey,
byte[] nonceReservationMacKey, Manifest manifest,
Map<String, EncryptedEntry> entries) {
private KeyringStore(Path path, Ownership ownership, KeyringRandomBytes random,
KeyringFileOperations fileOperations, Header header, byte[] masterKey, byte[] nonceReservationMacKey,
Manifest manifest, Map<String, EncryptedEntry> entries) {
this.path = path;
this.nonceReservationPath = nonceReservationPath(path);
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.
*
* @param path new keyring file
* @param password borrowed unlock password; the caller remains responsible
* for destroying it
* @param password borrowed unlock password; the caller remains responsible for
* destroying it
* @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
*/
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 protection operational protection policy
* @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
*/
public static KeyringStore create(Path path, KeyringPassword password,
KeyringProtection protection) throws IOException, GeneralSecurityException {
public static KeyringStore create(Path path, KeyringPassword password, KeyringProtection protection)
throws IOException, GeneralSecurityException {
return create(path, password, protection, SYSTEM_RANDOM);
}
/* default */ static KeyringStore create(Path path, KeyringPassword password,
KeyringProtection protection, KeyringRandomBytes random)
throws IOException, GeneralSecurityException {
/* default */ static KeyringStore create(Path path, KeyringPassword password, KeyringProtection protection,
KeyringRandomBytes random) throws IOException, GeneralSecurityException {
return create(path, password, protection, random, KeyringFileOperations.NIO);
}
@SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" })
/* default */ static KeyringStore create(Path path, KeyringPassword password,
KeyringProtection protection, KeyringRandomBytes random,
KeyringFileOperations fileOperations)
/* default */ static KeyringStore create(Path path, KeyringPassword password, KeyringProtection protection,
KeyringRandomBytes random, KeyringFileOperations fileOperations)
throws IOException, GeneralSecurityException {
Objects.requireNonNull(password, "password 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(salt);
random.nextBytes(storeId);
nonceReservationMacKey =
KeyringNonceReservationKdf.derive(masterKey, storeId);
nonceReservationMacKey = KeyringNonceReservationKdf.derive(masterKey, storeId);
fillMasterWrapNonce(random, wrapNonce);
int prefix = randomPrefix(random);
Header provisional = new Header(storeId, KeyringProtection.CREATION_ITERATIONS,
salt, wrapNonce, new byte[0]);
Header provisional = new Header(storeId, KeyringProtection.CREATION_ITERATIONS, salt, wrapNonce,
new byte[0]);
aad = masterWrapAad(provisional);
kek = deriveKek(password, salt, provisional.iterations);
wrapped = crypt(Cipher.ENCRYPT_MODE, kek, wrapNonce, aad, masterKey);
Header header = new Header(storeId, provisional.iterations, salt, wrapNonce, wrapped);
Manifest initial = new Manifest(prefix, 0, List.of());
store = new KeyringStore(normalized, ownership, random, fileOperations, header,
masterKey, nonceReservationMacKey, initial, new LinkedHashMap<>());
store = new KeyringStore(normalized, ownership, random, fileOperations, header, masterKey,
nonceReservationMacKey, initial, new LinkedHashMap<>());
store.persistSnapshot(new LinkedHashMap<>());
success = true;
return store;
@@ -328,12 +328,11 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
* @param path keyring file
* @param password borrowed unlock password
* @return open encrypted keyring
* @throws IOException if the format, filesystem, or authentication check
* fails
* @throws IOException if the format, filesystem, or authentication
* check fails
* @throws GeneralSecurityException if cryptographic initialization fails
*/
public static KeyringStore open(Path path, KeyringPassword password)
throws IOException, GeneralSecurityException {
public static KeyringStore open(Path path, KeyringPassword password) throws IOException, GeneralSecurityException {
return open(path, password, KeyringProtection.standard());
}
@@ -344,25 +343,23 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
* @param password borrowed unlock password
* @param protection operational protection policy
* @return open encrypted keyring
* @throws IOException if the format, filesystem, or authentication check
* fails
* @throws IOException if the format, filesystem, or authentication
* check fails
* @throws GeneralSecurityException if cryptographic initialization fails
*/
public static KeyringStore open(Path path, KeyringPassword password,
KeyringProtection protection) throws IOException, GeneralSecurityException {
public static KeyringStore open(Path path, KeyringPassword password, KeyringProtection protection)
throws IOException, GeneralSecurityException {
return open(path, password, protection, SYSTEM_RANDOM);
}
/* default */ static KeyringStore open(Path path, KeyringPassword password,
KeyringProtection protection, KeyringRandomBytes random)
throws IOException, GeneralSecurityException {
/* default */ static KeyringStore open(Path path, KeyringPassword password, KeyringProtection protection,
KeyringRandomBytes random) throws IOException, GeneralSecurityException {
return open(path, password, protection, random, KeyringFileOperations.NIO);
}
@SuppressWarnings({ "PMD.CloseResource", "PMD.UseTryWithResources" })
/* default */ static KeyringStore open(Path path, KeyringPassword password,
KeyringProtection protection, KeyringRandomBytes random,
KeyringFileOperations fileOperations)
/* default */ static KeyringStore open(Path path, KeyringPassword password, KeyringProtection protection,
KeyringRandomBytes random, KeyringFileOperations fileOperations)
throws IOException, GeneralSecurityException {
Objects.requireNonNull(password, "password 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);
Decoded decoded = decodeImage(image, password, protection);
masterKey = decoded.masterKey;
nonceReservationMacKey = KeyringNonceReservationKdf.derive(
masterKey, decoded.header.storeId);
long reservedHighWater = readNonceReservation(normalized, ownership.owner(),
decoded.header.storeId, decoded.manifest.noncePrefix,
nonceReservationMacKey);
nonceReservationMacKey = KeyringNonceReservationKdf.derive(masterKey, decoded.header.storeId);
long reservedHighWater = readNonceReservation(normalized, ownership.owner(), decoded.header.storeId,
decoded.manifest.noncePrefix, nonceReservationMacKey);
if (reservedHighWater < decoded.manifest.highWater) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
}
Manifest effectiveManifest = new Manifest(decoded.manifest.noncePrefix,
reservedHighWater, decoded.manifest.entries);
KeyringStore store = new KeyringStore(normalized, ownership,
random, fileOperations, decoded.header, masterKey, nonceReservationMacKey,
effectiveManifest, decoded.entries);
Manifest effectiveManifest = new Manifest(decoded.manifest.noncePrefix, reservedHighWater,
decoded.manifest.entries);
KeyringStore store = new KeyringStore(normalized, ownership, random, fileOperations, decoded.header,
masterKey, nonceReservationMacKey, effectiveManifest, decoded.entries);
success = true;
return store;
} finally {
@@ -446,8 +440,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
public void putSecret(String alias, String algorithmId, SecretKey key)
throws IOException, GeneralSecurityException {
Objects.requireNonNull(key, "key must not be null");
KeyringImportRegistry.HmacVariant hmacVariant =
KeyringImportRegistry.HmacVariant.forStoredKey(algorithmId, key);
KeyringImportRegistry.HmacVariant hmacVariant = KeyringImportRegistry.HmacVariant.forStoredKey(algorithmId,
key);
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
* 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
*/
@@ -647,9 +643,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
}
private void put(String alias, String algorithmId, Kind kind, Encoding encoding,
Key key, KeyringImportRegistry.HmacVariant hmacVariant)
throws IOException, GeneralSecurityException {
private void put(String alias, String algorithmId, Kind kind, Encoding encoding, Key key,
KeyringImportRegistry.HmacVariant hmacVariant) throws IOException, GeneralSecurityException {
Objects.requireNonNull(key, "key must not be null");
validateString(alias, MAX_ALIAS_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);
}
try {
KeyringImportRegistry.validateCanonical(algorithmId, kind, encoding,
hmacVariant, key, encoded);
KeyringImportRegistry.validateCanonical(algorithmId, kind, encoding, hmacVariant, key, encoded);
lifecycleLock.writeLock().lock();
try {
ensureOpen();
@@ -680,16 +674,14 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
byte[] aad = null;
byte[] ciphertext = null;
try {
plaintext = encodeEntryPlaintext(alias, algorithmId, kind, encoding,
hmacVariant, encoded);
plaintext = encodeEntryPlaintext(alias, algorithmId, kind, encoding, hmacVariant, encoded);
aad = entryAad(entryId, position);
ciphertext = crypt(Cipher.ENCRYPT_MODE, masterKey, nonce, aad, plaintext);
if (ciphertext.length > MAX_ENTRY_CIPHERTEXT_BYTES) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
}
EncryptedEntry entry = new EncryptedEntry(entryId, position, alias,
algorithmId, kind, encoding, hmacVariant, nonce, ciphertext,
digest(ciphertext));
EncryptedEntry entry = new EncryptedEntry(entryId, position, alias, algorithmId, kind, encoding,
hmacVariant, nonce, ciphertext, digest(ciphertext));
candidate.put(alias, entry);
persistSnapshot(candidate);
entries = candidate;
@@ -709,8 +701,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
@SuppressWarnings("PMD.PreserveStackTrace")
private Imported importEntry(String alias, Kind expected)
throws IOException, GeneralSecurityException {
private Imported importEntry(String alias, Kind expected) throws IOException, GeneralSecurityException {
lifecycleLock.readLock().lock();
try {
ensureOpen();
@@ -726,8 +717,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
plaintext = crypt(Cipher.DECRYPT_MODE, masterKey, entry.nonce, aad, entry.ciphertext);
decoded = decodeEntryPlaintext(plaintext);
validateEntryBinding(entry, decoded);
Key key = KeyringImportRegistry.importKey(decoded.algorithm, decoded.kind,
decoded.encoding, decoded.hmacVariant, decoded.encoded);
Key key = KeyringImportRegistry.importKey(decoded.algorithm, decoded.kind, decoded.encoding,
decoded.hmacVariant, decoded.encoded);
return new Imported(decoded.algorithm, key);
} catch (AEADBadTagException exception) {
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)
throws IOException, GeneralSecurityException {
private void persistSnapshot(Map<String, EncryptedEntry> candidate) throws IOException, GeneralSecurityException {
byte[] manifestNonce = null;
byte[] manifestPlain = null;
byte[] manifestAad = null;
@@ -754,8 +744,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
manifestNonce = nextNonce();
manifestPlain = encodeManifest(candidate);
manifestAad = manifestAad(candidate.size());
manifestCipher = crypt(Cipher.ENCRYPT_MODE, masterKey, manifestNonce,
manifestAad, manifestPlain);
manifestCipher = crypt(Cipher.ENCRYPT_MODE, masterKey, manifestNonce, manifestAad, manifestPlain);
image = encodeImage(candidate, manifestNonce, manifestCipher);
writeAtomically(image);
} catch (IOException | GeneralSecurityException exception) {
@@ -780,15 +769,14 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
@SuppressWarnings({ "PMD.PreserveStackTrace", "PMD.ExceptionAsFlowControl" })
private void writeAtomically(Target target, Path destination, byte[] image)
throws IOException {
private void writeAtomically(Target target, Path destination, byte[] image) throws IOException {
Path parent = destination.getParent();
Path temporary = null;
boolean moved = false;
IOException primary = null;
try {
temporary = fileOperations.createTemporary(target, parent,
"." + destination.getFileName() + ".", ".tmp", FILE_ATTRIBUTE);
temporary = fileOperations.createTemporary(target, parent, "." + destination.getFileName() + ".", ".tmp",
FILE_ATTRIBUTE);
validateOwnerOnly(temporary, false);
fileOperations.writeTemporary(target, temporary, image);
fileOperations.forceTemporary(target, temporary);
@@ -844,15 +832,13 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
}
private void writeNonceReservation(long highWater)
throws IOException, GeneralSecurityException {
private void writeNonceReservation(long highWater) throws IOException, GeneralSecurityException {
byte[] authenticated = encodeNonceReservation(storeId, noncePrefix, highWater);
byte[] tag = null;
byte[] image = null;
try {
tag = hmac(nonceReservationMacKey, authenticated);
image = ByteBuffer.allocate(authenticated.length + tag.length)
.put(authenticated).put(tag).array();
image = ByteBuffer.allocate(authenticated.length + tag.length).put(authenticated).put(tag).array();
writeSidecarAtomically(nonceReservationPath, image);
} finally {
wipe(authenticated);
@@ -861,19 +847,14 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
}
private static long readNonceReservation(Path keyring, UserPrincipal owner,
byte[] expectedStoreId, int expectedPrefix,
byte[] nonceReservationMacKey)
throws IOException, GeneralSecurityException {
private static long readNonceReservation(Path keyring, UserPrincipal owner, byte[] expectedStoreId,
int expectedPrefix, byte[] nonceReservationMacKey) throws IOException, GeneralSecurityException {
Path reservation = nonceReservationPath(keyring);
validateExistingFile(reservation, owner);
byte[] image = readFixedImage(reservation,
NONCE_RESERVATION_MAGIC.length + Integer.BYTES + UUID_BYTES
byte[] image = readFixedImage(reservation, NONCE_RESERVATION_MAGIC.length + Integer.BYTES + UUID_BYTES
+ Integer.BYTES + Long.BYTES + NONCE_RESERVATION_TAG_BYTES);
byte[] authenticated = Arrays.copyOf(image,
image.length - NONCE_RESERVATION_TAG_BYTES);
byte[] actualTag = Arrays.copyOfRange(image,
authenticated.length, image.length);
byte[] authenticated = Arrays.copyOf(image, image.length - NONCE_RESERVATION_TAG_BYTES);
byte[] actualTag = Arrays.copyOfRange(image, authenticated.length, image.length);
byte[] expectedTag = null;
try {
expectedTag = hmac(nonceReservationMacKey, authenticated);
@@ -889,11 +870,10 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
int prefix = buffer.getInt();
long highWater = buffer.getLong();
if (!MessageDigest.isEqual(magic, NONCE_RESERVATION_MAGIC)
|| ByteBuffer.wrap(authenticated,
NONCE_RESERVATION_MAGIC.length, Integer.BYTES).getInt()
!= NONCE_RESERVATION_VERSION
|| !MessageDigest.isEqual(storeId, expectedStoreId)
|| prefix != expectedPrefix || highWater < MIN_NONCE_HIGH_WATER) {
|| ByteBuffer.wrap(authenticated, NONCE_RESERVATION_MAGIC.length, Integer.BYTES)
.getInt() != NONCE_RESERVATION_VERSION
|| !MessageDigest.isEqual(storeId, expectedStoreId) || prefix != expectedPrefix
|| highWater < MIN_NONCE_HIGH_WATER) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
}
return highWater;
@@ -905,20 +885,14 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
}
private static byte[] encodeNonceReservation(byte[] storeId, int prefix,
long highWater) {
return ByteBuffer.allocate(NONCE_RESERVATION_MAGIC.length + Integer.BYTES
+ UUID_BYTES + Integer.BYTES + Long.BYTES)
.put(NONCE_RESERVATION_MAGIC)
.putInt(NONCE_RESERVATION_VERSION)
.put(storeId)
.putInt(prefix)
.putLong(highWater)
.array();
private static byte[] encodeNonceReservation(byte[] storeId, int prefix, long highWater) {
return ByteBuffer
.allocate(NONCE_RESERVATION_MAGIC.length + Integer.BYTES + UUID_BYTES + Integer.BYTES + Long.BYTES)
.put(NONCE_RESERVATION_MAGIC).putInt(NONCE_RESERVATION_VERSION).put(storeId).putInt(prefix)
.putLong(highWater).array();
}
private static byte[] hmac(byte[] key, byte[] input)
throws GeneralSecurityException {
private static byte[] hmac(byte[] key, byte[] input) throws GeneralSecurityException {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
return mac.doFinal(input);
@@ -928,8 +902,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
writeAtomically(Target.NONCE_RESERVATION, target, image);
}
private byte[] encodeImage(Map<String, EncryptedEntry> candidate,
byte[] manifestNonce, byte[] manifestCipher) throws IOException, KeyringException {
private byte[] encodeImage(Map<String, EncryptedEntry> candidate, byte[] manifestNonce, byte[] manifestCipher)
throws IOException, KeyringException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(bytes)) {
out.write(MAGIC);
@@ -963,8 +937,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
return result;
}
private byte[] encodeManifest(Map<String, EncryptedEntry> candidate)
throws IOException {
private byte[] encodeManifest(Map<String, EncryptedEntry> candidate) throws IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (DataOutputStream out = new DataOutputStream(bytes)) {
out.writeInt(MANIFEST_FORMAT_VERSION);
@@ -987,11 +960,10 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
return bytes.toByteArray();
}
@SuppressWarnings({ "PMD.NcssCount", "PMD.CyclomaticComplexity",
"PMD.PreserveStackTrace", "PMD.ExceptionAsFlowControl",
"PMD.AvoidCatchingGenericException" })
private static Decoded decodeImage(byte[] image, KeyringPassword password,
KeyringProtection protection) throws IOException, GeneralSecurityException {
@SuppressWarnings({ "PMD.NcssCount", "PMD.CyclomaticComplexity", "PMD.PreserveStackTrace",
"PMD.ExceptionAsFlowControl", "PMD.AvoidCatchingGenericException" })
private static Decoded decodeImage(byte[] image, KeyringPassword password, KeyringProtection protection)
throws IOException, GeneralSecurityException {
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(image))) {
byte[] magic = readFixed(in, MAGIC.length);
if (!MessageDigest.isEqual(MAGIC, magic)) {
@@ -1014,8 +986,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
if (wrapNonce[0] != MASTER_WRAP_NONCE_DOMAIN) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
}
byte[] wrapped = readBoundedBytes(in, MASTER_KEY_BYTES + GCM_TAG_BYTES,
MASTER_KEY_BYTES + GCM_TAG_BYTES);
byte[] wrapped = readBoundedBytes(in, MASTER_KEY_BYTES + GCM_TAG_BYTES, MASTER_KEY_BYTES + GCM_TAG_BYTES);
if (wrapped.length != MASTER_KEY_BYTES + GCM_TAG_BYTES) {
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++) {
byte[] entryId = readFixed(in, UUID_BYTES);
byte[] nonce = readFixed(in, NONCE_BYTES);
byte[] ciphertext = readBoundedBytes(in, MAX_ENTRY_CIPHERTEXT_BYTES,
MAX_ENTRY_CIPHERTEXT_BYTES);
byte[] ciphertext = readBoundedBytes(in, MAX_ENTRY_CIPHERTEXT_BYTES, MAX_ENTRY_CIPHERTEXT_BYTES);
wireEntries.add(new EncryptedWireEntry(entryId, nonce, ciphertext));
}
byte[] manifestNonce = readFixed(in, NONCE_BYTES);
@@ -1061,14 +1031,12 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
try {
aad = manifestAad(storeId, count);
try {
manifestPlain = crypt(Cipher.DECRYPT_MODE, masterKey,
manifestNonce, aad, manifestCipher);
manifestPlain = crypt(Cipher.DECRYPT_MODE, masterKey, manifestNonce, aad, manifestCipher);
} catch (AEADBadTagException exception) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
}
Manifest manifest = decodeManifest(manifestPlain, count);
Map<String, EncryptedEntry> entries =
bindManifest(manifest, wireEntries, manifestNonce);
Map<String, EncryptedEntry> entries = bindManifest(manifest, wireEntries, manifestNonce);
return new Decoded(header, masterKey, manifest, entries);
} finally {
wipe(aad);
@@ -1086,8 +1054,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
@SuppressWarnings("PMD.PreserveStackTrace")
private static Manifest decodeManifest(byte[] plaintext, int expectedCount)
throws IOException {
private static Manifest decodeManifest(byte[] plaintext, int expectedCount) throws IOException {
try (DataInputStream in = new DataInputStream(new ByteArrayInputStream(plaintext))) {
if (in.readInt() != MANIFEST_FORMAT_VERSION) {
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);
Kind kind = Kind.fromCode(in.readUnsignedByte());
Encoding encoding = Encoding.fromCode(in.readUnsignedByte());
KeyringImportRegistry.HmacVariant hmacVariant =
KeyringImportRegistry.HmacVariant.fromCode(in.readUnsignedByte());
KeyringImportRegistry.HmacVariant hmacVariant = KeyringImportRegistry.HmacVariant
.fromCode(in.readUnsignedByte());
byte[] nonce = readFixed(in, NONCE_BYTES);
int length = in.readInt();
if (length < GCM_TAG_BYTES || length > MAX_ENTRY_CIPHERTEXT_BYTES) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
}
byte[] digest = readFixed(in, SHA256_BYTES);
descriptors.add(new ManifestEntry(entryId, position, alias, algorithm,
kind, encoding, hmacVariant, nonce, length, digest));
descriptors.add(new ManifestEntry(entryId, position, alias, algorithm, kind, encoding, hmacVariant,
nonce, length, digest));
}
if (in.read() != -1) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
@@ -1130,9 +1097,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
private static Map<String, EncryptedEntry> bindManifest(Manifest manifest,
List<EncryptedWireEntry> wireEntries, byte[] manifestNonce)
throws KeyringException {
private static Map<String, EncryptedEntry> bindManifest(Manifest manifest, List<EncryptedWireEntry> wireEntries,
byte[] manifestNonce) throws KeyringException {
if (manifest.entries.size() != wireEntries.size()) {
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++) {
ManifestEntry descriptor = manifest.entries.get(index);
EncryptedWireEntry wire = wireEntries.get(index);
if (descriptor.position != index
|| !MessageDigest.isEqual(descriptor.entryId, wire.entryId)
if (descriptor.position != index || !MessageDigest.isEqual(descriptor.entryId, wire.entryId)
|| !MessageDigest.isEqual(descriptor.nonce, wire.nonce)
|| descriptor.ciphertextLength != wire.ciphertext.length
|| !MessageDigest.isEqual(descriptor.digest, digest(wire.ciphertext))) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
}
validateNonce(wire.nonce, manifest.noncePrefix, manifest.highWater);
if (!aliases.add(descriptor.alias)
|| !ids.add(uuid(descriptor.entryId))
if (!aliases.add(descriptor.alias) || !ids.add(uuid(descriptor.entryId))
|| !nonces.add(new Nonce(wire.nonce))) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
}
KeyringImportRegistry.validateMapping(descriptor.algorithm,
descriptor.kind, descriptor.encoding, descriptor.hmacVariant);
result.put(descriptor.alias, new EncryptedEntry(wire.entryId, index,
descriptor.alias, descriptor.algorithm, descriptor.kind,
descriptor.encoding, descriptor.hmacVariant, wire.nonce,
wire.ciphertext, descriptor.digest));
KeyringImportRegistry.validateMapping(descriptor.algorithm, descriptor.kind, descriptor.encoding,
descriptor.hmacVariant);
result.put(descriptor.alias,
new EncryptedEntry(wire.entryId, index, descriptor.alias, descriptor.algorithm, descriptor.kind,
descriptor.encoding, descriptor.hmacVariant, wire.nonce, wire.ciphertext,
descriptor.digest));
}
return result;
}
@SuppressWarnings("PMD.PreserveStackTrace")
private static byte[] encodeEntryPlaintext(String alias, String algorithm,
Kind kind, Encoding encoding, KeyringImportRegistry.HmacVariant hmacVariant,
byte[] encoded)
throws KeyringException {
private static byte[] encodeEntryPlaintext(String alias, String algorithm, Kind kind, Encoding encoding,
KeyringImportRegistry.HmacVariant hmacVariant, byte[] encoded) throws KeyringException {
if (encoded.length <= 0 || encoded.length > MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
}
byte[] aliasBytes = alias.getBytes(StandardCharsets.UTF_8);
byte[] algorithmBytes = algorithm.getBytes(StandardCharsets.UTF_8);
try {
int size = Math.addExact(Integer.BYTES + 3,
Math.addExact(lengthPrefixedSize(aliasBytes),
Math.addExact(lengthPrefixedSize(algorithmBytes),
lengthPrefixedSize(encoded))));
int size = Math.addExact(Integer.BYTES + 3, Math.addExact(lengthPrefixedSize(aliasBytes),
Math.addExact(lengthPrefixedSize(algorithmBytes), lengthPrefixedSize(encoded))));
if (size > MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES) {
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);
Kind kind = Kind.fromCode(in.readUnsignedByte());
Encoding encoding = Encoding.fromCode(in.readUnsignedByte());
KeyringImportRegistry.HmacVariant hmacVariant =
KeyringImportRegistry.HmacVariant.fromCode(in.readUnsignedByte());
byte[] encoded = readBoundedBytes(in,
MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES,
KeyringImportRegistry.HmacVariant hmacVariant = KeyringImportRegistry.HmacVariant
.fromCode(in.readUnsignedByte());
byte[] encoded = readBoundedBytes(in, MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES,
MAX_ENTRY_CIPHERTEXT_BYTES - GCM_TAG_BYTES);
if (in.read() != -1) {
wipe(encoded);
@@ -1237,8 +1196,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
}
private static void validateEntryBinding(EncryptedEntry entry, EntryPlain decoded)
throws KeyringException {
private static void validateEntryBinding(EncryptedEntry entry, EntryPlain decoded) throws KeyringException {
if (!entry.alias.equals(decoded.alias) || !entry.algorithm.equals(decoded.algorithm)
|| entry.kind != decoded.kind || entry.encoding != decoded.encoding
|| entry.hmacVariant != decoded.hmacVariant) {
@@ -1305,8 +1263,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
return buffer.array();
}
private static void validateNonce(byte[] nonce, int prefix, long highWater)
throws KeyringException {
private static void validateNonce(byte[] nonce, int prefix, long highWater) throws KeyringException {
if (nonce.length != NONCE_BYTES) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
}
@@ -1325,8 +1282,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
byte[] prefix = new byte[3];
try {
random.nextBytes(prefix);
return (Byte.toUnsignedInt(prefix[0]) << 16)
| (Byte.toUnsignedInt(prefix[1]) << 8)
return (Byte.toUnsignedInt(prefix[0]) << 16) | (Byte.toUnsignedInt(prefix[1]) << 8)
| Byte.toUnsignedInt(prefix[2]);
} finally {
wipe(prefix);
@@ -1351,8 +1307,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
private static int readThreeBytePrefix(ByteBuffer buffer) {
return (Byte.toUnsignedInt(buffer.get()) << 16)
| (Byte.toUnsignedInt(buffer.get()) << 8)
return (Byte.toUnsignedInt(buffer.get()) << 16) | (Byte.toUnsignedInt(buffer.get()) << 8)
| 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);
Arrays.fill(chars, '\0');
try {
return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
.generateSecret(spec).getEncoded();
return SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).getEncoded();
} finally {
spec.clearPassword();
}
}
private static byte[] crypt(int mode, byte[] key, byte[] nonce, byte[] aad,
byte[] input) throws GeneralSecurityException {
private static byte[] crypt(int mode, byte[] key, byte[] nonce, byte[] aad, byte[] input)
throws GeneralSecurityException {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(mode, new SecretKeySpec(key, "AES"),
new GCMParameterSpec(GCM_TAG_BITS, nonce));
cipher.init(mode, new SecretKeySpec(key, "AES"), new GCMParameterSpec(GCM_TAG_BITS, nonce));
cipher.updateAAD(aad);
return cipher.doFinal(input);
}
@@ -1387,10 +1340,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
}
private static void validateIterations(int iterations, KeyringProtection protection)
throws KeyringException {
if (iterations < KeyringProtection.CREATION_ITERATIONS
|| iterations > KeyringProtection.MAX_DECODED_ITERATIONS
private static void validateIterations(int iterations, KeyringProtection protection) throws KeyringException {
if (iterations < KeyringProtection.CREATION_ITERATIONS || iterations > KeyringProtection.MAX_DECODED_ITERATIONS
|| iterations > protection.operationalIterationMaximum()) {
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,
String alias) {
private static int positionFor(Map<String, EncryptedEntry> values, String alias) {
int index = 0;
for (String current : values.keySet()) {
if (current.equals(alias)) {
@@ -1442,10 +1392,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
}
private static void validateString(String value, int maximumBytes)
throws KeyringException {
if (value == null || value.isBlank()
|| value.getBytes(StandardCharsets.UTF_8).length > maximumBytes) {
private static void validateString(String value, int maximumBytes) throws KeyringException {
if (value == null || value.isBlank() || value.getBytes(StandardCharsets.UTF_8).length > maximumBytes) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
}
}
@@ -1485,11 +1433,9 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
return value;
}
private static byte[] readBoundedBytes(DataInputStream in, int maximum,
int exactOrMaximum) throws IOException {
private static byte[] readBoundedBytes(DataInputStream in, int maximum, int exactOrMaximum) throws IOException {
int length = in.readInt();
if (length < 0 || length > maximum
|| exactOrMaximum < maximum && length != exactOrMaximum) {
if (length < 0 || length > maximum || exactOrMaximum < maximum && length != exactOrMaximum) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
}
byte[] value = new byte[length];
@@ -1512,10 +1458,8 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
byte[] encoded = readBoundedBytes(in, maximum, maximum);
try {
try {
return StandardCharsets.UTF_8.newDecoder()
.onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT)
.decode(ByteBuffer.wrap(encoded)).toString();
return StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT)
.onUnmappableCharacter(CodingErrorAction.REPORT).decode(ByteBuffer.wrap(encoded)).toString();
} catch (CharacterCodingException exception) {
throw new KeyringException(Code.KEYRING_FORMAT_INVALID);
}
@@ -1541,8 +1485,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
@SuppressWarnings("PMD.PreserveStackTrace")
private static byte[] readStoreImage(Path source) throws IOException {
try (FileChannel channel = FileChannel.open(source,
StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS)) {
try (FileChannel channel = FileChannel.open(source, StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS)) {
long size = channel.size();
if (size <= 0 || size > MAX_FILE_BYTES) {
throw new KeyringException(Code.KEYRING_LIMIT_EXCEEDED);
@@ -1576,8 +1519,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
@SuppressWarnings("PMD.PreserveStackTrace")
private static Path securePath(Path requested, boolean createParent)
throws IOException {
private static Path securePath(Path requested, boolean createParent) throws IOException {
Objects.requireNonNull(requested, "path must not be null");
Path absolute = requested.toAbsolutePath().normalize();
Path parent = absolute.getParent();
@@ -1609,15 +1551,13 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
@SuppressWarnings("PMD.PreserveStackTrace")
private static void validateExistingFile(Path file, UserPrincipal expectedOwner)
throws IOException {
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS)
|| Files.isSymbolicLink(file)) {
private static void validateExistingFile(Path file, UserPrincipal expectedOwner) throws IOException {
if (!Files.isRegularFile(file, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(file)) {
throw new KeyringException(Code.KEYRING_FILESYSTEM_UNSUPPORTED);
}
validateOwnerOnly(file, false);
PosixFileAttributes attributes = Files.readAttributes(file,
PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
PosixFileAttributes attributes = Files.readAttributes(file, PosixFileAttributes.class,
LinkOption.NOFOLLOW_LINKS);
if (!attributes.owner().equals(expectedOwner)) {
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)
throws IOException {
PosixFileAttributes attributes = Files.readAttributes(path,
PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
private static void validateOwnerOnly(Path path, boolean directory) throws IOException {
PosixFileAttributes attributes = Files.readAttributes(path, PosixFileAttributes.class,
LinkOption.NOFOLLOW_LINKS);
Set<PosixFilePermission> expected = directory ? DIRECTORY_PERMISSIONS : FILE_PERMISSIONS;
if (!attributes.permissions().equals(expected)) {
throw new KeyringException(Code.KEYRING_FILESYSTEM_UNSUPPORTED);
@@ -1642,8 +1581,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
@SuppressWarnings("PMD.PreserveStackTrace")
/* default */ static void destroyTemporarySpec(
zeroecho.core.spec.AlgorithmKeySpec spec, Throwable primary)
/* default */ static void destroyTemporarySpec(zeroecho.core.spec.AlgorithmKeySpec spec, Throwable primary)
throws GeneralSecurityException {
if (!(spec instanceof Destroyable destroyable)) {
return;
@@ -1667,22 +1605,19 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
}
private record Header(byte[] storeId, int iterations, byte[] salt,
byte[] masterWrapNonce, byte[] wrappedMasterKey) {
private record Header(byte[] storeId, int iterations, byte[] salt, byte[] masterWrapNonce,
byte[] wrappedMasterKey) {
}
private record Manifest(int noncePrefix, long highWater,
List<ManifestEntry> entries) {
private record Manifest(int noncePrefix, long highWater, List<ManifestEntry> entries) {
}
private record ManifestEntry(byte[] entryId, int position, String alias,
String algorithm, Kind kind, Encoding encoding,
KeyringImportRegistry.HmacVariant hmacVariant,
byte[] nonce, int ciphertextLength, byte[] digest) {
private record ManifestEntry(byte[] entryId, int position, String alias, String algorithm, Kind kind,
Encoding encoding, KeyringImportRegistry.HmacVariant hmacVariant, byte[] nonce, int ciphertextLength,
byte[] digest) {
}
private record EncryptedWireEntry(byte[] entryId, byte[] nonce,
byte[] ciphertext) {
private record EncryptedWireEntry(byte[] entryId, byte[] nonce, byte[] ciphertext) {
}
/** 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[] digest;
private EncryptedEntry(byte[] entryId, int position, String alias,
String algorithm, Kind kind, Encoding encoding,
KeyringImportRegistry.HmacVariant hmacVariant,
byte[] nonce, byte[] ciphertext, byte[] digest) {
private EncryptedEntry(byte[] entryId, int position, String alias, String algorithm, Kind kind,
Encoding encoding, KeyringImportRegistry.HmacVariant hmacVariant, byte[] nonce, byte[] ciphertext,
byte[] digest) {
this.entryId = entryId.clone();
this.position = position;
this.alias = alias;
@@ -1715,16 +1649,14 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
}
private record EntryPlain(String alias, String algorithm, Kind kind,
Encoding encoding, KeyringImportRegistry.HmacVariant hmacVariant,
byte[] encoded) {
private record EntryPlain(String alias, String algorithm, Kind kind, Encoding encoding,
KeyringImportRegistry.HmacVariant hmacVariant, byte[] encoded) {
}
private record Imported(String algorithm, Key key) {
}
private record Decoded(Header header, byte[] masterKey, Manifest manifest,
Map<String, EncryptedEntry> entries) {
private record Decoded(Header header, byte[] masterKey, Manifest manifest, Map<String, EncryptedEntry> entries) {
}
/** 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 {
Path lockPath = keyring.resolveSibling(keyring.getFileName() + ".lock");
try {
UserPrincipal expectedOwner = Files.readAttributes(lockPath.getParent(),
PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS).owner();
UserPrincipal expectedOwner = Files
.readAttributes(lockPath.getParent(), PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS)
.owner();
if (!Files.exists(lockPath, LinkOption.NOFOLLOW_LINKS)) {
try {
Files.createFile(lockPath, FILE_ATTRIBUTE);
@@ -1774,8 +1707,7 @@ public final class KeyringStore implements AutoCloseable, Destroyable { // NOPMD
}
}
validateExistingFile(lockPath, expectedOwner);
FileChannel channel = FileChannel.open(lockPath,
StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS);
FileChannel channel = FileChannel.open(lockPath, StandardOpenOption.WRITE, LinkOption.NOFOLLOW_LINKS);
try {
FileLock lock;
try {

View File

@@ -53,15 +53,15 @@
* </p>
*
* <p>
* Unlock passwords are destroyable, transfer ownership to the receiver, and
* are destroyed immediately after the master key is unwrapped. The unlocked
* store retains the master key, its domain-separated nonce-reservation MAC
* key, and encrypted entry records; closing the store clears this material.
* The store requires a POSIX filesystem on which owner-only permissions can be
* verified. A directory-force failure after atomic replacement makes the open
* instance unusable until close and authenticated reopen resolves which
* complete image is current. Non-exportable keys must remain behind an
* external provider reference.
* Unlock passwords are destroyable, transfer ownership to the receiver, and are
* destroyed immediately after the master key is unwrapped. The unlocked store
* retains the master key, its domain-separated nonce-reservation MAC key, and
* encrypted entry records; closing the store clears this material. The store
* requires a POSIX filesystem on which owner-only permissions can be verified.
* A directory-force failure after atomic replacement makes the open instance
* unusable until close and authenticated reopen resolves which complete image
* is current. Non-exportable keys must remain behind an external provider
* reference.
* </p>
*
* <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) {
Objects.requireNonNull(session, "session");
final DigestSpec s = spec == null ? DigestSpec.sha256() : spec;
return new TagEngineBuilder<>(
() -> session.createContext("DIGEST", KeyUsage.DIGEST, NullKey.INSTANCE, s));
return new TagEngineBuilder<>(() -> 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
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder<Signature> ed25519Sign(final ZeroEchoSession session,
final PrivateKey privateKey) {
public static TagEngineBuilder<Signature> ed25519Sign(final ZeroEchoSession session, final PrivateKey privateKey) {
Objects.requireNonNull(privateKey, PRIVATE_KEY);
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
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder<Signature> ed25519Verify(final ZeroEchoSession session,
final PublicKey publicKey) {
public static TagEngineBuilder<Signature> ed25519Verify(final ZeroEchoSession session, final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
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,
final RsaSigSpec spec) {
Objects.requireNonNull(privateKey, PRIVATE_KEY);
return signature(session, "RSA", privateKey,
spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
return signature(session, "RSA", privateKey, 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,
final RsaSigSpec spec) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
return signature(session, "RSA", publicKey,
spec == null ? RsaSigSpec.pss(RsaSigSpec.Hash.SHA256, 32) : spec);
return signature(session, "RSA", publicKey, 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
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder<Signature> slhDsaSign(final ZeroEchoSession session,
final PrivateKey privateKey) {
public static TagEngineBuilder<Signature> slhDsaSign(final ZeroEchoSession session, final PrivateKey privateKey) {
Objects.requireNonNull(privateKey, PRIVATE_KEY);
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
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder<Signature> slhDsaVerify(final ZeroEchoSession session,
final PublicKey publicKey) {
public static TagEngineBuilder<Signature> slhDsaVerify(final ZeroEchoSession session, final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
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
* @throws NullPointerException if {@code privateKey} is {@code null}
*/
public static TagEngineBuilder<Signature> mldsaSign(final ZeroEchoSession session,
final PrivateKey privateKey) {
public static TagEngineBuilder<Signature> mldsaSign(final ZeroEchoSession session, final PrivateKey privateKey) {
Objects.requireNonNull(privateKey, PRIVATE_KEY);
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
* @throws NullPointerException if {@code publicKey} is {@code null}
*/
public static TagEngineBuilder<Signature> mldsaVerify(final ZeroEchoSession session,
final PublicKey publicKey) {
public static TagEngineBuilder<Signature> mldsaVerify(final ZeroEchoSession session, final PublicKey publicKey) {
Objects.requireNonNull(publicKey, PUBLIC_KEY);
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.
*
* <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
* 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
*/
@@ -74,8 +76,7 @@ public final class KeyBuilders {
* @return guaranteed generator
* @throws IllegalArgumentException if the capability is absent
*/
public <S extends AlgorithmKeySpec> SymmetricKeyGenerator<S> generator(String algorithmId,
Class<S> specType) {
public <S extends AlgorithmKeySpec> SymmetricKeyGenerator<S> generator(String algorithmId, Class<S> specType) {
CryptoAlgorithm algorithm = session.require(algorithmId);
SymmetricKeyGenerator<S> delegate = algorithm.symmetricKeyGenerator(specType);
return spec -> {
@@ -94,8 +95,7 @@ public final class KeyBuilders {
* @return guaranteed importer
* @throws IllegalArgumentException if the capability is absent
*/
public <S extends AlgorithmKeySpec> SymmetricKeyImporter<S> importer(String algorithmId,
Class<S> specType) {
public <S extends AlgorithmKeySpec> SymmetricKeyImporter<S> importer(String algorithmId, Class<S> specType) {
CryptoAlgorithm algorithm = session.require(algorithmId);
SymmetricKeyImporter<S> delegate = algorithm.symmetricKeyImporter(specType);
return spec -> {
@@ -114,7 +114,8 @@ public final class KeyBuilders {
* @return generated secret key
* @throws java.security.GeneralSecurityException if generation fails
* @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)
throws java.security.GeneralSecurityException {
@@ -133,7 +134,8 @@ public final class KeyBuilders {
* @return imported secret key
* @throws java.security.GeneralSecurityException if import fails
* @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)
throws java.security.GeneralSecurityException {
@@ -180,8 +182,7 @@ public final class KeyBuilders {
* @return guaranteed importer
* @throws IllegalArgumentException if the capability is absent
*/
public <S extends AlgorithmKeySpec> PublicKeyImporter<S> publicImporter(String algorithmId,
Class<S> specType) {
public <S extends AlgorithmKeySpec> PublicKeyImporter<S> publicImporter(String algorithmId, Class<S> specType) {
CryptoAlgorithm algorithm = session.require(algorithmId);
PublicKeyImporter<S> delegate = algorithm.publicKeyImporter(specType);
return spec -> {
@@ -220,7 +221,8 @@ public final class KeyBuilders {
* @return generated key pair
* @throws java.security.GeneralSecurityException if generation fails
* @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)
throws java.security.GeneralSecurityException {
@@ -239,7 +241,8 @@ public final class KeyBuilders {
* @return imported public key
* @throws java.security.GeneralSecurityException if import fails
* @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)
throws java.security.GeneralSecurityException {
@@ -258,7 +261,8 @@ public final class KeyBuilders {
* @return imported private key
* @throws java.security.GeneralSecurityException if import fails
* @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)
throws java.security.GeneralSecurityException {

View File

@@ -8,10 +8,11 @@
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
* configuration
* @param operationalMaximum largest iteration count accepted from trusted
* local configuration
* @param absoluteDecodedMaximum hard safety ceiling for untrusted decoded data
* @since 1.0
*/
@@ -41,8 +42,8 @@ public record Pbkdf2Limits(int operationalMaximum, int absoluteDecodedMaximum) {
*/
public void validateTrusted(int iterations) {
if (iterations < MINIMUM || iterations > operationalMaximum) {
throw new IllegalArgumentException("PBKDF2 iterations must be in range " + MINIMUM + ".."
+ operationalMaximum + ": " + iterations);
throw new IllegalArgumentException(
"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);
}
private ZeroEchoSession(CryptoPolicy<ContextSpec, Key> policy, AuditListener auditListener,
AuditMode auditMode, Pbkdf2Limits pbkdf2Limits) {
private ZeroEchoSession(CryptoPolicy<ContextSpec, Key> policy, AuditListener auditListener, AuditMode auditMode,
Pbkdf2Limits pbkdf2Limits) {
this.policy = Objects.requireNonNull(policy, "policy must not be null");
this.auditListener = Objects.requireNonNull(auditListener, "auditListener must not be null");
this.auditSink = AuditListeners.bestEffort(auditListener);
@@ -143,8 +143,7 @@ public final class ZeroEchoSession {
*/
public ZeroEchoSession withAuditListener(AuditListener newAuditListener) {
return new ZeroEchoSession(policy,
Objects.requireNonNull(newAuditListener, "newAuditListener must not be null"), auditMode,
pbkdf2Limits);
Objects.requireNonNull(newAuditListener, "newAuditListener must not be null"), auditMode, pbkdf2Limits);
}
/**
@@ -198,8 +197,8 @@ public final class ZeroEchoSession {
* Returns the audit listener owned by this session.
*
* <p>
* The returned listener is the configured strategy, not mutable session
* state. It is exposed to support manual audit mode.
* The returned listener is the configured strategy, not mutable session state.
* It is exposed to support manual audit mode.
* </p>
*
* @return the non-null audit listener
@@ -254,8 +253,8 @@ public final class ZeroEchoSession {
* @param id canonical algorithm identifier
* @param role intended key usage
* @param key key compatible with the selected algorithm and role
* @param spec optional context specification, or {@code null} for the
* algorithm default
* @param spec optional context specification, or {@code null} for the algorithm
* default
* @param <C> context type
* @param <K> key type
* @param <S> context specification type
@@ -275,8 +274,8 @@ public final class ZeroEchoSession {
return finishContext(algorithm, context, role, spec);
}
private <C extends CryptoContext, S extends ContextSpec> C finishContext(CryptoAlgorithm algorithm,
C context, KeyUsage role, S spec) {
private <C extends CryptoContext, S extends ContextSpec> C finishContext(CryptoAlgorithm algorithm, C context,
KeyUsage role, S spec) {
if (auditMode == AuditMode.OFF) {
notifyContextCreated(algorithm, role, spec);
return context;
@@ -328,7 +327,8 @@ public final class ZeroEchoSession {
* @throws NullPointerException if {@code key} is {@code null}
* @throws DestroyFailedException if destruction fails or the key does not
* 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 {
Objects.requireNonNull(key, "key must not be null");
@@ -352,12 +352,10 @@ public final class ZeroEchoSession {
return true;
}
private <S extends ContextSpec> void notifyContextCreated(CryptoAlgorithm algorithm,
KeyUsage role, S spec) {
Map<String, Object> metadata = spec == null ? Map.of()
: Map.of("specType", spec.getClass().getName());
auditSink.onContextCreatedMeta(UUID.randomUUID().toString(), algorithm.id(), algorithm.providerName(),
role, "n/a", metadata);
private <S extends ContextSpec> void notifyContextCreated(CryptoAlgorithm algorithm, KeyUsage role, S spec) {
Map<String, Object> metadata = spec == null ? Map.of() : Map.of("specType", spec.getClass().getName());
auditSink.onContextCreatedMeta(UUID.randomUUID().toString(), algorithm.id(), algorithm.providerName(), role,
"n/a", metadata);
}
/* default */ void notifyKeyPairGenerated(CryptoAlgorithm algorithm, AlgorithmKeySpec spec, KeyPair keyPair) {

View File

@@ -342,10 +342,8 @@ public final class HybridKexBuilder {
if (pqcAlgId == null) {
throw new IllegalStateException("pqc algorithm id must be set");
}
if (classicMode == ClassicMode.CLASSIC_AGREEMENT
&& (classicPrivate == null || classicPeerPublic == null)) {
throw new IllegalStateException(
"classic private key and peer public must be set for CLASSIC_AGREEMENT");
if (classicMode == ClassicMode.CLASSIC_AGREEMENT && (classicPrivate == null || classicPeerPublic == null)) {
throw new IllegalStateException("classic private key and peer public must be set for CLASSIC_AGREEMENT");
}
if (classicMode == ClassicMode.PAIR_MESSAGE && classicKeyPair == null) {
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
* {@code SignatureContext}.</li>
* <li>{@link #hybrid(ZeroEchoSession)}: constructs a hybrid
* {@code SignatureContext} via
* {@link HybridSignatureContexts}.</li>
* {@code SignatureContext} via {@link HybridSignatureContexts}.</li>
* </ul>
*
* <p>Context construction is in-memory. Checked I/O failures arise only when a
* built stream is attached or processed.</p>
* <p>
* Context construction is in-memory. Checked I/O failures arise only when a
* built stream is attached or processed.
* </p>
*
* @since 1.0
*/
@@ -253,8 +254,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
Objects.requireNonNull(algorithmId, "algorithmId");
Objects.requireNonNull(privateKey, "privateKey");
Supplier<TagEngine<Signature>> factory = () -> session.createContext(algorithmId, KeyUsage.SIGN,
privateKey, spec);
Supplier<TagEngine<Signature>> factory = () -> session.createContext(algorithmId, KeyUsage.SIGN, privateKey,
spec);
return core(factory);
}
@@ -293,8 +294,8 @@ public final class SignatureTrailerDataContentBuilder implements DataContentBuil
Objects.requireNonNull(algorithmId, "algorithmId");
Objects.requireNonNull(publicKey, "publicKey");
Supplier<TagEngine<Signature>> factory = () -> session.createContext(algorithmId,
KeyUsage.VERIFY, publicKey, spec);
Supplier<TagEngine<Signature>> factory = () -> session.createContext(algorithmId, KeyUsage.VERIFY,
publicKey, spec);
return core(factory);
}

View File

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

View File

@@ -113,6 +113,7 @@ import zeroecho.sdk.content.api.PlainContent;
*/
public final class DigestDataContentBuilder implements DataContentBuilder<PlainContent> {
private final ZeroEchoSession session;
/**
* OutputMode selects how the digest-computing pipeline presents its result to
* 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 zeroecho.core.context.EncryptionContext} for the "ElGamal" algorithm
* in {@link zeroecho.core.KeyUsage#ENCRYPT} role via
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String,
* zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* attaches the upstream stream, and returns a pull-based stream that encrypts
* on the fly.
* </p>
@@ -426,8 +425,7 @@ public final class ElgamalEncDataContentBuilder implements DataContentBuilder<Da
* {@link ElgamalEncSpec}. When {@link #getStream()} is called, it creates an
* {@link zeroecho.core.context.EncryptionContext} for the "ElGamal" algorithm
* in {@link zeroecho.core.KeyUsage#DECRYPT} role via
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String,
* zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* {@link zeroecho.sdk.ZeroEchoSession#createContext(String, zeroecho.core.KeyUsage, java.security.Key, zeroecho.core.spec.ContextSpec)},
* attaches the upstream stream, and returns a pull-based stream that decrypts
* on the fly.
* </p>

View File

@@ -91,6 +91,7 @@ import zeroecho.sdk.content.api.PlainContent;
public final class HmacDataContentBuilder implements DataContentBuilder<PlainContent> {
private static final String ALGORITHM_ID = "HMAC";
private final ZeroEchoSession session;
/**
* 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"
try {
if (genKeyBits != null) {
return session.keyBuilders().symmetric().generate(ALGORITHM_ID,
new HmacKeyGenSpec(mac, genKeyBits));
return session.keyBuilders().symmetric().generate(ALGORITHM_ID, new HmacKeyGenSpec(mac, genKeyBits));
}
if (importRaw != null || importHex != null || importBase64 != null) {
HmacKeyImportSpec ispec;

View File

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

View File

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

View File

@@ -70,7 +70,8 @@
* {@link zeroecho.sdk.builders.alg.ElgamalEncDataContentBuilder}.</li>
* <li>RSA signatures:
* {@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},
* {@link zeroecho.sdk.builders.alg.DigestDataContentBuilder}.</li>
* <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
* array is cloned and remains owned by the caller.
* Constructs a password from a caller-owned character array. The supplied array
* is cloned and remains owned by the caller.
*
* @param password password characters; must not be {@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) {
// wrong key/password for that entry, continue scanning
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "recipient authentication failed: {0}",
ex.getClass().getSimpleName());
LOG.log(Level.FINE, "recipient authentication failed: {0}", ex.getClass().getSimpleName());
}
} catch (GeneralSecurityException | IOException | IllegalArgumentException ex) {
// 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)
throws IOException {
private void closeAfterAttempt(InputStream input, boolean transferred, Throwable primary) throws IOException {
IOException cleanupFailure = null;
if (!transferred) {
try {
@@ -282,8 +280,7 @@ final class Decryptor implements PlainContent, MultiRecipientContent {
closeOpeners(openers, primary);
}
/* default */ static void closeOpeners(List<RecipientOpener> ownedOpeners, Throwable primary)
throws IOException {
/* default */ static void closeOpeners(List<RecipientOpener> ownedOpeners, Throwable primary) throws IOException {
IOException cleanupFailure = null;
for (RecipientOpener opener : ownedOpeners) { // NOPMD - each opener is closed in this loop
try {
@@ -309,14 +306,13 @@ final class Decryptor implements PlainContent, MultiRecipientContent {
}
}
private void rejectOversizedCek(byte[] candidate, int fieldIndex, String recipientId,
RecipientOpener opener) {
private void rejectOversizedCek(byte[] candidate, int fieldIndex, String recipientId, RecipientOpener opener) {
try {
if (LOG.isLoggable(Level.WARNING)) {
LOG.log(Level.WARNING,
"Suspicious material in field {0}: {1}/{2} returned length {3}, while {4} is the limit. Ignoring.",
new Object[] { fieldIndex, recipientId, opener.getClass().getName(),
candidate.length, keyBytes });
new Object[] { fieldIndex, recipientId, opener.getClass().getName(), candidate.length,
keyBytes });
}
} finally {
Arrays.fill(candidate, (byte) 0);

View File

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

View File

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

View File

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

View File

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

View File

@@ -58,6 +58,7 @@ public final class PasswordOpener implements RecipientOpener {
public PasswordOpener(Pbkdf2Limits limits) {
this.limits = java.util.Objects.requireNonNull(limits, "limits must not be null");
}
/**
* Attempts to open a password-based recipient entry using a password unlock
* 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
* recipient to minimize exposure in memory.</li>
* <li>Choose an iteration count appropriate to the target platform to balance
* password-guessing resistance against recipient creation and opening
* latency. Counts below {@value Pbkdf2Limits#MINIMUM} are rejected.</li>
* password-guessing resistance against recipient creation and opening latency.
* Counts below {@value Pbkdf2Limits#MINIMUM} are rejected.</li>
* <li>Decoy recipients increase confidentiality by hiding the number of real
* recipients but cannot successfully unwrap the CEK.</li>
* </ul>

View File

@@ -8,9 +8,11 @@ package zeroecho.sdk.guard;
* Defines the KEK sizes supported by recipient entries without an encoded size
* 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
* remains openable by the corresponding recipient opener.</p>
* remains openable by the corresponding recipient opener.
* </p>
*
* @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.
*
* <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
* password material afterwards.</p>
* password material afterwards.
* </p>
*/
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.
*
* <p>Construction and access use defensive copies. Destruction is idempotent
* and prevents subsequent access.</p>
* <p>
* Construction and access use defensive copies. Destruction is idempotent and
* prevents subsequent access.
* </p>
*/
final class Password implements UnlockMaterial, Destroyable {
private final char[] characters;

View File

@@ -108,8 +108,8 @@
* generation and recipient entries; the symmetric builder manages algorithm
* parameters and payload framing.</li>
* <li><strong>Reusable opener strategies:</strong> recipients encode entries;
* openers attempt every applicable entry and create fresh cryptographic contexts
* per attempt. Neither carries long-lived secret state.</li>
* openers attempt every applicable entry and create fresh cryptographic
* contexts per attempt. Neither carries long-lived secret state.</li>
* <li><strong>Defensive parsing:</strong> the builder applies limits to the
* number of recipients and the size of each entry blob; the symmetric stage
* 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
* ChaCha builder.
* Derives a ChaCha key and applies it with optional AAD to the provided ChaCha
* builder.
*
* <p>
* 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);
}
/* default */ HybridKexContext(HybridKexProfile profile, AgreementContext classic,
MessageAgreementContext pqc, SecretDeriver secretDeriver) {
/* default */ HybridKexContext(HybridKexProfile profile, AgreementContext classic, MessageAgreementContext pqc,
SecretDeriver secretDeriver) {
this.profile = Objects.requireNonNull(profile, "profile");
this.classic = Objects.requireNonNull(classic, "classic");
this.pqc = Objects.requireNonNull(pqc, "pqc");
@@ -365,8 +365,7 @@ public final class HybridKexContext implements MessageAgreementContext {
* @return derived output transferred to the caller
* @throws GeneralSecurityException if derivation fails
*/
byte[] derive(byte[] ikm, byte[] salt, byte[] info, int outputLength)
throws GeneralSecurityException;
byte[] derive(byte[] ikm, byte[] salt, byte[] info, int outputLength) throws GeneralSecurityException;
}
/**

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