Skip to content

exceptionfactory/jagged

Repository files navigation

Jagged

Java implementation of age encryption

build codecov vulnerabilities javadoc maven-central age-encryption.org specification

Build Requirements

  • Java 21
  • Maven 3.9

Runtime Requirements

Java Cryptography Architecture

Jagged uses the Java Cryptography Architecture framework for the following algorithms:

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.

Bouncy Castle Security Provider

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.

Versioning

Jagged follows the Semantic Versioning Specification 2.0.0.

Features

Jagged supports streaming encryption and decryption using standard recipient types.

Specifications

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.

Formatting 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

Recipient Standards

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.

The X25519 type uses Bech32 for encoding public keys and private keys.

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

Modules

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

jagged-api

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.

jagged-bech32

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.

jagged-framework

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.

jagged-scrypt

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.

jagged-ssh

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.

jagged-x25519

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.

jagged-test

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.

Building

Run the following Maven command to build the libraries:

./mvnw clean install

Code Quality

Jagged uses the following build plugins and services to evaluate code quality:

Integrating

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.

X25519 Key Pair Generation

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);

Binary File Encryption with X25519

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);
}

Binary File Decryption with X25519

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);
}

Channel Processing

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();
    }
}

Licensing

Jagged is released under the Apache License, Version 2.0.