age-encryption is a TypeScript
implementation of the age file encryption format.
It depends only on the noble cryptography libraries, and uses the Web Crypto API when available.
It also provides support for symmetric encryption using passkeys and hardware security keys in the browser via WebAuthn, and an interoperable CLI plugin and Go library for FIDO2 tokens.
This package is age-encryption on npm, and jsr:@age/age-encryption on JSR.
It's compiled for ES2023, and compatible with Node.js 20+, Bun, Deno, and all recent browsers.
npm install age-encryptiondeno add jsr:@age/age-encryptionage-encryption is a modern ES Module with built-in types.
import*asagefrom"age-encryption"constidentity=awaitage.generateIdentity()constrecipient=awaitage.identityToRecipient(identity)console.log(identity)console.log(recipient)conste=newage.Encrypter()e.addRecipient(recipient)constciphertext=awaite.encrypt("Hello, age!")constd=newage.Decrypter()d.addIdentity(identity)constout=awaitd.decrypt(ciphertext,"text")console.log(out)import*asagefrom"age-encryption"constidentity=awaitage.generateHybridIdentity()constrecipient=awaitage.identityToRecipient(identity)console.log(identity)console.log(recipient)// ...age encrypted files (the inputs of Decrypter.decrypt and outputs of
Encrypter.encrypt) are binary files, of type Uint8Array. There is an official ASCII
"armor" format, based on PEM, which provides a way to encode an encrypted file as text.
import*asagefrom"age-encryption"constidentity=awaitage.generateIdentity()constrecipient=awaitage.identityToRecipient(identity)console.log(identity)console.log(recipient)conste=newage.Encrypter()e.addRecipient(recipient)constciphertext=awaite.encrypt("Hello, age!")constarmored=age.armor.encode(ciphertext)console.log(armored)// -----BEGIN AGE ENCRYPTED FILE-----// YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0QXVkQmNwZ3ZzYnNRZDJP// WlFId3hyeFNmRS9SdUVUTkFhY1FXSno5VUFBClNOSWhEbnhoK21TaEs3SWRGdklw// OW9pdlBZbDg3SEVSQ1FZZHBvUS90YjgKLS0tIGRCVXNNWmdJS0ZkNlNZbStPZWh4// N2FBNUJZdTFxMmYwVTEzUWwvTFVNeUkKrNZnrZjMlXvoCHz0FUS/bp9129XtSV1Q// 2twDjjAOwgBtBYoji9gKWgOG4w==// -----END AGE ENCRYPTED FILE-----constd=newage.Decrypter()d.addIdentity(identity)constdecoded=age.armor.decode(armored)constout=awaitd.decrypt(decoded,"text")console.log(out)import{Encrypter,Decrypter}from"age-encryption"conste=newEncrypter()e.setPassphrase("burst-swarm-slender-curve-ability-various-crystal-moon-affair-three")constciphertext=awaite.encrypt("Hello, age!")constd=newDecrypter()d.addPassphrase("burst-swarm-slender-curve-ability-various-crystal-moon-affair-three")constout=awaitd.decrypt(ciphertext,"text")console.log(out)You can also encrypt and decrypt ReadableStreams. This can be useful for encrypting or decrypting large files, requests, or responses on the fly.
import{Encrypter,Decrypter}from"age-encryption"constfile=newFile([newTextEncoder().encode("age")],"age.txt")conste=newEncrypter()e.setScryptWorkFactor(12)e.setPassphrase("light-original-energy-average-wish-blind-vendor-pencil-illness-scorpion")constencryptedStream=awaite.encrypt(file.stream())console.log(encryptedStream.size(file.size))constd=newDecrypter()d.addPassphrase("light-original-energy-average-wish-blind-vendor-pencil-illness-scorpion")constdecryptedStream=awaitd.decrypt(encryptedStream)console.log(decryptedStream.size(encryptedStream.size(file.size)))console.log(awaitnewResponse(decryptedStream).text())age-encryption is compatible with modern bundlers such as esbuild.
To produce a classic library file that sets age as a global variable, you can run
cd"$(mktemp -d)"&& npm init -y && npm install esbuild age-encryption
npx esbuild --target=es2022 --bundle --minify --outfile=age.js --global-name=age age-encryptionor download a pre-built one from the Releases page.
Then, you can use it like this
<scriptsrc="age.js"></script><script>(async()=>{constidentity=awaitage.generateIdentity()constrecipient=awaitage.identityToRecipient(identity)console.log(identity)console.log(recipient)conste=newage.Encrypter()e.addRecipient(recipient)constciphertext=awaite.encrypt("Hello, age!")constd=newage.Decrypter()d.addIdentity(identity)constout=awaitd.decrypt(ciphertext,"text")console.log(out)})()</script>In the browser, age-encryption supports symmetric encryption with passkeys,
discoverable credentials that can be stored and synced by platforms (e.g. iCloud
Keychain) or password managers (e.g. 1Password).
This functionality uses the WebAuthn PRF extension, which is supported by recent
browsers and authenticators. When encrypting or decrypting a file, the user will
be prompted to select a passkey associated with the replying party ID (usually
the website origin). Passkeys not generated by createCredential can be used if
they have the prf extension enabled. The identity string returned by
createCredential can be optionally provided at encryption/decryption time to
prevent the user from selecting other passkeys.
awaitage.webauthn.createCredential({keyName: "age encryption key 🦈"})conste=newage.Encrypter()e.addRecipient(newage.webauthn.WebAuthnRecipient())constciphertext=awaite.encrypt("Hello, age!")constarmored=age.armor.encode(ciphertext)console.log(armored)constd=newage.Decrypter()d.addIdentity(newage.webauthn.WebAuthnIdentity())constdecoded=age.armor.decode(armored)constout=awaitd.decrypt(decoded,"text")console.log(out)Each encryption and decryption operation requires the authenticator and user confirmation, there is no extractable key, and encrypted files can't be linked to an identity or to each other without the ability to decrypt them.
age-encryption also supports non-discoverable FIDO2 credentials, usually
useful to encrypt files with hardware security keys (e.g. YubiKeys).
Encryption and decryption work the same as with passkeys, but the identity string is mandatory, because these credentials are not discoverable.
constidentity=awaitage.webauthn.createCredential({type: "security-key",keyName: "age encryption key"})console.log(identity)// AGE-PLUGIN-FIDO2PRF-1...conste=newage.Encrypter()e.addRecipient(newage.webauthn.WebAuthnRecipient({identity: identity}))constciphertext=awaite.encrypt("Hello, age!")constarmored=age.armor.encode(ciphertext)console.log(armored)constd=newage.Decrypter()d.addIdentity(newage.webauthn.WebAuthnIdentity({identity: identity}))constdecoded=age.armor.decode(armored)constout=awaitd.decrypt(decoded,"text")console.log(out)If a credential is associated with a USB FIDO2 security key (e.g. a YubiKey),
its identity string can be used outside the browser with the provided
age-plugin-fido2prf plugin.
Files encrypted in the browser will decrypt from the CLI, and vice-versa. Since
WebAuthn encryption is symmetric, there is no recipient encoding, only
identities. To encrypt to an identity, use age -e -i.
go install filippo.io/typage/fido2prf/cmd/age-plugin-fido2prf@latest
cat <<EOF > identity.txtAGE-PLUGIN-FIDO2PRF-1Q9VGPY2E7S5FJJS3N7P03TZMMEJ94S6HYLDJLU8WVX2HP8SXQUGJUZ68LN6GP705662VS06UEX5J42W80NZT8Y2DQ8GTDN50VGATCNYLJ4HY2W5J67KYCTM858UFDCNUUDZ6U28WEMUKGVG9RNELRJDH8NFEP999Z8XFSS8XLS448A3TSQKWG9DMPL8ZCRRA02KSUC2UXTYDFNVYAE5KCMMRV9KXSMMNWJQKXATNVGTXV35GEOF
age -d -i identity.txt <<EOF-----BEGIN AGE ENCRYPTED FILE-----YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IGFnZS1lbmNyeXB0aW9uLm9yZy9maWRvMnByZiBJSFFHd0poUkNSYThuVnB6b1R1bjdBClJ3d3dRUUtiRjN4TkU0VWx1SUZUWnJtTVBFUTZoR0d4eUx2WXFOSFBsQzQKLS0tIFNWVGRnNzV4L00wblRENUZyYlFhWU5wQmVsdG5hL0lmcXhTVzZHTVRtdFkK2rYiueXr8dgM1GiLVrBMC/LQRzkDacMwGEtVcMZyh7b90z6VR3KT92EIlA==-----END AGE ENCRYPTED FILE-----EOFCredentials can be generated from the command line with age-plugin-fido2prf -generate RPID. Note that they will be usable inside the browser only if the
relying party ID matches the website's origin.
All the features of the plugin are also available as a Go library at filippo.io/typage/fido2prf.
You can use a CryptoKey as an identity. It must have an algorithm of X25519,
and support the deriveBits key usage. It doesn't need to be extractable.
constkeyPair=awaitcrypto.subtle.generateKey({name: "X25519"},false,["deriveBits"])constidentity=(keyPairasCryptoKeyPair).privateKeyconstrecipient=awaitage.identityToRecipient(identity)console.log(recipient)conste=newage.Encrypter()e.addRecipient(recipient)constfile=awaite.encrypt("age")constd=newage.Decrypter()d.addIdentity(identity)constout=awaitd.decrypt(file,"text")console.log(out)You can implement the Recipient and Identity interfaces to use custom types
as recipients and identities.
This lets you use use remote APIs and secrets managers to wrap files keys, and interoperate with age plugins.