Skip to content

Commit ad25c15

Browse files
authored
encrypt-web - feat: adding new web crypto module (#1928)
* encrypt-web - feat: adding new web crypto module * Update test.ts * moving to web * fix with slice * jsDocs
1 parent e0ce437 commit ad25c15

11 files changed

Lines changed: 752 additions & 21 deletions

File tree

encryption/encrypt-node/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# @keyv/encrypt-node [<img width="100" align="right" src="https://jaredwray.com/images/keyv-symbol.svg" alt="keyv">](https://github.com/jaredwray/keyv)
2+
3+
> Node.js crypto encryption for Keyv
4+
5+
[![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
6+
[![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv)
7+
[![npm](https://img.shields.io/npm/v/@keyv/encrypt-node.svg)](https://www.npmjs.com/package/@keyv/encrypt-node)
8+
[![npm](https://img.shields.io/npm/dm/@keyv/encrypt-node)](https://npmjs.com/package/@keyv/encrypt-node)
9+
10+
Encrypt and decrypt values stored in [Keyv](https://github.com/jaredwray/keyv) using the Node.js `crypto` module. Supports AES-GCM (default), AES-CCM, ChaCha20-Poly1305, AES-CBC, and any cipher available in your Node.js installation.
11+
12+
## Install
13+
14+
```shell
15+
npm install --save keyv @keyv/encrypt-node
16+
```
17+
18+
## Usage
19+
20+
```javascript
21+
import Keyv from 'keyv';
22+
import KeyvEncryptNode from '@keyv/encrypt-node';
23+
24+
const encryption = new KeyvEncryptNode({ key: 'your-secret-key' });
25+
const keyv = new Keyv({ encryption });
26+
27+
await keyv.set('foo', 'bar');
28+
const value = await keyv.get('foo'); // 'bar' (decrypted automatically)
29+
```
30+
31+
## API
32+
33+
### new KeyvEncryptNode(options)
34+
35+
#### options.key
36+
37+
Type: `string | Buffer`\
38+
**Required**
39+
40+
The encryption key. String keys are hashed with SHA-256 and truncated to the required length for the algorithm. Buffer keys are used directly and must match the expected key length.
41+
42+
#### options.algorithm
43+
44+
Type: `string`\
45+
Default: `'aes-256-gcm'`
46+
47+
The cipher algorithm to use. Supports any algorithm available via Node.js `crypto.getCipherInfo()`, including:
48+
49+
- `aes-256-gcm`, `aes-192-gcm`, `aes-128-gcm` (AEAD)
50+
- `aes-256-ccm`, `aes-192-ccm`, `aes-128-ccm` (AEAD)
51+
- `chacha20-poly1305` (AEAD)
52+
- `aes-256-cbc`, `aes-192-cbc`, `aes-128-cbc`
53+
54+
#### options.encoding
55+
56+
Type: `BufferEncoding`\
57+
Default: `'base64'`
58+
59+
The encoding used for the encrypted output string. Common options: `'base64'`, `'hex'`.
60+
61+
## Cross-Compatibility
62+
63+
Data encrypted with `@keyv/encrypt-node` using AES-GCM or AES-CBC can be decrypted by `@keyv/encrypt-web` (and vice versa) when using the same key and algorithm. Both packages use the same wire format:
64+
65+
- **AES-GCM**: `base64([IV (12 bytes) || AuthTag (16 bytes) || Ciphertext])`
66+
- **AES-CBC**: `base64([IV (16 bytes) || Ciphertext])`
67+
68+
## License
69+
70+
[MIT © Jared Wray](LICENSE)

encryption/encrypt-node/src/index.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,39 @@ const CCM_MODES = new Set(["ccm"]);
1414

1515
const AUTH_TAG_LENGTH = 16;
1616

17+
/**
18+
* Options for {@link KeyvEncryptNode}.
19+
*/
1720
export type KeyvEncryptNodeOptions = {
18-
/** Encryption key. Strings are hashed with SHA-256 to derive a 32-byte key. Buffers are used directly. */
21+
/** Encryption key. Strings are hashed with SHA-256 and truncated to the required length. Buffers are used directly and must match the algorithm's key length. */
1922
key: string | Buffer;
20-
/** Cipher algorithm to use. Default: "aes-256-gcm". */
23+
/** Cipher algorithm to use. Any algorithm supported by Node.js `crypto.getCipherInfo()`. @defaultValue `"aes-256-gcm"` */
2124
algorithm?: string;
22-
/** Output encoding for the encrypted string. Default: "base64". */
25+
/** Output encoding for the encrypted string. @defaultValue `"base64"` */
2326
encoding?: BufferEncoding;
2427
};
2528

29+
/**
30+
* Node.js `crypto`-based encryption adapter for Keyv.
31+
*
32+
* Encrypts and decrypts string values using any cipher supported by the
33+
* Node.js `crypto` module. Defaults to AES-256-GCM with authenticated
34+
* encryption. The encrypted output is a base64 string containing the IV,
35+
* authentication tag (for AEAD ciphers), and ciphertext.
36+
*
37+
* Wire format (AEAD): `[IV || AuthTag (16 bytes) || Ciphertext]`
38+
* Wire format (non-AEAD): `[IV || Ciphertext]`
39+
*
40+
* @example
41+
* ```ts
42+
* import Keyv from "keyv";
43+
* import KeyvEncryptNode from "@keyv/encrypt-node";
44+
*
45+
* const encryption = new KeyvEncryptNode({ key: "my-secret" });
46+
* const keyv = new Keyv({ encryption });
47+
* await keyv.set("foo", "bar");
48+
* ```
49+
*/
2650
export class KeyvEncryptNode implements KeyvEncryptionAdapter {
2751
private readonly _key: Buffer;
2852
private readonly _algorithm: string;
@@ -31,6 +55,12 @@ export class KeyvEncryptNode implements KeyvEncryptionAdapter {
3155
private readonly _isAead: boolean;
3256
private readonly _isCcm: boolean;
3357

58+
/**
59+
* Creates a new encryption adapter.
60+
* @param options - Configuration options including key, algorithm, and encoding.
61+
* @throws If the algorithm is not supported by Node.js crypto.
62+
* @throws If a Buffer key does not match the expected length for the algorithm.
63+
*/
3464
constructor(options: KeyvEncryptNodeOptions) {
3565
this._algorithm = (options.algorithm ?? "aes-256-gcm").toLowerCase();
3666
this._encoding = options.encoding ?? "base64";
@@ -57,6 +87,11 @@ export class KeyvEncryptNode implements KeyvEncryptionAdapter {
5787
}
5888
}
5989

90+
/**
91+
* Encrypts a plaintext string.
92+
* @param data - The plaintext string to encrypt.
93+
* @returns The encrypted string encoded with the configured encoding.
94+
*/
6095
encrypt(data: string): string {
6196
const iv = randomBytes(this._ivLength);
6297
const cipherOptions = this._isCcm
@@ -81,6 +116,13 @@ export class KeyvEncryptNode implements KeyvEncryptionAdapter {
81116
return packed.toString(this._encoding);
82117
}
83118

119+
/**
120+
* Decrypts an encrypted string back to its original plaintext.
121+
* @param data - The encrypted string to decrypt.
122+
* @returns The original plaintext string.
123+
* @throws If the ciphertext has been tampered with (AEAD modes).
124+
* @throws If the wrong key is used for decryption.
125+
*/
84126
decrypt(data: string): string {
85127
const packed = Buffer.from(data, this._encoding);
86128

encryption/encrypt-web/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2021-2026 Jared Wray
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

encryption/encrypt-web/README.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# @keyv/encrypt-web [<img width="100" align="right" src="https://jaredwray.com/images/keyv-symbol.svg" alt="keyv">](https://github.com/jaredwray/keyv)
2+
3+
> Web Crypto API encryption for Keyv
4+
5+
[![build](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml/badge.svg)](https://github.com/jaredwray/keyv/actions/workflows/tests.yaml)
6+
[![codecov](https://codecov.io/gh/jaredwray/keyv/branch/main/graph/badge.svg?token=bRzR3RyOXZ)](https://codecov.io/gh/jaredwray/keyv)
7+
[![npm](https://img.shields.io/npm/v/@keyv/encrypt-web.svg)](https://www.npmjs.com/package/@keyv/encrypt-web)
8+
[![npm](https://img.shields.io/npm/dm/@keyv/encrypt-web)](https://npmjs.com/package/@keyv/encrypt-web)
9+
10+
Encrypt and decrypt values stored in [Keyv](https://github.com/jaredwray/keyv) using the Web Crypto API (`crypto.subtle`). Works in browsers, Deno, Cloudflare Workers, and Node.js 18+. No Node.js-specific dependencies.
11+
12+
## Install
13+
14+
```shell
15+
npm install --save keyv @keyv/encrypt-web
16+
```
17+
18+
## Usage
19+
20+
```javascript
21+
import Keyv from 'keyv';
22+
import KeyvEncryptWeb from '@keyv/encrypt-web';
23+
24+
const encryption = new KeyvEncryptWeb({ key: 'your-secret-key' });
25+
const keyv = new Keyv({ encryption });
26+
27+
await keyv.set('foo', 'bar');
28+
const value = await keyv.get('foo'); // 'bar' (decrypted automatically)
29+
```
30+
31+
## API
32+
33+
### new KeyvEncryptWeb(options)
34+
35+
#### options.key
36+
37+
Type: `string | Uint8Array`\
38+
**Required**
39+
40+
The encryption key. String keys are hashed with SHA-256 and truncated to the required length for the algorithm. Uint8Array keys are used directly and must match the expected key length.
41+
42+
#### options.algorithm
43+
44+
Type: `WebAlgorithm`\
45+
Default: `'aes-256-gcm'`
46+
47+
The cipher algorithm to use. Supported values:
48+
49+
- `aes-256-gcm`, `aes-192-gcm`, `aes-128-gcm` (AEAD, recommended)
50+
- `aes-256-cbc`, `aes-192-cbc`, `aes-128-cbc`
51+
52+
## Cross-Compatibility
53+
54+
Data encrypted with `@keyv/encrypt-web` using AES-GCM or AES-CBC can be decrypted by `@keyv/encrypt-node` (and vice versa) when using the same key and algorithm. Both packages use the same wire format:
55+
56+
- **AES-GCM**: `base64([IV (12 bytes) || AuthTag (16 bytes) || Ciphertext])`
57+
- **AES-CBC**: `base64([IV (16 bytes) || Ciphertext])`
58+
59+
## License
60+
61+
[MIT © Jared Wray](LICENSE)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"name": "@keyv/encrypt-web",
3+
"version": "6.0.0-alpha.4",
4+
"description": "Web Crypto API encryption adapter for Keyv",
5+
"type": "module",
6+
"main": "./dist/index.mjs",
7+
"module": "./dist/index.mjs",
8+
"types": "./dist/index.d.mts",
9+
"exports": {
10+
".": {
11+
"require": {
12+
"types": "./dist/index.d.cts",
13+
"default": "./dist/index.cjs"
14+
},
15+
"import": {
16+
"types": "./dist/index.d.mts",
17+
"default": "./dist/index.mjs"
18+
}
19+
}
20+
},
21+
"scripts": {
22+
"build": "tsdown",
23+
"prepublishOnly": "pnpm build",
24+
"lint": "biome check --write --error-on-warnings",
25+
"lint:ci": "biome check --error-on-warnings",
26+
"test": "pnpm lint && vitest run --coverage",
27+
"test:ci": "pnpm lint:ci && vitest --run --sequence.setupFiles=list --coverage",
28+
"clean": "rimraf ./node_modules ./coverage ./dist"
29+
},
30+
"repository": {
31+
"type": "git",
32+
"url": "git+https://github.com/jaredwray/keyv.git"
33+
},
34+
"keywords": [
35+
"encryption",
36+
"web-crypto",
37+
"webcrypto",
38+
"browser",
39+
"keyv",
40+
"storage",
41+
"adapter",
42+
"key",
43+
"value",
44+
"store",
45+
"cache",
46+
"aes",
47+
"gcm"
48+
],
49+
"author": "Jared Wray <me@jaredwray.com> (https://jaredwray.com)",
50+
"license": "MIT",
51+
"bugs": {
52+
"url": "https://github.com/jaredwray/keyv/issues"
53+
},
54+
"homepage": "https://github.com/jaredwray/keyv",
55+
"dependencies": {},
56+
"peerDependencies": {
57+
"keyv": "workspace:^"
58+
},
59+
"devDependencies": {
60+
"@biomejs/biome": "^2.3.13",
61+
"@faker-js/faker": "^9.8.0",
62+
"@vitest/coverage-v8": "^4.0.18",
63+
"rimraf": "^6.1.2",
64+
"vitest": "^4.0.18"
65+
},
66+
"engines": {
67+
"node": ">= 18"
68+
},
69+
"files": [
70+
"dist",
71+
"LICENSE"
72+
]
73+
}

0 commit comments

Comments
 (0)