Skip to content

Conversation

@overheadhunter
Copy link
Member

This drops all dependencies, internalizes the CMAC implementation and improves performance by avoiding byte[] allocations.

Compared to the baseline, allocations are reduced by 60%, which results in a speedup of 15-25 % depending on input size:

Benchmark  (cleartextSize)  Mode  Cnt      Score   Error  Units

baseline              1024  avgt    2      3,874          us/op
baseline           1048576  avgt    2   2940,706          us/op
baseline          10485760  avgt    2  29336,571          us/op

new                   1024  avgt    2      2,996          us/op
new                1048576  avgt    2   2450,376          us/op
new               10485760  avgt    2  25320,027          us/op

@overheadhunter overheadhunter marked this pull request as ready for review November 19, 2025 13:17
Copilot finished reviewing on behalf of overheadhunter November 19, 2025 13:19
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR modernizes the SIV-mode implementation by removing all external dependencies (BouncyCastle, Jetbrains Annotations), internalizing the CMAC implementation, and introducing a new API while maintaining backward compatibility. The changes achieve a 60% reduction in allocations resulting in 15-25% performance improvement.

Key Changes:

  • Introduced new SivEngine API for direct encryption/decryption operations
  • Implemented JCE Security Provider (SivProvider) with Cipher and Mac SPIs
  • Internalized CMAC implementation to eliminate BouncyCastle dependency
  • Deprecated legacy SivMode API while maintaining backward compatibility

Reviewed Changes

Copilot reviewed 40 out of 40 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/main/java/org/cryptomator/siv/SivEngine.java New core encryption engine with modern API
src/main/java/org/cryptomator/siv/SivCipher.java JCE Cipher SPI implementation for standard JCA usage
src/main/java/org/cryptomator/siv/SivProvider.java Security Provider registration for CMAC and AES/SIV/NoPadding
src/main/java/org/cryptomator/siv/CMac.java Internalized CMAC implementation following RFC 4493
src/main/java/org/cryptomator/siv/Utils.java Utility methods extracted for cryptographic operations
src/main/java/org/cryptomator/siv/SivMode.java Deprecated legacy API, now delegates to SivEngine
src/test/java/org/cryptomator/siv/SivEngineTest.java Comprehensive test coverage for new SivEngine API
pom.xml Removed BouncyCastle dependency and maven-shade-plugin
README.md Updated usage examples to reflect new API

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

Walkthrough

This PR refactors the AES-SIV implementation to remove external dependencies on BouncyCastle and Jetbrains Annotations. It introduces custom implementations for CMAC (RFC 4493) via a new CMac class and reimplements the SIV logic in a new SivEngine class. The legacy SivMode API is deprecated and redirected to SivEngine. New JCE integration is provided through SivCipher (CipherSpi) and SivProvider for standard Java security APIs. Internal utilities (ThreadLocals, placeholder shim classes) and older CTR computer implementations are removed. The Maven POM is updated to version 2.0.0-SNAPSHOT with build configuration changes including removal of maven-shade-plugin. Documentation and tests are updated to reflect the new APIs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • CMac implementation (src/main/java/org/cryptomator/siv/CMac.java): RFC 4493 CMAC requires careful verification of key derivation (K1/K2 generation), subkey doubling logic, padding rules, and constant-time comparisons. The dbl() operation and block processing flow need cryptographic review.
  • SivEngine implementation (src/main/java/org/cryptomator/siv/SivEngine.java): The s2v() RFC 5297 implementation and CTR-based encryption/decryption with authenticated decryption (constant-time tag comparison) are critical. Verify proper key splitting, IV handling, and error cases.
  • SivCipher JCE integration (src/main/java/org/cryptomator/siv/SivCipher.java): Ensure correct CipherSpi contract compliance, proper AAD handling across multiple updates, state management (opmode tracking, buffer resets), and exception mapping.
  • Deprecation and API migration: Verify that SivMode's delegations to SivEngine handle key material correctly and that the migration path doesn't introduce security gaps.
  • Test coverage verification: Confirm that RFC test vectors, edge cases, tampering detection, and JCE provider discovery are adequately tested.

Possibly related issues

  • Issue addressing removal of BouncyCastle dependency by implementing custom CMAC and SIV mode, which aligns with the code-level replacement of external BC CMAC dependency with the new internal CMac class.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title "Modernize implementation" is vague and does not clearly summarize the main changes in the pull request. Consider using a more specific title that captures the core change, such as "Remove dependencies and internalize CMAC implementation" or "Drop external dependencies and improve performance".
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly relates to the changeset by explaining the removal of dependencies, internalization of CMAC, and performance improvements with supporting benchmark data.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/zero-deps

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (15)
README.md (1)

11-17: Align README usage with the new primary APIs and actual signatures

The feature bullet (“No dependencies”) and version 2.0.0 look consistent with the refactor, but the usage section still only shows SivMode and a new SivMode(key) API. Given that SivMode is being deprecated in favor of SivEngine and JCE usage via SivProvider, consider:

  • Explicitly documenting SivEngine-based usage and the JCE provider flow as the preferred APIs.
  • Clearly marking the SivMode example as deprecated/legacy, or replacing it entirely if the public surface has changed.
  • Double-checking that the constructor and encrypt/decrypt overloads in the snippet match the actual SivMode (or SivEngine) signatures to avoid confusing downstream users.

Also applies to: 27-39, 49-50

src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java (1)

8-9: Complement the annotation with a @deprecated Javadoc hint

Marking UnauthenticCiphertextException as @Deprecated is consistent with the new API direction. Consider also adding a @deprecated Javadoc tag that briefly points users to the modern error path (e.g., via SivEngine and AEADBadTagException) to make the migration story explicit in generated Javadocs.

src/test/java/org/cryptomator/siv/CMacTest.java (1)

16-35: CMAC vector coverage looks solid; HexConverter could be a bit more defensive

The parameterized tests against RFC 4493 and NIST vectors are well-structured and should give strong coverage of the new CMac.tag implementation. As an optional improvement for future maintenance:

  • In HexConverter.convert, consider validating that the hex string has even length and contains only valid hex characters, and throw an ArgumentConversionException otherwise. That makes failures clearer if a vector line is ever edited incorrectly.

Not required for this PR, but it would make the test utility more robust to accidental input errors.

Also applies to: 49-85

src/main/java/org/cryptomator/siv/Utils.java (1)

10-13: Consider keeping Utils non-public and documenting in-place semantics

Since all methods in Utils are package-private and used internally by SIV/CMAC code, exposing the class itself as public slightly widens the library’s API surface without a strong need.

As an optional cleanup:

  • Make the class package-private (final class Utils) so it’s clearly an internal helper.
  • Optionally add brief comments on dbl and xor noting that they operate in-place on their first argument (and, for dbl, on the provided array), to prevent accidental misuse by future contributors.

No functional issues, but this helps keep the public API minimal and the in-place behavior explicit.

Also applies to: 34-57

src/main/java/org/cryptomator/siv/SivProvider.java (1)

15-29: Provider wiring for CMAC and AES/SIV looks good; consider minor hardening

The singleton provider and putService registrations are consistent with the new CMAC and SivCipher implementations. To make the API surface a bit tighter you could:

  • Declare SivProvider as final to avoid subclassing of a security-sensitive type.
  • Explicitly commit in Javadoc to the algorithm strings "CMAC" and "AES/SIV/NoPadding" as the long‑term JCE names, since changing them later would be a breaking change.

Please double‑check against your existing public documentation/usages that these transformation names are exactly what you want to support going forward.

src/test/java/org/cryptomator/siv/SivProviderTest.java (1)

18-50: Good coverage of provider integration; optionally isolate global Security state

The tests exercise CMAC and AES/SIV discovery both via explicit provider instance, provider name, and default lookup, plus a simple encrypt/decrypt flow—solid coverage. The only refinement I’d suggest is to centralize Security.addProvider(SivProvider.INSTANCE) in a @BeforeAll and restore the original provider list in an @AfterAll (or use insertProviderAt + removeProvider) to avoid leaking provider registration across unrelated tests.

