Skip to content

Add Java implementation of 3ncr.org v1#1

Merged
AndrianBdn merged 3 commits intomainfrom
add-java-implementation
Apr 22, 2026
Merged

Add Java implementation of 3ncr.org v1#1
AndrianBdn merged 3 commits intomainfrom
add-java-implementation

Conversation

@peo-machine
Copy link
Copy Markdown
Collaborator

Summary

Java implementation of the 3ncr.org v1 string encryption standard, bringing parity with the Go, Node.js, PHP, Python, and Rust reference libraries. Requires JDK 11+.

Mirrors the API surface and factory layout of the Python/Rust ports:

  • TokenCrypt.fromRawKey(byte[]) — raw 32-byte AES-256 key
  • TokenCrypt.fromSha3(String|byte[]) — single SHA3-256 hash for high-entropy secrets
  • TokenCrypt.fromArgon2id(String|byte[], byte[]) — recommended Argon2id KDF with the v1 parameters (m=19456 KiB, t=2, p=1), 16-byte minimum salt
  • TokenCrypt.fromPbkdf2Sha3(String|byte[], String|byte[], int)@deprecated legacy KDF, kept for decrypting data from earlier 3ncr.org libraries

AES-256-GCM and SHA3-256 come from the JDK (javax.crypto / java.security.MessageDigest). Argon2id and PBKDF2-SHA3 come from Bouncy Castle (bcprov-jdk18on) since they are not in the JDK.

Envelope details

  • Envelope header: 3ncr.org/1#
  • Binary layout: iv[12] || ciphertext || tag[16]
  • Base64 with no padding on encoding; decoder accepts padded or unpadded input for robustness
  • 128-bit GCM auth tag
  • Decryption failures (bad tag, truncated, malformed base64) throw TokenCryptException (unchecked)

Tests

19 JUnit 5 tests covering:

  • All four canonical v1 vectors (parameterized, decrypt + round-trip)
  • Round-trip edge cases (empty, single char, UUID, multibyte UTF-8, 4 KiB)
  • decryptIf3ncr returns non-3ncr input unchanged (including empty string)
  • IV uniqueness across repeated encryptions
  • Tampered-payload and truncated-payload rejection
  • Base64 decoder accepts padded input
  • Base64 encoder emits no padding
  • fromSha3(String) and fromSha3(byte[]) agree on UTF-8 input
  • fromArgon2id round-trip + short-salt rejection + wrong-secret failure
  • fromRawKey rejects wrong-length keys and defensively copies the input

CI

GitHub Actions matrix on Ubuntu with Temurin 11, 17, 21 (mvn verify).

Test plan

  • CI passes on all JDK versions (11, 17, 21)
  • All four canonical v1 vectors decrypt successfully
  • Round-trip, tamper, and truncation tests pass
  • Argon2id interop — same secret+salt derives the same AES key as the Python/Rust implementations (implicitly verified by cross-impl round-trip if tested manually)

TokenCrypt with four constructors (raw 32-byte key, SHA3-256, Argon2id,
legacy PBKDF2-SHA3) built on the JDK's javax.crypto AES-256-GCM and
BouncyCastle for the KDFs the JDK doesn't ship (Argon2id, PBKDF2-SHA3).

- Base64 with no padding on encoding; decoder accepts both for robustness
- Argon2id uses the 3ncr.org v1 recommended parameters
  (m=19456 KiB, t=2, p=1), 16-byte minimum salt
- Legacy PBKDF2-SHA3 factory is @deprecated, kept for decrypting data
  from earlier libraries
- 19 JUnit 5 tests covering canonical vectors, round-trip edge cases,
  IV uniqueness, tamper / truncation rejection, padded-input decode,
  and KDF-specific behavior
- GitHub Actions matrix: Temurin 11, 17, 21
Copy link
Copy Markdown
Contributor

@AndrianBdn AndrianBdn left a comment

Choose a reason for hiding this comment

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

for newer libraries we don't need pbkdf key derivation at all

Comment thread .github/workflows/test.yml Outdated
strategy:
fail-fast: false
matrix:
java: ['11', '17', '21']
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

java lts is 25

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Added Java 25 to the CI matrix in 4eed0e6 — matrix is now 11 / 17 / 21 / 25, all currently-supported Temurin LTS lines.

Copy link
Copy Markdown
Contributor

@AndrianBdn AndrianBdn left a comment

Choose a reason for hiding this comment

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

for newer libraries we don't need pbkdf key derivation at all

This is a brand-new implementation, so it has no existing data to remain
compatible with — the legacy PBKDF2-SHA3 KDF only exists in the older
Go/Node/PHP libraries for that reason and is not needed here. Drops
TokenCrypt.fromPbkdf2Sha3 along with the BouncyCastle SHA3Digest /
PKCS5S2ParametersGenerator / KeyParameter imports that only the PBKDF2
path used (BouncyCastle is still pulled in for Argon2id).

The canonical envelope test vectors are still verified for cross-
implementation interop by hardcoding the 32-byte AES key that those
vectors were originally derived from.
@peo-machine
Copy link
Copy Markdown
Collaborator Author

Thanks for the review. Pushed ad86a5d which drops the legacy PBKDF2-SHA3 KDF — TokenCrypt.fromPbkdf2Sha3 is gone, along with the BouncyCastle SHA3Digest / PKCS5S2ParametersGenerator / KeyParameter imports that only the PBKDF2 path used (BouncyCastle is still pulled in for Argon2id). As a brand-new implementation there's no pre-existing data to stay compatible with.

The canonical envelope test vectors are still verified for cross-implementation interop by hardcoding the 32-byte AES key those vectors were originally derived from (same approach used in the Rust PR).

@AndrianBdn AndrianBdn merged commit b17c910 into main Apr 22, 2026
4 checks passed
@AndrianBdn AndrianBdn deleted the add-java-implementation branch April 22, 2026 14:08
peo-machine added a commit that referenced this pull request Apr 23, 2026
- README: badge row (CI + Maven Central + License), three-section Usage
  reorder (raw key / Argon2id / legacy-PBKDF2 explainer), Install
  section moved above Usage, minor wording alignment with the other
  new-language READMEs.
- SECURITY.md: standard policy pointing at GitHub Private Vulnerability
  Reporting, consistent with tokencrypt-python and tokencrypt-rust.
- CHANGELOG.md: seeded with the v1.0.0 entry.

Per the 3ncr.org new-language convention and consistent with the actual
source (PR #1 dropped legacy PBKDF2-SHA3), the third Usage section
explains how to recover the legacy key externally rather than documenting
a nonexistent factory. LICENSE stays at 2026 (repo created this year).

Co-authored-by: Andrian Budantsov <a@hypersequent.com>
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.

2 participants