@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 CHANGED
@@ -28,15 +28,27 @@ every value sealed with it.
28
28
 
29
29
  ## API
30
30
 
31
- | Function | Returns | Notes |
32
- | ----------------------- | --------- | ------------------------------------------------------ |
33
- | `encrypt(plaintext)` | `string` | `v1.<iv>.<tag>.<data>`, base64url, fresh IV every call |
34
- | `decrypt(sealed)` | `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 |
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 declare function encrypt(plaintext: string): string;
2
- export declare function decrypt(sealed: string): string;
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.0.0",
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": {