@skrr-ai/auth-core 0.1.2

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.
Files changed (147) hide show
  1. package/README.md +21 -0
  2. package/dist/cjs/aead.d.ts +24 -0
  3. package/dist/cjs/aead.js +65 -0
  4. package/dist/cjs/authExpiredDetector.d.ts +65 -0
  5. package/dist/cjs/authExpiredDetector.js +89 -0
  6. package/dist/cjs/authHelper.d.ts +51 -0
  7. package/dist/cjs/authHelper.js +321 -0
  8. package/dist/cjs/credentialEnvelope.d.ts +85 -0
  9. package/dist/cjs/credentialEnvelope.js +230 -0
  10. package/dist/cjs/credentialEnvelopeBridge.d.ts +116 -0
  11. package/dist/cjs/credentialEnvelopeBridge.js +637 -0
  12. package/dist/cjs/credentialResolver.d.ts +278 -0
  13. package/dist/cjs/credentialResolver.js +423 -0
  14. package/dist/cjs/daemonPair.d.ts +172 -0
  15. package/dist/cjs/daemonPair.js +495 -0
  16. package/dist/cjs/daemonScopes.d.ts +74 -0
  17. package/dist/cjs/daemonScopes.js +84 -0
  18. package/dist/cjs/deviceIdentityBridge.d.ts +113 -0
  19. package/dist/cjs/deviceIdentityBridge.js +591 -0
  20. package/dist/cjs/deviceKey.d.ts +167 -0
  21. package/dist/cjs/deviceKey.js +332 -0
  22. package/dist/cjs/fdToken.d.ts +17 -0
  23. package/dist/cjs/fdToken.js +238 -0
  24. package/dist/cjs/harnessTrust.d.ts +72 -0
  25. package/dist/cjs/harnessTrust.js +103 -0
  26. package/dist/cjs/headers-helper.d.ts +25 -0
  27. package/dist/cjs/headers-helper.js +323 -0
  28. package/dist/cjs/index.d.ts +37 -0
  29. package/dist/cjs/index.js +305 -0
  30. package/dist/cjs/jwtUtils.d.ts +35 -0
  31. package/dist/cjs/jwtUtils.js +61 -0
  32. package/dist/cjs/kek/index.d.ts +89 -0
  33. package/dist/cjs/kek/index.js +208 -0
  34. package/dist/cjs/kek/linux.d.ts +92 -0
  35. package/dist/cjs/kek/linux.js +596 -0
  36. package/dist/cjs/kek/macos.d.ts +112 -0
  37. package/dist/cjs/kek/macos.js +599 -0
  38. package/dist/cjs/kek/types.d.ts +111 -0
  39. package/dist/cjs/kek/types.js +43 -0
  40. package/dist/cjs/kek/windows.d.ts +71 -0
  41. package/dist/cjs/kek/windows.js +350 -0
  42. package/dist/cjs/kek/zeroize-registry.d.ts +44 -0
  43. package/dist/cjs/kek/zeroize-registry.js +63 -0
  44. package/dist/cjs/legacyStatePreflight.d.ts +67 -0
  45. package/dist/cjs/legacyStatePreflight.js +78 -0
  46. package/dist/cjs/localIdentity.d.ts +54 -0
  47. package/dist/cjs/localIdentity.js +57 -0
  48. package/dist/cjs/loginLocalhost.d.ts +115 -0
  49. package/dist/cjs/loginLocalhost.js +368 -0
  50. package/dist/cjs/loginWithLocalhost.d.ts +131 -0
  51. package/dist/cjs/loginWithLocalhost.js +359 -0
  52. package/dist/cjs/machineId.d.ts +35 -0
  53. package/dist/cjs/machineId.js +239 -0
  54. package/dist/cjs/messages.d.ts +9 -0
  55. package/dist/cjs/messages.js +44 -0
  56. package/dist/cjs/package.json +3 -0
  57. package/dist/cjs/pkce.d.ts +62 -0
  58. package/dist/cjs/pkce.js +158 -0
  59. package/dist/cjs/recoveryCode.d.ts +61 -0
  60. package/dist/cjs/recoveryCode.js +213 -0
  61. package/dist/cjs/refresh.d.ts +215 -0
  62. package/dist/cjs/refresh.js +877 -0
  63. package/dist/cjs/refreshClassification.d.ts +53 -0
  64. package/dist/cjs/refreshClassification.js +105 -0
  65. package/dist/cjs/refreshScheduler.d.ts +53 -0
  66. package/dist/cjs/refreshScheduler.js +332 -0
  67. package/dist/cjs/runtime.d.ts +149 -0
  68. package/dist/cjs/runtime.js +136 -0
  69. package/dist/cjs/spawnEnv.d.ts +76 -0
  70. package/dist/cjs/spawnEnv.js +153 -0
  71. package/dist/cjs/ttlParser.d.ts +54 -0
  72. package/dist/cjs/ttlParser.js +87 -0
  73. package/dist/cjs/types.d.ts +91 -0
  74. package/dist/cjs/types.js +26 -0
  75. package/dist/esm/aead.d.ts +24 -0
  76. package/dist/esm/aead.js +57 -0
  77. package/dist/esm/authExpiredDetector.d.ts +65 -0
  78. package/dist/esm/authExpiredDetector.js +85 -0
  79. package/dist/esm/authHelper.d.ts +51 -0
  80. package/dist/esm/authHelper.js +311 -0
  81. package/dist/esm/credentialEnvelope.d.ts +85 -0
  82. package/dist/esm/credentialEnvelope.js +213 -0
  83. package/dist/esm/credentialEnvelopeBridge.d.ts +116 -0
  84. package/dist/esm/credentialEnvelopeBridge.js +620 -0
  85. package/dist/esm/credentialResolver.d.ts +278 -0
  86. package/dist/esm/credentialResolver.js +414 -0
  87. package/dist/esm/daemonPair.d.ts +172 -0
  88. package/dist/esm/daemonPair.js +476 -0
  89. package/dist/esm/daemonScopes.d.ts +74 -0
  90. package/dist/esm/daemonScopes.js +80 -0
  91. package/dist/esm/deviceIdentityBridge.d.ts +113 -0
  92. package/dist/esm/deviceIdentityBridge.js +572 -0
  93. package/dist/esm/deviceKey.d.ts +167 -0
  94. package/dist/esm/deviceKey.js +320 -0
  95. package/dist/esm/fdToken.d.ts +17 -0
  96. package/dist/esm/fdToken.js +231 -0
  97. package/dist/esm/harnessTrust.d.ts +72 -0
  98. package/dist/esm/harnessTrust.js +97 -0
  99. package/dist/esm/headers-helper.d.ts +25 -0
  100. package/dist/esm/headers-helper.js +316 -0
  101. package/dist/esm/index.d.ts +37 -0
  102. package/dist/esm/index.js +144 -0
  103. package/dist/esm/jwtUtils.d.ts +35 -0
  104. package/dist/esm/jwtUtils.js +57 -0
  105. package/dist/esm/kek/index.d.ts +89 -0
  106. package/dist/esm/kek/index.js +162 -0
  107. package/dist/esm/kek/linux.d.ts +92 -0
  108. package/dist/esm/kek/linux.js +585 -0
  109. package/dist/esm/kek/macos.d.ts +112 -0
  110. package/dist/esm/kek/macos.js +586 -0
  111. package/dist/esm/kek/types.d.ts +111 -0
  112. package/dist/esm/kek/types.js +39 -0
  113. package/dist/esm/kek/windows.d.ts +71 -0
  114. package/dist/esm/kek/windows.js +344 -0
  115. package/dist/esm/kek/zeroize-registry.d.ts +44 -0
  116. package/dist/esm/kek/zeroize-registry.js +58 -0
  117. package/dist/esm/legacyStatePreflight.d.ts +67 -0
  118. package/dist/esm/legacyStatePreflight.js +71 -0
  119. package/dist/esm/localIdentity.d.ts +54 -0
  120. package/dist/esm/localIdentity.js +54 -0
  121. package/dist/esm/loginLocalhost.d.ts +115 -0
  122. package/dist/esm/loginLocalhost.js +355 -0
  123. package/dist/esm/loginWithLocalhost.d.ts +131 -0
  124. package/dist/esm/loginWithLocalhost.js +353 -0
  125. package/dist/esm/machineId.d.ts +35 -0
  126. package/dist/esm/machineId.js +231 -0
  127. package/dist/esm/messages.d.ts +9 -0
  128. package/dist/esm/messages.js +40 -0
  129. package/dist/esm/pkce.d.ts +62 -0
  130. package/dist/esm/pkce.js +148 -0
  131. package/dist/esm/recoveryCode.d.ts +61 -0
  132. package/dist/esm/recoveryCode.js +207 -0
  133. package/dist/esm/refresh.d.ts +215 -0
  134. package/dist/esm/refresh.js +863 -0
  135. package/dist/esm/refreshClassification.d.ts +53 -0
  136. package/dist/esm/refreshClassification.js +106 -0
  137. package/dist/esm/refreshScheduler.d.ts +53 -0
  138. package/dist/esm/refreshScheduler.js +329 -0
  139. package/dist/esm/runtime.d.ts +149 -0
  140. package/dist/esm/runtime.js +121 -0
  141. package/dist/esm/spawnEnv.d.ts +76 -0
  142. package/dist/esm/spawnEnv.js +149 -0
  143. package/dist/esm/ttlParser.d.ts +54 -0
  144. package/dist/esm/ttlParser.js +83 -0
  145. package/dist/esm/types.d.ts +91 -0
  146. package/dist/esm/types.js +21 -0
  147. package/package.json +82 -0
