@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.
- package/README.md +21 -0
- package/dist/cjs/aead.d.ts +24 -0
- package/dist/cjs/aead.js +65 -0
- package/dist/cjs/authExpiredDetector.d.ts +65 -0
- package/dist/cjs/authExpiredDetector.js +89 -0
- package/dist/cjs/authHelper.d.ts +51 -0
- package/dist/cjs/authHelper.js +321 -0
- package/dist/cjs/credentialEnvelope.d.ts +85 -0
- package/dist/cjs/credentialEnvelope.js +230 -0
- package/dist/cjs/credentialEnvelopeBridge.d.ts +116 -0
- package/dist/cjs/credentialEnvelopeBridge.js +637 -0
- package/dist/cjs/credentialResolver.d.ts +278 -0
- package/dist/cjs/credentialResolver.js +423 -0
- package/dist/cjs/daemonPair.d.ts +172 -0
- package/dist/cjs/daemonPair.js +495 -0
- package/dist/cjs/daemonScopes.d.ts +74 -0
- package/dist/cjs/daemonScopes.js +84 -0
- package/dist/cjs/deviceIdentityBridge.d.ts +113 -0
- package/dist/cjs/deviceIdentityBridge.js +591 -0
- package/dist/cjs/deviceKey.d.ts +167 -0
- package/dist/cjs/deviceKey.js +332 -0
- package/dist/cjs/fdToken.d.ts +17 -0
- package/dist/cjs/fdToken.js +238 -0
- package/dist/cjs/harnessTrust.d.ts +72 -0
- package/dist/cjs/harnessTrust.js +103 -0
- package/dist/cjs/headers-helper.d.ts +25 -0
- package/dist/cjs/headers-helper.js +323 -0
- package/dist/cjs/index.d.ts +37 -0
- package/dist/cjs/index.js +305 -0
- package/dist/cjs/jwtUtils.d.ts +35 -0
- package/dist/cjs/jwtUtils.js +61 -0
- package/dist/cjs/kek/index.d.ts +89 -0
- package/dist/cjs/kek/index.js +208 -0
- package/dist/cjs/kek/linux.d.ts +92 -0
- package/dist/cjs/kek/linux.js +596 -0
- package/dist/cjs/kek/macos.d.ts +112 -0
- package/dist/cjs/kek/macos.js +599 -0
- package/dist/cjs/kek/types.d.ts +111 -0
- package/dist/cjs/kek/types.js +43 -0
- package/dist/cjs/kek/windows.d.ts +71 -0
- package/dist/cjs/kek/windows.js +350 -0
- package/dist/cjs/kek/zeroize-registry.d.ts +44 -0
- package/dist/cjs/kek/zeroize-registry.js +63 -0
- package/dist/cjs/legacyStatePreflight.d.ts +67 -0
- package/dist/cjs/legacyStatePreflight.js +78 -0
- package/dist/cjs/localIdentity.d.ts +54 -0
- package/dist/cjs/localIdentity.js +57 -0
- package/dist/cjs/loginLocalhost.d.ts +115 -0
- package/dist/cjs/loginLocalhost.js +368 -0
- package/dist/cjs/loginWithLocalhost.d.ts +131 -0
- package/dist/cjs/loginWithLocalhost.js +359 -0
- package/dist/cjs/machineId.d.ts +35 -0
- package/dist/cjs/machineId.js +239 -0
- package/dist/cjs/messages.d.ts +9 -0
- package/dist/cjs/messages.js +44 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/pkce.d.ts +62 -0
- package/dist/cjs/pkce.js +158 -0
- package/dist/cjs/recoveryCode.d.ts +61 -0
- package/dist/cjs/recoveryCode.js +213 -0
- package/dist/cjs/refresh.d.ts +215 -0
- package/dist/cjs/refresh.js +877 -0
- package/dist/cjs/refreshClassification.d.ts +53 -0
- package/dist/cjs/refreshClassification.js +105 -0
- package/dist/cjs/refreshScheduler.d.ts +53 -0
- package/dist/cjs/refreshScheduler.js +332 -0
- package/dist/cjs/runtime.d.ts +149 -0
- package/dist/cjs/runtime.js +136 -0
- package/dist/cjs/spawnEnv.d.ts +76 -0
- package/dist/cjs/spawnEnv.js +153 -0
- package/dist/cjs/ttlParser.d.ts +54 -0
- package/dist/cjs/ttlParser.js +87 -0
- package/dist/cjs/types.d.ts +91 -0
- package/dist/cjs/types.js +26 -0
- package/dist/esm/aead.d.ts +24 -0
- package/dist/esm/aead.js +57 -0
- package/dist/esm/authExpiredDetector.d.ts +65 -0
- package/dist/esm/authExpiredDetector.js +85 -0
- package/dist/esm/authHelper.d.ts +51 -0
- package/dist/esm/authHelper.js +311 -0
- package/dist/esm/credentialEnvelope.d.ts +85 -0
- package/dist/esm/credentialEnvelope.js +213 -0
- package/dist/esm/credentialEnvelopeBridge.d.ts +116 -0
- package/dist/esm/credentialEnvelopeBridge.js +620 -0
- package/dist/esm/credentialResolver.d.ts +278 -0
- package/dist/esm/credentialResolver.js +414 -0
- package/dist/esm/daemonPair.d.ts +172 -0
- package/dist/esm/daemonPair.js +476 -0
- package/dist/esm/daemonScopes.d.ts +74 -0
- package/dist/esm/daemonScopes.js +80 -0
- package/dist/esm/deviceIdentityBridge.d.ts +113 -0
- package/dist/esm/deviceIdentityBridge.js +572 -0
- package/dist/esm/deviceKey.d.ts +167 -0
- package/dist/esm/deviceKey.js +320 -0
- package/dist/esm/fdToken.d.ts +17 -0
- package/dist/esm/fdToken.js +231 -0
- package/dist/esm/harnessTrust.d.ts +72 -0
- package/dist/esm/harnessTrust.js +97 -0
- package/dist/esm/headers-helper.d.ts +25 -0
- package/dist/esm/headers-helper.js +316 -0
- package/dist/esm/index.d.ts +37 -0
- package/dist/esm/index.js +144 -0
- package/dist/esm/jwtUtils.d.ts +35 -0
- package/dist/esm/jwtUtils.js +57 -0
- package/dist/esm/kek/index.d.ts +89 -0
- package/dist/esm/kek/index.js +162 -0
- package/dist/esm/kek/linux.d.ts +92 -0
- package/dist/esm/kek/linux.js +585 -0
- package/dist/esm/kek/macos.d.ts +112 -0
- package/dist/esm/kek/macos.js +586 -0
- package/dist/esm/kek/types.d.ts +111 -0
- package/dist/esm/kek/types.js +39 -0
- package/dist/esm/kek/windows.d.ts +71 -0
- package/dist/esm/kek/windows.js +344 -0
- package/dist/esm/kek/zeroize-registry.d.ts +44 -0
- package/dist/esm/kek/zeroize-registry.js +58 -0
- package/dist/esm/legacyStatePreflight.d.ts +67 -0
- package/dist/esm/legacyStatePreflight.js +71 -0
- package/dist/esm/localIdentity.d.ts +54 -0
- package/dist/esm/localIdentity.js +54 -0
- package/dist/esm/loginLocalhost.d.ts +115 -0
- package/dist/esm/loginLocalhost.js +355 -0
- package/dist/esm/loginWithLocalhost.d.ts +131 -0
- package/dist/esm/loginWithLocalhost.js +353 -0
- package/dist/esm/machineId.d.ts +35 -0
- package/dist/esm/machineId.js +231 -0
- package/dist/esm/messages.d.ts +9 -0
- package/dist/esm/messages.js +40 -0
- package/dist/esm/pkce.d.ts +62 -0
- package/dist/esm/pkce.js +148 -0
- package/dist/esm/recoveryCode.d.ts +61 -0
- package/dist/esm/recoveryCode.js +207 -0
- package/dist/esm/refresh.d.ts +215 -0
- package/dist/esm/refresh.js +863 -0
- package/dist/esm/refreshClassification.d.ts +53 -0
- package/dist/esm/refreshClassification.js +106 -0
- package/dist/esm/refreshScheduler.d.ts +53 -0
- package/dist/esm/refreshScheduler.js +329 -0
- package/dist/esm/runtime.d.ts +149 -0
- package/dist/esm/runtime.js +121 -0
- package/dist/esm/spawnEnv.d.ts +76 -0
- package/dist/esm/spawnEnv.js +149 -0
- package/dist/esm/ttlParser.d.ts +54 -0
- package/dist/esm/ttlParser.js +83 -0
- package/dist/esm/types.d.ts +91 -0
- package/dist/esm/types.js +21 -0
- 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 {};
|