Java implementation of age encryption
- Java 21
- Maven 3.9
- Java 21, 17, or 11
- Java 8 with Bouncy Castle Security Provider
Jagged uses the Java Cryptography Architecture framework for the following algorithms:
ChaCha20-Poly1305
with javax.crypto.CipherHmacSHA256
with javax.crypto.MacPBKDF2WithHmacSHA256
with javax.crypto.SecretKeyFactoryRSA
with java.security.KeyFactoryRSA/ECB/OAEPPadding
with javax.crypto.CipherX25519
with javax.crypto.KeyAgreementX25519
with java.security.KeyFactoryX25519
with java.security.KeyPairGenerator
JEP 324 introduced X25519 Key Agreement in Java 11. JEP 329 added ChaCha20-Poly1305 in Java 11.
Jagged does not require additional dependencies when running on Java 11 or higher.
Jagged on Java 8 requires an additional Security Provider to support X25519 and ChaCha20-Poly1305.
The Bouncy Castle framework includes the BouncyCastleProvider which can be installed to support using Jagged on Java 8.
The jagged-x25519
library requires access to X25519 encoded keys. The default behavior of the Bouncy Castle library
includes the public key together with the private key in the encoded representation, which differs from the standard
Java implementation. The Jagged library provides conversion between encoded formats.
Jagged follows the Semantic Versioning Specification 2.0.0.
Jagged supports streaming encryption and decryption using standard recipient types.
- Encryption and decryption of binary age files
- Encryption and decryption of armored age files
- X25519 recipients and identities
- scrypt recipients and identities
- ssh-rsa recipients and identities
- ssh-ed25519 recipients and identities
Jagged supports version 1 of the age-encryption.org specification.
The age encryption specification builds on a number of common cryptographic algorithms and encoding standards.
Files encrypted using the age specification include a textual header and binary payload.
File headers include a message authentication code computed using HMAC-SHA-256.
- RFC 2104 HMAC: Keyed-Hashing for Message Authentication
File headers include recipient stanza binary body elements encoded using Base64 Canonical Encoding.
- RFC 4648 The Base16, Base32, and Base64 Data Encodings
File payloads use a key derived using HKDF-SHA-256.
- RFC 5869 HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
File payload encryption uses ChaCha20-Poly1305 for as the algorithm for Authenticated Encryption with Additional Data.
- RFC 7539 ChaCha20 and Poly1305 for IETF Protocols
Standard recipient types include asymmetric encryption using X25519 and passphrase encryption using scrypt.
The X25519 type uses Curve25519 for Elliptic Curve Diffie-Hellman shared secret key exchanges.
- RFC 7748 Elliptic Curves for Security
The X25519 type uses Bech32 for encoding public keys and private keys.
- BIP 0173 Base32 address format
The X25519 type encrypts a File Key with ChaCha20-Poly1305 using a key derived with HKDF-SHA-256.
The scrypt type uses a passphrase and configurable work factor with other preset values to derive the key for encrypting a File Key.
- RFC 7914 The scrypt Password-Based Key Derivation Function
The scrypt type encrypts a File Key with ChaCha20-Poly1305.
The ssh-ed25519 and ssh-rsa types support reading private key pairs formatted using OpenSSH Private Key Version 1.
The ssh-ed25519 type uses Curve25519 for Elliptic Curve Diffie-Hellman shared secret key exchanges based on computing equivalent values from keys described in the Edwards-curve Digital Signature Algorithm edwards25519.
- RFC 8032 Edwards-Curve Digital Signature Algorithm
The ssh-ed25519 type reads SSH public keys encoded according to the SSH protocol.
- RFC 8709 Ed25519 and Ed448 Public Key Algorithms for the Secure Shell (SSH) Protocol
The ssh-ed25519 type encrypts a File Key with ChaCha20-Poly1305.
The ssh-rsa type reads SSH public keys encoded according to the SSH protocol.
- RFC 4253 The Secure Shell (SSH) Transport Layer Protocol
The ssh-rsa type encrypts a File Key with RSA-OAEP.
- RFC 8017 PKCS #1: RSA Cryptography Specifications Version 2.2
Jagged consists of multiple modules supporting different aspects of the age encryption specification.
- jagged-api
- jagged-bech32
- jagged-framework
- jagged-scrypt
- jagged-ssh
- jagged-test
- jagged-x25519
The
jagged-api
module contains the core public interfaces for encryption and decryption operations. The module
contains interfaces and classes in the com.exceptionfactory.jagged
package, which provide integration and extension
points for other components.
The FileKey class implements java.crypto.SecretKey and supports the primary contract for age identities and recipients.
The RecipientStanza interface follows the pattern of the age Stanza, providing access to the Type, Arguments, and binary Body elements.
The
RecipientStanzaReader
interface serves as the age Identity
abstraction, responsible for reading RecipientStanza
objects and return a decrypted FileKey
.
The
RecipientStanzaWriter
interface follows the age Recipient
abstraction, responsible for wrapping a FileKey
and returning a collection of RecipientStanza
objects.
The
EncryptingChannelFactory
interface wraps a provided
WritableByteChannel and returns
a WritableByteChannel
that supports streaming encryption to one or more recipients based on supplied
RecipientStanzaWriter
instances.
The
DecryptingChannelFactory
interface wraps a provided
ReadableByteChannel and returns
a ReadableByteChannel
that supports streaming decryption for a matched identity based on supplied
RecipientStanzaReader
instances.
The jagged-bech32 module contains an implementation of the Bech32 encoding specification defined according to Bitcoin Improvement Proposal 0173. Bech32 encoding supports a standard representation of X25519 private and public keys. The Bech32 class follows the pattern of java.util.Base64 and encloses Bech32.Decoder and Bech32.Encoder interfaces. Bech32 encoding consists of a Human-Readable Part prefix, a separator, and data part that ends with a checksum.
The jagged-framework module includes shared components for common cryptographic operations.
The stream
package includes the
StandardDecryptingChannelFactory
and
StandardEncryptingChannelFactory
classes,
which implement the corresponding public interfaces for streaming cipher operations.
The armor
packaged includes the
ArmoredDecryptingChannelFactory
and
ArmoredEncryptingChannelFactory
classes,
supporting reading and writing ASCII armored files with standard PEM header and footer lines.
The jagged-scrypt module supports encryption and decryption using a passphrase and configurable work factor.
The
ScryptRecipientStanzaReaderFactory
creates instances of RecipientStanzaReader
using a passphrase.
The
ScryptRecipientStanzaWriterFactory
creates instances of RecipientStanzaWriter
using a passphrase and
a work factor with a minimum value of 2 and a maximum value of 20.
The module includes a custom implementation of the scrypt key derivation function with predefined settings that match age encryption scrypt recipient specifications.
The jagged-ssh module supports encryption and decryption using public and private SSH key pairs. The SSH key pair implementation is compatible with the agessh package, which defines recipient stanzas with an algorithm and an encoded fingerprint of the public key.
The
SshEd25519RecipientStanzaReaderFactory
creates instances of RecipientStanzaReader
using an
OpenSSH Version 1 Private Key.
The
SshEd25519RecipientStanzaWriterFactory
creates instances of RecipientStanzaWriter
using an SSH Ed25519 public
key encoded according to RFC 8709 Section 4.
The
SshRsaRecipientStanzaReaderFactory
creates instances of RecipientStanzaReader
using an RSA private key or an
OpenSSH Version 1 Private Key.
The
SshRsaRecipientStanzaWriterFactory
creates instances of RecipientStanzaWriter
using an RSA public key or an SSH
RSA public key encoded according to RFC 4253 Section 6.6.
The SSH Ed25519 implementation uses Elliptic Curve Diffie-Hellman with Curve25519 as defined in
RFC 7748 Section 6.1. As integrated in the age reference
implementation, the SSH Ed25519 implementation converts the public key coordinate from the twisted Edwards curve to the
corresponding coordinate on the Montgomery curve according to the birational maps described in
RFC 7748 Section 4.1. The implementation converts the Ed25519
private key seed to the corresponding X25519 private key using the first 32 bytes of an SHA-512
hash of the seed.
The SSH Ed25519 implementation uses ChaCha20-Poly1305 for encrypting and decrypting File Keys.
The SSH RSA implementation uses Optimal Asymmetric Encryption Padding as defined in
RFC 8017 Section 7.1. Following the age implementation, RSA OAEP
cipher operations use SHA-256
as the hash algorithm with the mask generation function.
The
jagged-x25519
module supports encryption and decryption using public and private key pairs. Key generation and
key agreement functions use the Java Cryptography Architecture framework. Key encoding and decoding functions use the
jagged-bech32
library.
The
X25519KeyFactory
class implements
java.security.KeyFactory
and supports translating an encoded X25519 private key to the corresponding X25519 public key. The translateKey
method
accepts an instance of the
javax.crypto.spec.SecretKeySpec
class. The SecretKeySpec
must be constructed with the key
byte array containing the encoded private key, and with
X25519
set as the value of the algorithm
argument.
The X25519KeyPairGenerator class implements java.security.KeyPairGenerator and returns public and private key pairs encoded using Bech32.
The
X25519RecipientStanzaReaderFactory
creates instances of RecipientStanzaReader
using a private key encoded using
Bech32. Encoded private keys begin with AGE-SECRET-KEY-1
as the Bech32 Human-Readable Part and separator.
The
X25519RecipientStanzaWriterFactory
creates instances of RecipientStanzaWriter
using a public key encoded using
Bech32. Encoded public keys begin with age1
as the Bech32 Human-Readable Part and separator.
The jagged-test
module includes framework tests for age test vectors
defined in the Community Cryptography Test Vectors project. The
CommunityCryptographyTest
runs a test method for each file in the test data directory. The FrameworkTest
class
exercises binary and armored encryption and decryption methods using supported recipient types.
Run the following Maven command to build the libraries:
./mvnw clean install
Jagged uses the following build plugins and services to evaluate code quality:
- Apache Maven Checkstyle Plugin
- Apache Maven PMD Plugin
- Codecov
- GitHub CodeQL
- JaCoCo Maven Plugin
- SpotBugs Maven Plugin
Jagged supports streaming encryption and decryption using Java NIO buffers and channels. Java NIO supports efficient file read and write operations, minimizing memory impact using instances of java.nio.ByteBuffer to process bytes. The java.nio.channel.Channels class provides several methods supporting interoperation with Java IO streams.
The X25519 recipient type with binary formatting provides the optimal solution for integrating age encryption. X25519 public and private keys encoded using Bech32 avoid the cost of password-based key derivation, and binary formatting for encrypted files does not have the overhead of armored Base64 encoding and decoding.
Jagged supports public and private keys produced using the age-keygen command and also provides key pair generation using the X25519KeyPairGenerator class. The class implements KeyPairGenerator and supports standard methods for generating KeyPair instances. Both PublicKey and PrivateKey implementations return Bech32 encoded representations following the age specification.
final KeyPairGenerator keyPairGenerator = new X25519KeyPairGenerator();
final KeyPair keyPair = keyPairGenerator.generateKeyPair();
final PublicKey publicKey = keyPair.getPublic();
System.out.printf("Public key: %s", publicKey);
Encryption operations require one or more X25519 public keys. Jagged provides the
X25519RecipientStanzaWriterFactory
class for creating instances of
RecpientStanzaWriter
to support encryption operations. The factory class accepts a
standard Java String containing a Bech32 encoded public key starting with age1
and also supports other implementations
of CharSequence to provide
more control over encoded keys.
The java.nio.file.Path class represents file locations and enables creation of java.nio.Channel objects for reading input files and writing encrypted output files.
final CharSequence publicKey = getPublicKey();
final RecipientStanzaWriter stanzaWriter = X25519RecipientStanzaWriterFactory.newRecipientStanzaWriter(publicKey);
final EncryptingChannelFactory channelFactory = new StandardEncryptingChannelFactory();
final Path inputPath = getInputPath();
final Path outputPath = getOutputPath();
try (
final ReadableByteChannel inputChannel = Files.newByteChannel(inputPath);
final WritableByteChannel encryptingChannel = channelFactory.newEncryptingChannel(
Files.newByteChannel(outputPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE),
Collections.singletonList(stanzaWriter)
);
) {
copy(inputChannel, encryptingChannel);
}
Decryption operations require a private key corresponding to a recipient from the age file header. Jagged provides the
X25519RecipientStanzaReaderFactory
class for creating instances of
RecipientStanzaReader
to support decryption
operations. The factory class accepts a Bech32 encoded private key starting with AGE-SECRET-KEY-1
represented as a
Java String or sequence of characters.
final CharSequence privateKey = getPrivateKey();
final RecipientStanzaReader stanzaReader = X25519RecipientStanzaReaderFactory.newRecipientStanzaReader(privateKey);
final DecryptingChannelFactory channelFactory = new StandardDecryptingChannelFactory();
final Path inputPath = getInputPath();
final Path outputPath = getOutputPath();
try (
final WritableByteChannel outputChannel = Files.newByteChannel(
outputPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE
);
final ReadableByteChannel decryptingChannel = channelFactory.newDecryptingChannel(
Files.newByteChannel(inputPath),
Collections.singletonList(stanzaReader)
);
) {
copy(decryptingChannel, outputChannel);
}
The age specification defines the encrypted binary payload as
consisting of chunks containing 64 kilobytes. Allocating a ByteBuffer
with a capacity of 65536
enables integrating
components to process chunks with an optimal number of method invocations. Transferring bytes from a
ReadableByteChannel
to a
WritableByteChannel
requires iterative processing to avoid partial reads or writes.
void copy(
final ReadableByteChannel inputChannel,
final WritableByteChannel outputChannel
) throws IOException {
final ByteBuffer buffer = ByteBuffer.allocate(65536);
while (inputChannel.read(buffer) != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
outputChannel.write(buffer);
}
buffer.clear();
}
}
Jagged is released under the Apache License, Version 2.0.