Please ensure no other tests depend on a specific provider ordering before/after these run.

src/main/java/org/cryptomator/siv/CMac.java (1)

95-135: Optional: reuse the last-block buffer to reduce per-call allocations

engineDoFinal allocates a fresh byte[BLOCK_SIZE] for m_last on every MAC computation. Given CMAC may be invoked in tight loops and this PR targets allocation reductions, you could reuse a dedicated field for that buffer:

 public class CMac extends MacSpi {

@@
-    private final byte[] buffer = new byte[BLOCK_SIZE];
+    private final byte[] buffer = new byte[BLOCK_SIZE];
+    private final byte[] mLast = new byte[BLOCK_SIZE];
@@
     @Override
     protected byte[] engineDoFinal() {
@@
-        byte[] m_last = new byte[BLOCK_SIZE];
+        Arrays.fill(mLast, (byte) 0);
         if (flag) {
             // M_last := M_n XOR K1;
-            xor(buffer, k1, m_last);
+            xor(buffer, k1, mLast);
         } else {
@@
-            xor(buffer, k2, m_last);
+            xor(buffer, k2, mLast);
         }
@@
-        xor(x, m_last, y); // Y := M_last XOR X;
+        xor(x, mLast, y); // Y := M_last XOR X;
@@
     @Override
     protected void engineReset() {
@@
         Arrays.fill(buffer, (byte) 0);
         Arrays.fill(x, (byte) 0);
         Arrays.fill(y, (byte) 0);
+        Arrays.fill(mLast, (byte) 0);
     }

You still need to allocate the output tag array for each call (to avoid sharing internal buffers with the caller), but this change removes one transient 16‑byte allocation per MAC.

Please confirm this micro‑optimization is actually meaningful in your benchmarks before adopting it; the change is purely optional.

src/main/java/org/cryptomator/siv/SivCipher.java (1)

110-121: Optional: reduce copying in engineUpdate for large inputs

engineUpdate currently grows inputBuffer via Arrays.copyOf on every call, which is O(n²) if callers feed data in many small chunks. Since SIV inherently needs the full plaintext in memory, you might still want to avoid quadratic copying by accumulating into a growable buffer:

  • Use a ByteArrayOutputStream (with an initial capacity hint) instead of a raw byte[], converting to a byte array only in engineDoFinal, or
  • Maintain your own (byte[] buffer, int len) pair, expanding geometrically (e.g., 2×) when capacity is exceeded.

This is not correctness‑critical but could matter for workloads that stream input in smaller pieces.

If you have benchmarks with many small update calls, it’s worth checking whether this change delivers measurable gains before implementing it.

src/test/java/org/cryptomator/siv/UtilsTest.java (1)

56-63: Remove duplicated XOR assertion and exercise the 3‑argument overload

Lines 60 and 61 perform the exact same assertion, which is redundant and misses the chance to test xor(in1, in2, out).

You could refactor like this to keep the semantics but cover the 3‑arg variant:

@@
-		Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, xor(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82}));
-		Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, xor(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82}));
+		Assertions.assertArrayEquals(new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03}, xor(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82}));
+		byte[] out = new byte[3];
+		Assertions.assertArrayEquals(
+				new byte[]{(byte) 0x01, (byte) 0x02, (byte) 0x03},
+				xor(new byte[]{(byte) 0xFF, (byte) 0x55, (byte) 0x81}, new byte[]{(byte) 0xFE, (byte) 0x57, (byte) 0x82}, out));
src/test/java/org/cryptomator/siv/SivCipherTest.java (1)

108-134: Wrap/unwrap test gives good streaming and AAD coverage; consider adding a tampering case

The wrap/unwrap test nicely covers:

  • AAD via both ByteBuffer and (byte[], off, len) forms.
  • Streaming engineUpdate calls that produce no output until engineDoFinal.
  • Full round‑trip correctness across WRAP/UNWRAP modes.

As a follow‑up, you might add a second test that flips one byte in wrapped or changes AAD and asserts AEADBadTagException from engineDoFinal, to lock in the authentication failure behaviour as well.

src/main/java/org/cryptomator/siv/SivEngine.java (2)

46-57: Optionally zeroize intermediate key material after creating SecretKeySpecs

The constructor copies the user key into two temporary arrays (macKey, ctrKey) and then passes them into SecretKeySpec, which itself copies the bytes. At that point the local arrays are no longer needed but still hold key material until GC.

You could clear them to slightly reduce key lifetime in process memory:

		final byte[] macKey = new byte[subkeyLen];
		final byte[] ctrKey = new byte[subkeyLen];
		System.arraycopy(key, 0, macKey, 0, macKey.length);
		System.arraycopy(key, macKey.length, ctrKey, 0, ctrKey.length);
		this.macKey = new SecretKeySpec(macKey, "AES");
		this.ctrKey = new SecretKeySpec(ctrKey, "AES");
+
+		// Best‑effort cleanup of intermediate key material
+		Arrays.fill(macKey, (byte) 0);
+		Arrays.fill(ctrKey, (byte) 0);

Not mandatory, but a cheap hardening step given this is cryptographic code.


83-92: Consider mirroring the overflow guard in the convenience encrypt overload

The streaming encrypt overload defends against plaintext.length > Integer.MAX_VALUE - IV_LENGTH, but the encrypt(byte[] plaintext, byte[]... associatedData) variant allocates new byte[IV_LENGTH + plaintext.length] without that check. For extremely large inputs this could overflow the length computation before the allocation fails cleanly.

