@substrate-system/simple-hpke
v0.0.8
Published
Hybrid Public Key Encryption
Downloads
1,289
Readme
Simple HPKE
Hybrid Public Key Encryption (RFC 9180)
1 dependency -- uint8arrays.
Install
npm i -S @substrate-system/simple-hpkeExample
Wrap an AES key, or encrypt a message.
Key Wrapping
Encrypt an AES key to yourself, then recover it later.
import { seal, open } from '@substrate-system/simple-hpke'
// An X25519 keypair. The private key can be non-extractable.
// HPKE needs only `deriveBits`.
const keypair = await crypto.subtle.generateKey(
{ name: 'X25519' },
false, // extractable
['deriveBits']
)
// create a new AES key, and encrypt it to your public key.
const { wrapped, key } = await seal(keypair)
// Or wrap an existing AES key. The supplied key must be extractable.
const aesKey = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true, // extractable
['encrypt', 'decrypt']
)
// pass in a key. The return value is just the wrapped key
const { wrapped } = await seal(keypair, aesKey)
// Later, recover the same key with your private key.
const recoveredKey = await open(keypair, wrapped)See docs/README.md for the full API and rationale.
Hybrid Encryption
Seal a key, then use it to encrypt a message with AES-GCM. The wrapped key is concattenated with the cipher text, along with the IV. The recipient uses their private key to open the AES key and decrypt the message.
import { seal, open } from '@substrate-system/simple-hpke'
const recipient = await crypto.subtle.generateKey(
{ name: 'X25519' },
false, // not extractable
['deriveBits']
)
// Create and seal a fresh AES-GCM key, and encrypt a message with it.
const { wrapped, key } = await seal(recipient)
const iv = crypto.getRandomValues(new Uint8Array(12))
const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
new TextEncoder().encode('attack at dawn')
)
// Send `wrapped`, `iv`, and `ciphertext` together. `wrapped` is a fixed
// 80 bytes for this suite, and the AES-GCM IV is 12 bytes, so the recipient
// can slice the payload back apart at known offsets.
const ciphertext = new Uint8Array(ciphertext)
const message = new Uint8Array(wrapped.length + iv.length + ciphertext.length)
message.set(wrapped, 0)
message.set(iv, wrapped.length)
message.set(ciphertext, wrapped.length + iv.length)
// On the other side, split the payload back into its parts.
const wrapped2 = message.subarray(0, 80)
const iv2 = message.subarray(80, 80 + 12)
const ciphertext2 = message.subarray(80 + 12)
// Recover the key, then decrypt the message.
const recovered = await open(recipient, wrapped2)
const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv2 },
recovered,
ciphertext2
)
new TextDecoder().decode(plaintext) // => 'attack at dawn'Encrypt / Decrypt
So that was a lot of code to encrypt and decrypt a message...
This package exposes functions encrypt and decrypt which do the same thing.
encrypt seals an AES key to the recipient, encrypts the message under
that key, and returns a single envelope: wrappedLen + wrapped + iv + ciphertext
(a 2-byte length prefix, the wrapped key, the 12-byte AES-GCM IV, and the
cipher text). decrypt reverses it, returning the plaintext bytes.
[!NOTE]
Seedecrypt.asString&decrypt.fromStringbelow for a convenient way to decrypt from a string.See
encrypt.asStringfor encrypting and ecoding to a string.
import {
encrypt,
decrypt
} from '@substrate-system/simple-hpke'
// need a public key for the recipient
const recipient = await crypto.subtle.generateKey(
{ name: 'X25519' },
false,
['deriveBits']
)
// create a new AES key, encrypt a message, and get back one envelope
const encryptedMessage = await encrypt(recipient, 'hello encryption')
// the recipient recovers the message with their private key
const text = await decrypt.asString(recipient, encryptedMessage)
// use `decrypt` to get a Uint8Array
const bytes = await decrypt(recipient, encryptedMessage)
//
// use an existing AES key
//
const existingKey = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 },
true, // extractable
['encrypt', 'decrypt']
)
const anotherEncryptedMsg = await encrypt(
recipient,
'hello again',
existingKey
)encrypt
The recipient can be a crypto key, a Uint8Array, or a string public key.
type RecipientKey =
| CryptoKey
| CryptoKeyPair
| Uint8Array
| { publicKey:string; encoding?:Uint8ArrayEncodings }
async function encrypt (
recipient:RecipientKey,
message:Uint8Array|string,
aesKey?:CryptoKey|Uint8Array|null,
opts?:{
keysize?:128|256
info?:Uint8Array|string
}
):Promise<Uint8Array>Encrypt to a string
encrypt.asString is encrypt with the envelope encoded to a string, handy
for transports that carry text (JSON, URLs, headers). opts.encoding sets the
string encoding. Default encoding is base64url.
import { encrypt, decrypt } from '@substrate-system/ecies'
import { fromString } from 'uint8arrays'
// recipient is any RecipientKey; keypair holds the matching private key
const encryptedString = await encrypt.asString(
recipient,
'message for them',
null, // an AES key if you want
{ encoding: 'base64url' }
)
// Decode it back to bytes before decrypting.
const message = fromString(encryptedString, 'base64url')
const plaintext = await decrypt.asString(keypair, message)
// 'message for them'The returned string encodes the same envelope encrypt returns, so the
recipient decodes it with a matching decoder (here fromString) and passes the
bytes to decrypt / decrypt.asString.
decrypt
Descrypt the given data, return a Uint8Array.
async function decrypt (
keypair:CryptoKeyPair,
message:Uint8Array,
opts?:{ info?:Uint8Array|string }
):Promise<Uint8Array>decrypt.asString
Take a Uint8Array, return a sttring.
decrypt.asString = async function decryptToString (
keypair:CryptoKeyPair,
message:Uint8Array,
opts?:{ info?:Uint8Array|string }
):Promise<string>decrypt.fromString
Take a string as input. Return either a string, or if opts.buffer is true,
aUint8Array.
decrypt.fromString = async function decryptFromString (
keypair:CryptoKeyPair,
message:string,
opts?:{ info?:Uint8Array|string, buffer?:boolean }
):Promise<string|Uint8Array>Modules
This exposes ESM and common JS via
package.json exports field.
ESM
import { seal, open, encrypt, decrypt } from '@substrate-system/simple-hpke'Common JS
require('@substrate-system/simple-hpke')pre-built JS
This package exposes minified JS files too. Copy them to a location that is accessible to your web server, then link to them in HTML.
copy
cp ./node_modules/@substrate-system/simple-hpke/dist/index.min.js ./public/hpke.min.jsHTML
<script type="module" src="./hpke.min.js"></script>