@ultimatelemon-eu/crypto 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -7
- package/dist/index.d.ts +9 -2
- package/dist/index.js +8 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,15 +28,27 @@ every value sealed with it.
|
|
|
28
28
|
|
|
29
29
|
## API
|
|
30
30
|
|
|
31
|
-
| Function
|
|
32
|
-
|
|
|
33
|
-
| `encrypt(plaintext)`
|
|
34
|
-
| `decrypt(sealed)`
|
|
35
|
-
| `isEncrypted(value)`
|
|
36
|
-
| `randomToken(bytes=32)`
|
|
37
|
-
| `sha256Hex(value)`
|
|
31
|
+
| Function | Returns | Notes |
|
|
32
|
+
| ---------------------------------- | --------- | ------------------------------------------------------ |
|
|
33
|
+
| `encrypt(plaintext, { context? })` | `string` | `v1.<iv>.<tag>.<data>`, base64url, fresh IV every call |
|
|
34
|
+
| `decrypt(sealed, { context? })` | `string` | Throws on tampering, a wrong key or a non-sealed value |
|
|
35
|
+
| `isEncrypted(value)` | `boolean` | Type guard for the `v1.` format |
|
|
36
|
+
| `randomToken(bytes=32)` | `string` | base64url, for invite links, CSRF, state |
|
|
37
|
+
| `sha256Hex(value)` | `string` | Store a token's hash, not the token |
|
|
38
38
|
|
|
39
39
|
`decrypt` also reads the same format written with standard base64, which is
|
|
40
40
|
what Cellar stored before this package.
|
|
41
41
|
|
|
42
42
|
Store sealed values in a `text` column.
|
|
43
|
+
|
|
44
|
+
## Context
|
|
45
|
+
|
|
46
|
+
Pass the row a value belongs to as `context`, and it only decrypts with that
|
|
47
|
+
same context. A sealed password copied into another user's row then fails
|
|
48
|
+
instead of opening their mailbox. The context is not stored; supply it again
|
|
49
|
+
when decrypting.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const sealed = encrypt(password, { context: `${userId}:${accountId}` });
|
|
53
|
+
const password = decrypt(sealed, { context: `${userId}:${accountId}` });
|
|
54
|
+
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
1
|
+
export interface SealOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Bound into the tag but not stored: decrypting needs the same value, so a
|
|
4
|
+
* sealed value copied to another row (another user's id) no longer opens.
|
|
5
|
+
*/
|
|
6
|
+
context?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function encrypt(plaintext: string, options?: SealOptions): string;
|
|
9
|
+
export declare function decrypt(sealed: string, options?: SealOptions): string;
|
|
3
10
|
export declare function isEncrypted(value: unknown): value is string;
|
|
4
11
|
export declare function randomToken(bytes?: number): string;
|
|
5
12
|
export declare function sha256Hex(value: string): string;
|
package/dist/index.js
CHANGED
|
@@ -20,11 +20,14 @@ function key() {
|
|
|
20
20
|
cached = { raw, key };
|
|
21
21
|
return key;
|
|
22
22
|
}
|
|
23
|
-
export function encrypt(plaintext) {
|
|
23
|
+
export function encrypt(plaintext, options = {}) {
|
|
24
24
|
const iv = randomBytes(IV_BYTES);
|
|
25
25
|
const cipher = createCipheriv(ALGORITHM, key(), iv, {
|
|
26
26
|
authTagLength: TAG_BYTES
|
|
27
27
|
});
|
|
28
|
+
if (options.context !== undefined) {
|
|
29
|
+
cipher.setAAD(Buffer.from(options.context, 'utf8'));
|
|
30
|
+
}
|
|
28
31
|
const data = Buffer.concat([
|
|
29
32
|
cipher.update(plaintext, 'utf8'),
|
|
30
33
|
cipher.final()
|
|
@@ -33,7 +36,7 @@ export function encrypt(plaintext) {
|
|
|
33
36
|
return [VERSION, iv, tag, data].map(encode).join('.');
|
|
34
37
|
}
|
|
35
38
|
// Throws when the value was tampered with or sealed under another key.
|
|
36
|
-
export function decrypt(sealed) {
|
|
39
|
+
export function decrypt(sealed, options = {}) {
|
|
37
40
|
if (!isEncrypted(sealed))
|
|
38
41
|
throw new Error('Not an encrypted value');
|
|
39
42
|
const [, iv, tag, data] = sealed.split('.').map(decode);
|
|
@@ -43,6 +46,9 @@ export function decrypt(sealed) {
|
|
|
43
46
|
const decipher = createDecipheriv(ALGORITHM, key(), iv, {
|
|
44
47
|
authTagLength: TAG_BYTES
|
|
45
48
|
});
|
|
49
|
+
if (options.context !== undefined) {
|
|
50
|
+
decipher.setAAD(Buffer.from(options.context, 'utf8'));
|
|
51
|
+
}
|
|
46
52
|
decipher.setAuthTag(tag);
|
|
47
53
|
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
|
|
48
54
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimatelemon-eu/crypto",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "AES-256-GCM encryption for secrets at rest, keyed by ENCRYPTION_KEY, plus random tokens and SHA-256. Node only, no dependencies.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|