You could early‑reject the same way here:

	public byte[] encrypt(byte[] plaintext, byte[]... associatedData) {
+		if (plaintext.length > (Integer.MAX_VALUE - IV_LENGTH)) {
+			throw new IllegalArgumentException("Plaintext is too long");
+		}
		final byte[] ciphertext = new byte[IV_LENGTH + plaintext.length];
src/main/java/org/cryptomator/siv/SivMode.java (2)

69-90: Dual-byte[] encrypt: key concatenation is correct; consider explicit null validation

The core encrypt(byte[] ctrKey, byte[] macKey, ...):

  • Concatenates macKey then ctrKey into combinedKey, matching EncryptionTestCase.getKey() and RFC-style MAC||ENC ordering.
  • Delegates to new SivEngine(combinedKey).encrypt(...) and wipes combinedKey in finally.

One thing you might tighten: currently passing null for either key will throw NullPointerException when accessing .length, whereas the Javadoc promises IllegalArgumentException for invalid keys. If you care about strict API behavior (even though this is deprecated), add explicit null checks and throw IllegalArgumentException to match the contract.

-    public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... associatedData) {
-        byte[] combinedKey = new byte[ctrKey.length + macKey.length];
+    public byte[] encrypt(byte[] ctrKey, byte[] macKey, byte[] plaintext, byte[]... associatedData) {
+        if (ctrKey == null || macKey == null) {
+            throw new IllegalArgumentException("Keys must not be null.");
+        }
+        byte[] combinedKey = new byte[ctrKey.length + macKey.length];

146-171: Dual-byte[] decrypt: ordering and AEAD mapping look correct; same optional null-guard applies

The core decrypt(byte[] ctrKey, byte[] macKey, ...):

  • Builds combinedKey as macKey || ctrKey, aligned with encryption and EncryptionTestCase.getKey().
  • Delegates to SivEngine.decrypt, translating AEADBadTagException into UnauthenticCiphertextException.
  • Always wipes combinedKey in finally, even on failure.

As with the encrypt counterpart, you may want to explicitly guard against null keys and throw IllegalArgumentException instead of an implicit NullPointerException to better match the Javadoc on invalid keys.

-    public byte[] decrypt(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]... associatedData)
+    public byte[] decrypt(byte[] ctrKey, byte[] macKey, byte[] ciphertext, byte[]... associatedData)
             throws UnauthenticCiphertextException, IllegalBlockSizeException {
-        byte[] combinedKey = new byte[ctrKey.length + macKey.length];
+        if (ctrKey == null || macKey == null) {
+            throw new IllegalArgumentException("Keys must not be null.");
+        }
+        byte[] combinedKey = new byte[ctrKey.length + macKey.length];
src/test/java/org/cryptomator/siv/SivEngineTest.java (1)

315-329: Generated testcases loader fixes prior resource leak; consider handling missing resource explicitly

The GeneratedTestCases.loadTestCases() method now uses a try-with-resources block that closes InputStream, Reader, and BufferedReader, addressing the earlier static-analysis warning about leaking InputStreamReader.

One optional enhancement: if /testcases.txt is missing, getResourceAsStream will return null and new InputStreamReader(in, ...) will throw a NullPointerException. If you want clearer diagnostics, you could either:

  • Assert with a descriptive failure, or
  • Use Assumptions.assumeTrue(in != null, "...") to skip the generated-vector tests when the resource is unavailable.

This is non-blocking but would make failures easier to interpret.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 98bb349 and afd2cd2.

📒 Files selected for processing (40)
  • .github/workflows/build.yml (4 hunks)
  • .github/workflows/codeql-analysis.yml (1 hunks)
  • .github/workflows/publish-central.yml (0 hunks)
  • .github/workflows/publish-github.yml (0 hunks)
  • CHANGELOG.md (1 hunks)
  • README.md (3 hunks)
  • pom.xml (6 hunks)
  • src/main/java/org/cryptomator/siv/CMac.java (1 hunks)
  • src/main/java/org/cryptomator/siv/CustomCtrComputer.java (0 hunks)
  • src/main/java/org/cryptomator/siv/JceAesBlockCipher.java (0 hunks)
  • src/main/java/org/cryptomator/siv/JceAesCtrComputer.java (0 hunks)
  • src/main/java/org/cryptomator/siv/SivCipher.java (1 hunks)
  • src/main/java/org/cryptomator/siv/SivEngine.java (1 hunks)
  • src/main/java/org/cryptomator/siv/SivMode.java (7 hunks)
  • src/main/java/org/cryptomator/siv/SivProvider.java (1 hunks)
  • src/main/java/org/cryptomator/siv/ThreadLocals.java (0 hunks)
  • src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java (2 hunks)
  • src/main/java/org/cryptomator/siv/Utils.java (1 hunks)
  • src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/Placeholder.java (0 hunks)
  • src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/macs/Placeholder.java (0 hunks)
  • src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/modes/Placeholder.java (0 hunks)
  • src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/paddings/Placeholder.java (0 hunks)
  • src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/params/Placeholder.java (0 hunks)
  • src/main/java/org/cryptomator/siv/org/bouncycastle/util/Placeholder.java (0 hunks)
  • src/main/java/org/cryptomator/siv/package-info.java (1 hunks)
  • src/main/java9/module-info.java (1 hunks)
  • src/main/java9/org.cryptomator.siv/ThreadLocals.java (0 hunks)
  • src/main/resources/META-INF/services/java.security.Provider (1 hunks)
  • src/test/java/org/cryptomator/siv/BenchmarkTest.java (0 hunks)
  • src/test/java/org/cryptomator/siv/CMacTest.java (1 hunks)
  • src/test/java/org/cryptomator/siv/CustomCtrComputerTest.java (0 hunks)
  • src/test/java/org/cryptomator/siv/EncryptionTestCase.java (2 hunks)
  • src/test/java/org/cryptomator/siv/JceAesBlockCipherTest.java (0 hunks)
  • src/test/java/org/cryptomator/siv/JceAesCtrComputerTest.java (0 hunks)
  • src/test/java/org/cryptomator/siv/SivCipherTest.java (1 hunks)
  • src/test/java/org/cryptomator/siv/SivEngineTest.java (1 hunks)
  • src/test/java/org/cryptomator/siv/SivModeBenchmark.java (2 hunks)
  • src/test/java/org/cryptomator/siv/SivModeTest.java (1 hunks)
  • src/test/java/org/cryptomator/siv/SivProviderTest.java (1 hunks)
  • src/test/java/org/cryptomator/siv/UtilsTest.java (1 hunks)
💤 Files with no reviewable changes (17)
  • src/test/java/org/cryptomator/siv/BenchmarkTest.java
  • .github/workflows/publish-central.yml
  • src/main/java9/org.cryptomator.siv/ThreadLocals.java
  • src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/paddings/Placeholder.java
  • src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/Placeholder.java
  • src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/modes/Placeholder.java
  • src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/macs/Placeholder.java
  • src/test/java/org/cryptomator/siv/JceAesBlockCipherTest.java
  • src/main/java/org/cryptomator/siv/JceAesCtrComputer.java
  • src/main/java/org/cryptomator/siv/ThreadLocals.java
  • src/test/java/org/cryptomator/siv/CustomCtrComputerTest.java
  • src/main/java/org/cryptomator/siv/org/bouncycastle/crypto/params/Placeholder.java
  • src/main/java/org/cryptomator/siv/CustomCtrComputer.java
  • src/main/java/org/cryptomator/siv/org/bouncycastle/util/Placeholder.java
  • src/test/java/org/cryptomator/siv/JceAesCtrComputerTest.java
  • .github/workflows/publish-github.yml
  • src/main/java/org/cryptomator/siv/JceAesBlockCipher.java
🧰 Additional context used
🧬 Code graph analysis (10)
src/test/java/org/cryptomator/siv/UtilsTest.java (1)
src/main/java/org/cryptomator/siv/Utils.java (1)
  • Utils (10-59)
src/test/java/org/cryptomator/siv/SivProviderTest.java (1)
src/main/java/org/cryptomator/siv/SivProvider.java (1)
  • SivProvider (15-31)
src/test/java/org/cryptomator/siv/SivModeTest.java (2)
src/main/java/org/cryptomator/siv/SivMode.java (1)
  • Deprecated (13-173)
src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java (1)
  • Deprecated (8-21)
src/main/java/org/cryptomator/siv/CMac.java (1)
src/main/java/org/cryptomator/siv/Utils.java (1)
  • Utils (10-59)
src/test/java/org/cryptomator/siv/CMacTest.java (1)
src/main/java/org/cryptomator/siv/CMac.java (1)
  • CMac (23-191)
src/main/java/org/cryptomator/siv/SivEngine.java (3)
src/main/java/org/cryptomator/siv/Utils.java (1)
  • Utils (10-59)
src/main/java/org/cryptomator/siv/CMac.java (1)
  • CMac (23-191)
src/main/java/org/cryptomator/siv/SivProvider.java (1)
  • SivProvider (15-31)
src/main/java/org/cryptomator/siv/SivMode.java (2)
src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java (1)
  • Deprecated (8-21)
src/test/java/org/cryptomator/siv/SivModeTest.java (1)
  • Deprecated (10-36)
src/main/java9/module-info.java (1)
src/main/java/org/cryptomator/siv/SivProvider.java (1)
  • SivProvider (15-31)
src/main/java/org/cryptomator/siv/UnauthenticCiphertextException.java (2)
src/main/java/org/cryptomator/siv/SivMode.java (1)
  • Deprecated (13-173)
src/test/java/org/cryptomator/siv/SivModeTest.java (1)
  • Deprecated (10-36)
src/test/java/org/cryptomator/siv/SivEngineTest.java (1)
src/test/java/org/cryptomator/siv/EncryptionTestCase.java (1)
  • EncryptionTestCase (15-90)
🪛 ast-grep (0.40.0)
src/main/java/org/cryptomator/siv/CMac.java

[warning] 48-48: Triple DES (3DES or DESede) is considered deprecated. AES is the recommended cipher. Upgrade to use AES.
Context: Cipher.getInstance(AES_ECB_NO_PADDING)
Note: [CWE-326]: Inadequate Encryption Strength [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
- https://find-sec-bugs.github.io/bugs.htm#TDES_USAGE
- https://csrc.nist.gov/News/2017/Update-to-Current-Use-and-Deprecation-of-TDEA

(desede-is-deprecated-java)


[warning] 48-48: Use of AES with ECB mode detected. ECB doesn't provide message confidentiality and is not semantically secure so should not be used. Instead, use a strong, secure cipher: Cipher.getInstance("AES/CBC/PKCS7PADDING"). See https://owasp.org/www-community/Using_the_Java_Cryptographic_Extensions for more information.
Context: Cipher.getInstance(AES_ECB_NO_PADDING)
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures
- https://googleprojectzero.blogspot.com/2022/10/rc4-is-still-considered-harmful.html

(use-of-aes-ecb-java)

🪛 LanguageTool
CHANGELOG.md

[grammar] ~11-~11: Ensure spelling is correct
Context: .../compare/1.6.1...HEAD) ### Added - new lowlevel API: * `new SivEngine(key).encrypt(p...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~30-~30: Ensure spelling is correct
Context: ...ate build plugins ### Deprecated - old lowlevel API: * `new SivMode().encrypt(key, p...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

🪛 markdownlint-cli2 (0.18.1)
CHANGELOG.md

12-12: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


13-13: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


15-15: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


31-31: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


32-32: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


33-33: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)


34-34: Unordered list indentation
Expected: 2; Actual: 3

(MD007, ul-indent)

🔇 Additional comments (32)
.github/workflows/codeql-analysis.yml (1)

29-29: ✓ Distribution change aligns with best practices.

Temurin is a supported distribution for actions/setup-java@v5, and migrating from AdoptOpenJDK to temurin is highly recommended to receive software and security updates. Additionally, LTS versions of Eclipse Temurin are cached on GitHub Hosted Runners and the tool cache is updated weekly, which provides performance benefits for workflow execution. This change aligns well with the PR's objective to update builds to Java 21 with temurin distribution.

.github/workflows/build.yml (2)

135-135: Verify the prerelease flag setting.

Line 135 marks all releases as prerelease. For a major version bump (2.0.0), confirm whether releases should be marked as prerelease or if this should be conditional/false.

Consider whether the prerelease flag should be:

  • Always true (current state)
  • false for production releases
  • Conditional based on version pattern (e.g., ${{ contains(github.ref, '-') }} for alpha/beta/rc versions)

84-88: Version verification logic is sound.

The version verification steps correctly extract the project version from pom.xml and compare it against the git tag, ensuring consistency before deployment to Maven Central and GitHub Packages.

Also applies to: 111-115

pom.xml (4)

7-7: Version bump to 2.0.0 is appropriate.

The major version bump reflects the significant breaking change of dropping BouncyCastle and JetBrains Annotations dependencies, aligning with semantic versioning principles. This change eliminates external transitive dependencies, justifying the major bump.


319-319: Verify autoPublish behavior for autonomous deployments.

Line 319 sets autoPublish to true in the deploy-central profile. This enables automatic publishing to Maven Central without manual approval steps. Confirm that this aligns with your release process, especially for a major version bump (2.0.0).

If manual approval is preferred, consider setting autoPublish to false and implementing a separate approval workflow step.


173-175: Test configuration with JaCoCo and Mockito is well-structured.

The surefire configuration correctly wires the JaCoCo Java agent and Mockito support via the property placeholder established in the coverage profile. The Maven coordinate reference syntax for the Mockito JAR is correct.


124-136: Build plugin additions support modernized test infrastructure.

The new maven-dependency-plugin, JMH annotation processor path, and JaCoCo property integration support the modernized build approach with improved test coverage and benchmark code generation. The configuration properly chains dependencies through property placeholders.

Also applies to: 144-150, 268-270

src/main/java/org/cryptomator/siv/package-info.java (1)

4-10: Package Javadoc clearly reflects the dual API surface

The added description of direct SivEngine usage and the JCE SivProvider path is clear and accurate; no changes needed.

src/test/java/org/cryptomator/siv/EncryptionTestCase.java (2)

53-58: Combined key helper matches engine expectations

getKey() allocates a new array and concatenates macKey followed by ctrKey, which is consistent with the combined-key layout used in the SIV engine wrappers. This is correct and keeps internal state encapsulated.


86-89: Helpful toString() for test diagnostics

Overriding toString() to emit TestCase #<n> is a nice improvement for parameterized test output and failure messages.

src/main/resources/META-INF/services/java.security.Provider (1)

1-1: Service registration for SivProvider looks correct

Declaring org.cryptomator.siv.SivProvider in META-INF/services/java.security.Provider is the expected way to make the provider discoverable in classpath (non‑module) environments; no issues here.

src/test/java/org/cryptomator/siv/SivModeTest.java (1)

10-34: Minimal regression coverage for deprecated SivMode is appropriate

Marking the test class as @Deprecated and explicitly labeling it via @DisplayName while keeping just two straightforward round‑trip tests is a good balance: existing SivMode behavior is still guarded without investing more in a deprecated API. No changes needed from my side.

src/test/java/org/cryptomator/siv/UtilsTest.java (2)

12-27: shiftLeft test vectors look correct and nicely cover carry propagation

The four cases match the big-endian 1‑bit left shift semantics of Utils.shiftLeft; no changes needed.


29-54: dbl test coverage is solid for CMAC subkey generation

The selected 16‑byte inputs (including all‑zero) exercise both branches of the doubling logic and implicitly validate shiftLeft’s carry handling; this is good coverage for RFC‑4493 style doubling.

src/main/java9/module-info.java (1)

1-7: Module descriptor correctly exports API and registers the security provider

The module exports only the public API package and declares SivProvider as a java.security.Provider service, which aligns with JPMS + JCA provider best practices. No changes needed; just ensure you verify at runtime that the provider is discoverable when the JAR is placed on the module path.

src/test/java/org/cryptomator/siv/SivCipherTest.java (1)

31-106: Mode, padding, key‑size, and output‑size tests align with AES‑SIV expectations

The parameterized tests around engineSetMode, engineSetPadding, key sizes, and engineGetOutputSize (for both encrypt and decrypt) exercise the main configuration and sizing edge cases expected from the SPI; they look consistent with the SivEngine key requirements and SIV layout (tag + ciphertext).

src/test/java/org/cryptomator/siv/SivModeBenchmark.java (1)

32-55: SivEngine‑based benchmark wiring looks correct and exercises realistic sizes

The benchmark now parameterizes plaintext size via @Param, re‑initializes key, SivEngine, and data per trial, and runs a full encrypt/decrypt round‑trip with an assertion to guard against regressions. This is a good smoke/perf test for the new zero‑dependency engine.

src/main/java/org/cryptomator/siv/SivMode.java (5)

8-14: Deprecation messaging and class-level annotation are consistent

Javadoc and @Deprecated on SivMode clearly direct callers to SivEngine while keeping the façade intact, which is good for a 2.x migration path.


22-41: Single-key SecretKey encrypt wrapper: delegation & zeroization look correct

encrypt(SecretKey, ...) now:

  • Validates key.getEncoded() is non-null.
  • Delegates to new SivEngine(keyBytes).encrypt(...).
  • Zeroes keyBytes in a finally block.

This preserves behavior while improving key hygiene; no issues spotted.


43-67: Dual-SecretKey encrypt wrapper correctly reuses byte[] variant and wipes material

encrypt(SecretKey ctrKey, SecretKey macKey, ...):

  • Obtains both encoded keys, checks for null, and delegates to the byte[] overload.
  • Clears both temporary key arrays in finally.

Behavior and ordering are consistent; nothing blocking here.


92-116: Single-key SecretKey decrypt wrapper: exception mapping and zeroization preserve legacy API

decrypt(SecretKey, ...):

  • Validates key.getEncoded() is non-null.
  • Delegates to SivEngine.decrypt.
  • Translates AEADBadTagException into the deprecated UnauthenticCiphertextException with a clear message.
  • Zeroes keyBytes in finally.

This maintains the old checked-exception surface while using the new engine, which is the right trade-off for a deprecated façade.


118-144: Dual-SecretKey decrypt wrapper: consistent handling of key encoding and cleanup

The dual-SecretKey decrypt overload mirrors the encrypt path:

  • Enforces non-null encoded keys.
  • Delegates to the byte[] decrypt overload where AEAD-to-UnauthenticCiphertextException mapping occurs.
  • Clears both temporary key arrays in the finally block.

Looks consistent and safe.

src/test/java/org/cryptomator/siv/SivEngineTest.java (10)

31-45: Parameter-validation tests are well targeted to SivEngine’s contract

The ParameterValidation nested class exercises:

  • Invalid key lengths via a parameterized test.
  • Decrypt with non-block-multiple ciphertext producing IllegalBlockSizeException.

This is a good front-line defense against accidental misuse of the engine API.


57-89: AAD count and short-buffer tests meaningfully stress edge conditions

The tests for:

  • AAD element-count limits on both encrypt and decrypt.
  • Insufficient output buffer in encrypt(plaintext, output, offset) expecting ShortBufferException.

These are useful for catching regression in bounds checking; the chosen sizes (127 AAD entries, 95-byte buffer vs. 96-byte need) are clear and intentional.


92-101: Basic round-trip test across key sizes is straightforward and sufficient

testEncryptionAndDecryption(int keylen) validates a simple encrypt/decrypt round trip for 32/48/64-byte keys. This nicely complements the more detailed vector-based tests.


103-210: RFC 5297 Vector 1 tests thoroughly cover CTR, S2V, SIV encrypt/decrypt, and failure modes

The RfcTestVector1 group:

  • Verifies computeCtr, s2v, encrypt, and decrypt against the appendix A.1 vector.
  • Includes negative tests for wrong key, corrupted ciphertext, and truncated ciphertext (expecting AEADBadTagException vs. IllegalBlockSizeException appropriately).

This is excellent coverage for both correctness and error signaling of the core SIV operations.


212-313: RFC 5297 Vector 2 tests validate multi-AAD and nonce handling

The RfcTestVector2 nested tests check:

  • computeCtr with a non-trivial counter input.
  • Full SIV encrypt/decrypt with two AAD values plus a nonce.

Given how subtle multi-AAD + nonce combinations can be, this is a valuable set of regression tests.


331-347: Encrypt/decrypt parameterized tests over generated cases nicely complement RFC vectors

The parameterized testDecrypt and testEncrypt methods:

  • Use @FieldSource("testCases") with @TestInstance(PER_CLASS) to pull from the preloaded list.
  • Round-trip all generated cases and assert exact ciphertext/plaintext matches.

This is a strong regression net over a broader input space than the official RFC vectors alone.


349-367: MAC-key tampering test exercises authentication robustness per test case

In testDecryptFailsDueToTamperedMacKey you:

  • Derive an index into the MAC half of the combined key using testCaseNumber % halfKeyLen.
  • Flip a single bit and expect AEADBadTagException on decrypt.

This is a clear and efficient way to validate that MAC key corruption is reliably detected across the generated suite.


369-385: Ciphertext tampering test correctly uses per-case variation

testDecryptFailsDueToTamperedCiphertext:

  • Flips one bit in a position derived from testCaseNumber % ciphertext.length.
  • Asserts AEADBadTagException from decrypt(...).

Because each ciphertext copy is per-test-instance, there’s no cross-test contamination, and this provides broad coverage of different tamper positions.


387-419: AAD tampering loop is careful about empty elements and restores state between attempts

In testDecryptFailsDueToTamperedAAD:

  • You skip cases without AAD entirely.
  • For each non-empty AD element, you flip a bit, assert AEADBadTagException, and then flip it back.

Since testCase.getAssociatedData() returns deep copies, mutations are confined to this test, and the explicit restoration maintains clear test intent. Nicely done.


421-461: Prepended/appended AAD tests exercise structural AAD changes and engine limits

The prepended and appended AAD tests:

  • Skip cases that would exceed the engine’s AAD-element limit.
  • Construct new AAD arrays with extra empty-ish elements at the front or back.
  • Expect AEADBadTagException on decrypt.

These patterns are very helpful for ensuring that AAD ordering and length are strictly enforced by SivEngine.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/main/java/org/cryptomator/siv/Utils.java (1)

15-18: Guard pad against invalid desiredLength to avoid OOB writes

pad still assumes in.length < desiredLength and unconditionally writes result[in.length]. If a future caller ever passes in.length >= desiredLength (or a null input), this will blow up with ArrayIndexOutOfBoundsException instead of a clear argument error.

Given this is a low-level crypto primitive, it’s safer to enforce the precondition explicitly:

-	// First bit 1, following bits 0.
-	static byte[] pad(byte[] in, int desiredLength) {
-		final byte[] result = Arrays.copyOf(in, desiredLength);
-		result[in.length] = (byte) 0x80;
-		return result;
-	}
+	// First bit 1, following bits 0.
+	static byte[] pad(byte[] in, int desiredLength) {
+		if (in == null || in.length >= desiredLength) {
+			throw new IllegalArgumentException("pad input must be non-null and shorter than desiredLength");
+		}
+		final byte[] result = Arrays.copyOf(in, desiredLength);
+		result[in.length] = (byte) 0x80;
+		return result;
+	}

This keeps the current behavior for valid callers while turning latent OOBs into explicit, descriptive failures.

🧹 Nitpick comments (5)
src/test/java/org/cryptomator/siv/CMacTest.java (1)

20-111: CMAC vector coverage and reset behavior look solid

The RFC 4493 + NIST vector coverage and the engineReset test give strong confidence in the CMac implementation. The HexConverter and hex helpers are fine as-is; if you ever reuse them more broadly, you might optionally add an assertion that hex strings have even length to fail fast on malformed test data, but that’s not required here.

src/test/java/org/cryptomator/siv/SivCipherTest.java (1)

70-107: Comprehensive SivCipher SPI tests; consider one provider-level Cipher test

The SPI tests cover the important behaviors (mode/padding checks, key sizes, output size, AAD handling, wrap/unwrap, reuse, determinism) very well. To catch any future regressions around engineGetOutputSize and buffering semantics, you might optionally add a SivProvider-based test that uses Cipher.getInstance("AES/SIV/NoPadding"), performs one or more update() calls, then calls getOutputSize() followed by doFinal(), asserting that no ShortBufferException occurs and the plaintext round-trips correctly. That would complement these SPI-level tests nicely.

Also applies to: 108-149

src/main/java/org/cryptomator/siv/SivEngine.java (2)

75-118: encrypt(..., output, outputOffset, ...) now correctly honors the offset; consider explicit offset validation

The buffer-based encrypt overload now (a) guards against integer overflow, (b) checks remaining capacity relative to outputOffset, and (c) passes outputOffset through both the IV copy and computeCtr call. This fixes the earlier bug where non-zero offsets were ignored.

If you want clearer error semantics for obviously invalid offsets (e.g., negative or greater than output.length), you could add an explicit argument check and reserve ShortBufferException purely for “too small buffer” cases:

	public int encrypt(byte[] plaintext, byte[] output, int outputOffset, byte[]... associatedData) throws ShortBufferException {
+		if (outputOffset < 0 || outputOffset > output.length) {
+			throw new IllegalArgumentException("outputOffset out of range");
+		}
+
 		// Check if plaintext length will cause overflows
 		if (plaintext.length > (Integer.MAX_VALUE - IV_LENGTH)) {
 			throw new IllegalArgumentException("Plaintext is too long");
 		}
@@
-		System.arraycopy(iv, 0, output, outputOffset, IV_LENGTH);
-		return IV_LENGTH + computeCtr(plaintext, 0, plaintext.length, iv, 0, IV_LENGTH, output, outputOffset + IV_LENGTH);
+		System.arraycopy(iv, 0, output, outputOffset, IV_LENGTH);
+		return IV_LENGTH + computeCtr(plaintext, 0, plaintext.length, iv, 0, IV_LENGTH, output, outputOffset + IV_LENGTH);
	}

This is a behavioral polish, not a correctness fix.


171-188: Optional: avoid per-call IV allocation in computeCtr

computeCtr currently does Arrays.copyOfRange on the IV for every call, which allocates a fresh 16-byte array per encrypt/decrypt. If you want to further trim allocations, you can reuse a single scratch IV buffer per engine instance (the class is already documented as non–thread-safe):

 public final class SivEngine {
@@
-	private final Mac cmac;
-	private final Cipher ctrCipher;
+	private final Mac cmac;
+	private final Cipher ctrCipher;
+	// Scratch buffer for adjusted IV in CTR mode (not thread-safe by design)
+	private final byte[] adjustedIv = new byte[IV_LENGTH];
@@
-	private int computeCtr(byte[] input, int inOff, int inLen, final byte[] iv, int ivOff, int ivLen, byte[] output, int outputOffset) throws ShortBufferException {
-		// clear out the 31st and 63rd (rightmost) bit:
-		assert ivLen == IV_LENGTH;
-		final byte[] adjustedIv = Arrays.copyOfRange(iv, ivOff, ivOff + ivLen);
+	private int computeCtr(byte[] input, int inOff, int inLen, final byte[] iv, int ivOff, int ivLen, byte[] output, int outputOffset) throws ShortBufferException {
+		// clear out the 31st and 63rd (rightmost) bit:
+		assert ivLen == IV_LENGTH;
+		System.arraycopy(iv, ivOff, adjustedIv, 0, ivLen);
 		adjustedIv[8] = (byte) (adjustedIv[8] & 0x7F);
 		adjustedIv[12] = (byte) (adjustedIv[12] & 0x7F);
@@
-			ctrCipher.init(Cipher.ENCRYPT_MODE, ctrKey, new IvParameterSpec(adjustedIv));
+			ctrCipher.init(Cipher.ENCRYPT_MODE, ctrKey, new IvParameterSpec(adjustedIv));

Functionality remains the same, but you avoid a small but hot-path allocation.

src/test/java/org/cryptomator/siv/SivEngineTest.java (1)

323-471: Generated test cases and tamper scenarios are strong; consider a clearer failure for missing testcases.txt

The GeneratedTestCases nested class:

  • Uses @TestInstance(PER_CLASS) + @BeforeAll and @FieldSource("testCases") to load and reuse a list of EncryptionTestCases.
  • Employs try‑with‑resources to correctly close InputStream, InputStreamReader, and BufferedReader, addressing the prior “potential input resource leak” warning.
  • Exercises positive decrypt/encrypt as well as a wide range of tampering scenarios (MAC key, ciphertext, AAD modified, prepended, and appended), which is excellent for robustness.

One optional robustness tweak: if /testcases.txt is ever missing from the classpath, getResourceAsStream will return null, leading to a NullPointerException when constructing the InputStreamReader. You could turn that into a more explicit failure or assumption:

		@BeforeAll
		public void loadTestCases() throws IOException {
-			try (InputStream in = EncryptionTestCase.class.getResourceAsStream("/testcases.txt");
+			try (InputStream in = EncryptionTestCase.class.getResourceAsStream("/testcases.txt");
+				 // Fail fast with a clearer message if the resource is missing
+			     ) {
+				Assertions.assertNotNull(in, "Resource /testcases.txt not found on classpath");
-				 Reader reader = new InputStreamReader(in, StandardCharsets.US_ASCII);
+				Reader reader = new InputStreamReader(in, StandardCharsets.US_ASCII);
 				BufferedReader bufferedReader = new BufferedReader(reader)) {
 				this.testCases = bufferedReader.lines().map(EncryptionTestCase::fromLine).collect(Collectors.toList());
 			}
		}

or, if you prefer skipping tests when the resource is unavailable, use Assumptions.assumeTrue(in != null, "...") instead of assertNotNull.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between afd2cd2 and 4546454.

📒 Files selected for processing (9)
  • CHANGELOG.md (1 hunks)
  • README.md (3 hunks)
  • src/main/java/org/cryptomator/siv/CMac.java (1 hunks)
  • src/main/java/org/cryptomator/siv/SivCipher.java (1 hunks)
  • src/main/java/org/cryptomator/siv/SivEngine.java (1 hunks)
  • src/main/java/org/cryptomator/siv/Utils.java (1 hunks)
  • src/test/java/org/cryptomator/siv/CMacTest.java (1 hunks)
  • src/test/java/org/cryptomator/siv/SivCipherTest.java (1 hunks)
  • src/test/java/org/cryptomator/siv/SivEngineTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md
🧰 Additional context used
🧬 Code graph analysis (4)
src/main/java/org/cryptomator/siv/CMac.java (1)
src/main/java/org/cryptomator/siv/Utils.java (1)
  • Utils (10-59)
src/main/java/org/cryptomator/siv/SivEngine.java (3)
src/main/java/org/cryptomator/siv/Utils.java (1)
  • Utils (10-59)
src/main/java/org/cryptomator/siv/CMac.java (1)
  • CMac (23-194)
src/main/java/org/cryptomator/siv/SivProvider.java (1)
  • SivProvider (15-31)
src/test/java/org/cryptomator/siv/SivEngineTest.java (1)
src/test/java/org/cryptomator/siv/EncryptionTestCase.java (1)
  • EncryptionTestCase (15-90)
src/test/java/org/cryptomator/siv/CMacTest.java (1)
src/main/java/org/cryptomator/siv/CMac.java (1)
  • CMac (23-194)
🪛 ast-grep (0.40.0)
src/main/java/org/cryptomator/siv/CMac.java

[warning] 48-48: Triple DES (3DES or DESede) is considered deprecated. AES is the recommended cipher. Upgrade to use AES.
Context: Cipher.getInstance(AES_ECB_NO_PADDING)
Note: [CWE-326]: Inadequate Encryption Strength [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
- https://find-sec-bugs.github.io/bugs.htm#TDES_USAGE
- https://csrc.nist.gov/News/2017/Update-to-Current-Use-and-Deprecation-of-TDEA

(desede-is-deprecated-java)


[warning] 48-48: Use of AES with ECB mode detected. ECB doesn't provide message confidentiality and is not semantically secure so should not be used. Instead, use a strong, secure cipher: Cipher.getInstance("AES/CBC/PKCS7PADDING"). See https://owasp.org/www-community/Using_the_Java_Cryptographic_Extensions for more information.
Context: Cipher.getInstance(AES_ECB_NO_PADDING)
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures
- https://googleprojectzero.blogspot.com/2022/10/rc4-is-still-considered-harmful.html

(use-of-aes-ecb-java)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build and Test
🔇 Additional comments (9)
CHANGELOG.md (1)

8-35: All previous review issues have been resolved and changes are well-structured.

The unreleased section correctly documents the major API changes:

  • Spelling corrections ("low-level" hyphenation) applied consistently
  • Markdown list indentation fixed to 2 spaces
  • Code block language specifier added for syntax highlighting
  • Content accurately reflects the PR objectives: new SivEngine/SivCipher APIs, BouncyCastle removal, and SivMode deprecation

The organization follows Keep a Changelog conventions with clear Added, Changed, and Deprecated subsections, and version headers include correct comparison ranges.

src/main/java/org/cryptomator/siv/CMac.java (1)

25-69: CMAC core logic matches RFC 4493 semantics

Subkey derivation (LK1/K2 via dbl), block processing, padding of the final block, and the reset behavior all look correct and align with RFC 4493. engineReset() at the end of engineInit ensures stale message state doesn’t leak across reinitializations. No changes needed from my side here.

Also applies to: 71-138

src/main/java/org/cryptomator/siv/SivEngine.java (3)

46-73: Key splitting and primitive initialization look correct

The constructor’s key-length validation, split into MAC/CTR subkeys, and initialization of CMAC and CTR cipher are consistent with AES-SIV requirements and fail fast with clear exceptions if the environment is misconfigured. No changes needed here.


120-157: Decrypt path and tag verification are robust and side‑channel aware

The decrypt method correctly:

  • Rejects inputs shorter than one block.
  • Decrypts using CTR with the SIV as IV.
  • Recomputes S2V over the resulting plaintext and associated data.
  • Compares the stored tag and recomputed tag in a fixed-time loop and zeroizes the plaintext buffer before throwing on mismatch.

This aligns well with AEAD expectations and SIV’s misuse-resistant design; no changes needed.


190-219: S2V implementation matches RFC 5297 and reuses CMAC efficiently

s2v correctly enforces the AD count limit, computes D0 = CMAC(K, 0^128), iterates D_i = dbl(D_{i-1}) XOR CMAC(K, S_i) for associated data, and then applies the “long” (≥16 bytes) vs “short” (<16 bytes) plaintext cases using xor, dbl, and pad as per RFC 5297.

Reusing a single Mac instance and relying on doFinal to reset internal state is a nice allocation win and is well-covered by the RFC vector tests. No changes needed.

src/test/java/org/cryptomator/siv/SivEngineTest.java (4)

36-90: Parameter validation tests cover key and input preconditions well

The ParameterValidation nested class nicely exercises invalid key lengths, too-short ciphertext, excessive associated-data count, and insufficient output buffer scenarios. This closely matches the engine’s guards and should prevent regressions in argument checking.


92-101: Basic encrypt/decrypt round‑trip across all supported key sizes

The testEncryptionAndDecryption parameterized test verifies a simple round-trip for 32/48/64-byte keys, ensuring all supported key sizes behave consistently. This is a good sanity check on top of the vector tests.


103-218: RFC 5297 vector 1 tests thoroughly exercise core SIV behavior

The RfcTestVector1 suite:

  • Verifies CTR output (testComputeCtr), S2V tag (testS2v), and full SIV encrypt/decrypt.
  • Includes testSivEncryptWithOffset, which specifically validates the buffer-offset encrypt API.
  • Covers failure modes for invalid key, invalid ciphertext bytes, and truncated ciphertext.

This gives excellent coverage of the first RFC appendix and directly guards against regressions in the SIV engine logic and offset handling.


220-321: RFC 5297 vector 2 tests validate multi‑AAD and nonce handling

RfcTestVector2 checks CTR, encrypt, and decrypt against the second official vector set, including multiple associated data items and a nonce. This confirms the multi-AAD and longer-message behavior of the implementation. No changes needed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
src/main/java/org/cryptomator/siv/CMac.java (1)

46-67: engineInit now correctly clears per-message state and derives CMAC subkeys per RFC 4493

The added engineReset() at the start of engineInit (Line 48) ensures any buffered message state (bufferPos, msgLen, x, y, buffer) is cleared when a CMac instance is re‑initialized with a new key, fixing the stale‑state issue from earlier revisions. Subkey derivation (Lk1 via dbl, then k2 via dbl(k1.clone())) matches RFC 4493 section 2.3, and the temporary L is zeroed in the finally block.

Using AES/ECB/NoPadding here is appropriate: it is only used as a raw AES block primitive for CMAC, not as an encryption mode for multi‑block data, so the static‑analysis warning about ECB can be safely ignored.

src/main/java/org/cryptomator/siv/SivCipher.java (1)

54-63: engineGetOutputSize violates JCE contract by risking NullPointerException instead of IllegalStateException

The JCE Cipher.getOutputSize(int) contract requires throwing IllegalStateException when the Cipher object has not been initialized.

The current code has a contract violation: inputBuffer is declared without initialization (line 33), so it remains null until engineInit sets it to EMPTY (line 84). If engineGetOutputSize is called before engineInit, accessing inputBuffer.length throws NullPointerException rather than the required IllegalStateException.

The suggested fix is correct—check initialization state first and throw IllegalStateException explicitly:

 @Override
 protected int engineGetOutputSize(int inputLen) {
+    if (key == null) {
+        throw new IllegalStateException("Cipher not initialized");
+    }
+    int buffered = (inputBuffer != null) ? inputBuffer.length : 0;
+    int totalLen = buffered + inputLen;
     if (opmode == Cipher.ENCRYPT_MODE || opmode == Cipher.WRAP_MODE) {
-        return 16 + inputBuffer.length + inputLen;
+        return 16 + totalLen;
     } else if (opmode == Cipher.DECRYPT_MODE || opmode == Cipher.UNWRAP_MODE) {
-        return inputBuffer.length + inputLen - 16;
+        return totalLen - 16;
     } else {
         throw new IllegalStateException("Invalid opmode " + this.opmode);
     }
 }
🧹 Nitpick comments (5)
src/main/java/org/cryptomator/siv/CMac.java (2)

69-101: Streaming update + finalization logic look correct; consider simplifying the complete‑block flag

The interplay of engineUpdate (both overloads), processBlock(), and engineDoFinal() yields the expected CMAC behavior for all chunkings I can think of (single call, many small engineUpdate calls, and arbitrary mixes):

  • Full blocks before the last one are processed via processBlock() as new data arrives.
  • At engineDoFinal() the buffer always holds exactly the last complete block (bufferPos == 16) or a partial block (0 < bufferPos < 16), never an empty block for a non‑empty message.
  • The flag condition msgLen > 0 && bufferPos % BLOCK_SIZE == 0 therefore behaves as intended: complete last block → use K1, otherwise pad and use K2.

For readability, you could consider changing the flag to boolean flag = msgLen > 0 && bufferPos == BLOCK_SIZE; and optionally add a brief comment that bufferPos cannot be 0 when msgLen > 0, so future readers don’t have to reason about the modulo case.

Also applies to: 103-136


159-191: Static create/tag helpers are convenient; consider explicit null/length contracts

The create(byte[] key) and tag(byte[] key, byte[] message) helpers are a nice zero‑dependency way to use CMAC directly from within the library. Right now, null keys or messages will trigger NullPointerExceptions via key.length or engineUpdate, which is acceptable but a bit implicit.

If you want a slightly stricter API, you could:

  • Add explicit Objects.requireNonNull(key, "key") / Objects.requireNonNull(message, "message"), and/or
  • Mention in the Javadoc that NullPointerException is thrown for null arguments, alongside the existing IllegalArgumentException for invalid key lengths.
src/main/java/org/cryptomator/siv/SivCipher.java (3)

75-85: Key length validation is correct; consider handling key.getEncoded() == null and zeroing old keys

The key length check against 32/48/64 bytes (256/384/512 bits) is appropriate for AES‑SIV. Two optional hardening tweaks:

  • Some Key implementations may return null from getEncoded(). In that case, the current code will throw a NullPointerException when accessing keybytes.length. You could turn that into a clearer InvalidKeyException:
byte[] keybytes = key.getEncoded();
-if (keybytes.length != 64 && keybytes.length != 48 && keybytes.length != 32) {
+if (keybytes == null || (keybytes.length != 64 && keybytes.length != 48 && keybytes.length != 32)) {
    throw new InvalidKeyException("Key length must be 256, 384, or 512 bits.");
}
  • When re‑initializing the cipher with a new key, you might optionally zero out the previous this.key array before overwriting it to reduce key material lifetime in memory (guarding against this.key == null on the first init).

These are not functional issues, just defense‑in‑depth improvements.


110-122: Input buffering via Arrays.copyOf is correct but can be quadratic for many small updates

The engineUpdate implementation that repeatedly does:

int oldLen = inputBuffer.length;
inputBuffer = Arrays.copyOf(inputBuffer, oldLen + inputLen);
System.arraycopy(input, inputOffset, inputBuffer, oldLen, inputLen);

is functionally fine for SIV (you need the full message at doFinal time), and will perform well if callers typically pass reasonably sized chunks.

If engineUpdate is used with very many tiny fragments (e.g., 1–few bytes at a time), this pattern becomes O(n²) due to repeated copying of the accumulated buffer. If that usage pattern is plausible, consider an internal ByteArrayOutputStream or chunked List<byte[]> accumulator that only flattens once in engineDoFinal.

Given the SIV use case, this is probably acceptable as‑is, so this is just a performance note rather than a required change.


140-175: ---

Move state cleanup into finally block to ensure robustness under exceptions

The JCE Cipher API specifies that if an exception is thrown by doFinal, the Cipher "may need to be reset" before reuse, and provider implementations should ensure per-message state is cleared/reset on completion and exception paths are handled to avoid inconsistent state.

The current code clears inputBuffer and aad only on the happy path. If siv.encrypt() or siv.decrypt() throws, those lines are skipped, leaving stale state. Wrapping the encrypt/decrypt logic in a try-finally block (as suggested in the diff) ensures cleanup always runs, making the implementation more defensive and aligned with JCE best practices:

-        int resultLength;
-        SivEngine siv = new SivEngine(this.key);
-        byte[][] aad = this.aad.toArray(new byte[this.aad.size()][]);
-        if (this.opmode == Cipher.ENCRYPT_MODE || this.opmode == Cipher.WRAP_MODE) {
-            resultLength = siv.encrypt(inputBuffer, output, outputOffset, aad);
-        } else if (this.opmode == Cipher.DECRYPT_MODE || this.opmode == Cipher.UNWRAP_MODE) {
-            // for security reasons we can't write into output directly before checking integrity:
-            byte[] plaintext = new byte[0];
-            try {
-                plaintext = siv.decrypt(inputBuffer, aad);
-                System.arraycopy(plaintext, 0, output, outputOffset, plaintext.length);
-                resultLength = plaintext.length;
-            } finally {
-                Arrays.fill(plaintext, (byte) 0x00);
-            }
-        } else {
-            throw new IllegalStateException("Invalid opmode " + this.opmode);
-        }
-
-        // reset internal state:
-        this.inputBuffer = EMPTY;
-        this.aad.clear();
-        return resultLength;
+        int resultLength;
+        try {
+            SivEngine siv = new SivEngine(this.key);
+            byte[][] aadArr = this.aad.toArray(new byte[this.aad.size()][]);
+            if (this.opmode == Cipher.ENCRYPT_MODE || this.opmode == Cipher.WRAP_MODE) {
+                resultLength = siv.encrypt(inputBuffer, output, outputOffset, aadArr);
+            } else if (this.opmode == Cipher.DECRYPT_MODE || this.opmode == Cipher.UNWRAP_MODE) {
+                // for security reasons we can't write into output directly before checking integrity:
+                byte[] plaintext = new byte[0];
+                try {
+                    plaintext = siv.decrypt(inputBuffer, aadArr);
+                    System.arraycopy(plaintext, 0, output, outputOffset, plaintext.length);
+                    resultLength = plaintext.length;
+                } finally {
+                    Arrays.fill(plaintext, (byte) 0x00);
+                }
+            } else {
+                throw new IllegalStateException("Invalid opmode " + this.opmode);
+            }
+        } finally {
+            // reset internal state regardless of success
+            this.inputBuffer = EMPTY;
+            this.aad.clear();
+        }
+        return resultLength;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4546454 and a456e7b.

📒 Files selected for processing (3)
  • src/main/java/org/cryptomator/siv/CMac.java (1 hunks)
  • src/main/java/org/cryptomator/siv/SivCipher.java (1 hunks)
  • src/main/java/org/cryptomator/siv/Utils.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/cryptomator/siv/Utils.java
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/org/cryptomator/siv/CMac.java (1)
src/main/java/org/cryptomator/siv/Utils.java (1)
  • Utils (10-62)
🪛 ast-grep (0.40.0)
src/main/java/org/cryptomator/siv/CMac.java

[warning] 49-49: Triple DES (3DES or DESede) is considered deprecated. AES is the recommended cipher. Upgrade to use AES.
Context: Cipher.getInstance(AES_ECB_NO_PADDING)
Note: [CWE-326]: Inadequate Encryption Strength [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
- https://find-sec-bugs.github.io/bugs.htm#TDES_USAGE
- https://csrc.nist.gov/News/2017/Update-to-Current-Use-and-Deprecation-of-TDEA

(desede-is-deprecated-java)


[warning] 49-49: Use of AES with ECB mode detected. ECB doesn't provide message confidentiality and is not semantically secure so should not be used. Instead, use a strong, secure cipher: Cipher.getInstance("AES/CBC/PKCS7PADDING"). See https://owasp.org/www-community/Using_the_Java_Cryptographic_Extensions for more information.
Context: Cipher.getInstance(AES_ECB_NO_PADDING)
Note: [CWE-327]: Use of a Broken or Risky Cryptographic Algorithm [OWASP A03:2017]: Sensitive Data Exposure [OWASP A02:2021]: Cryptographic Failures [REFERENCES]
- https://owasp.org/Top10/A02_2021-Cryptographic_Failures
- https://googleprojectzero.blogspot.com/2022/10/rc4-is-still-considered-harmful.html

(use-of-aes-ecb-java)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Build and Test

@overheadhunter overheadhunter requested a review from a team November 19, 2025 20:03
# Conflicts:
#	.github/workflows/build.yml

[deploy]
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
pom.xml (2)

124-136: Add explicit version to maven-dependency-plugin for reproducibility.

The maven-dependency-plugin lacks an explicit version declaration. For reproducible builds, all plugin versions should be pinned.

  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
+   <version>3.11.1</version>
    <executions>
      <execution>
        <id>jar-paths-to-properties</id>
        <phase>validate</phase>
        <goals>
          <goal>properties</goal>
        </goals>
      </execution>
    </executions>
  </plugin>

332-332: Flag autoPublish=true for operational impact review.

The central-publishing-maven-plugin's autoPublish is now enabled. This automatically publishes artifacts to Maven Central without manual intervention. Ensure this change is intentional and documented in release notes, as it affects the release workflow.

Consider whether automated publishing without a manual gate is appropriate for this major version bump (2.0.0). If you want to retain a manual approval step, set autoPublish to false and require manual invocation.

  <central-publishing-maven-plugin>
    ...
-   <autoPublish>true</autoPublish>
+   <autoPublish>false</autoPublish>
  </central-publishing-maven-plugin>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a456e7b and b629ddf.

📒 Files selected for processing (2)
  • .github/workflows/build.yml (1 hunks)
  • pom.xml (6 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/build.yml
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Analyse
  • GitHub Check: Build and Test
🔇 Additional comments (4)
pom.xml (4)

52-52: Verify surefire property is populated when coverage profile is not active.

The surefire.jacoco.args property (line 52) defaults to empty, but it's referenced in the surefire argLine (line 174). When the coverage profile is not active, this will expand to an empty string, which is safe but should be verified to have no unintended side effects on test execution.


144-150: Confirm JMH annotation processor is required for test compilation.

The added annotationProcessorPaths for jmh-generator-annprocess is necessary for JMH benchmark classes to be processed during test compilation. Verify that this aligns with the benchmark implementations and that the JMH version is consistent across build and test dependencies.


173-175: Verify argLine property substitution and mockito javaagent availability.

The argLine uses @{surefire.jacoco.args} (deferred evaluation) and ${org.mockito:mockito-core:jar} (dependency artifact path). The latter relies on maven-dependency-plugin's properties goal to generate the org.mockito:mockito-core:jar property. Confirm that:

  1. The mockito JAR path is correctly resolved when the dependency-plugin executes in the validate phase.
  2. The argLine does not fail when mockito-core is missing or the property is unresolved.
  3. Tests pass consistently across CI/CD environments.

268-270: JaCoCo property mapping ensures code coverage integration.

The surefire.jacoco.args property name mapping correctly wires JaCoCo's agent arguments into surefire, enabling code coverage collection during test execution when the coverage profile is active.

Copy link
Member

@SailReal SailReal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested it on Android, works fine 👍

@overheadhunter overheadhunter merged commit 419d56b into develop Nov 21, 2025
10 checks passed
@overheadhunter overheadhunter deleted the feature/zero-deps branch November 21, 2025 19:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants