micro-aes-gcm
Advanced tools
Comparing version 0.3.3 to 0.4.0
@@ -1,5 +0,4 @@ | ||
declare function hexToBytes(hex: string): Uint8Array; | ||
declare function concatBytes(...arrays: Uint8Array[]): Uint8Array; | ||
export declare function encrypt(sharedKey: Uint8Array, plaintext: string | Uint8Array): Promise<Uint8Array>; | ||
export declare function decrypt(sharedKey: Uint8Array, encoded: string | Uint8Array): Promise<Uint8Array>; | ||
export declare function encrypt(sharedKey: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array>; | ||
export declare function decrypt(sharedKey: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array>; | ||
export declare const utils: { | ||
@@ -9,10 +8,5 @@ randomBytes: (bytesLength?: number) => any; | ||
utf8ToBytes(string: string): Uint8Array; | ||
hexToBytes: typeof hexToBytes; | ||
concatBytes: typeof concatBytes; | ||
}; | ||
declare const aes: { | ||
encrypt: typeof encrypt; | ||
decrypt: typeof decrypt; | ||
}; | ||
export default aes; | ||
export {}; | ||
//# sourceMappingURL=index.d.ts.map |
103
index.js
@@ -1,23 +0,2 @@ | ||
import * as nodeCrypto from 'crypto'; | ||
const crypto = { | ||
node: nodeCrypto, | ||
web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined, | ||
}; | ||
function hexToBytes(hex) { | ||
if (typeof hex !== 'string') { | ||
throw new TypeError('hexToBytes: expected string, got ' + typeof hex); | ||
} | ||
if (hex.length % 2) | ||
throw new Error('hexToBytes: received invalid unpadded hex' + hex.length); | ||
const array = new Uint8Array(hex.length / 2); | ||
for (let i = 0; i < array.length; i++) { | ||
const j = i * 2; | ||
const hexByte = hex.slice(j, j + 2); | ||
const byte = Number.parseInt(hexByte, 16); | ||
if (Number.isNaN(byte) || byte < 0) | ||
throw new Error('Invalid byte sequence'); | ||
array[i] = byte; | ||
} | ||
return array; | ||
} | ||
const cr = () => typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined; | ||
function concatBytes(...arrays) { | ||
@@ -38,58 +17,35 @@ if (!arrays.every((arr) => arr instanceof Uint8Array)) | ||
const MD = { e: 'AES-GCM', i: { name: 'AES-GCM', length: 256 } }; | ||
function ensureBytes(b, len) { | ||
if (!(b instanceof Uint8Array)) | ||
throw new Error('Uint8Array expected'); | ||
if (typeof len === 'number') | ||
if (b.length !== len) | ||
throw new Error(`Uint8Array length ${len} expected`); | ||
} | ||
function ensureCrypto() { | ||
if (!cr()) | ||
throw new Error('globalThis.crypto is not available: use nodejs 19+ or browser'); | ||
} | ||
export async function encrypt(sharedKey, plaintext) { | ||
if (typeof plaintext === 'string') | ||
plaintext = utils.utf8ToBytes(plaintext); | ||
ensureCrypto(); | ||
ensureBytes(sharedKey, 32); | ||
ensureBytes(plaintext); | ||
const iv = utils.randomBytes(12); | ||
if (crypto.web) { | ||
const iKey = await crypto.web.subtle.importKey('raw', sharedKey, MD.i, true, ['encrypt']); | ||
const cipher = await crypto.web.subtle.encrypt({ name: MD.e, iv }, iKey, plaintext); | ||
const ciphertext = new Uint8Array(cipher); | ||
const encrypted = new Uint8Array(iv.length + ciphertext.byteLength); | ||
encrypted.set(iv, 0); | ||
encrypted.set(ciphertext, iv.length); | ||
return encrypted; | ||
} | ||
else { | ||
const cipher = crypto.node.createCipheriv('aes-256-gcm', sharedKey, iv); | ||
let ciphertext = cipher.update(plaintext, undefined, 'hex'); | ||
ciphertext += cipher.final('hex'); | ||
const ciphertextBytes = hexToBytes(ciphertext); | ||
const tag = cipher.getAuthTag(); | ||
const encrypted = concatBytes(iv, ciphertextBytes, tag); | ||
return encrypted; | ||
} | ||
const iKey = await cr().subtle.importKey('raw', sharedKey, MD.i, true, ['encrypt']); | ||
const cipher = await cr().subtle.encrypt({ name: MD.e, iv }, iKey, plaintext); | ||
return concatBytes(iv, new Uint8Array(cipher)); | ||
} | ||
export async function decrypt(sharedKey, encoded) { | ||
if (typeof encoded === 'string') | ||
encoded = hexToBytes(encoded); | ||
const iv = encoded.slice(0, 12); | ||
if (crypto.web) { | ||
const ciphertextWithTag = encoded.slice(12); | ||
const iKey = await crypto.web.subtle.importKey('raw', sharedKey, MD.i, true, ['decrypt']); | ||
const plaintext = await crypto.web.subtle.decrypt({ name: MD.e, iv }, iKey, ciphertextWithTag); | ||
return new Uint8Array(plaintext); | ||
} | ||
else { | ||
const ciphertext = encoded.slice(12, -16); | ||
const authTag = encoded.slice(-16); | ||
const decipher = crypto.node.createDecipheriv('aes-256-gcm', sharedKey, iv); | ||
decipher.setAuthTag(authTag); | ||
const plaintext = decipher.update(ciphertext); | ||
const final = Uint8Array.from(decipher.final()); | ||
const res = concatBytes(plaintext, final); | ||
return res; | ||
} | ||
export async function decrypt(sharedKey, ciphertext) { | ||
ensureCrypto(); | ||
ensureBytes(sharedKey, 32); | ||
ensureBytes(ciphertext); | ||
const iv = ciphertext.slice(0, 12); | ||
const ciphertextWithTag = ciphertext.slice(12); | ||
const iKey = await cr().subtle.importKey('raw', sharedKey, MD.i, true, ['decrypt']); | ||
const plaintext = await cr().subtle.decrypt({ name: MD.e, iv }, iKey, ciphertextWithTag); | ||
return new Uint8Array(plaintext); | ||
} | ||
export const utils = { | ||
randomBytes: (bytesLength = 32) => { | ||
if (crypto.web) { | ||
return crypto.web.getRandomValues(new Uint8Array(bytesLength)); | ||
} | ||
else if (crypto.node) { | ||
const { randomBytes } = crypto.node; | ||
return Uint8Array.from(randomBytes(bytesLength)); | ||
} | ||
else { | ||
throw new Error("The environment doesn't have randomBytes function"); | ||
} | ||
return cr().getRandomValues(new Uint8Array(bytesLength)); | ||
}, | ||
@@ -102,6 +58,3 @@ bytesToUtf8(bytes) { | ||
}, | ||
hexToBytes, | ||
concatBytes, | ||
}; | ||
const aes = { encrypt, decrypt }; | ||
export default aes; |
110
index.ts
@@ -1,37 +0,14 @@ | ||
import * as nodeCrypto from 'crypto'; | ||
// Global symbol available in browsers only. Ensure we do not depend on @types/dom | ||
declare const self: Record<string, any> | undefined; | ||
const crypto = { | ||
node: nodeCrypto, | ||
web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined, | ||
}; | ||
// Caching slows it down 2-3x | ||
function hexToBytes(hex: string): Uint8Array { | ||
if (typeof hex !== 'string') { | ||
throw new TypeError('hexToBytes: expected string, got ' + typeof hex); | ||
} | ||
if (hex.length % 2) throw new Error('hexToBytes: received invalid unpadded hex' + hex.length); | ||
const array = new Uint8Array(hex.length / 2); | ||
for (let i = 0; i < array.length; i++) { | ||
const j = i * 2; | ||
const hexByte = hex.slice(j, j + 2); | ||
const byte = Number.parseInt(hexByte, 16); | ||
if (Number.isNaN(byte) || byte < 0) throw new Error('Invalid byte sequence'); | ||
array[i] = byte; | ||
} | ||
return array; | ||
} | ||
declare const globalThis: Record<string, any> | undefined; // Typescript symbol present in browsers | ||
const cr = () => | ||
// We support: 1) browsers 2) node.js 19+ | ||
typeof globalThis === 'object' && 'crypto' in globalThis ? globalThis.crypto : undefined; | ||
// Concatenates several Uint8Arrays into one. | ||
// TODO: check if we're copying data instead of moving it and if that's ok | ||
function concatBytes(...arrays: Uint8Array[]): Uint8Array { | ||
if (!arrays.every((arr) => arr instanceof Uint8Array)) | ||
throw new Error('Uint8Array list expected'); | ||
if (arrays.length === 1) return arrays[0]; | ||
if (arrays.length === 1) return arrays[0]!; | ||
const length = arrays.reduce((a, arr) => a + arr.length, 0); | ||
const result = new Uint8Array(length); | ||
for (let i = 0, pad = 0; i < arrays.length; i++) { | ||
const arr = arrays[i]; | ||
const arr = arrays[i]!; | ||
result.set(arr, pad); | ||
@@ -45,42 +22,30 @@ pad += arr.length; | ||
export async function encrypt(sharedKey: Uint8Array, plaintext: string | Uint8Array) { | ||
if (typeof plaintext === 'string') plaintext = utils.utf8ToBytes(plaintext); | ||
function ensureBytes(b: any, len?: number) { | ||
if (!(b instanceof Uint8Array)) throw new Error('Uint8Array expected'); | ||
if (typeof len === 'number') | ||
if (b.length !== len) throw new Error(`Uint8Array length ${len} expected`); | ||
} | ||
function ensureCrypto() { | ||
if (!cr()) throw new Error('globalThis.crypto is not available: use nodejs 19+ or browser'); | ||
} | ||
export async function encrypt(sharedKey: Uint8Array, plaintext: Uint8Array) { | ||
ensureCrypto(); | ||
ensureBytes(sharedKey, 32); | ||
ensureBytes(plaintext); | ||
const iv = utils.randomBytes(12); | ||
if (crypto.web) { | ||
const iKey = await crypto.web.subtle.importKey('raw', sharedKey, MD.i, true, ['encrypt']); | ||
const cipher = await crypto.web.subtle.encrypt({ name: MD.e, iv }, iKey, plaintext); | ||
const ciphertext = new Uint8Array(cipher); | ||
const encrypted = new Uint8Array(iv.length + ciphertext.byteLength); | ||
encrypted.set(iv, 0); | ||
encrypted.set(ciphertext, iv.length); | ||
return encrypted; | ||
} else { | ||
const cipher = crypto.node.createCipheriv('aes-256-gcm', sharedKey, iv); | ||
let ciphertext = cipher.update(plaintext, undefined, 'hex'); | ||
ciphertext += cipher.final('hex'); | ||
const ciphertextBytes = hexToBytes(ciphertext); | ||
const tag = cipher.getAuthTag(); | ||
const encrypted = concatBytes(iv, ciphertextBytes, tag); | ||
return encrypted; | ||
} | ||
const iKey = await cr().subtle.importKey('raw', sharedKey, MD.i, true, ['encrypt']); | ||
const cipher = await cr().subtle.encrypt({ name: MD.e, iv }, iKey, plaintext); | ||
return concatBytes(iv, new Uint8Array(cipher)); | ||
} | ||
export async function decrypt(sharedKey: Uint8Array, encoded: string | Uint8Array) { | ||
if (typeof encoded === 'string') encoded = hexToBytes(encoded); | ||
const iv = encoded.slice(0, 12); | ||
if (crypto.web) { | ||
const ciphertextWithTag = encoded.slice(12); | ||
const iKey = await crypto.web.subtle.importKey('raw', sharedKey, MD.i, true, ['decrypt']); | ||
const plaintext = await crypto.web.subtle.decrypt({ name: MD.e, iv }, iKey, ciphertextWithTag); | ||
return new Uint8Array(plaintext); | ||
} else { | ||
const ciphertext = encoded.slice(12, -16); | ||
const authTag = encoded.slice(-16); | ||
const decipher = crypto.node.createDecipheriv('aes-256-gcm', sharedKey, iv); | ||
decipher.setAuthTag(authTag); | ||
const plaintext = decipher.update(ciphertext); | ||
const final = Uint8Array.from(decipher.final()); | ||
const res = concatBytes(plaintext, final); | ||
return res; | ||
} | ||
export async function decrypt(sharedKey: Uint8Array, ciphertext: Uint8Array) { | ||
ensureCrypto(); | ||
ensureBytes(sharedKey, 32); | ||
ensureBytes(ciphertext); | ||
const iv = ciphertext.slice(0, 12); | ||
const ciphertextWithTag = ciphertext.slice(12); | ||
const iKey = await cr().subtle.importKey('raw', sharedKey, MD.i, true, ['decrypt']); | ||
const plaintext = await cr().subtle.decrypt({ name: MD.e, iv }, iKey, ciphertextWithTag); | ||
return new Uint8Array(plaintext); | ||
} | ||
@@ -93,10 +58,3 @@ | ||
randomBytes: (bytesLength = 32) => { | ||
if (crypto.web) { | ||
return crypto.web.getRandomValues(new Uint8Array(bytesLength)); | ||
} else if (crypto.node) { | ||
const { randomBytes } = crypto.node; | ||
return Uint8Array.from(randomBytes(bytesLength)); | ||
} else { | ||
throw new Error("The environment doesn't have randomBytes function"); | ||
} | ||
return cr().getRandomValues(new Uint8Array(bytesLength)); | ||
}, | ||
@@ -109,7 +67,3 @@ bytesToUtf8(bytes: Uint8Array): string { | ||
}, | ||
hexToBytes, | ||
concatBytes, | ||
}; | ||
const aes = { encrypt, decrypt }; | ||
export default aes; |
{ | ||
"name": "micro-aes-gcm", | ||
"version": "0.3.3", | ||
"description": "Simple 0-dep wrapper over node.js/browser AES-GCM.", | ||
"version": "0.4.0", | ||
"description": "0-dep wrapper around webcrypto AES-GCM. Has optional RFC 8452 SIV implementation", | ||
"main": "index.js", | ||
@@ -12,9 +12,11 @@ "module": "index.js", | ||
"index.d.ts.map", | ||
"index.ts" | ||
"index.ts", | ||
"siv.js", | ||
"siv.d.ts", | ||
"siv.d.ts.map", | ||
"siv.ts" | ||
], | ||
"browser": { | ||
"crypto": false | ||
}, | ||
"scripts": { | ||
"build": "tsc", | ||
"lint": "prettier --check index.ts", | ||
"test": "node test.mjs" | ||
@@ -30,23 +32,20 @@ }, | ||
"devDependencies": { | ||
"@types/node": "18.0.0", | ||
"micro-should": "~0.1.0", | ||
"typescript": "4.7.3" | ||
"@types/node": "18.11.18", | ||
"micro-bmark": "0.3.0", | ||
"micro-should": "0.4.0", | ||
"prettier": "2.8.4", | ||
"typescript": "5.0.2" | ||
}, | ||
"keywords": [ | ||
"secretbox", | ||
"box", | ||
"nacl", | ||
"aes", | ||
"aes-gcm", | ||
"gcm", | ||
"symmetric encryption", | ||
"symmetric", | ||
"mac", | ||
"gmac", | ||
"aead", | ||
"authenticated", | ||
"authentication", | ||
"security", | ||
"noble" | ||
"crypto", | ||
"cryptography", | ||
"siv", | ||
"nonce", | ||
"misuse", | ||
"RFC 8452", | ||
"RFC8452" | ||
] | ||
} |
# micro-aes-gcm | ||
Authenticated data encryption with AES-GCM. Allows to encrypt arbitrary data in a cryptographically secure & modern way. | ||
0-dep wrapper around webcrypto AES-GCM. Has optional RFC 8452 SIV implementation. | ||
A simple wrapper over node.js and browser aes-gcm implementations. No dependencies. | ||
Can be imported in browsers as ESM import. For Node.js, v19+ is required. For older node.js, use shim: `globalThis.crypto = require('node.crypto').webcrypto`. | ||
Inserts IV and MAC into the output `iv + ciphertext + mac`: | ||
- `iv` is 12 bytes; it's an CSPRNG-sourced initialization vector for AES-GCM mode. | ||
- `ciphertext` length depends on plaintext | ||
- `mac` is 16 bytes; AES-GCM calculates this authentication tag for us. | ||
- `const c = await encrypt(key, plaintext), iv = c.slice(0, 12), mac = c.slice(-16);` | ||
Has optional implementation of AES-GCM-SIV RFC 8452 nonce-misuse resistance in a separate file. | ||
## Usage | ||
@@ -12,59 +21,35 @@ | ||
```js | ||
import * as aes from "micro-aes-gcm"; | ||
import * as aes from 'micro-aes-gcm'; | ||
const key = Uint8Array.from([ | ||
64, 196, 127, 247, 172, 2, 34, | ||
159, 6, 241, 30, 174, 183, 229, | ||
41, 114, 253, 122, 119, 168, 177, | ||
243, 155, 236, 164, 159, 98, 72, | ||
162, 243, 224, 195 | ||
64, 196, 127, 247, 172, 2, 34, 159, 6, 241, 30, 174, 183, 229, 41, 114, 253, 122, 119, 168, 177, | ||
243, 155, 236, 164, 159, 98, 72, 162, 243, 224, 195, | ||
]); | ||
const plaintext = "Hello world"; | ||
const ciphertext = await aes.encrypt(key, message); | ||
const message = 'Hello world'; | ||
const ciphertext = await aes.encrypt(key, aes.utils.utf8ToBytes(message)); | ||
const plaintext = await aes.decrypt(key, ciphertext); | ||
console.log(aes.toUTF8(plaintext) === message); | ||
console.log(aes.utils.bytesToUtf8(plaintext) === message); | ||
// Also works in browsers | ||
``` | ||
## API | ||
API is: | ||
```typescript | ||
function encrypt(key: Uint8Array, plaintext: Uint8Array|string): Promise<Uint8Array>; | ||
``` | ||
`plaintext` in `encrypt` can be either a Uint8Array, or a string. If it's a string, | ||
`new TextDecoder().encode(plaintext)` would be executed before passing it further. | ||
```typescript | ||
function encrypt(key: Uint8Array, plaintext: Uint8Array): Promise<Uint8Array>; | ||
function decrypt(key: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array>; | ||
``` | ||
Note that `decrypt` always returns `Uint8Array`. If you've encrypted UTF-8 string, | ||
`toUTF8(result)` should be enough to get it back. | ||
### SIV | ||
## Internals | ||
Secretbox receives one key, and one plaintext. | ||
The output format is: `iv + ciphertext + mac`: | ||
- `iv` is 12 bytes; it's an initialization vector for AES-GCM mode. | ||
- `ciphertext` length depends on plaintext | ||
- `mac` is 16 bytes; AES-GCM calculates this authentication tag for us. | ||
To slice through IV and MAC, you can use `Uint8Array.prototype.slice()`: | ||
```js | ||
const ciphertext = await encrypt(key, plaintext); | ||
const iv = ciphertext.slice(0, 12); | ||
const mac = ciphertext.slice(-16); | ||
```ts | ||
import { AES } from 'micro-aes-gcm/siv.js'; | ||
const cr = await aes(KEY, NONCE); | ||
await cr.encrypt(buf, AAD), | ||
``` | ||
## Notes | ||
[AES-GCM-SIV](https://en.wikipedia.org/wiki/AES-GCM-SIV) is designed to preserve both privacy and integrity even if nonces are repeated. To accomplish this, encryption is a function of a nonce, the plaintext message, and optional additional associated data (AAD). In the event a nonce is misused (i.e. used more than once), nothing is revealed except in the case that same message is encrypted multiple times with the same nonce. When that happens, an attacker is able to observe repeat encryptions, since encryption is a deterministic function of the nonce and message. However, beyond that, no additional information is revealed to the attacker. For this reason, AES-GCM-SIV is an ideal choice in cases that unique nonces cannot be guaranteed, such as multiple servers or network devices encrypting messages under the same key without coordination. | ||
DJB's [secretbox](https://nacl.cr.yp.to/secretbox.html) uses XSalsa20-Poly1305. We'll use AES-GCM, which is also a good choice. DJB mentioned the AES box in his TODOs. | ||
AEADs that can withstand nonce duplication are called [“nonce-misuse resistant”](https://www.imperialviolet.org/2017/05/14/aesgcmsiv.html) and that name appears to have caused some people to believe that they are infinitely resistant. I.e. that an unlimited number of messages can be encrypted with a fixed nonce with no loss in security. That is not the case, and the term wasn't defined that way originally by Rogaway and Shrimpton (nor does their SIV mode have that property). So it's important to emphasise that AES-GCM-SIV (and nonce-misuse resistant modes in general) are not a magic invulnerability shield. Figure four and section five of the the paper give precise bounds but, if in doubt, consider AES-GCM-SIV to be a safety net for accidental nonce duplication and otherwise treat it like a traditional AEAD. | ||
AES has been selected over Salsa, because it's natively implemented in Node & browsers and doesn't require any 3rd-party libraries. | ||
## License | ||
MIT (c) Paul Miller [(https://paulmillr.com)](https://paulmillr.com), see LICENSE file. |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
29921
11
587
5
55
1