@agentuity/cli 0.0.42 → 0.0.44
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/AGENTS.md +1 -1
- package/README.md +1 -1
- package/bin/cli.ts +7 -5
- package/dist/api.d.ts +3 -3
- package/dist/api.d.ts.map +1 -1
- package/dist/auth.d.ts +10 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/banner.d.ts.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cmd/auth/api.d.ts +4 -4
- package/dist/cmd/auth/api.d.ts.map +1 -1
- package/dist/cmd/auth/index.d.ts.map +1 -1
- package/dist/cmd/auth/login.d.ts.map +1 -1
- package/dist/cmd/auth/signup.d.ts.map +1 -1
- package/dist/cmd/auth/ssh/add.d.ts +2 -0
- package/dist/cmd/auth/ssh/add.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/api.d.ts +16 -0
- package/dist/cmd/auth/ssh/api.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/delete.d.ts +2 -0
- package/dist/cmd/auth/ssh/delete.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/index.d.ts +3 -0
- package/dist/cmd/auth/ssh/index.d.ts.map +1 -0
- package/dist/cmd/auth/ssh/list.d.ts +2 -0
- package/dist/cmd/auth/ssh/list.d.ts.map +1 -0
- package/dist/cmd/auth/whoami.d.ts +2 -0
- package/dist/cmd/auth/whoami.d.ts.map +1 -0
- package/dist/cmd/bundle/ast.d.ts +14 -3
- package/dist/cmd/bundle/ast.d.ts.map +1 -1
- package/dist/cmd/bundle/ast.test.d.ts +2 -0
- package/dist/cmd/bundle/ast.test.d.ts.map +1 -0
- package/dist/cmd/bundle/bundler.d.ts +6 -1
- package/dist/cmd/bundle/bundler.d.ts.map +1 -1
- package/dist/cmd/bundle/file.d.ts.map +1 -1
- package/dist/cmd/bundle/fix-duplicate-exports.d.ts +2 -0
- package/dist/cmd/bundle/fix-duplicate-exports.d.ts.map +1 -0
- package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts +2 -0
- package/dist/cmd/bundle/fix-duplicate-exports.test.d.ts.map +1 -0
- package/dist/cmd/bundle/index.d.ts +1 -1
- package/dist/cmd/bundle/index.d.ts.map +1 -1
- package/dist/cmd/bundle/plugin.d.ts +2 -0
- package/dist/cmd/bundle/plugin.d.ts.map +1 -1
- package/dist/cmd/cloud/deploy.d.ts.map +1 -0
- package/dist/cmd/cloud/domain.d.ts +17 -0
- package/dist/cmd/cloud/domain.d.ts.map +1 -0
- package/dist/cmd/cloud/index.d.ts.map +1 -0
- package/dist/cmd/cloud/resource/add.d.ts +2 -0
- package/dist/cmd/cloud/resource/add.d.ts.map +1 -0
- package/dist/cmd/cloud/resource/delete.d.ts +2 -0
- package/dist/cmd/cloud/resource/delete.d.ts.map +1 -0
- package/dist/cmd/cloud/resource/index.d.ts +3 -0
- package/dist/cmd/cloud/resource/index.d.ts.map +1 -0
- package/dist/cmd/cloud/resource/list.d.ts +2 -0
- package/dist/cmd/cloud/resource/list.d.ts.map +1 -0
- package/dist/cmd/cloud/scp/download.d.ts +2 -0
- package/dist/cmd/cloud/scp/download.d.ts.map +1 -0
- package/dist/cmd/cloud/scp/index.d.ts +3 -0
- package/dist/cmd/cloud/scp/index.d.ts.map +1 -0
- package/dist/cmd/cloud/scp/upload.d.ts +2 -0
- package/dist/cmd/cloud/scp/upload.d.ts.map +1 -0
- package/dist/cmd/cloud/ssh.d.ts +2 -0
- package/dist/cmd/cloud/ssh.d.ts.map +1 -0
- package/dist/cmd/dev/api.d.ts +18 -0
- package/dist/cmd/dev/api.d.ts.map +1 -0
- package/dist/cmd/dev/download.d.ts +11 -0
- package/dist/cmd/dev/download.d.ts.map +1 -0
- package/dist/cmd/dev/index.d.ts.map +1 -1
- package/dist/cmd/dev/templates.d.ts +3 -0
- package/dist/cmd/dev/templates.d.ts.map +1 -0
- package/dist/cmd/env/delete.d.ts +2 -0
- package/dist/cmd/env/delete.d.ts.map +1 -0
- package/dist/cmd/env/get.d.ts +2 -0
- package/dist/cmd/env/get.d.ts.map +1 -0
- package/dist/cmd/env/import.d.ts +2 -0
- package/dist/cmd/env/import.d.ts.map +1 -0
- package/dist/cmd/env/index.d.ts +2 -0
- package/dist/cmd/env/index.d.ts.map +1 -0
- package/dist/cmd/env/list.d.ts.map +1 -0
- package/dist/cmd/env/pull.d.ts +2 -0
- package/dist/cmd/env/pull.d.ts.map +1 -0
- package/dist/cmd/env/push.d.ts +2 -0
- package/dist/cmd/env/push.d.ts.map +1 -0
- package/dist/cmd/env/set.d.ts +2 -0
- package/dist/cmd/env/set.d.ts.map +1 -0
- package/dist/cmd/profile/show.d.ts.map +1 -1
- package/dist/cmd/project/create.d.ts.map +1 -1
- package/dist/cmd/project/delete.d.ts.map +1 -1
- package/dist/cmd/project/download.d.ts +1 -1
- package/dist/cmd/project/download.d.ts.map +1 -1
- package/dist/cmd/project/list.d.ts.map +1 -1
- package/dist/cmd/project/show.d.ts.map +1 -1
- package/dist/cmd/project/template-flow.d.ts +5 -1
- package/dist/cmd/project/template-flow.d.ts.map +1 -1
- package/dist/cmd/secret/delete.d.ts +2 -0
- package/dist/cmd/secret/delete.d.ts.map +1 -0
- package/dist/cmd/secret/get.d.ts +2 -0
- package/dist/cmd/secret/get.d.ts.map +1 -0
- package/dist/cmd/secret/import.d.ts +2 -0
- package/dist/cmd/secret/import.d.ts.map +1 -0
- package/dist/cmd/secret/index.d.ts +2 -0
- package/dist/cmd/secret/index.d.ts.map +1 -0
- package/dist/cmd/secret/list.d.ts +2 -0
- package/dist/cmd/secret/list.d.ts.map +1 -0
- package/dist/cmd/secret/pull.d.ts +2 -0
- package/dist/cmd/secret/pull.d.ts.map +1 -0
- package/dist/cmd/secret/push.d.ts +2 -0
- package/dist/cmd/secret/push.d.ts.map +1 -0
- package/dist/cmd/secret/set.d.ts +2 -0
- package/dist/cmd/secret/set.d.ts.map +1 -0
- package/dist/cmd/version/index.d.ts.map +1 -1
- package/dist/config.d.ts +11 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/crypto/box.d.ts +65 -0
- package/dist/crypto/box.d.ts.map +1 -0
- package/dist/crypto/box.test.d.ts +2 -0
- package/dist/crypto/box.test.d.ts.map +1 -0
- package/dist/download.d.ts.map +1 -1
- package/dist/env-util.d.ts +67 -0
- package/dist/env-util.d.ts.map +1 -0
- package/dist/env-util.test.d.ts +2 -0
- package/dist/env-util.test.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/schema-parser.d.ts.map +1 -1
- package/dist/steps.d.ts +4 -1
- package/dist/steps.d.ts.map +1 -1
- package/dist/terminal.d.ts.map +1 -1
- package/dist/tui.d.ts +32 -2
- package/dist/tui.d.ts.map +1 -1
- package/dist/types.d.ts +250 -127
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/detectSubagent.d.ts +15 -0
- package/dist/utils/detectSubagent.d.ts.map +1 -0
- package/dist/utils/zip.d.ts +7 -0
- package/dist/utils/zip.d.ts.map +1 -0
- package/package.json +11 -3
- package/src/api-errors.md +2 -2
- package/src/api.ts +12 -7
- package/src/auth.ts +116 -7
- package/src/banner.ts +13 -6
- package/src/cli.ts +709 -36
- package/src/cmd/auth/api.ts +10 -16
- package/src/cmd/auth/index.ts +3 -1
- package/src/cmd/auth/login.ts +24 -8
- package/src/cmd/auth/signup.ts +15 -11
- package/src/cmd/auth/ssh/add.ts +263 -0
- package/src/cmd/auth/ssh/api.ts +94 -0
- package/src/cmd/auth/ssh/delete.ts +102 -0
- package/src/cmd/auth/ssh/index.ts +10 -0
- package/src/cmd/auth/ssh/list.ts +74 -0
- package/src/cmd/auth/whoami.ts +69 -0
- package/src/cmd/bundle/ast.test.ts +565 -0
- package/src/cmd/bundle/ast.ts +457 -44
- package/src/cmd/bundle/bundler.ts +255 -57
- package/src/cmd/bundle/file.ts +6 -12
- package/src/cmd/bundle/fix-duplicate-exports.test.ts +387 -0
- package/src/cmd/bundle/fix-duplicate-exports.ts +204 -0
- package/src/cmd/bundle/index.ts +11 -11
- package/src/cmd/bundle/patch/aisdk.ts +1 -1
- package/src/cmd/bundle/plugin.ts +373 -53
- package/src/cmd/cloud/deploy.ts +336 -0
- package/src/cmd/cloud/domain.ts +92 -0
- package/src/cmd/cloud/index.ts +11 -0
- package/src/cmd/cloud/resource/add.ts +56 -0
- package/src/cmd/cloud/resource/delete.ts +120 -0
- package/src/cmd/cloud/resource/index.ts +11 -0
- package/src/cmd/cloud/resource/list.ts +69 -0
- package/src/cmd/cloud/scp/download.ts +59 -0
- package/src/cmd/cloud/scp/index.ts +9 -0
- package/src/cmd/cloud/scp/upload.ts +62 -0
- package/src/cmd/cloud/ssh.ts +68 -0
- package/src/cmd/dev/api.ts +46 -0
- package/src/cmd/dev/download.ts +111 -0
- package/src/cmd/dev/index.ts +362 -34
- package/src/cmd/dev/templates.ts +84 -0
- package/src/cmd/env/delete.ts +47 -0
- package/src/cmd/env/get.ts +53 -0
- package/src/cmd/env/import.ts +102 -0
- package/src/cmd/env/index.ts +22 -0
- package/src/cmd/env/list.ts +56 -0
- package/src/cmd/env/pull.ts +80 -0
- package/src/cmd/env/push.ts +37 -0
- package/src/cmd/env/set.ts +71 -0
- package/src/cmd/index.ts +2 -2
- package/src/cmd/profile/show.ts +15 -6
- package/src/cmd/project/create.ts +7 -2
- package/src/cmd/project/delete.ts +75 -18
- package/src/cmd/project/download.ts +3 -3
- package/src/cmd/project/list.ts +8 -8
- package/src/cmd/project/show.ts +3 -7
- package/src/cmd/project/template-flow.ts +186 -48
- package/src/cmd/secret/delete.ts +40 -0
- package/src/cmd/secret/get.ts +54 -0
- package/src/cmd/secret/import.ts +64 -0
- package/src/cmd/secret/index.ts +22 -0
- package/src/cmd/secret/list.ts +56 -0
- package/src/cmd/secret/pull.ts +78 -0
- package/src/cmd/secret/push.ts +37 -0
- package/src/cmd/secret/set.ts +45 -0
- package/src/cmd/version/index.ts +2 -1
- package/src/config.ts +257 -27
- package/src/crypto/box.test.ts +431 -0
- package/src/crypto/box.ts +477 -0
- package/src/download.ts +1 -0
- package/src/env-util.test.ts +194 -0
- package/src/env-util.ts +290 -0
- package/src/index.ts +5 -1
- package/src/schema-parser.ts +2 -3
- package/src/steps.ts +144 -10
- package/src/terminal.ts +24 -23
- package/src/tui.ts +208 -68
- package/src/types.ts +292 -202
- package/src/utils/detectSubagent.ts +31 -0
- package/src/utils/zip.ts +38 -0
- package/dist/cmd/example/create-user.d.ts +0 -2
- package/dist/cmd/example/create-user.d.ts.map +0 -1
- package/dist/cmd/example/create.d.ts +0 -2
- package/dist/cmd/example/create.d.ts.map +0 -1
- package/dist/cmd/example/deploy.d.ts.map +0 -1
- package/dist/cmd/example/index.d.ts.map +0 -1
- package/dist/cmd/example/list.d.ts.map +0 -1
- package/dist/cmd/example/optional-auth.d.ts +0 -3
- package/dist/cmd/example/optional-auth.d.ts.map +0 -1
- package/dist/cmd/example/run-command.d.ts +0 -2
- package/dist/cmd/example/run-command.d.ts.map +0 -1
- package/dist/cmd/example/sound.d.ts +0 -3
- package/dist/cmd/example/sound.d.ts.map +0 -1
- package/dist/cmd/example/spinner.d.ts +0 -2
- package/dist/cmd/example/spinner.d.ts.map +0 -1
- package/dist/cmd/example/steps.d.ts +0 -2
- package/dist/cmd/example/steps.d.ts.map +0 -1
- package/dist/cmd/example/version.d.ts +0 -2
- package/dist/cmd/example/version.d.ts.map +0 -1
- package/dist/logger.d.ts +0 -24
- package/dist/logger.d.ts.map +0 -1
- package/src/cmd/example/create-user.ts +0 -38
- package/src/cmd/example/create.ts +0 -31
- package/src/cmd/example/deploy.ts +0 -36
- package/src/cmd/example/index.ts +0 -29
- package/src/cmd/example/list.ts +0 -32
- package/src/cmd/example/optional-auth.ts +0 -38
- package/src/cmd/example/run-command.ts +0 -45
- package/src/cmd/example/sound.ts +0 -14
- package/src/cmd/example/spinner.ts +0 -44
- package/src/cmd/example/steps.ts +0 -66
- package/src/cmd/example/version.ts +0 -13
- package/src/logger.ts +0 -235
- /package/dist/cmd/{example → cloud}/deploy.d.ts +0 -0
- /package/dist/cmd/{example → cloud}/index.d.ts +0 -0
- /package/dist/cmd/{example → env}/list.d.ts +0 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package crypto implements a **FIPS 140-3 compliant KEM-DEM envelope encryption scheme**
|
|
3
|
+
* suitable for multi-gigabyte streams using ECDH P-256 and AES-256-GCM.
|
|
4
|
+
* This design is compatible with the Go implementation and depends only on standard
|
|
5
|
+
* Node.js crypto packages.
|
|
6
|
+
*
|
|
7
|
+
* ────────────────────────── Design summary ─────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* ⚙ KEM (Key-Encapsulation Mechanism)
|
|
10
|
+
* • ECDH P-256 + AES-256-GCM for DEK wrapping
|
|
11
|
+
* • Output: variable-size encrypted DEK (48-byte DEK + 16-byte GCM tag + ephemeral pubkey)
|
|
12
|
+
* • Provides forward secrecy for each blob
|
|
13
|
+
*
|
|
14
|
+
* ⚙ DEM (Data-Encapsulation Mechanism)
|
|
15
|
+
* • AES-256-GCM in ~64 KiB framed chunks (65519 bytes max)
|
|
16
|
+
* • Nonce = 4-byte random prefix ∥ 8-byte little-endian counter
|
|
17
|
+
* • First frame authenticates header via associated data (prevents tampering)
|
|
18
|
+
* • Constant ~64 KiB RAM, O(1) header re-wrap for key rotation
|
|
19
|
+
*
|
|
20
|
+
* ⚙ Fleet key
|
|
21
|
+
* • Single ECDSA P-256 key-pair per customer
|
|
22
|
+
* • Public key used directly for ECDH operations
|
|
23
|
+
* • Private key stored in cloud secret store and fetched at boot
|
|
24
|
+
*
|
|
25
|
+
* File layout
|
|
26
|
+
* ┌─────────────────────────────────────────────────────────────────────────┐
|
|
27
|
+
* │ uint16 wrappedLen │ 125B wrapped DEK │ 12B base nonce │ frames... │
|
|
28
|
+
* └─────────────────────────────────────────────────────────────────────────┘
|
|
29
|
+
* ▲ ▲
|
|
30
|
+
* │ └─ AES-256-GCM frames
|
|
31
|
+
* └─ ECDH + AES-GCM wrapped DEK
|
|
32
|
+
*
|
|
33
|
+
* Security properties
|
|
34
|
+
* • Confidentiality & integrity: AES-256-GCM per frame
|
|
35
|
+
* • Header authentication: first frame includes header as associated data
|
|
36
|
+
* • Forward-secrecy per object: new ephemeral ECDH key each encryption
|
|
37
|
+
* • Key rotation: requires re-wrapping only the ~139-byte header
|
|
38
|
+
* • FIPS 140-3 compliant: uses only approved algorithms
|
|
39
|
+
*
|
|
40
|
+
* Typical workflow
|
|
41
|
+
* ────────────────
|
|
42
|
+
* Publisher:
|
|
43
|
+
* 1) generate DEK, encrypt stream → dst
|
|
44
|
+
* 2) ephemeral ECDH + AES-GCM wrap DEK with fleet public key
|
|
45
|
+
* 3) write header {len, wrapped DEK, nonce} - ~139 bytes total
|
|
46
|
+
* 4) first frame includes header as associated data for authentication
|
|
47
|
+
*
|
|
48
|
+
* Machine node:
|
|
49
|
+
* 1) read header, unwrap DEK with fleet private key via ECDH
|
|
50
|
+
* 2) stream-decrypt frames on the fly (first frame verifies header)
|
|
51
|
+
*
|
|
52
|
+
* Public API
|
|
53
|
+
* ──────────
|
|
54
|
+
*
|
|
55
|
+
* encryptFIPSKEMDEMStream(publicKey: KeyObject, src: Readable, dst: Writable): Promise<number>
|
|
56
|
+
* decryptFIPSKEMDEMStream(privateKey: KeyObject, src: Readable, dst: Writable): Promise<number>
|
|
57
|
+
*
|
|
58
|
+
* Both return the number of plaintext bytes processed and ensure that
|
|
59
|
+
* every error path is authenticated-failure-safe.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
import { createCipheriv, createDecipheriv, createECDH, randomBytes, KeyObject } from 'node:crypto';
|
|
63
|
+
import { Readable, Writable } from 'node:stream';
|
|
64
|
+
import { createHash } from 'node:crypto';
|
|
65
|
+
|
|
66
|
+
const FRAME = 65519;
|
|
67
|
+
const DEK_SIZE = 32;
|
|
68
|
+
const GCM_TAG = 16;
|
|
69
|
+
const PUBKEY_LEN = 65;
|
|
70
|
+
|
|
71
|
+
function concatKDFSHA256(z: Buffer, keyDataLen: number, ...otherInfo: Buffer[]): Buffer {
|
|
72
|
+
const h = createHash('sha256');
|
|
73
|
+
h.update(Buffer.from([0x00, 0x00, 0x00, 0x01]));
|
|
74
|
+
h.update(z);
|
|
75
|
+
for (const info of otherInfo) {
|
|
76
|
+
h.update(info);
|
|
77
|
+
}
|
|
78
|
+
const keyDataLenBits = keyDataLen * 8;
|
|
79
|
+
h.update(
|
|
80
|
+
Buffer.from([
|
|
81
|
+
(keyDataLenBits >> 24) & 0xff,
|
|
82
|
+
(keyDataLenBits >> 16) & 0xff,
|
|
83
|
+
(keyDataLenBits >> 8) & 0xff,
|
|
84
|
+
keyDataLenBits & 0xff,
|
|
85
|
+
])
|
|
86
|
+
);
|
|
87
|
+
return h.digest();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function wrapDEKWithECDH(dek: Buffer, recipientPub: KeyObject): Buffer {
|
|
91
|
+
const ephemeral = createECDH('prime256v1');
|
|
92
|
+
ephemeral.generateKeys();
|
|
93
|
+
|
|
94
|
+
const jwk = recipientPub.export({ format: 'jwk' });
|
|
95
|
+
if (!jwk.x || !jwk.y) {
|
|
96
|
+
throw new Error('Invalid EC public key');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const xBuf = Buffer.from(jwk.x, 'base64url');
|
|
100
|
+
const yBuf = Buffer.from(jwk.y, 'base64url');
|
|
101
|
+
const pubKeyPoint = Buffer.concat([Buffer.from([0x04]), xBuf, yBuf]);
|
|
102
|
+
|
|
103
|
+
const sharedSecret = ephemeral.computeSecret(pubKeyPoint);
|
|
104
|
+
const kek = concatKDFSHA256(sharedSecret, 32, Buffer.from('AES-256-GCM'));
|
|
105
|
+
sharedSecret.fill(0);
|
|
106
|
+
|
|
107
|
+
const nonce = randomBytes(12);
|
|
108
|
+
const cipher = createCipheriv('aes-256-gcm', kek, nonce);
|
|
109
|
+
const ciphertext = Buffer.concat([cipher.update(dek), cipher.final()]);
|
|
110
|
+
const tag = cipher.getAuthTag();
|
|
111
|
+
kek.fill(0);
|
|
112
|
+
|
|
113
|
+
const ephemeralPubBytes = ephemeral.getPublicKey(undefined, 'uncompressed');
|
|
114
|
+
return Buffer.concat([ephemeralPubBytes, nonce, ciphertext, tag]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function unwrapDEKWithECDH(wrapped: Buffer, recipientPriv: KeyObject): Buffer {
|
|
118
|
+
if (wrapped.length < PUBKEY_LEN + 12 + DEK_SIZE + GCM_TAG) {
|
|
119
|
+
throw new Error('wrapped DEK too short');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ephemeralPubBytes = wrapped.subarray(0, PUBKEY_LEN);
|
|
123
|
+
const remaining = wrapped.subarray(PUBKEY_LEN);
|
|
124
|
+
|
|
125
|
+
const jwk = recipientPriv.export({ format: 'jwk' });
|
|
126
|
+
if (!jwk.d) {
|
|
127
|
+
throw new Error('Invalid EC private key');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const ecdh = createECDH('prime256v1');
|
|
131
|
+
const dBuf = Buffer.from(jwk.d, 'base64url');
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
ecdh.setPrivateKey(dBuf);
|
|
135
|
+
|
|
136
|
+
const sharedSecret = ecdh.computeSecret(ephemeralPubBytes);
|
|
137
|
+
const kek = concatKDFSHA256(sharedSecret, 32, Buffer.from('AES-256-GCM'));
|
|
138
|
+
sharedSecret.fill(0);
|
|
139
|
+
|
|
140
|
+
const nonceSize = 12;
|
|
141
|
+
if (remaining.length < nonceSize) {
|
|
142
|
+
throw new Error('invalid wrapped DEK format');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const nonce = remaining.subarray(0, nonceSize);
|
|
146
|
+
const ciphertextAndTag = remaining.subarray(nonceSize);
|
|
147
|
+
|
|
148
|
+
if (ciphertextAndTag.length < GCM_TAG) {
|
|
149
|
+
throw new Error('invalid wrapped DEK format');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const ciphertext = ciphertextAndTag.subarray(0, ciphertextAndTag.length - GCM_TAG);
|
|
153
|
+
const tag = ciphertextAndTag.subarray(ciphertextAndTag.length - GCM_TAG);
|
|
154
|
+
|
|
155
|
+
const decipher = createDecipheriv('aes-256-gcm', kek, nonce);
|
|
156
|
+
decipher.setAuthTag(tag);
|
|
157
|
+
|
|
158
|
+
let plaintext: Buffer;
|
|
159
|
+
try {
|
|
160
|
+
plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
161
|
+
} catch (_err) {
|
|
162
|
+
throw new Error('DEK unwrap failed');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
kek.fill(0);
|
|
166
|
+
return plaintext;
|
|
167
|
+
} finally {
|
|
168
|
+
dBuf.fill(0);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function makeNonce(prefix: Buffer, counter: bigint): Buffer {
|
|
173
|
+
const nonce = Buffer.alloc(12);
|
|
174
|
+
prefix.copy(nonce, 0, 0, 4);
|
|
175
|
+
nonce.writeBigUInt64LE(counter, 4);
|
|
176
|
+
return nonce;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function encryptFIPSKEMDEMStream(
|
|
180
|
+
pub: KeyObject,
|
|
181
|
+
src: Readable,
|
|
182
|
+
dst: Writable
|
|
183
|
+
): Promise<number> {
|
|
184
|
+
if (pub.asymmetricKeyType !== 'ec') {
|
|
185
|
+
throw new Error('only EC keys supported');
|
|
186
|
+
}
|
|
187
|
+
const keyDetails = pub.asymmetricKeyDetails;
|
|
188
|
+
if (!keyDetails || keyDetails.namedCurve !== 'prime256v1') {
|
|
189
|
+
throw new Error('only P-256 keys supported');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const dek = randomBytes(DEK_SIZE);
|
|
193
|
+
let buf: Buffer | undefined;
|
|
194
|
+
const it = src[Symbol.asyncIterator]();
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const wrapped = wrapDEKWithECDH(dek, pub);
|
|
198
|
+
|
|
199
|
+
const baseNonce = Buffer.alloc(12);
|
|
200
|
+
randomBytes(4).copy(baseNonce, 0);
|
|
201
|
+
|
|
202
|
+
const lenBuf = Buffer.alloc(2);
|
|
203
|
+
lenBuf.writeUInt16BE(wrapped.length, 0);
|
|
204
|
+
await writeAsync(dst, lenBuf);
|
|
205
|
+
await writeAsync(dst, wrapped);
|
|
206
|
+
await writeAsync(dst, baseNonce);
|
|
207
|
+
|
|
208
|
+
let counter = 0n;
|
|
209
|
+
let total = 0;
|
|
210
|
+
|
|
211
|
+
const headerAD = Buffer.alloc(2 + 12);
|
|
212
|
+
headerAD.writeUInt16BE(wrapped.length, 0);
|
|
213
|
+
baseNonce.copy(headerAD, 2);
|
|
214
|
+
|
|
215
|
+
buf = Buffer.alloc(FRAME);
|
|
216
|
+
|
|
217
|
+
while (true) {
|
|
218
|
+
const bytesRead = await readFull(it, src, buf);
|
|
219
|
+
if (bytesRead === 0) {
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const plaintext = buf.subarray(0, bytesRead);
|
|
224
|
+
const nonce = makeNonce(baseNonce, counter);
|
|
225
|
+
|
|
226
|
+
const cipher = createCipheriv('aes-256-gcm', dek, nonce);
|
|
227
|
+
|
|
228
|
+
if (counter === 0n) {
|
|
229
|
+
cipher.setAAD(headerAD);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
233
|
+
const tag = cipher.getAuthTag();
|
|
234
|
+
const ct = Buffer.concat([ciphertext, tag]);
|
|
235
|
+
|
|
236
|
+
if (ct.length > 0xffff) {
|
|
237
|
+
throw new Error('ciphertext length exceeds uint16 limit');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const ctLenBuf = Buffer.alloc(2);
|
|
241
|
+
ctLenBuf.writeUInt16BE(ct.length, 0);
|
|
242
|
+
await writeAsync(dst, ctLenBuf);
|
|
243
|
+
await writeAsync(dst, ct);
|
|
244
|
+
|
|
245
|
+
counter++;
|
|
246
|
+
total += bytesRead;
|
|
247
|
+
|
|
248
|
+
if (bytesRead < FRAME) {
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return total;
|
|
254
|
+
} finally {
|
|
255
|
+
dek.fill(0);
|
|
256
|
+
if (buf) buf.fill(0);
|
|
257
|
+
await it.return?.().catch(() => {});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export async function decryptFIPSKEMDEMStream(
|
|
262
|
+
priv: KeyObject,
|
|
263
|
+
src: Readable,
|
|
264
|
+
dst: Writable
|
|
265
|
+
): Promise<number> {
|
|
266
|
+
if (priv.asymmetricKeyType !== 'ec') {
|
|
267
|
+
throw new Error('only EC keys supported');
|
|
268
|
+
}
|
|
269
|
+
const keyDetails = priv.asymmetricKeyDetails;
|
|
270
|
+
if (!keyDetails || keyDetails.namedCurve !== 'prime256v1') {
|
|
271
|
+
throw new Error('only P-256 keys supported');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const it = src[Symbol.asyncIterator]();
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
const lenBuf = Buffer.alloc(2);
|
|
278
|
+
await readExact(it, src, lenBuf);
|
|
279
|
+
const wrappedLen = lenBuf.readUInt16BE(0);
|
|
280
|
+
|
|
281
|
+
if (wrappedLen === 0 || wrappedLen > 200) {
|
|
282
|
+
throw new Error('invalid wrapped DEK length');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const wrapped = Buffer.alloc(wrappedLen);
|
|
286
|
+
await readExact(it, src, wrapped);
|
|
287
|
+
|
|
288
|
+
const baseNonce = Buffer.alloc(12);
|
|
289
|
+
await readExact(it, src, baseNonce);
|
|
290
|
+
|
|
291
|
+
const dek = unwrapDEKWithECDH(wrapped, priv);
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
let counter = 0n;
|
|
295
|
+
let total = 0;
|
|
296
|
+
|
|
297
|
+
const headerAD = Buffer.alloc(2 + 12);
|
|
298
|
+
headerAD.writeUInt16BE(wrappedLen, 0);
|
|
299
|
+
baseNonce.copy(headerAD, 2);
|
|
300
|
+
|
|
301
|
+
while (true) {
|
|
302
|
+
const chunkLenBuf = Buffer.alloc(2);
|
|
303
|
+
const chunkLenRead = await readUpTo(it, src, chunkLenBuf);
|
|
304
|
+
if (chunkLenRead === 0) {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
if (chunkLenRead < 2) {
|
|
308
|
+
throw new Error('unexpected EOF reading chunk length');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const chunkLen = chunkLenBuf.readUInt16BE(0);
|
|
312
|
+
if (chunkLen > FRAME + GCM_TAG) {
|
|
313
|
+
throw new Error('chunk too large');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const cipherBuf = Buffer.alloc(chunkLen);
|
|
317
|
+
await readExact(it, src, cipherBuf);
|
|
318
|
+
|
|
319
|
+
if (cipherBuf.length < GCM_TAG) {
|
|
320
|
+
throw new Error('chunk too short for auth tag');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const ciphertext = cipherBuf.subarray(0, cipherBuf.length - GCM_TAG);
|
|
324
|
+
const tag = cipherBuf.subarray(cipherBuf.length - GCM_TAG);
|
|
325
|
+
|
|
326
|
+
const nonce = makeNonce(baseNonce, counter);
|
|
327
|
+
const decipher = createDecipheriv('aes-256-gcm', dek, nonce);
|
|
328
|
+
decipher.setAuthTag(tag);
|
|
329
|
+
|
|
330
|
+
if (counter === 0n) {
|
|
331
|
+
decipher.setAAD(headerAD);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let plain: Buffer;
|
|
335
|
+
try {
|
|
336
|
+
plain = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
cipherBuf.fill(0);
|
|
339
|
+
throw err;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
cipherBuf.fill(0);
|
|
343
|
+
|
|
344
|
+
await writeAsync(dst, plain);
|
|
345
|
+
counter++;
|
|
346
|
+
total += plain.length;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return total;
|
|
350
|
+
} finally {
|
|
351
|
+
dek.fill(0);
|
|
352
|
+
}
|
|
353
|
+
} finally {
|
|
354
|
+
await it.return?.().catch(() => {});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function writeAsync(stream: Writable, chunk: Buffer): Promise<void> {
|
|
359
|
+
return new Promise((resolve, reject) => {
|
|
360
|
+
let callbackCompleted = false;
|
|
361
|
+
let drainOccurred = false;
|
|
362
|
+
|
|
363
|
+
const cleanup = () => {
|
|
364
|
+
stream.off('drain', onDrain);
|
|
365
|
+
stream.off('error', onError);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const tryResolve = () => {
|
|
369
|
+
if (callbackCompleted && (canContinue || drainOccurred)) {
|
|
370
|
+
cleanup();
|
|
371
|
+
resolve();
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const onDrain = () => {
|
|
376
|
+
drainOccurred = true;
|
|
377
|
+
tryResolve();
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const onError = (err: Error) => {
|
|
381
|
+
cleanup();
|
|
382
|
+
reject(err);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const canContinue = stream.write(chunk, (err) => {
|
|
386
|
+
callbackCompleted = true;
|
|
387
|
+
if (err) {
|
|
388
|
+
cleanup();
|
|
389
|
+
reject(err);
|
|
390
|
+
} else {
|
|
391
|
+
tryResolve();
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
if (!canContinue) {
|
|
396
|
+
// Need to wait for drain - attach listeners
|
|
397
|
+
stream.once('drain', onDrain);
|
|
398
|
+
stream.once('error', onError);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function readFull(
|
|
404
|
+
iterator: AsyncIterator<Buffer | string>,
|
|
405
|
+
stream: Readable,
|
|
406
|
+
buf: Buffer
|
|
407
|
+
): Promise<number> {
|
|
408
|
+
let offset = 0;
|
|
409
|
+
|
|
410
|
+
while (offset < buf.length) {
|
|
411
|
+
const result = await iterator.next();
|
|
412
|
+
if (result.done) {
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const chunk = result.value;
|
|
417
|
+
const chunkBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
418
|
+
const toCopy = Math.min(chunkBuf.length, buf.length - offset);
|
|
419
|
+
chunkBuf.copy(buf, offset, 0, toCopy);
|
|
420
|
+
offset += toCopy;
|
|
421
|
+
|
|
422
|
+
if (offset >= buf.length && toCopy < chunkBuf.length) {
|
|
423
|
+
stream.unshift(chunkBuf.subarray(toCopy));
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return offset;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function readExact(
|
|
432
|
+
iterator: AsyncIterator<Buffer | string>,
|
|
433
|
+
stream: Readable,
|
|
434
|
+
buf: Buffer
|
|
435
|
+
): Promise<void> {
|
|
436
|
+
let offset = 0;
|
|
437
|
+
|
|
438
|
+
while (offset < buf.length) {
|
|
439
|
+
const result = await iterator.next();
|
|
440
|
+
if (result.done) {
|
|
441
|
+
throw new Error('unexpected EOF');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const chunk = result.value;
|
|
445
|
+
const chunkBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
446
|
+
const toCopy = Math.min(chunkBuf.length, buf.length - offset);
|
|
447
|
+
chunkBuf.copy(buf, offset, 0, toCopy);
|
|
448
|
+
offset += toCopy;
|
|
449
|
+
|
|
450
|
+
if (offset >= buf.length && toCopy < chunkBuf.length) {
|
|
451
|
+
stream.unshift(chunkBuf.subarray(toCopy));
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function readUpTo(
|
|
458
|
+
iterator: AsyncIterator<Buffer | string>,
|
|
459
|
+
stream: Readable,
|
|
460
|
+
buf: Buffer
|
|
461
|
+
): Promise<number> {
|
|
462
|
+
const result = await iterator.next();
|
|
463
|
+
if (result.done) {
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const chunk = result.value;
|
|
468
|
+
const chunkBuf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
469
|
+
const toCopy = Math.min(chunkBuf.length, buf.length);
|
|
470
|
+
chunkBuf.copy(buf, 0, 0, toCopy);
|
|
471
|
+
|
|
472
|
+
if (toCopy < chunkBuf.length) {
|
|
473
|
+
stream.unshift(chunkBuf.subarray(toCopy));
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return toCopy;
|
|
477
|
+
}
|
package/src/download.ts
CHANGED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { looksLikeSecret } from './env-util';
|
|
3
|
+
|
|
4
|
+
describe('looksLikeSecret', () => {
|
|
5
|
+
describe('key name patterns', () => {
|
|
6
|
+
test('detects _SECRET suffix', () => {
|
|
7
|
+
expect(looksLikeSecret('API_SECRET', 'value')).toBe(true);
|
|
8
|
+
expect(looksLikeSecret('DB_SECRET', 'value')).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('detects _KEY suffix', () => {
|
|
12
|
+
expect(looksLikeSecret('API_KEY', 'value')).toBe(true);
|
|
13
|
+
expect(looksLikeSecret('STRIPE_KEY', 'value')).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('detects _TOKEN suffix', () => {
|
|
17
|
+
expect(looksLikeSecret('AUTH_TOKEN', 'value')).toBe(true);
|
|
18
|
+
expect(looksLikeSecret('GITHUB_TOKEN', 'value')).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('detects _PASSWORD suffix', () => {
|
|
22
|
+
expect(looksLikeSecret('DB_PASSWORD', 'value')).toBe(true);
|
|
23
|
+
expect(looksLikeSecret('ADMIN_PASSWORD', 'value')).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('detects _PRIVATE suffix', () => {
|
|
27
|
+
expect(looksLikeSecret('SSH_PRIVATE', 'value')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('detects _CERT and _CERTIFICATE suffixes', () => {
|
|
31
|
+
expect(looksLikeSecret('SSL_CERT', 'value')).toBe(true);
|
|
32
|
+
expect(looksLikeSecret('SSL_CERTIFICATE', 'value')).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('detects SECRET_ prefix', () => {
|
|
36
|
+
expect(looksLikeSecret('SECRET_VALUE', 'value')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('detects APIKEY and API_KEY patterns', () => {
|
|
40
|
+
expect(looksLikeSecret('APIKEY', 'value')).toBe(true);
|
|
41
|
+
expect(looksLikeSecret('API_KEY', 'value')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('detects JWT prefix', () => {
|
|
45
|
+
expect(looksLikeSecret('JWT_SECRET', 'value')).toBe(true);
|
|
46
|
+
expect(looksLikeSecret('JWT', 'value')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('detects PASSWORD in key name', () => {
|
|
50
|
+
expect(looksLikeSecret('DATABASE_PASSWORD', 'value')).toBe(true);
|
|
51
|
+
expect(looksLikeSecret('PASSWORD', 'value')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('detects CREDENTIAL in key name', () => {
|
|
55
|
+
expect(looksLikeSecret('AWS_CREDENTIALS', 'value')).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('detects AUTH.*KEY pattern', () => {
|
|
59
|
+
expect(looksLikeSecret('AUTH_API_KEY', 'value')).toBe(true);
|
|
60
|
+
expect(looksLikeSecret('AUTHKEY', 'value')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('is case insensitive for key patterns', () => {
|
|
64
|
+
expect(looksLikeSecret('api_secret', 'value')).toBe(true);
|
|
65
|
+
expect(looksLikeSecret('Api_Key', 'value')).toBe(true);
|
|
66
|
+
expect(looksLikeSecret('AUTH_token', 'value')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('value patterns', () => {
|
|
71
|
+
test('detects JWT tokens', () => {
|
|
72
|
+
const jwt =
|
|
73
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
|
74
|
+
expect(looksLikeSecret('TOKEN', jwt)).toBe(true);
|
|
75
|
+
expect(looksLikeSecret('SOME_VAR', jwt)).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('detects Bearer tokens', () => {
|
|
79
|
+
expect(looksLikeSecret('AUTH', 'Bearer abc123def456ghi789jkl012mno345pqr')).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('detects AWS access keys', () => {
|
|
83
|
+
expect(looksLikeSecret('AWS', 'AKIAIOSFODNN7EXAMPLE')).toBe(true);
|
|
84
|
+
expect(looksLikeSecret('AWS', 'ASIAIOSFODNN7EXAMPLE')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('detects GitHub tokens', () => {
|
|
88
|
+
expect(looksLikeSecret('GH', 'ghp_1234567890abcdefghijklmnopqrstuvwxyz')).toBe(true);
|
|
89
|
+
expect(looksLikeSecret('GH', 'ghs_1234567890abcdefghijklmnopqrstuvwxyz')).toBe(true);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('detects long alphanumeric strings (API keys)', () => {
|
|
93
|
+
// 32+ characters, mixed alphanumeric
|
|
94
|
+
expect(
|
|
95
|
+
looksLikeSecret('KEY', 'sk_test_51A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6')
|
|
96
|
+
).toBe(true);
|
|
97
|
+
expect(looksLikeSecret('KEY', 'abc123def456ghi789jkl012mno345pqr678stu901vwx234yz')).toBe(
|
|
98
|
+
true
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('does not flag numeric-only long strings', () => {
|
|
103
|
+
expect(looksLikeSecret('ID', '12345678901234567890123456789012')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('detects PEM certificates', () => {
|
|
107
|
+
expect(looksLikeSecret('CERT', '-----BEGIN CERTIFICATE-----\nMIIC...')).toBe(true);
|
|
108
|
+
expect(looksLikeSecret('CERT', '-----BEGIN PRIVATE KEY-----\nMIIC...')).toBe(true);
|
|
109
|
+
expect(looksLikeSecret('CERT', '-----BEGIN RSA PRIVATE KEY-----\nMIIC...')).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('does not flag short values', () => {
|
|
113
|
+
expect(looksLikeSecret('VAR', 'short')).toBe(false);
|
|
114
|
+
expect(looksLikeSecret('VAR', '1234567')).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('does not flag empty values', () => {
|
|
118
|
+
expect(looksLikeSecret('VAR', '')).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('non-secret patterns', () => {
|
|
123
|
+
test('regular environment variables are not flagged', () => {
|
|
124
|
+
expect(looksLikeSecret('NODE_ENV', 'production')).toBe(false);
|
|
125
|
+
expect(looksLikeSecret('PORT', '3500')).toBe(false);
|
|
126
|
+
expect(looksLikeSecret('HOST', 'localhost')).toBe(false);
|
|
127
|
+
expect(looksLikeSecret('DATABASE_URL', 'postgres://localhost:5432/mydb')).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('configuration values are not flagged', () => {
|
|
131
|
+
expect(looksLikeSecret('LOG_LEVEL', 'debug')).toBe(false);
|
|
132
|
+
expect(looksLikeSecret('CACHE_TTL', '3600')).toBe(false);
|
|
133
|
+
expect(looksLikeSecret('MAX_CONNECTIONS', '100')).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('URLs without secrets are not flagged', () => {
|
|
137
|
+
expect(looksLikeSecret('API_URL', 'https://api.example.com')).toBe(false);
|
|
138
|
+
expect(looksLikeSecret('WEBHOOK_URL', 'https://example.com/webhook')).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('paths are not flagged', () => {
|
|
142
|
+
expect(looksLikeSecret('DATA_PATH', '/var/data/app')).toBe(false);
|
|
143
|
+
expect(looksLikeSecret('CONFIG_FILE', '/etc/app/config.json')).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
describe('edge cases', () => {
|
|
148
|
+
test('handles mixed key and value patterns', () => {
|
|
149
|
+
// Key pattern triggers detection
|
|
150
|
+
expect(looksLikeSecret('API_KEY', 'simple')).toBe(true);
|
|
151
|
+
|
|
152
|
+
// Value pattern triggers detection even without key pattern
|
|
153
|
+
const jwt =
|
|
154
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
|
155
|
+
expect(looksLikeSecret('CONFIG', jwt)).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('real-world API key formats', () => {
|
|
159
|
+
// Stripe (contains underscore, 32+ chars)
|
|
160
|
+
expect(looksLikeSecret('STRIPE', 'sk_test_51HqL7xAbCdEfGhIjK12345678901234567890')).toBe(
|
|
161
|
+
true
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// Long API key format (32+ alphanumeric)
|
|
165
|
+
expect(looksLikeSecret('OPENAI', 'sk-proj-1234567890abcdefghijklmnopqrstuvwxyz')).toBe(
|
|
166
|
+
true
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
// Contains dots (periods not in our pattern, but key name helps)
|
|
170
|
+
expect(
|
|
171
|
+
looksLikeSecret(
|
|
172
|
+
'SENDGRID_API_KEY',
|
|
173
|
+
'SG.1234567890abcdefghijklmnopqrstuvwxyz.1234567890abcdefghijklmnopqrstuvwxyz'
|
|
174
|
+
)
|
|
175
|
+
).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('UUIDs are correctly identified as non-secrets', () => {
|
|
179
|
+
// Standard UUID format should not be flagged
|
|
180
|
+
expect(looksLikeSecret('REQUEST_ID', '550e8400-e29b-41d4-a716-446655440000')).toBe(false);
|
|
181
|
+
expect(looksLikeSecret('USER_ID', '123e4567-e89b-12d3-a456-426614174000')).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('hex hashes are flagged (better safe than sorry)', () => {
|
|
185
|
+
// 32+ character hex strings could be secrets or hashes - we flag them
|
|
186
|
+
// Users can confirm they're just hashes if needed
|
|
187
|
+
expect(looksLikeSecret('BUILD_HASH', 'a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6')).toBe(true);
|
|
188
|
+
|
|
189
|
+
// But with context that clearly indicates it's not a secret, the key name won't trigger
|
|
190
|
+
// So short hex strings without secret-like key names won't be flagged
|
|
191
|
+
expect(looksLikeSecret('COMMIT', 'abc123def')).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|