@@ -0,0 +1,85 @@
1
+ /** Serialized magic prefix. Bumped only on a breaking format change. */
2
+ export declare const ENVELOPE_MAGIC = "osk-env1";
3
+ /** Key length in bytes (256 bits). */
4
+ export declare const KEY_LEN = 32;
5
+ /**
6
+ * In-memory representation of a sealed value. Carries a version byte so a
7
+ * future `osk-env2` migration can dual-read both formats from disk.
8
+ */
9
+ export interface SealedBlob {
10
+ readonly v: 1;
11
+ readonly nonce: Buffer;
12
+ readonly ct: Buffer;
13
+ readonly tag: Buffer;
14
+ }
15
+ /**
16
+ * Generate a fresh 256-bit Data Encryption Key. Uses Node's crypto-random
17
+ * source (libsodium-grade on macOS/Linux, BCryptGenRandom on Windows).
18
+ */
19
+ export declare function generateDek(): Buffer;
20
+ /**
21
+ * Build the canonical AAD for binding a sealed credential to a specific
22
+ * profile + envelope version. Including profile in the AAD prevents an
23
+ * attacker who copies a sealed blob from profile `dev` into profile
24
+ * `prod`'s slot from getting a successful decrypt — the AAD won't match.
25
+ *
26
+ * Stable string format so a re-derivation in a future process produces
27
+ * the exact same AAD bytes for the same profile.
28
+ */
29
+ export declare function buildCredentialAad(profile: string, version?: number): Buffer;
30
+ /**
31
+ * Low-level AEAD seal. The shared primitive used by both DEK-wrapping and
32
+ * credential-encryption — the only difference between those operations is
33
+ * the semantic role of the key, not the cipher. Caller chooses the AAD.
34
+ */
35
+ export declare function sealBuffer(plaintext: Buffer, key: Buffer, aad?: Buffer): SealedBlob;
36
+ /**
37
+ * Low-level AEAD open. Throws on tag mismatch (wrong key, AAD mismatch,
38
+ * truncated/corrupted ciphertext) — never silently returns garbage.
39
+ */
40
+ export declare function openBuffer(sealed: SealedBlob, key: Buffer, aad?: Buffer): Buffer;
41
+ /**
42
+ * Wrap a 32-byte DEK under a KEK. The wrapped DEK is what gets persisted
43
+ * to disk — the KEK itself never leaves its platform store.
44
+ */
45
+ export declare function wrap(dek: Buffer, kek: Buffer, aad?: Buffer): SealedBlob;
46
+ /**
47
+ * Unwrap a previously-wrapped DEK using the same KEK. AAD must match the
48
+ * value used at wrap time or the GCM tag check fails.
49
+ */
50
+ export declare function unwrap(wrapped: SealedBlob, kek: Buffer, aad?: Buffer): Buffer;
51
+ /**
52
+ * Encrypt a credential (string or Buffer) under a DEK. Strings are
53
+ * encoded as UTF-8 before sealing; the open path returns a Buffer so the
54
+ * caller decides how to interpret it.
55
+ */
56
+ export declare function encrypt(plaintext: Buffer | string, dek: Buffer, aad?: Buffer): SealedBlob;
57
+ /**
58
+ * Decrypt a credential previously sealed with `encrypt`. Returns the raw
59
+ * plaintext bytes; UTF-8-decode at the call site if the original was a
60
+ * string.
61
+ */
62
+ export declare function decrypt(sealed: SealedBlob, dek: Buffer, aad?: Buffer): Buffer;
63
+ /**
64
+ * Serialize a SealedBlob to a single text token. Stable across processes
65
+ * and platforms — same blob serializes to the same string everywhere.
66
+ */
67
+ export declare function serialize(sealed: SealedBlob): string;
68
+ /**
69
+ * Parse a serialized envelope string. Returns null on any malformed
70
+ * input — callers should treat null as "this is not an envelope" and
71
+ * fall through to the legacy plaintext path.
72
+ *
73
+ * Deliberately tolerant: garbage in returns null, never throws. Throws
74
+ * are reserved for the cipher path so a wrong key surfaces as a real
75
+ * authentication failure instead of being silently swallowed.
76
+ */
77
+ export declare function deserialize(s: string): SealedBlob | null;
78
+ /**
79
+ * Cheap structural check — does this string LOOK like a serialized
80
+ * envelope? Used by dual-read paths to decide whether to attempt a
81
+ * decrypt or to treat the value as legacy plaintext. A true return does
82
+ * NOT guarantee the contents are valid; the actual decrypt is what
83
+ * authenticates the value.
84
+ */
85
+ export declare function isEnvelopeString(s: unknown): s is string;
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.KEY_LEN = exports.ENVELOPE_MAGIC = void 0;
7
+ exports.generateDek = generateDek;
8
+ exports.buildCredentialAad = buildCredentialAad;
9
+ exports.sealBuffer = sealBuffer;
10
+ exports.openBuffer = openBuffer;
11
+ exports.wrap = wrap;
12
+ exports.unwrap = unwrap;
13
+ exports.encrypt = encrypt;
14
+ exports.decrypt = decrypt;
15
+ exports.serialize = serialize;
16
+ exports.deserialize = deserialize;
17
+ exports.isEnvelopeString = isEnvelopeString;
18
+ /**
19
+ * credentialEnvelope.ts — at-rest envelope encryption primitives (L12 Layer 0).
20
+ *
21
+ * NAMING NOTE — this module is the "credential at-rest envelope." It is
22
+ * structurally distinct from `daemon/src/envelope-encryption.ts`, which
23
+ * encrypts wire-protocol session envelopes with a server-known DEK. The
24
+ * two should never share a key, a serialized format, or a rotation
25
+ * policy. We deliberately do NOT use the bare word "envelope" in this
26
+ * module's filename to avoid that confusion; future readers grepping for
27
+ * "envelope" must hit a name that immediately disambiguates which layer
28
+ * they are looking at.
29
+ *
30
+ * Threat model — what this module protects:
31
+ * - Cold copy of `~/Library/Keychains/login.keychain-db` taken from a
32
+ * sleeping laptop and brute-forced offline.
33
+ * - Backup snapshot of `~/.skrr/...` in Time Machine / Backblaze /
34
+ * Restic that ends up on a less-trusted disk.
35
+ * - Same-UID malware that reads the on-disk JSON file backend without
36
+ * standing up an OS prompt.
37
+ *
38
+ * What it does NOT protect:
39
+ * - Privileged on-host malware (root) reading the daemon's process memory.
40
+ * - Server-side compromise of refresh tokens that the server already
41
+ * holds by design.
42
+ *
43
+ * Hierarchy:
44
+ * plaintext credential
45
+ * ─ AES-256-GCM(DEK, nonce, AAD) ─▶ sealed credential blob
46
+ * plaintext DEK (32 bytes, generated locally)
47
+ * ─ AES-256-GCM(KEK, nonce, AAD) ─▶ wrapped DEK blob
48
+ * KEK (platform-native; Secure Enclave / DPAPI / libsecret-derived)
49
+ * never appears in JS memory in unwrappable form.
50
+ *
51
+ * This module owns ONLY the AES-256-GCM primitive plus the serialized
52
+ * format. Platform KEK acquisition lives in `./kek/`. DEK lifecycle
53
+ * (generation, rotation, persistence) lives in the daemon-side bridge.
54
+ */
55
+ const node_crypto_1 = __importDefault(require("node:crypto"));
56
+ const aead_js_1 = require("./aead.js");
57
+ /** Serialized magic prefix. Bumped only on a breaking format change. */
58
+ exports.ENVELOPE_MAGIC = 'osk-env1';
59
+ /** Key length in bytes (256 bits). */
60
+ exports.KEY_LEN = aead_js_1.AEAD_KEY_LEN;
61
+ /** Nonce length in bytes (96 bits — GCM standard). */
62
+ const NONCE_LEN = aead_js_1.AEAD_NONCE_LEN;
63
+ /** GCM authentication tag length in bytes (128 bits). */
64
+ const TAG_LEN = aead_js_1.AEAD_TAG_LEN;
65
+ /**
66
+ * Generate a fresh 256-bit Data Encryption Key. Uses Node's crypto-random
67
+ * source (libsodium-grade on macOS/Linux, BCryptGenRandom on Windows).
68
+ */
69
+ function generateDek() {
70
+ return node_crypto_1.default.randomBytes(exports.KEY_LEN);
71
+ }
72
+ /**
73
+ * Build the canonical AAD for binding a sealed credential to a specific
74
+ * profile + envelope version. Including profile in the AAD prevents an
75
+ * attacker who copies a sealed blob from profile `dev` into profile
76
+ * `prod`'s slot from getting a successful decrypt — the AAD won't match.
77
+ *
78
+ * Stable string format so a re-derivation in a future process produces
79
+ * the exact same AAD bytes for the same profile.
80
+ */
81
+ function buildCredentialAad(profile, version = 1) {
82
+ if (typeof profile !== 'string' || profile.length === 0) {
83
+ throw new Error('buildCredentialAad: profile must be a non-empty string');
84
+ }
85
+ if (!Number.isInteger(version) || version < 1) {
86
+ throw new Error('buildCredentialAad: version must be a positive integer');
87
+ }
88
+ return Buffer.from(`oversky:cred:v${version}:profile=${profile}`, 'utf-8');
89
+ }
90
+ /**
91
+ * Low-level AEAD seal. The shared primitive used by both DEK-wrapping and
92
+ * credential-encryption — the only difference between those operations is
93
+ * the semantic role of the key, not the cipher. Caller chooses the AAD.
94
+ */
95
+ function sealBuffer(plaintext, key, aad) {
96
+ if (!Buffer.isBuffer(plaintext)) {
97
+ throw new TypeError('sealBuffer: plaintext must be a Buffer');
98
+ }
99
+ if (!Buffer.isBuffer(key) || key.length !== exports.KEY_LEN) {
100
+ throw new TypeError(`sealBuffer: key must be a ${exports.KEY_LEN}-byte Buffer`);
101
+ }
102
+ const { nonce, ct, tag } = (0, aead_js_1.aeadSeal)(plaintext, key, aad);
103
+ return { v: 1, nonce, ct, tag };
104
+ }
105
+ /**
106
+ * Low-level AEAD open. Throws on tag mismatch (wrong key, AAD mismatch,
107
+ * truncated/corrupted ciphertext) — never silently returns garbage.
108
+ */
109
+ function openBuffer(sealed, key, aad) {
110
+ if (!sealed || sealed.v !== 1) {
111
+ throw new Error('openBuffer: unsupported envelope version');
112
+ }
113
+ if (!Buffer.isBuffer(key) || key.length !== exports.KEY_LEN) {
114
+ throw new TypeError(`openBuffer: key must be a ${exports.KEY_LEN}-byte Buffer`);
115
+ }
116
+ if (!Buffer.isBuffer(sealed.nonce) || sealed.nonce.length !== NONCE_LEN) {
117
+ throw new Error('openBuffer: invalid nonce length');
118
+ }
119
+ if (!Buffer.isBuffer(sealed.tag) || sealed.tag.length !== TAG_LEN) {
120
+ throw new Error('openBuffer: invalid auth tag length');
121
+ }
122
+ if (!Buffer.isBuffer(sealed.ct)) {
123
+ throw new Error('openBuffer: invalid ciphertext');
124
+ }
125
+ return (0, aead_js_1.aeadOpen)(sealed, key, aad);
126
+ }
127
+ /**
128
+ * Wrap a 32-byte DEK under a KEK. The wrapped DEK is what gets persisted
129
+ * to disk — the KEK itself never leaves its platform store.
130
+ */
131
+ function wrap(dek, kek, aad) {
132
+ if (!Buffer.isBuffer(dek) || dek.length !== exports.KEY_LEN) {
133
+ throw new TypeError(`wrap: dek must be a ${exports.KEY_LEN}-byte Buffer`);
134
+ }
135
+ return sealBuffer(dek, kek, aad);
136
+ }
137
+ /**
138
+ * Unwrap a previously-wrapped DEK using the same KEK. AAD must match the
139
+ * value used at wrap time or the GCM tag check fails.
140
+ */
141
+ function unwrap(wrapped, kek, aad) {
142
+ const out = openBuffer(wrapped, kek, aad);
143
+ if (out.length !== exports.KEY_LEN) {
144
+ throw new Error(`unwrap: unwrapped DEK has wrong length (got ${out.length})`);
145
+ }
146
+ return out;
147
+ }
148
+ /**
149
+ * Encrypt a credential (string or Buffer) under a DEK. Strings are
150
+ * encoded as UTF-8 before sealing; the open path returns a Buffer so the
151
+ * caller decides how to interpret it.
152
+ */
153
+ function encrypt(plaintext, dek, aad) {
154
+ const buf = typeof plaintext === 'string' ? Buffer.from(plaintext, 'utf-8') : plaintext;
155
+ return sealBuffer(buf, dek, aad);
156
+ }
157
+ /**
158
+ * Decrypt a credential previously sealed with `encrypt`. Returns the raw
159
+ * plaintext bytes; UTF-8-decode at the call site if the original was a
160
+ * string.
161
+ */
162
+ function decrypt(sealed, dek, aad) {
163
+ return openBuffer(sealed, dek, aad);
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // String serialization — keychain slots and JSON files store strings, so a
167
+ // SealedBlob has to round-trip through a single text token.
168
+ //
169
+ // Format: osk-env1:<b64u(nonce)>:<b64u(ct)>:<b64u(tag)>
170
+ // Length: ~88 chars for a typical 100-byte plaintext (well under the
171
+ // keychain's per-slot limit).
172
+ // ---------------------------------------------------------------------------
173
+ /** URL-safe base64 (no padding) — keychain-friendly, no `=` to confuse parsers. */
174
+ function b64u(buf) {
175
+ return buf.toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
176
+ }
177
+ function b64uDecode(s) {
178
+ const padded = s.replace(/-/g, '+').replace(/_/g, '/');
179
+ const pad = padded.length % 4;
180
+ return Buffer.from(pad === 0 ? padded : padded + '='.repeat(4 - pad), 'base64');
181
+ }
182
+ /**
183
+ * Serialize a SealedBlob to a single text token. Stable across processes
184
+ * and platforms — same blob serializes to the same string everywhere.
185
+ */
186
+ function serialize(sealed) {
187
+ if (!sealed || sealed.v !== 1) {
188
+ throw new Error('serialize: unsupported envelope version');
189
+ }
190
+ return `${exports.ENVELOPE_MAGIC}:${b64u(sealed.nonce)}:${b64u(sealed.ct)}:${b64u(sealed.tag)}`;
191
+ }
192
+ /**
193
+ * Parse a serialized envelope string. Returns null on any malformed
194
+ * input — callers should treat null as "this is not an envelope" and
195
+ * fall through to the legacy plaintext path.
196
+ *
197
+ * Deliberately tolerant: garbage in returns null, never throws. Throws
198
+ * are reserved for the cipher path so a wrong key surfaces as a real
199
+ * authentication failure instead of being silently swallowed.
200
+ */
201
+ function deserialize(s) {
202
+ if (typeof s !== 'string' || s.length === 0)
203
+ return null;
204
+ const parts = s.split(':');
205
+ if (parts.length !== 4)
206
+ return null;
207
+ if (parts[0] !== exports.ENVELOPE_MAGIC)
208
+ return null;
209
+ try {
210
+ const nonce = b64uDecode(parts[1]);
211
+ const ct = b64uDecode(parts[2]);
212
+ const tag = b64uDecode(parts[3]);
213
+ if (nonce.length !== NONCE_LEN || tag.length !== TAG_LEN)
214
+ return null;
215
+ return { v: 1, nonce, ct, tag };
216
+ }
217
+ catch {
218
+ return null;
219
+ }
220
+ }
221
+ /**
222
+ * Cheap structural check — does this string LOOK like a serialized
223
+ * envelope? Used by dual-read paths to decide whether to attempt a
224
+ * decrypt or to treat the value as legacy plaintext. A true return does
225
+ * NOT guarantee the contents are valid; the actual decrypt is what
226
+ * authenticates the value.
227
+ */
228
+ function isEnvelopeString(s) {
229
+ return typeof s === 'string' && s.startsWith(`${exports.ENVELOPE_MAGIC}:`);
230
+ }
@@ -0,0 +1,116 @@
1
+ type CredEnvelopeState = {
2
+ kind: 'uninit';
3
+ } | {
4
+ kind: 'disabled';
5
+ reason: DisabledReason;
6
+ } | {
7
+ kind: 'active';
8
+ dek: Buffer;
9
+ kekId: string;
10
+ /**
11
+ * Backend-tier identifier captured at init time
12
+ * (e.g. `macos-keychain-shellout`, `in-memory-test`). Snapshotted
13
+ * here so transform-side events can surface the kind without
14
+ * holding a live `kek` reference.
15
+ */
16
+ kekKind: string | null;
17
+ profile: string;
18
+ };
19
+ type DisabledReason = 'kek_unavailable' | 'init_failed' | 'platform_unsupported' | 'kek_rotated';
20
+ /** @internal Test seam — force-set state, returns previous. */
21
+ export declare function __setStateForTest(next: CredEnvelopeState): CredEnvelopeState;
22
+ /** @internal Test seam — read state for assertions. */
23
+ export declare function __getStateForTest(): CredEnvelopeState;
24
+ /**
25
+ * Initialize the envelope path. Idempotent — second call when state is
26
+ * non-uninit returns immediately.
27
+ *
28
+ * Order:
29
+ * 1. Probe KEK; unavailable → disabled+kek_unavailable.
30
+ * 2. Try to read+unwrap an existing DEK; success → active.
31
+ * 3. Generate fresh DEK, wrap, persist atomically; → active.
32
+ * 4. Any throw → disabled+init_failed; transforms stay passthrough.
33
+ */
34
+ export declare function initCredEnvelope(profile: string): Promise<void>;
35
+ /**
36
+ * Reset state. With `clearOnDisk` deletes the wrapped DEK file too —
37
+ * used by `oversky logout` / `sky logout`. Zeroes the in-memory DEK
38
+ * before dropping state for forward secrecy on the active credential.
39
+ */
40
+ export declare function resetCredEnvelope(opts?: {
41
+ profile?: string;
42
+ clearOnDisk?: boolean;
43
+ }): Promise<void>;
44
+ export declare function shutdownCredEnvelope(): void;
45
+ /** @internal Test seam — re-arm `shutdownCredEnvelope` so it can run again. */
46
+ export declare function __resetShutdownGuardForTest(): void;
47
+ /**
48
+ * True iff the envelope path is initialized AND active. Hot-path
49
+ * predicate used by sync transforms; never throws.
50
+ */
51
+ export declare function isCredEnvelopeActive(): boolean;
52
+ /**
53
+ * Operator-readable summary of envelope state. Used by `oversky status`
54
+ * and CLI debug commands. Never includes the DEK or any key material.
55
+ *
56
+ * `kekRotated` is true when the most recent init pass detected a wrapped
57
+ * DEK that could not be unwrapped under the currently-available KEK and
58
+ * had to fall back to a fresh enrollment. Operators see this in
59
+ * `oversky status`; clears on the next clean `resetCredEnvelope`.
60
+ */
61
+ export declare function describeCredEnvelopeState(): {
62
+ kind: CredEnvelopeState['kind'];
63
+ reason?: DisabledReason;
64
+ kekId?: string;
65
+ kekKind?: string | null;
66
+ profile?: string;
67
+ kekRotated?: boolean;
68
+ };
69
+ /**
70
+ * Sync write transform. Returns the wrapped serialized form when active.
71
+ *
72
+ * Behavior when not active:
73
+ * - If `credEnvelopeFailClosed` was set via `configureAuthCore()` (or
74
+ * `OVERSKY_KEK_REQUIRED=1` is set as a back-compat trigger), emits
75
+ * `cred_envelope.wrap.refused` and throws — the caller must not
76
+ * write plaintext.
77
+ * - Otherwise, returns the plaintext (legacy fail-open behavior used
78
+ * by CLI today during dark-ship rollout).
79
+ *
80
+ * Empty-string input is universally passthrough — there is no secret
81
+ * to protect.
82
+ */
83
+ export declare function maybeEncryptForWrite(plaintext: string): string;
84
+ /**
85
+ * Always-fail-open variant of `maybeEncryptForWrite` for callers that
86
+ * cannot afford to abort their write when the envelope path is inactive
87
+ * (today: device-identity persistence, which writes the private key as
88
+ * plaintext JSON before the L12 KEK ships per-platform). The wire form
89
+ * is identical to `maybeEncryptForWrite` when active (envelope-prefixed)
90
+ * and identical to the input when not.
91
+ *
92
+ * Distinct from `maybeEncryptForWrite` because that helper honors the
93
+ * daemon's `credEnvelopeFailClosed: true` policy — designed for callers
94
+ * that MUST refuse a plaintext write rather than silently degrade.
95
+ */
96
+ export declare function wrapIfActiveOrPassthrough(plaintext: string): string;
97
+ /**
98
+ * Sync read transform. Three cases:
99
+ * 1. Envelope on disk + path active → decrypt; null on tag mismatch
100
+ * (caller treats null as a forced re-login signal).
101
+ * 2. Envelope on disk + path NOT active → null (rolled-back binary
102
+ * while envelopes are persisted).
103
+ * 3. Legacy plaintext → return as-is; if path is active, set
104
+ * `needsMigration=true` so caller can upgrade in place.
105
+ */
106
+ export interface ReadTransformResult {
107
+ /** Decoded plaintext, or null on hard failure. */
108
+ plaintext: string | null;
109
+ /** True when the on-disk form was legacy plaintext AND the envelope
110
+ * path is active — caller should re-write to upgrade. */
111
+ needsMigration: boolean;
112
+ }
113
+ export declare function maybeDecryptOnRead(stored: string): ReadTransformResult;
114
+ /** @internal Test seam — wrapped-DEK path resolver. */
115
+ export declare function __wrappedDekFilePathForTest(profile: string): string;
116
+ export {};