@wishknish/knishio-client-ts 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/index.cjs +1290 -116
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +152 -18
- package/dist/index.d.ts +152 -18
- package/dist/index.iife.js +1283 -109
- package/dist/index.iife.js.map +1 -1
- package/dist/index.js +1281 -117
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/exception/SecretStorageException.ts +9 -0
- package/src/index.ts +16 -2
- package/src/storage/FileStorageBackend.ts +176 -0
- package/src/storage/MemorySecretStorageProvider.ts +75 -3
- package/src/storage/NonExtractableKeySecretStorageProvider.ts +547 -0
- package/src/storage/WebAuthnPrfSecretStorageProvider.ts +679 -0
- package/src/storage/WebCryptoSecretStorageProvider.ts +105 -134
- package/src/storage/WebStorageBackend.ts +102 -0
- package/src/storage/index.ts +26 -2
- package/src/storage/secretEnvelope.ts +200 -0
- package/src/types/storage.ts +22 -2
package/dist/index.iife.js
CHANGED
|
@@ -20109,6 +20109,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
20109
20109
|
}
|
|
20110
20110
|
);
|
|
20111
20111
|
}
|
|
20112
|
+
/**
|
|
20113
|
+
* Validation error for storage options or parameters
|
|
20114
|
+
*/
|
|
20115
|
+
static validationError(message) {
|
|
20116
|
+
return new _SecretStorageException(message, {
|
|
20117
|
+
code: "VALIDATION_ERROR"
|
|
20118
|
+
});
|
|
20119
|
+
}
|
|
20112
20120
|
};
|
|
20113
20121
|
}
|
|
20114
20122
|
});
|
|
@@ -52072,10 +52080,121 @@ ${operationTypes.join("\n")}
|
|
|
52072
52080
|
return result === 0;
|
|
52073
52081
|
}
|
|
52074
52082
|
|
|
52083
|
+
// src/storage/secretEnvelope.ts
|
|
52084
|
+
init_SecretStorageException();
|
|
52085
|
+
var ENVELOPE_ALGORITHM = "AES-GCM";
|
|
52086
|
+
var DEFAULT_ITERATIONS = 1e5;
|
|
52087
|
+
var SECRET_KEY_PREFIX = "knishio:secret:";
|
|
52088
|
+
var RECOVERY_KEY_PREFIX = "knishio:recovery:";
|
|
52089
|
+
var GCM_IV_LENGTH = 12;
|
|
52090
|
+
var SALT_LENGTH = 16;
|
|
52091
|
+
var textEncoder2 = new TextEncoder();
|
|
52092
|
+
function uint8ArrayToBase642(bytes) {
|
|
52093
|
+
let binary = "";
|
|
52094
|
+
const len = bytes.byteLength;
|
|
52095
|
+
for (let i4 = 0; i4 < len; i4++) {
|
|
52096
|
+
const byte = bytes[i4];
|
|
52097
|
+
if (byte !== void 0) {
|
|
52098
|
+
binary += String.fromCharCode(byte);
|
|
52099
|
+
}
|
|
52100
|
+
}
|
|
52101
|
+
return btoa(binary);
|
|
52102
|
+
}
|
|
52103
|
+
function base64ToUint8Array2(base643) {
|
|
52104
|
+
const binary = atob(base643);
|
|
52105
|
+
const len = binary.length;
|
|
52106
|
+
const bytes = new Uint8Array(len);
|
|
52107
|
+
for (let i4 = 0; i4 < len; i4++) {
|
|
52108
|
+
bytes[i4] = binary.charCodeAt(i4);
|
|
52109
|
+
}
|
|
52110
|
+
return bytes;
|
|
52111
|
+
}
|
|
52112
|
+
async function deriveEnvelopeKey(passphrase, salt, iterations = DEFAULT_ITERATIONS) {
|
|
52113
|
+
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
52114
|
+
throw new exports.SecretStorageException("WebCrypto API is not available");
|
|
52115
|
+
}
|
|
52116
|
+
const passphraseBytes = textEncoder2.encode(passphrase);
|
|
52117
|
+
try {
|
|
52118
|
+
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
52119
|
+
"raw",
|
|
52120
|
+
passphraseBytes,
|
|
52121
|
+
"PBKDF2",
|
|
52122
|
+
false,
|
|
52123
|
+
["deriveKey"]
|
|
52124
|
+
);
|
|
52125
|
+
return await globalThis.crypto.subtle.deriveKey(
|
|
52126
|
+
{
|
|
52127
|
+
name: "PBKDF2",
|
|
52128
|
+
salt,
|
|
52129
|
+
iterations,
|
|
52130
|
+
hash: "SHA-256"
|
|
52131
|
+
},
|
|
52132
|
+
baseKey,
|
|
52133
|
+
{ name: "AES-GCM", length: 256 },
|
|
52134
|
+
false,
|
|
52135
|
+
["encrypt", "decrypt"]
|
|
52136
|
+
);
|
|
52137
|
+
} finally {
|
|
52138
|
+
zeroizeBytes(passphraseBytes);
|
|
52139
|
+
}
|
|
52140
|
+
}
|
|
52141
|
+
async function sealEnvelope(secret, passphrase, metadata) {
|
|
52142
|
+
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
52143
|
+
throw new exports.SecretStorageException("WebCrypto API is not available");
|
|
52144
|
+
}
|
|
52145
|
+
const salt = new Uint8Array(SALT_LENGTH);
|
|
52146
|
+
const iv = new Uint8Array(GCM_IV_LENGTH);
|
|
52147
|
+
globalThis.crypto.getRandomValues(salt);
|
|
52148
|
+
globalThis.crypto.getRandomValues(iv);
|
|
52149
|
+
const key = await deriveEnvelopeKey(passphrase, salt, DEFAULT_ITERATIONS);
|
|
52150
|
+
const secretBytes = textEncoder2.encode(secret);
|
|
52151
|
+
try {
|
|
52152
|
+
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
52153
|
+
{
|
|
52154
|
+
name: ENVELOPE_ALGORITHM,
|
|
52155
|
+
iv
|
|
52156
|
+
},
|
|
52157
|
+
key,
|
|
52158
|
+
secretBytes
|
|
52159
|
+
);
|
|
52160
|
+
const ciphertext = uint8ArrayToBase642(new Uint8Array(encryptedBuffer));
|
|
52161
|
+
return {
|
|
52162
|
+
version: 1,
|
|
52163
|
+
ciphertext,
|
|
52164
|
+
iv: uint8ArrayToBase642(iv),
|
|
52165
|
+
salt: uint8ArrayToBase642(salt),
|
|
52166
|
+
algorithm: ENVELOPE_ALGORITHM,
|
|
52167
|
+
iterations: DEFAULT_ITERATIONS,
|
|
52168
|
+
metadata
|
|
52169
|
+
};
|
|
52170
|
+
} finally {
|
|
52171
|
+
zeroizeBytes(secretBytes);
|
|
52172
|
+
}
|
|
52173
|
+
}
|
|
52174
|
+
async function openEnvelope(payload, passphrase) {
|
|
52175
|
+
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
52176
|
+
throw new exports.SecretStorageException("WebCrypto API is not available");
|
|
52177
|
+
}
|
|
52178
|
+
const salt = base64ToUint8Array2(payload.salt);
|
|
52179
|
+
const iv = base64ToUint8Array2(payload.iv);
|
|
52180
|
+
const ciphertext = base64ToUint8Array2(payload.ciphertext);
|
|
52181
|
+
const key = await deriveEnvelopeKey(passphrase, salt, payload.iterations ?? DEFAULT_ITERATIONS);
|
|
52182
|
+
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
52183
|
+
{
|
|
52184
|
+
name: ENVELOPE_ALGORITHM,
|
|
52185
|
+
iv
|
|
52186
|
+
},
|
|
52187
|
+
key,
|
|
52188
|
+
ciphertext
|
|
52189
|
+
);
|
|
52190
|
+
return new Uint8Array(decryptedBuffer);
|
|
52191
|
+
}
|
|
52192
|
+
|
|
52075
52193
|
// src/storage/MemorySecretStorageProvider.ts
|
|
52076
52194
|
var MemorySecretStorageProvider = class {
|
|
52077
52195
|
providerType = "memory";
|
|
52078
52196
|
secrets = /* @__PURE__ */ new Map();
|
|
52197
|
+
recoverySecrets = /* @__PURE__ */ new Map();
|
|
52079
52198
|
/**
|
|
52080
52199
|
* Memory storage is not hardware backed
|
|
52081
52200
|
*/
|
|
@@ -52106,6 +52225,17 @@ ${operationTypes.join("\n")}
|
|
|
52106
52225
|
providerType: this.providerType
|
|
52107
52226
|
};
|
|
52108
52227
|
this.secrets.set(bundleHash, { secret, metadata });
|
|
52228
|
+
if (options?.recoveryPassphrase) {
|
|
52229
|
+
const recoveryMetadata = {
|
|
52230
|
+
bundleHash,
|
|
52231
|
+
label: options?.label,
|
|
52232
|
+
createdAt: Date.now(),
|
|
52233
|
+
hardwareBacked: false,
|
|
52234
|
+
providerType: "webcrypto-aes-gcm"
|
|
52235
|
+
};
|
|
52236
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
52237
|
+
this.recoverySecrets.set(bundleHash, JSON.stringify(recoveryPayload));
|
|
52238
|
+
}
|
|
52109
52239
|
}
|
|
52110
52240
|
/**
|
|
52111
52241
|
* Retrieve a secret from memory
|
|
@@ -52118,6 +52248,7 @@ ${operationTypes.join("\n")}
|
|
|
52118
52248
|
* Delete a stored secret
|
|
52119
52249
|
*/
|
|
52120
52250
|
async deleteSecret(bundleHash) {
|
|
52251
|
+
this.recoverySecrets.delete(bundleHash);
|
|
52121
52252
|
return this.secrets.delete(bundleHash);
|
|
52122
52253
|
}
|
|
52123
52254
|
/**
|
|
@@ -52147,6 +52278,48 @@ ${operationTypes.join("\n")}
|
|
|
52147
52278
|
*/
|
|
52148
52279
|
clear() {
|
|
52149
52280
|
this.secrets.clear();
|
|
52281
|
+
this.recoverySecrets.clear();
|
|
52282
|
+
}
|
|
52283
|
+
/**
|
|
52284
|
+
* Recover a secret using its recovery envelope and restore it
|
|
52285
|
+
*/
|
|
52286
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
52287
|
+
if (!bundleHash) {
|
|
52288
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
52289
|
+
}
|
|
52290
|
+
if (!recoveryPassphrase) {
|
|
52291
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
52292
|
+
}
|
|
52293
|
+
const raw = this.recoverySecrets.get(bundleHash);
|
|
52294
|
+
if (!raw) {
|
|
52295
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
52296
|
+
}
|
|
52297
|
+
let payload;
|
|
52298
|
+
try {
|
|
52299
|
+
payload = JSON.parse(raw);
|
|
52300
|
+
} catch {
|
|
52301
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
52302
|
+
}
|
|
52303
|
+
let decryptedBytes;
|
|
52304
|
+
try {
|
|
52305
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
52306
|
+
} catch (err) {
|
|
52307
|
+
if (err instanceof exports.SecretStorageException) {
|
|
52308
|
+
throw err;
|
|
52309
|
+
}
|
|
52310
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
52311
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
52312
|
+
}
|
|
52313
|
+
let secretStr;
|
|
52314
|
+
try {
|
|
52315
|
+
secretStr = new TextDecoder().decode(decryptedBytes);
|
|
52316
|
+
} finally {
|
|
52317
|
+
zeroizeBytes(decryptedBytes);
|
|
52318
|
+
}
|
|
52319
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
52320
|
+
...options,
|
|
52321
|
+
recoveryPassphrase
|
|
52322
|
+
});
|
|
52150
52323
|
}
|
|
52151
52324
|
};
|
|
52152
52325
|
|
|
@@ -53968,45 +54141,25 @@ ${operationTypes.join("\n")}
|
|
|
53968
54141
|
return Array.from(this.store.keys());
|
|
53969
54142
|
}
|
|
53970
54143
|
};
|
|
53971
|
-
function uint8ArrayToBase642(bytes) {
|
|
53972
|
-
let binary = "";
|
|
53973
|
-
const len = bytes.byteLength;
|
|
53974
|
-
for (let i4 = 0; i4 < len; i4++) {
|
|
53975
|
-
const byte = bytes[i4];
|
|
53976
|
-
if (byte !== void 0) {
|
|
53977
|
-
binary += String.fromCharCode(byte);
|
|
53978
|
-
}
|
|
53979
|
-
}
|
|
53980
|
-
return btoa(binary);
|
|
53981
|
-
}
|
|
53982
|
-
function base64ToUint8Array2(base643) {
|
|
53983
|
-
const binary = atob(base643);
|
|
53984
|
-
const len = binary.length;
|
|
53985
|
-
const bytes = new Uint8Array(len);
|
|
53986
|
-
for (let i4 = 0; i4 < len; i4++) {
|
|
53987
|
-
bytes[i4] = binary.charCodeAt(i4);
|
|
53988
|
-
}
|
|
53989
|
-
return bytes;
|
|
53990
|
-
}
|
|
53991
|
-
var textEncoder2 = new TextEncoder();
|
|
53992
54144
|
var textDecoder = new TextDecoder();
|
|
53993
|
-
var KEY_PREFIX =
|
|
53994
|
-
var DEFAULT_ITERATIONS = 1e5;
|
|
54145
|
+
var KEY_PREFIX = SECRET_KEY_PREFIX;
|
|
53995
54146
|
var WebCryptoSecretStorageProvider = class {
|
|
53996
54147
|
providerType = "webcrypto-aes-gcm";
|
|
53997
54148
|
backend;
|
|
53998
54149
|
defaultPassphrase;
|
|
53999
|
-
hardwareBacked;
|
|
54000
54150
|
constructor(options = {}) {
|
|
54001
54151
|
this.backend = options.backend ?? new MemoryStorageBackend();
|
|
54002
54152
|
this.defaultPassphrase = options.defaultPassphrase;
|
|
54003
|
-
this.hardwareBacked = options.hardwareBacked ?? false;
|
|
54004
54153
|
}
|
|
54005
54154
|
/**
|
|
54006
|
-
*
|
|
54155
|
+
* True only when this provider holds a non-exportable key inside platform-secure
|
|
54156
|
+
* hardware (Android TEE/StrongBox, Secure Enclave, TPM) and learned that from the
|
|
54157
|
+
* platform itself — never from a caller argument. Software envelope providers
|
|
54158
|
+
* return false. The value is persisted as `metadata.hardwareBacked` in every
|
|
54159
|
+
* envelope this provider writes.
|
|
54007
54160
|
*/
|
|
54008
54161
|
isHardwareBacked() {
|
|
54009
|
-
return
|
|
54162
|
+
return false;
|
|
54010
54163
|
}
|
|
54011
54164
|
/**
|
|
54012
54165
|
* Check if WebCrypto subtle API is available
|
|
@@ -54014,38 +54167,6 @@ ${operationTypes.join("\n")}
|
|
|
54014
54167
|
async isAvailable() {
|
|
54015
54168
|
return typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
|
|
54016
54169
|
}
|
|
54017
|
-
/**
|
|
54018
|
-
* Derive an AES-GCM CryptoKey from a passphrase and salt using PBKDF2
|
|
54019
|
-
*/
|
|
54020
|
-
async deriveKey(passphrase, salt, iterations = DEFAULT_ITERATIONS) {
|
|
54021
|
-
if (!await this.isAvailable()) {
|
|
54022
|
-
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
54023
|
-
}
|
|
54024
|
-
const passphraseBytes = textEncoder2.encode(passphrase);
|
|
54025
|
-
try {
|
|
54026
|
-
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
54027
|
-
"raw",
|
|
54028
|
-
passphraseBytes,
|
|
54029
|
-
"PBKDF2",
|
|
54030
|
-
false,
|
|
54031
|
-
["deriveKey"]
|
|
54032
|
-
);
|
|
54033
|
-
return await globalThis.crypto.subtle.deriveKey(
|
|
54034
|
-
{
|
|
54035
|
-
name: "PBKDF2",
|
|
54036
|
-
salt,
|
|
54037
|
-
iterations,
|
|
54038
|
-
hash: "SHA-256"
|
|
54039
|
-
},
|
|
54040
|
-
baseKey,
|
|
54041
|
-
{ name: "AES-GCM", length: 256 },
|
|
54042
|
-
false,
|
|
54043
|
-
["encrypt", "decrypt"]
|
|
54044
|
-
);
|
|
54045
|
-
} finally {
|
|
54046
|
-
zeroizeBytes(passphraseBytes);
|
|
54047
|
-
}
|
|
54048
|
-
}
|
|
54049
54170
|
/**
|
|
54050
54171
|
* Store and encrypt a master secret
|
|
54051
54172
|
*/
|
|
@@ -54060,44 +54181,36 @@ ${operationTypes.join("\n")}
|
|
|
54060
54181
|
if (!passphrase) {
|
|
54061
54182
|
throw new exports.SecretStorageException("Passphrase required for envelope encryption");
|
|
54062
54183
|
}
|
|
54063
|
-
|
|
54064
|
-
|
|
54065
|
-
|
|
54066
|
-
globalThis.crypto.getRandomValues(iv);
|
|
54067
|
-
const key = await this.deriveKey(passphrase, salt, DEFAULT_ITERATIONS);
|
|
54068
|
-
const secretBytes = textEncoder2.encode(secret);
|
|
54184
|
+
if (!await this.isAvailable()) {
|
|
54185
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
54186
|
+
}
|
|
54069
54187
|
try {
|
|
54070
|
-
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
54071
|
-
{
|
|
54072
|
-
name: "AES-GCM",
|
|
54073
|
-
iv
|
|
54074
|
-
},
|
|
54075
|
-
key,
|
|
54076
|
-
secretBytes
|
|
54077
|
-
);
|
|
54078
|
-
const ciphertext = uint8ArrayToBase642(new Uint8Array(encryptedBuffer));
|
|
54079
54188
|
const metadata = {
|
|
54080
54189
|
bundleHash,
|
|
54081
54190
|
label: options?.label,
|
|
54082
54191
|
createdAt: Date.now(),
|
|
54083
|
-
hardwareBacked:
|
|
54192
|
+
hardwareBacked: false,
|
|
54084
54193
|
providerType: this.providerType
|
|
54085
54194
|
};
|
|
54086
|
-
const payload =
|
|
54087
|
-
version: 1,
|
|
54088
|
-
ciphertext,
|
|
54089
|
-
iv: uint8ArrayToBase642(iv),
|
|
54090
|
-
salt: uint8ArrayToBase642(salt),
|
|
54091
|
-
algorithm: "AES-GCM",
|
|
54092
|
-
iterations: DEFAULT_ITERATIONS,
|
|
54093
|
-
metadata
|
|
54094
|
-
};
|
|
54195
|
+
const payload = await sealEnvelope(secret, passphrase, metadata);
|
|
54095
54196
|
await this.backend.setItem(`${KEY_PREFIX}${bundleHash}`, JSON.stringify(payload));
|
|
54197
|
+
if (options?.recoveryPassphrase) {
|
|
54198
|
+
const recoveryMetadata = {
|
|
54199
|
+
bundleHash,
|
|
54200
|
+
label: options?.label,
|
|
54201
|
+
createdAt: Date.now(),
|
|
54202
|
+
hardwareBacked: false,
|
|
54203
|
+
providerType: "webcrypto-aes-gcm"
|
|
54204
|
+
};
|
|
54205
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
54206
|
+
await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
|
|
54207
|
+
}
|
|
54096
54208
|
} catch (err) {
|
|
54209
|
+
if (err instanceof exports.SecretStorageException) {
|
|
54210
|
+
throw err;
|
|
54211
|
+
}
|
|
54097
54212
|
const msg = err instanceof Error ? err.message : String(err);
|
|
54098
54213
|
throw new exports.SecretStorageException(`Encryption failed: ${msg}`);
|
|
54099
|
-
} finally {
|
|
54100
|
-
zeroizeBytes(secretBytes);
|
|
54101
54214
|
}
|
|
54102
54215
|
}
|
|
54103
54216
|
/**
|
|
@@ -54118,26 +54231,20 @@ ${operationTypes.join("\n")}
|
|
|
54118
54231
|
if (!passphrase) {
|
|
54119
54232
|
throw new exports.SecretStorageException("Passphrase required for secret decryption");
|
|
54120
54233
|
}
|
|
54121
|
-
|
|
54122
|
-
|
|
54123
|
-
|
|
54234
|
+
if (!await this.isAvailable()) {
|
|
54235
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
54236
|
+
}
|
|
54124
54237
|
try {
|
|
54125
|
-
const
|
|
54126
|
-
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
54127
|
-
{
|
|
54128
|
-
name: "AES-GCM",
|
|
54129
|
-
iv
|
|
54130
|
-
},
|
|
54131
|
-
key,
|
|
54132
|
-
ciphertext
|
|
54133
|
-
);
|
|
54134
|
-
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
54238
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
54135
54239
|
try {
|
|
54136
54240
|
return textDecoder.decode(decryptedBytes);
|
|
54137
54241
|
} finally {
|
|
54138
54242
|
zeroizeBytes(decryptedBytes);
|
|
54139
54243
|
}
|
|
54140
54244
|
} catch (err) {
|
|
54245
|
+
if (err instanceof exports.SecretStorageException) {
|
|
54246
|
+
throw err;
|
|
54247
|
+
}
|
|
54141
54248
|
const msg = err instanceof Error ? err.message : String(err);
|
|
54142
54249
|
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
54143
54250
|
}
|
|
@@ -54147,7 +54254,9 @@ ${operationTypes.join("\n")}
|
|
|
54147
54254
|
*/
|
|
54148
54255
|
async deleteSecret(bundleHash) {
|
|
54149
54256
|
const key = `${KEY_PREFIX}${bundleHash}`;
|
|
54257
|
+
const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
|
|
54150
54258
|
const result = await this.backend.removeItem(key);
|
|
54259
|
+
await this.backend.removeItem(recoveryKey);
|
|
54151
54260
|
return result !== false;
|
|
54152
54261
|
}
|
|
54153
54262
|
/**
|
|
@@ -54162,7 +54271,7 @@ ${operationTypes.join("\n")}
|
|
|
54162
54271
|
*/
|
|
54163
54272
|
async listSecrets() {
|
|
54164
54273
|
const keys = await this.backend.keys();
|
|
54165
|
-
const matchingKeys = keys.filter((k2) => k2.startsWith(KEY_PREFIX));
|
|
54274
|
+
const matchingKeys = keys.filter((k2) => k2.startsWith(KEY_PREFIX) && !k2.startsWith(RECOVERY_KEY_PREFIX));
|
|
54166
54275
|
const results = [];
|
|
54167
54276
|
for (const key of matchingKeys) {
|
|
54168
54277
|
const raw = await this.backend.getItem(key);
|
|
@@ -54196,22 +54305,633 @@ ${operationTypes.join("\n")}
|
|
|
54196
54305
|
if (!passphrase) {
|
|
54197
54306
|
throw new exports.SecretStorageException("Passphrase required for secret decryption");
|
|
54198
54307
|
}
|
|
54199
|
-
|
|
54200
|
-
|
|
54201
|
-
|
|
54308
|
+
if (!await this.isAvailable()) {
|
|
54309
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
54310
|
+
}
|
|
54311
|
+
try {
|
|
54312
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
54313
|
+
return await withSecureBytes(decryptedBytes, async (bytes) => {
|
|
54314
|
+
const secretString = textDecoder.decode(bytes);
|
|
54315
|
+
return await fn(secretString);
|
|
54316
|
+
});
|
|
54317
|
+
} catch (err) {
|
|
54318
|
+
if (err instanceof exports.SecretStorageException) {
|
|
54319
|
+
throw err;
|
|
54320
|
+
}
|
|
54321
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
54322
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
54323
|
+
}
|
|
54324
|
+
}
|
|
54325
|
+
/**
|
|
54326
|
+
* Recover a secret using its recovery envelope and re-enroll it
|
|
54327
|
+
*/
|
|
54328
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
54329
|
+
if (!bundleHash) {
|
|
54330
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
54331
|
+
}
|
|
54332
|
+
if (!recoveryPassphrase) {
|
|
54333
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
54334
|
+
}
|
|
54335
|
+
const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
|
|
54336
|
+
if (!raw) {
|
|
54337
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
54338
|
+
}
|
|
54339
|
+
let payload;
|
|
54340
|
+
try {
|
|
54341
|
+
payload = JSON.parse(raw);
|
|
54342
|
+
} catch {
|
|
54343
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
54344
|
+
}
|
|
54345
|
+
let decryptedBytes;
|
|
54346
|
+
try {
|
|
54347
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
54348
|
+
} catch (err) {
|
|
54349
|
+
if (err instanceof exports.SecretStorageException) {
|
|
54350
|
+
throw err;
|
|
54351
|
+
}
|
|
54352
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
54353
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
54354
|
+
}
|
|
54355
|
+
let secretStr;
|
|
54356
|
+
try {
|
|
54357
|
+
secretStr = textDecoder.decode(decryptedBytes);
|
|
54358
|
+
} finally {
|
|
54359
|
+
zeroizeBytes(decryptedBytes);
|
|
54360
|
+
}
|
|
54361
|
+
const storePassphrase = options?.passphrase ?? this.defaultPassphrase ?? recoveryPassphrase;
|
|
54362
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
54363
|
+
...options,
|
|
54364
|
+
passphrase: storePassphrase,
|
|
54365
|
+
recoveryPassphrase
|
|
54366
|
+
});
|
|
54367
|
+
}
|
|
54368
|
+
};
|
|
54369
|
+
|
|
54370
|
+
// src/storage/FileStorageBackend.ts
|
|
54371
|
+
init_SecretStorageException();
|
|
54372
|
+
var FileStorageBackend = class {
|
|
54373
|
+
filePath;
|
|
54374
|
+
store = /* @__PURE__ */ new Map();
|
|
54375
|
+
loaded = false;
|
|
54376
|
+
constructor(filePath) {
|
|
54377
|
+
if (!filePath) {
|
|
54378
|
+
throw new exports.SecretStorageException("Storage file path cannot be empty");
|
|
54379
|
+
}
|
|
54380
|
+
this.filePath = filePath;
|
|
54381
|
+
}
|
|
54382
|
+
async getFs() {
|
|
54383
|
+
try {
|
|
54384
|
+
const fs = await import('fs/promises');
|
|
54385
|
+
const path = await import('path');
|
|
54386
|
+
return { fs, path };
|
|
54387
|
+
} catch {
|
|
54388
|
+
throw exports.SecretStorageException.unavailable(
|
|
54389
|
+
"file-storage",
|
|
54390
|
+
"FileStorageBackend is only supported in Node.js environments with node:fs access"
|
|
54391
|
+
);
|
|
54392
|
+
}
|
|
54393
|
+
}
|
|
54394
|
+
async ensureLoaded() {
|
|
54395
|
+
if (this.loaded) {
|
|
54396
|
+
return this.store;
|
|
54397
|
+
}
|
|
54398
|
+
const { fs } = await this.getFs();
|
|
54399
|
+
try {
|
|
54400
|
+
const content = await fs.readFile(this.filePath, "utf8");
|
|
54401
|
+
let parsed;
|
|
54402
|
+
try {
|
|
54403
|
+
parsed = JSON.parse(content);
|
|
54404
|
+
} catch {
|
|
54405
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted storage file format");
|
|
54406
|
+
}
|
|
54407
|
+
if (parsed && typeof parsed === "object") {
|
|
54408
|
+
this.store = new Map(Object.entries(parsed).map(([k2, v3]) => [k2, String(v3)]));
|
|
54409
|
+
}
|
|
54410
|
+
} catch (err) {
|
|
54411
|
+
if (err instanceof exports.SecretStorageException) {
|
|
54412
|
+
throw err;
|
|
54413
|
+
}
|
|
54414
|
+
const nodeErr = err;
|
|
54415
|
+
if (nodeErr?.code !== "ENOENT") {
|
|
54416
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
54417
|
+
throw new exports.SecretStorageException(`Failed to read storage file: ${msg}`);
|
|
54418
|
+
}
|
|
54419
|
+
this.store = /* @__PURE__ */ new Map();
|
|
54420
|
+
}
|
|
54421
|
+
this.loaded = true;
|
|
54422
|
+
return this.store;
|
|
54423
|
+
}
|
|
54424
|
+
async persist() {
|
|
54425
|
+
const { fs, path } = await this.getFs();
|
|
54426
|
+
const dir = path.dirname(this.filePath);
|
|
54427
|
+
if (dir && dir !== ".") {
|
|
54428
|
+
await fs.mkdir(dir, { recursive: true });
|
|
54429
|
+
}
|
|
54430
|
+
const tmpPath = `${this.filePath}.tmp.${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
54431
|
+
const data = JSON.stringify(Object.fromEntries(this.store), null, 2);
|
|
54432
|
+
try {
|
|
54433
|
+
await fs.writeFile(tmpPath, data, { mode: 384, encoding: "utf8" });
|
|
54434
|
+
if (typeof process !== "undefined" && process.platform !== "win32") {
|
|
54435
|
+
try {
|
|
54436
|
+
await fs.chmod(tmpPath, 384);
|
|
54437
|
+
} catch {
|
|
54438
|
+
}
|
|
54439
|
+
}
|
|
54440
|
+
await fs.rename(tmpPath, this.filePath);
|
|
54441
|
+
} catch (err) {
|
|
54442
|
+
try {
|
|
54443
|
+
await fs.unlink(tmpPath);
|
|
54444
|
+
} catch {
|
|
54445
|
+
}
|
|
54446
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
54447
|
+
throw new exports.SecretStorageException(`Failed to persist storage file: ${msg}`);
|
|
54448
|
+
}
|
|
54449
|
+
}
|
|
54450
|
+
async getItem(key) {
|
|
54451
|
+
await this.ensureLoaded();
|
|
54452
|
+
return this.store.get(key) ?? null;
|
|
54453
|
+
}
|
|
54454
|
+
async setItem(key, value2) {
|
|
54455
|
+
await this.ensureLoaded();
|
|
54456
|
+
this.store.set(key, value2);
|
|
54457
|
+
await this.persist();
|
|
54458
|
+
}
|
|
54459
|
+
async removeItem(key) {
|
|
54460
|
+
await this.ensureLoaded();
|
|
54461
|
+
const existed = this.store.delete(key);
|
|
54462
|
+
if (existed) {
|
|
54463
|
+
await this.persist();
|
|
54464
|
+
}
|
|
54465
|
+
return existed;
|
|
54466
|
+
}
|
|
54467
|
+
async keys() {
|
|
54468
|
+
await this.ensureLoaded();
|
|
54469
|
+
return Array.from(this.store.keys());
|
|
54470
|
+
}
|
|
54471
|
+
};
|
|
54472
|
+
|
|
54473
|
+
// src/storage/WebStorageBackend.ts
|
|
54474
|
+
init_SecretStorageException();
|
|
54475
|
+
var WebStorageBackend = class {
|
|
54476
|
+
storage;
|
|
54477
|
+
prefix;
|
|
54478
|
+
constructor(storage, prefix = "knishio:") {
|
|
54479
|
+
if (storage) {
|
|
54480
|
+
this.storage = storage;
|
|
54481
|
+
} else if (typeof globalThis !== "undefined" && globalThis.localStorage) {
|
|
54482
|
+
this.storage = globalThis.localStorage;
|
|
54483
|
+
} else {
|
|
54484
|
+
throw exports.SecretStorageException.unavailable(
|
|
54485
|
+
"web-storage",
|
|
54486
|
+
"WebStorageBackend requires a Storage object or global localStorage"
|
|
54487
|
+
);
|
|
54488
|
+
}
|
|
54489
|
+
this.prefix = prefix;
|
|
54490
|
+
}
|
|
54491
|
+
getItem(key) {
|
|
54492
|
+
return this.storage.getItem(key);
|
|
54493
|
+
}
|
|
54494
|
+
setItem(key, value2) {
|
|
54495
|
+
this.storage.setItem(key, value2);
|
|
54496
|
+
}
|
|
54497
|
+
removeItem(key) {
|
|
54498
|
+
const existed = this.storage.getItem(key) !== null;
|
|
54499
|
+
this.storage.removeItem(key);
|
|
54500
|
+
return existed;
|
|
54501
|
+
}
|
|
54502
|
+
keys() {
|
|
54503
|
+
const result = [];
|
|
54504
|
+
const len = this.storage.length;
|
|
54505
|
+
for (let i4 = 0; i4 < len; i4++) {
|
|
54506
|
+
const k2 = this.storage.key(i4);
|
|
54507
|
+
if (k2 !== null) {
|
|
54508
|
+
if (!this.prefix || k2.startsWith(this.prefix)) {
|
|
54509
|
+
result.push(k2);
|
|
54510
|
+
}
|
|
54511
|
+
}
|
|
54512
|
+
}
|
|
54513
|
+
return result;
|
|
54514
|
+
}
|
|
54515
|
+
};
|
|
54516
|
+
|
|
54517
|
+
// src/storage/WebAuthnPrfSecretStorageProvider.ts
|
|
54518
|
+
init_SecretStorageException();
|
|
54519
|
+
var PRF_SALT_LABEL = "knishio:secret-storage:webauthn-prf:v1";
|
|
54520
|
+
var KEK_INFO = "knishio:secret-storage:kek:v1";
|
|
54521
|
+
var KEY_PREFIX2 = SECRET_KEY_PREFIX;
|
|
54522
|
+
var GCM_IV_LENGTH2 = 12;
|
|
54523
|
+
var textEncoder3 = new TextEncoder();
|
|
54524
|
+
var textDecoder2 = new TextDecoder();
|
|
54525
|
+
function base64UrlEncode(bytes) {
|
|
54526
|
+
return uint8ArrayToBase642(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
54527
|
+
}
|
|
54528
|
+
function base64UrlDecode(str) {
|
|
54529
|
+
let base643 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
54530
|
+
while (base643.length % 4 !== 0) {
|
|
54531
|
+
base643 += "=";
|
|
54532
|
+
}
|
|
54533
|
+
return base64ToUint8Array2(base643);
|
|
54534
|
+
}
|
|
54535
|
+
function toUint8Array(buf) {
|
|
54536
|
+
if (buf instanceof Uint8Array) {
|
|
54537
|
+
return buf;
|
|
54538
|
+
}
|
|
54539
|
+
if (ArrayBuffer.isView(buf)) {
|
|
54540
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
54541
|
+
}
|
|
54542
|
+
return new Uint8Array(buf);
|
|
54543
|
+
}
|
|
54544
|
+
async function computePrfSalt() {
|
|
54545
|
+
const hash2 = await globalThis.crypto.subtle.digest("SHA-256", textEncoder3.encode(PRF_SALT_LABEL));
|
|
54546
|
+
return new Uint8Array(hash2);
|
|
54547
|
+
}
|
|
54548
|
+
async function deriveKekFromPrf(prfOutput, prfSalt) {
|
|
54549
|
+
const hkdfKey = await globalThis.crypto.subtle.importKey(
|
|
54550
|
+
"raw",
|
|
54551
|
+
prfOutput,
|
|
54552
|
+
"HKDF",
|
|
54553
|
+
false,
|
|
54554
|
+
["deriveKey"]
|
|
54555
|
+
);
|
|
54556
|
+
return await globalThis.crypto.subtle.deriveKey(
|
|
54557
|
+
{
|
|
54558
|
+
name: "HKDF",
|
|
54559
|
+
hash: "SHA-256",
|
|
54560
|
+
salt: prfSalt,
|
|
54561
|
+
info: textEncoder3.encode(KEK_INFO)
|
|
54562
|
+
},
|
|
54563
|
+
hkdfKey,
|
|
54564
|
+
{ name: "AES-GCM", length: 256 },
|
|
54565
|
+
false,
|
|
54566
|
+
["encrypt", "decrypt"]
|
|
54567
|
+
);
|
|
54568
|
+
}
|
|
54569
|
+
var WebAuthnPrfSecretStorageProvider = class {
|
|
54570
|
+
providerType = "webauthn-prf";
|
|
54571
|
+
backend;
|
|
54572
|
+
rp;
|
|
54573
|
+
user;
|
|
54574
|
+
credentialsContainer;
|
|
54575
|
+
alias;
|
|
54576
|
+
cachedPassphrase;
|
|
54577
|
+
constructor(options) {
|
|
54578
|
+
this.backend = options.backend;
|
|
54579
|
+
this.rp = options.rp;
|
|
54580
|
+
this.user = options.user;
|
|
54581
|
+
this.credentialsContainer = options.credentials;
|
|
54582
|
+
this.alias = options.alias ?? "default";
|
|
54583
|
+
}
|
|
54584
|
+
get credentials() {
|
|
54585
|
+
if (this.credentialsContainer) {
|
|
54586
|
+
return this.credentialsContainer;
|
|
54587
|
+
}
|
|
54588
|
+
if (typeof globalThis.navigator !== "undefined" && globalThis.navigator.credentials) {
|
|
54589
|
+
return globalThis.navigator.credentials;
|
|
54590
|
+
}
|
|
54591
|
+
throw exports.SecretStorageException.unavailable(
|
|
54592
|
+
this.providerType,
|
|
54593
|
+
"WebAuthn credentials container is not available"
|
|
54594
|
+
);
|
|
54595
|
+
}
|
|
54596
|
+
get recordKey() {
|
|
54597
|
+
return `knishio:webauthn-prf:${this.alias}`;
|
|
54598
|
+
}
|
|
54599
|
+
isHardwareBacked() {
|
|
54600
|
+
return false;
|
|
54601
|
+
}
|
|
54602
|
+
async isAvailable() {
|
|
54603
|
+
const hasWebCrypto = typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
|
|
54604
|
+
const hasCredentials = Boolean(
|
|
54605
|
+
this.credentialsContainer || typeof globalThis.navigator !== "undefined" && globalThis.navigator.credentials && typeof globalThis.PublicKeyCredential !== "undefined"
|
|
54606
|
+
);
|
|
54607
|
+
return hasWebCrypto && hasCredentials;
|
|
54608
|
+
}
|
|
54609
|
+
/**
|
|
54610
|
+
* Enroll a new passkey credential with PRF support and wrap a random device passphrase
|
|
54611
|
+
*/
|
|
54612
|
+
async enroll() {
|
|
54613
|
+
const existing = await this.backend.getItem(this.recordKey);
|
|
54614
|
+
if (existing) {
|
|
54615
|
+
return;
|
|
54616
|
+
}
|
|
54617
|
+
if (!await this.isAvailable()) {
|
|
54618
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebAuthn PRF is not available");
|
|
54619
|
+
}
|
|
54620
|
+
const challenge = new Uint8Array(32);
|
|
54621
|
+
globalThis.crypto.getRandomValues(challenge);
|
|
54622
|
+
const credential = await this.credentials.create({
|
|
54623
|
+
publicKey: {
|
|
54624
|
+
rp: this.rp,
|
|
54625
|
+
user: {
|
|
54626
|
+
id: this.user.id,
|
|
54627
|
+
name: this.user.name,
|
|
54628
|
+
displayName: this.user.displayName
|
|
54629
|
+
},
|
|
54630
|
+
challenge,
|
|
54631
|
+
pubKeyCredParams: [
|
|
54632
|
+
{ type: "public-key", alg: -7 },
|
|
54633
|
+
{ type: "public-key", alg: -257 }
|
|
54634
|
+
],
|
|
54635
|
+
authenticatorSelection: {
|
|
54636
|
+
residentKey: "required",
|
|
54637
|
+
userVerification: "required"
|
|
54638
|
+
},
|
|
54639
|
+
extensions: {
|
|
54640
|
+
prf: {}
|
|
54641
|
+
}
|
|
54642
|
+
}
|
|
54643
|
+
});
|
|
54644
|
+
if (!credential) {
|
|
54645
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "Authenticator creation returned null");
|
|
54646
|
+
}
|
|
54647
|
+
const extResults = credential.getClientExtensionResults?.();
|
|
54648
|
+
if (extResults?.prf?.enabled !== true) {
|
|
54649
|
+
throw exports.SecretStorageException.unavailable(
|
|
54650
|
+
this.providerType,
|
|
54651
|
+
"authenticator does not support the PRF extension"
|
|
54652
|
+
);
|
|
54653
|
+
}
|
|
54654
|
+
const credentialIdBytes = new Uint8Array(credential.rawId);
|
|
54655
|
+
const prfSalt = await computePrfSalt();
|
|
54656
|
+
const getChallenge = new Uint8Array(32);
|
|
54657
|
+
globalThis.crypto.getRandomValues(getChallenge);
|
|
54658
|
+
let assertion;
|
|
54659
|
+
try {
|
|
54660
|
+
assertion = await this.credentials.get({
|
|
54661
|
+
publicKey: {
|
|
54662
|
+
challenge: getChallenge,
|
|
54663
|
+
rpId: this.rp.id,
|
|
54664
|
+
allowCredentials: [
|
|
54665
|
+
{
|
|
54666
|
+
type: "public-key",
|
|
54667
|
+
id: credentialIdBytes
|
|
54668
|
+
}
|
|
54669
|
+
],
|
|
54670
|
+
userVerification: "required",
|
|
54671
|
+
extensions: {
|
|
54672
|
+
prf: {
|
|
54673
|
+
eval: {
|
|
54674
|
+
first: prfSalt
|
|
54675
|
+
}
|
|
54676
|
+
}
|
|
54677
|
+
}
|
|
54678
|
+
}
|
|
54679
|
+
});
|
|
54680
|
+
} catch (err) {
|
|
54681
|
+
const isNotAllowed = err instanceof Error && err.name === "NotAllowedError" || err?.name === "NotAllowedError";
|
|
54682
|
+
if (isNotAllowed) {
|
|
54683
|
+
throw exports.SecretStorageException.unavailable(
|
|
54684
|
+
this.providerType,
|
|
54685
|
+
"authenticator refused or credential missing"
|
|
54686
|
+
);
|
|
54687
|
+
}
|
|
54688
|
+
throw err;
|
|
54689
|
+
}
|
|
54690
|
+
if (!assertion) {
|
|
54691
|
+
throw exports.SecretStorageException.unavailable(
|
|
54692
|
+
this.providerType,
|
|
54693
|
+
"authenticator refused or credential missing"
|
|
54694
|
+
);
|
|
54695
|
+
}
|
|
54696
|
+
const getExtResults = assertion?.getClientExtensionResults?.();
|
|
54697
|
+
const firstOutput = getExtResults?.prf?.results?.first;
|
|
54698
|
+
if (!firstOutput) {
|
|
54699
|
+
throw exports.SecretStorageException.unavailable(
|
|
54700
|
+
this.providerType,
|
|
54701
|
+
"authenticator returned no PRF result"
|
|
54702
|
+
);
|
|
54703
|
+
}
|
|
54704
|
+
const prfBytes = toUint8Array(firstOutput);
|
|
54705
|
+
const kek = await deriveKekFromPrf(prfBytes, prfSalt);
|
|
54706
|
+
const devicePassphraseBytes = new Uint8Array(32);
|
|
54707
|
+
globalThis.crypto.getRandomValues(devicePassphraseBytes);
|
|
54708
|
+
const devicePassphrase = uint8ArrayToBase642(devicePassphraseBytes);
|
|
54709
|
+
const iv = new Uint8Array(GCM_IV_LENGTH2);
|
|
54710
|
+
globalThis.crypto.getRandomValues(iv);
|
|
54711
|
+
const passphraseBytes = textEncoder3.encode(devicePassphrase);
|
|
54712
|
+
try {
|
|
54713
|
+
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
54714
|
+
{
|
|
54715
|
+
name: "AES-GCM",
|
|
54716
|
+
iv
|
|
54717
|
+
},
|
|
54718
|
+
kek,
|
|
54719
|
+
passphraseBytes
|
|
54720
|
+
);
|
|
54721
|
+
const record2 = {
|
|
54722
|
+
version: 1,
|
|
54723
|
+
credentialId: base64UrlEncode(credentialIdBytes),
|
|
54724
|
+
iv: uint8ArrayToBase642(iv),
|
|
54725
|
+
ciphertext: uint8ArrayToBase642(new Uint8Array(encryptedBuffer))
|
|
54726
|
+
};
|
|
54727
|
+
await this.backend.setItem(this.recordKey, JSON.stringify(record2));
|
|
54728
|
+
this.cachedPassphrase = devicePassphrase;
|
|
54729
|
+
} finally {
|
|
54730
|
+
zeroizeBytes(passphraseBytes);
|
|
54731
|
+
zeroizeBytes(devicePassphraseBytes);
|
|
54732
|
+
}
|
|
54733
|
+
}
|
|
54734
|
+
/**
|
|
54735
|
+
* Unlock the device passphrase using the enrolled WebAuthn PRF credential
|
|
54736
|
+
*/
|
|
54737
|
+
async unlock() {
|
|
54738
|
+
if (this.cachedPassphrase) {
|
|
54739
|
+
return this.cachedPassphrase;
|
|
54740
|
+
}
|
|
54741
|
+
const rawRecord = await this.backend.getItem(this.recordKey);
|
|
54742
|
+
if (!rawRecord) {
|
|
54743
|
+
throw exports.SecretStorageException.unavailable(
|
|
54744
|
+
this.providerType,
|
|
54745
|
+
"no enrolled credential; call enroll() first"
|
|
54746
|
+
);
|
|
54747
|
+
}
|
|
54748
|
+
let record2;
|
|
54749
|
+
try {
|
|
54750
|
+
record2 = JSON.parse(rawRecord);
|
|
54751
|
+
} catch {
|
|
54752
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted PRF record format");
|
|
54753
|
+
}
|
|
54754
|
+
const credentialIdBytes = base64UrlDecode(record2.credentialId);
|
|
54755
|
+
const prfSalt = await computePrfSalt();
|
|
54756
|
+
const challenge = new Uint8Array(32);
|
|
54757
|
+
globalThis.crypto.getRandomValues(challenge);
|
|
54758
|
+
let assertion;
|
|
54759
|
+
try {
|
|
54760
|
+
assertion = await this.credentials.get({
|
|
54761
|
+
publicKey: {
|
|
54762
|
+
challenge,
|
|
54763
|
+
rpId: this.rp.id,
|
|
54764
|
+
allowCredentials: [
|
|
54765
|
+
{
|
|
54766
|
+
type: "public-key",
|
|
54767
|
+
id: credentialIdBytes
|
|
54768
|
+
}
|
|
54769
|
+
],
|
|
54770
|
+
userVerification: "required",
|
|
54771
|
+
extensions: {
|
|
54772
|
+
prf: {
|
|
54773
|
+
eval: {
|
|
54774
|
+
first: prfSalt
|
|
54775
|
+
}
|
|
54776
|
+
}
|
|
54777
|
+
}
|
|
54778
|
+
}
|
|
54779
|
+
});
|
|
54780
|
+
} catch (err) {
|
|
54781
|
+
const isNotAllowed = err instanceof Error && err.name === "NotAllowedError" || err?.name === "NotAllowedError";
|
|
54782
|
+
if (isNotAllowed) {
|
|
54783
|
+
throw exports.SecretStorageException.unavailable(
|
|
54784
|
+
this.providerType,
|
|
54785
|
+
"authenticator refused or credential missing"
|
|
54786
|
+
);
|
|
54787
|
+
}
|
|
54788
|
+
throw err;
|
|
54789
|
+
}
|
|
54790
|
+
if (!assertion) {
|
|
54791
|
+
throw exports.SecretStorageException.unavailable(
|
|
54792
|
+
this.providerType,
|
|
54793
|
+
"authenticator refused or credential missing"
|
|
54794
|
+
);
|
|
54795
|
+
}
|
|
54796
|
+
const extResults = assertion?.getClientExtensionResults?.();
|
|
54797
|
+
const firstOutput = extResults?.prf?.results?.first;
|
|
54798
|
+
if (!firstOutput) {
|
|
54799
|
+
throw exports.SecretStorageException.unavailable(
|
|
54800
|
+
this.providerType,
|
|
54801
|
+
"authenticator returned no PRF result"
|
|
54802
|
+
);
|
|
54803
|
+
}
|
|
54804
|
+
const prfBytes = toUint8Array(firstOutput);
|
|
54805
|
+
const kek = await deriveKekFromPrf(prfBytes, prfSalt);
|
|
54806
|
+
const iv = base64ToUint8Array2(record2.iv);
|
|
54807
|
+
const ciphertext = base64ToUint8Array2(record2.ciphertext);
|
|
54202
54808
|
try {
|
|
54203
|
-
const key = await this.deriveKey(passphrase, salt, payload.iterations ?? DEFAULT_ITERATIONS);
|
|
54204
54809
|
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
54205
54810
|
{
|
|
54206
54811
|
name: "AES-GCM",
|
|
54207
54812
|
iv
|
|
54208
54813
|
},
|
|
54209
|
-
|
|
54814
|
+
kek,
|
|
54210
54815
|
ciphertext
|
|
54211
54816
|
);
|
|
54212
54817
|
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
54818
|
+
try {
|
|
54819
|
+
this.cachedPassphrase = textDecoder2.decode(decryptedBytes);
|
|
54820
|
+
return this.cachedPassphrase;
|
|
54821
|
+
} finally {
|
|
54822
|
+
zeroizeBytes(decryptedBytes);
|
|
54823
|
+
}
|
|
54824
|
+
} catch {
|
|
54825
|
+
throw exports.SecretStorageException.decryptionFailed(
|
|
54826
|
+
"wrapped device passphrase failed authentication under the enrolled credential"
|
|
54827
|
+
);
|
|
54828
|
+
}
|
|
54829
|
+
}
|
|
54830
|
+
/**
|
|
54831
|
+
* Lock the provider by clearing cached passphrase material
|
|
54832
|
+
*/
|
|
54833
|
+
lock() {
|
|
54834
|
+
this.cachedPassphrase = void 0;
|
|
54835
|
+
}
|
|
54836
|
+
/**
|
|
54837
|
+
* Unenroll the current credential, removing the stored PRF record and clearing cached passphrase
|
|
54838
|
+
*/
|
|
54839
|
+
async unenroll() {
|
|
54840
|
+
this.lock();
|
|
54841
|
+
await this.backend.removeItem(this.recordKey);
|
|
54842
|
+
}
|
|
54843
|
+
async storeSecret(bundleHash, secret, options) {
|
|
54844
|
+
if (!bundleHash) {
|
|
54845
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
54846
|
+
}
|
|
54847
|
+
if (!secret) {
|
|
54848
|
+
throw new exports.SecretStorageException("Secret cannot be empty");
|
|
54849
|
+
}
|
|
54850
|
+
if (options?.passphrase) {
|
|
54851
|
+
throw new exports.SecretStorageException(
|
|
54852
|
+
"WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
|
|
54853
|
+
);
|
|
54854
|
+
}
|
|
54855
|
+
if (!options?.recoveryPassphrase && !options?.allowUnrecoverable) {
|
|
54856
|
+
throw exports.SecretStorageException.validationError(
|
|
54857
|
+
"Recovery passphrase required for non-exportable hardware key unless allowUnrecoverable is true"
|
|
54858
|
+
);
|
|
54859
|
+
}
|
|
54860
|
+
const passphrase = await this.unlock();
|
|
54861
|
+
const metadata = {
|
|
54862
|
+
bundleHash,
|
|
54863
|
+
label: options?.label,
|
|
54864
|
+
createdAt: Date.now(),
|
|
54865
|
+
hardwareBacked: false,
|
|
54866
|
+
providerType: this.providerType
|
|
54867
|
+
};
|
|
54868
|
+
const payload = await sealEnvelope(secret, passphrase, metadata);
|
|
54869
|
+
await this.backend.setItem(`${KEY_PREFIX2}${bundleHash}`, JSON.stringify(payload));
|
|
54870
|
+
if (options?.recoveryPassphrase) {
|
|
54871
|
+
const recoveryMetadata = {
|
|
54872
|
+
bundleHash,
|
|
54873
|
+
label: options?.label,
|
|
54874
|
+
createdAt: Date.now(),
|
|
54875
|
+
hardwareBacked: false,
|
|
54876
|
+
providerType: "webcrypto-aes-gcm"
|
|
54877
|
+
};
|
|
54878
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
54879
|
+
await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
|
|
54880
|
+
}
|
|
54881
|
+
}
|
|
54882
|
+
async retrieveSecret(bundleHash, options) {
|
|
54883
|
+
if (options?.passphrase) {
|
|
54884
|
+
throw new exports.SecretStorageException(
|
|
54885
|
+
"WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
|
|
54886
|
+
);
|
|
54887
|
+
}
|
|
54888
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
|
|
54889
|
+
if (!raw) {
|
|
54890
|
+
return null;
|
|
54891
|
+
}
|
|
54892
|
+
let payload;
|
|
54893
|
+
try {
|
|
54894
|
+
payload = JSON.parse(raw);
|
|
54895
|
+
} catch {
|
|
54896
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
54897
|
+
}
|
|
54898
|
+
const passphrase = await this.unlock();
|
|
54899
|
+
try {
|
|
54900
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
54901
|
+
try {
|
|
54902
|
+
return textDecoder2.decode(decryptedBytes);
|
|
54903
|
+
} finally {
|
|
54904
|
+
zeroizeBytes(decryptedBytes);
|
|
54905
|
+
}
|
|
54906
|
+
} catch (err) {
|
|
54907
|
+
if (err instanceof exports.SecretStorageException) {
|
|
54908
|
+
throw err;
|
|
54909
|
+
}
|
|
54910
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
54911
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
54912
|
+
}
|
|
54913
|
+
}
|
|
54914
|
+
async withSecret(bundleHash, fn, options) {
|
|
54915
|
+
if (options?.passphrase) {
|
|
54916
|
+
throw new exports.SecretStorageException(
|
|
54917
|
+
"WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
|
|
54918
|
+
);
|
|
54919
|
+
}
|
|
54920
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
|
|
54921
|
+
if (!raw) {
|
|
54922
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
54923
|
+
}
|
|
54924
|
+
let payload;
|
|
54925
|
+
try {
|
|
54926
|
+
payload = JSON.parse(raw);
|
|
54927
|
+
} catch {
|
|
54928
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
54929
|
+
}
|
|
54930
|
+
const passphrase = await this.unlock();
|
|
54931
|
+
try {
|
|
54932
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
54213
54933
|
return await withSecureBytes(decryptedBytes, async (bytes) => {
|
|
54214
|
-
const secretString =
|
|
54934
|
+
const secretString = textDecoder2.decode(bytes);
|
|
54215
54935
|
return await fn(secretString);
|
|
54216
54936
|
});
|
|
54217
54937
|
} catch (err) {
|
|
@@ -54222,6 +54942,451 @@ ${operationTypes.join("\n")}
|
|
|
54222
54942
|
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
54223
54943
|
}
|
|
54224
54944
|
}
|
|
54945
|
+
async deleteSecret(bundleHash) {
|
|
54946
|
+
const key = `${KEY_PREFIX2}${bundleHash}`;
|
|
54947
|
+
const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
|
|
54948
|
+
const result = await this.backend.removeItem(key);
|
|
54949
|
+
await this.backend.removeItem(recoveryKey);
|
|
54950
|
+
return result !== false;
|
|
54951
|
+
}
|
|
54952
|
+
async hasSecret(bundleHash) {
|
|
54953
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
|
|
54954
|
+
return raw !== null;
|
|
54955
|
+
}
|
|
54956
|
+
async listSecrets() {
|
|
54957
|
+
const keys = await this.backend.keys();
|
|
54958
|
+
const matchingKeys = keys.filter((k2) => k2.startsWith(KEY_PREFIX2) && !k2.startsWith(RECOVERY_KEY_PREFIX));
|
|
54959
|
+
const results = [];
|
|
54960
|
+
for (const key of matchingKeys) {
|
|
54961
|
+
const raw = await this.backend.getItem(key);
|
|
54962
|
+
if (raw) {
|
|
54963
|
+
try {
|
|
54964
|
+
const payload = JSON.parse(raw);
|
|
54965
|
+
if (payload.metadata) {
|
|
54966
|
+
results.push(payload.metadata);
|
|
54967
|
+
}
|
|
54968
|
+
} catch {
|
|
54969
|
+
}
|
|
54970
|
+
}
|
|
54971
|
+
}
|
|
54972
|
+
return results;
|
|
54973
|
+
}
|
|
54974
|
+
/**
|
|
54975
|
+
* Recover a secret using its recovery envelope and re-enroll it under a fresh WebAuthn PRF credential
|
|
54976
|
+
*/
|
|
54977
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
54978
|
+
if (!bundleHash) {
|
|
54979
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
54980
|
+
}
|
|
54981
|
+
if (!recoveryPassphrase) {
|
|
54982
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
54983
|
+
}
|
|
54984
|
+
const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
|
|
54985
|
+
if (!raw) {
|
|
54986
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
54987
|
+
}
|
|
54988
|
+
let payload;
|
|
54989
|
+
try {
|
|
54990
|
+
payload = JSON.parse(raw);
|
|
54991
|
+
} catch {
|
|
54992
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
54993
|
+
}
|
|
54994
|
+
let decryptedBytes;
|
|
54995
|
+
try {
|
|
54996
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
54997
|
+
} catch (err) {
|
|
54998
|
+
if (err instanceof exports.SecretStorageException) {
|
|
54999
|
+
throw err;
|
|
55000
|
+
}
|
|
55001
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55002
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
55003
|
+
}
|
|
55004
|
+
let secretStr;
|
|
55005
|
+
try {
|
|
55006
|
+
secretStr = textDecoder2.decode(decryptedBytes);
|
|
55007
|
+
} finally {
|
|
55008
|
+
zeroizeBytes(decryptedBytes);
|
|
55009
|
+
}
|
|
55010
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
55011
|
+
...options,
|
|
55012
|
+
recoveryPassphrase
|
|
55013
|
+
});
|
|
55014
|
+
}
|
|
55015
|
+
};
|
|
55016
|
+
|
|
55017
|
+
// src/storage/NonExtractableKeySecretStorageProvider.ts
|
|
55018
|
+
init_SecretStorageException();
|
|
55019
|
+
var KEY_PREFIX3 = SECRET_KEY_PREFIX;
|
|
55020
|
+
var GCM_IV_LENGTH3 = 12;
|
|
55021
|
+
var textEncoder4 = new TextEncoder();
|
|
55022
|
+
var textDecoder3 = new TextDecoder();
|
|
55023
|
+
var MemoryKeyStore = class {
|
|
55024
|
+
keys = /* @__PURE__ */ new Map();
|
|
55025
|
+
async get(name2) {
|
|
55026
|
+
return this.keys.get(name2);
|
|
55027
|
+
}
|
|
55028
|
+
async put(name2, key) {
|
|
55029
|
+
this.keys.set(name2, key);
|
|
55030
|
+
}
|
|
55031
|
+
async delete(name2) {
|
|
55032
|
+
return this.keys.delete(name2);
|
|
55033
|
+
}
|
|
55034
|
+
};
|
|
55035
|
+
var IndexedDbKeyStore = class {
|
|
55036
|
+
dbName;
|
|
55037
|
+
storeName = "keys";
|
|
55038
|
+
constructor(dbName = "knishio-secret-storage") {
|
|
55039
|
+
this.dbName = dbName;
|
|
55040
|
+
}
|
|
55041
|
+
// Executor form intentionally retained for browser runtime compatibility with ES2022 / browsers without Promise.withResolvers polyfill
|
|
55042
|
+
async getDb() {
|
|
55043
|
+
if (typeof globalThis.indexedDB === "undefined") {
|
|
55044
|
+
throw exports.SecretStorageException.unavailable(
|
|
55045
|
+
"webcrypto-nonextractable",
|
|
55046
|
+
"IndexedDB is not available"
|
|
55047
|
+
);
|
|
55048
|
+
}
|
|
55049
|
+
return new Promise((resolve, reject) => {
|
|
55050
|
+
const request = globalThis.indexedDB.open(this.dbName, 1);
|
|
55051
|
+
request.onupgradeneeded = () => {
|
|
55052
|
+
const db = request.result;
|
|
55053
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
55054
|
+
db.createObjectStore(this.storeName);
|
|
55055
|
+
}
|
|
55056
|
+
};
|
|
55057
|
+
request.onsuccess = () => resolve(request.result);
|
|
55058
|
+
request.onerror = () => reject(request.error);
|
|
55059
|
+
});
|
|
55060
|
+
}
|
|
55061
|
+
async get(name2) {
|
|
55062
|
+
const db = await this.getDb();
|
|
55063
|
+
return new Promise((resolve, reject) => {
|
|
55064
|
+
const tx = db.transaction(this.storeName, "readonly");
|
|
55065
|
+
const store = tx.objectStore(this.storeName);
|
|
55066
|
+
const request = store.get(name2);
|
|
55067
|
+
request.onsuccess = () => resolve(request.result);
|
|
55068
|
+
request.onerror = () => reject(request.error);
|
|
55069
|
+
});
|
|
55070
|
+
}
|
|
55071
|
+
async put(name2, key) {
|
|
55072
|
+
const db = await this.getDb();
|
|
55073
|
+
return new Promise((resolve, reject) => {
|
|
55074
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
55075
|
+
const store = tx.objectStore(this.storeName);
|
|
55076
|
+
const request = store.put(key, name2);
|
|
55077
|
+
request.onsuccess = () => resolve();
|
|
55078
|
+
request.onerror = () => reject(request.error);
|
|
55079
|
+
});
|
|
55080
|
+
}
|
|
55081
|
+
async delete(name2) {
|
|
55082
|
+
const db = await this.getDb();
|
|
55083
|
+
return new Promise((resolve, reject) => {
|
|
55084
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
55085
|
+
const store = tx.objectStore(this.storeName);
|
|
55086
|
+
const request = store.delete(name2);
|
|
55087
|
+
request.onsuccess = () => resolve(true);
|
|
55088
|
+
request.onerror = () => reject(request.error);
|
|
55089
|
+
});
|
|
55090
|
+
}
|
|
55091
|
+
};
|
|
55092
|
+
var NonExtractableKeySecretStorageProvider = class {
|
|
55093
|
+
providerType = "webcrypto-nonextractable";
|
|
55094
|
+
backend;
|
|
55095
|
+
keyStore;
|
|
55096
|
+
alias;
|
|
55097
|
+
cachedPassphrase;
|
|
55098
|
+
constructor(options) {
|
|
55099
|
+
this.backend = options.backend;
|
|
55100
|
+
this.keyStore = options.keyStore ?? new IndexedDbKeyStore();
|
|
55101
|
+
this.alias = options.alias ?? "default";
|
|
55102
|
+
}
|
|
55103
|
+
get recordKey() {
|
|
55104
|
+
return `knishio:kek:webcrypto-nonextractable:${this.alias}`;
|
|
55105
|
+
}
|
|
55106
|
+
get kekStoreKey() {
|
|
55107
|
+
return `knishio:kek:${this.alias}`;
|
|
55108
|
+
}
|
|
55109
|
+
isHardwareBacked() {
|
|
55110
|
+
return false;
|
|
55111
|
+
}
|
|
55112
|
+
async isAvailable() {
|
|
55113
|
+
return typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
|
|
55114
|
+
}
|
|
55115
|
+
/**
|
|
55116
|
+
* Unlock or initialize the device passphrase using the non-extractable KEK
|
|
55117
|
+
*/
|
|
55118
|
+
async unlock() {
|
|
55119
|
+
if (this.cachedPassphrase) {
|
|
55120
|
+
return this.cachedPassphrase;
|
|
55121
|
+
}
|
|
55122
|
+
if (!await this.isAvailable()) {
|
|
55123
|
+
throw exports.SecretStorageException.unavailable(
|
|
55124
|
+
this.providerType,
|
|
55125
|
+
"WebCrypto API is not available"
|
|
55126
|
+
);
|
|
55127
|
+
}
|
|
55128
|
+
const rawRecord = await this.backend.getItem(this.recordKey);
|
|
55129
|
+
if (!rawRecord) {
|
|
55130
|
+
let kek2 = await this.keyStore.get(this.kekStoreKey);
|
|
55131
|
+
if (!kek2) {
|
|
55132
|
+
kek2 = await globalThis.crypto.subtle.generateKey(
|
|
55133
|
+
{ name: "AES-GCM", length: 256 },
|
|
55134
|
+
false,
|
|
55135
|
+
["encrypt", "decrypt"]
|
|
55136
|
+
);
|
|
55137
|
+
await this.keyStore.put(this.kekStoreKey, kek2);
|
|
55138
|
+
}
|
|
55139
|
+
const devicePassphraseBytes = new Uint8Array(32);
|
|
55140
|
+
globalThis.crypto.getRandomValues(devicePassphraseBytes);
|
|
55141
|
+
const devicePassphrase = uint8ArrayToBase642(devicePassphraseBytes);
|
|
55142
|
+
const iv2 = new Uint8Array(GCM_IV_LENGTH3);
|
|
55143
|
+
globalThis.crypto.getRandomValues(iv2);
|
|
55144
|
+
const passphraseBytes = textEncoder4.encode(devicePassphrase);
|
|
55145
|
+
try {
|
|
55146
|
+
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
55147
|
+
{
|
|
55148
|
+
name: "AES-GCM",
|
|
55149
|
+
iv: iv2
|
|
55150
|
+
},
|
|
55151
|
+
kek2,
|
|
55152
|
+
passphraseBytes
|
|
55153
|
+
);
|
|
55154
|
+
const record3 = {
|
|
55155
|
+
version: 1,
|
|
55156
|
+
iv: uint8ArrayToBase642(iv2),
|
|
55157
|
+
ciphertext: uint8ArrayToBase642(new Uint8Array(encryptedBuffer))
|
|
55158
|
+
};
|
|
55159
|
+
await this.backend.setItem(this.recordKey, JSON.stringify(record3));
|
|
55160
|
+
this.cachedPassphrase = devicePassphrase;
|
|
55161
|
+
return devicePassphrase;
|
|
55162
|
+
} finally {
|
|
55163
|
+
zeroizeBytes(passphraseBytes);
|
|
55164
|
+
zeroizeBytes(devicePassphraseBytes);
|
|
55165
|
+
}
|
|
55166
|
+
}
|
|
55167
|
+
let record2;
|
|
55168
|
+
try {
|
|
55169
|
+
record2 = JSON.parse(rawRecord);
|
|
55170
|
+
} catch {
|
|
55171
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted key record format");
|
|
55172
|
+
}
|
|
55173
|
+
const kek = await this.keyStore.get(this.kekStoreKey);
|
|
55174
|
+
if (!kek) {
|
|
55175
|
+
throw exports.SecretStorageException.unavailable(
|
|
55176
|
+
this.providerType,
|
|
55177
|
+
`no non-extractable key found for alias '${this.alias}'`
|
|
55178
|
+
);
|
|
55179
|
+
}
|
|
55180
|
+
const iv = base64ToUint8Array2(record2.iv);
|
|
55181
|
+
const ciphertext = base64ToUint8Array2(record2.ciphertext);
|
|
55182
|
+
try {
|
|
55183
|
+
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
55184
|
+
{
|
|
55185
|
+
name: "AES-GCM",
|
|
55186
|
+
iv
|
|
55187
|
+
},
|
|
55188
|
+
kek,
|
|
55189
|
+
ciphertext
|
|
55190
|
+
);
|
|
55191
|
+
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
55192
|
+
try {
|
|
55193
|
+
this.cachedPassphrase = textDecoder3.decode(decryptedBytes);
|
|
55194
|
+
return this.cachedPassphrase;
|
|
55195
|
+
} finally {
|
|
55196
|
+
zeroizeBytes(decryptedBytes);
|
|
55197
|
+
}
|
|
55198
|
+
} catch {
|
|
55199
|
+
throw exports.SecretStorageException.decryptionFailed(
|
|
55200
|
+
"wrapped device passphrase failed authentication under non-extractable key"
|
|
55201
|
+
);
|
|
55202
|
+
}
|
|
55203
|
+
}
|
|
55204
|
+
/**
|
|
55205
|
+
* Lock the provider by clearing cached passphrase material
|
|
55206
|
+
*/
|
|
55207
|
+
lock() {
|
|
55208
|
+
this.cachedPassphrase = void 0;
|
|
55209
|
+
}
|
|
55210
|
+
/**
|
|
55211
|
+
* Unenroll the non-extractable key, removing the stored wrapped record and KEK
|
|
55212
|
+
*/
|
|
55213
|
+
async unenroll() {
|
|
55214
|
+
this.lock();
|
|
55215
|
+
await this.backend.removeItem(this.recordKey);
|
|
55216
|
+
await this.keyStore.delete(this.kekStoreKey);
|
|
55217
|
+
}
|
|
55218
|
+
async storeSecret(bundleHash, secret, options) {
|
|
55219
|
+
if (!bundleHash) {
|
|
55220
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
55221
|
+
}
|
|
55222
|
+
if (!secret) {
|
|
55223
|
+
throw new exports.SecretStorageException("Secret cannot be empty");
|
|
55224
|
+
}
|
|
55225
|
+
if (options?.passphrase) {
|
|
55226
|
+
throw new exports.SecretStorageException(
|
|
55227
|
+
"NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
|
|
55228
|
+
);
|
|
55229
|
+
}
|
|
55230
|
+
if (!options?.recoveryPassphrase && !options?.allowUnrecoverable) {
|
|
55231
|
+
throw exports.SecretStorageException.validationError(
|
|
55232
|
+
"Recovery passphrase required for non-exportable hardware key unless allowUnrecoverable is true"
|
|
55233
|
+
);
|
|
55234
|
+
}
|
|
55235
|
+
const passphrase = await this.unlock();
|
|
55236
|
+
const metadata = {
|
|
55237
|
+
bundleHash,
|
|
55238
|
+
label: options?.label,
|
|
55239
|
+
createdAt: Date.now(),
|
|
55240
|
+
hardwareBacked: false,
|
|
55241
|
+
providerType: this.providerType
|
|
55242
|
+
};
|
|
55243
|
+
const payload = await sealEnvelope(secret, passphrase, metadata);
|
|
55244
|
+
await this.backend.setItem(`${KEY_PREFIX3}${bundleHash}`, JSON.stringify(payload));
|
|
55245
|
+
if (options?.recoveryPassphrase) {
|
|
55246
|
+
const recoveryMetadata = {
|
|
55247
|
+
bundleHash,
|
|
55248
|
+
label: options?.label,
|
|
55249
|
+
createdAt: Date.now(),
|
|
55250
|
+
hardwareBacked: false,
|
|
55251
|
+
providerType: "webcrypto-aes-gcm"
|
|
55252
|
+
};
|
|
55253
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
55254
|
+
await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
|
|
55255
|
+
}
|
|
55256
|
+
}
|
|
55257
|
+
async retrieveSecret(bundleHash, options) {
|
|
55258
|
+
if (options?.passphrase) {
|
|
55259
|
+
throw new exports.SecretStorageException(
|
|
55260
|
+
"NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
|
|
55261
|
+
);
|
|
55262
|
+
}
|
|
55263
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
|
|
55264
|
+
if (!raw) {
|
|
55265
|
+
return null;
|
|
55266
|
+
}
|
|
55267
|
+
let payload;
|
|
55268
|
+
try {
|
|
55269
|
+
payload = JSON.parse(raw);
|
|
55270
|
+
} catch {
|
|
55271
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
55272
|
+
}
|
|
55273
|
+
const passphrase = await this.unlock();
|
|
55274
|
+
try {
|
|
55275
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
55276
|
+
try {
|
|
55277
|
+
return textDecoder3.decode(decryptedBytes);
|
|
55278
|
+
} finally {
|
|
55279
|
+
zeroizeBytes(decryptedBytes);
|
|
55280
|
+
}
|
|
55281
|
+
} catch (err) {
|
|
55282
|
+
if (err instanceof exports.SecretStorageException) {
|
|
55283
|
+
throw err;
|
|
55284
|
+
}
|
|
55285
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55286
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
55287
|
+
}
|
|
55288
|
+
}
|
|
55289
|
+
async withSecret(bundleHash, fn, options) {
|
|
55290
|
+
if (options?.passphrase) {
|
|
55291
|
+
throw new exports.SecretStorageException(
|
|
55292
|
+
"NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
|
|
55293
|
+
);
|
|
55294
|
+
}
|
|
55295
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
|
|
55296
|
+
if (!raw) {
|
|
55297
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
55298
|
+
}
|
|
55299
|
+
let payload;
|
|
55300
|
+
try {
|
|
55301
|
+
payload = JSON.parse(raw);
|
|
55302
|
+
} catch {
|
|
55303
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
55304
|
+
}
|
|
55305
|
+
const passphrase = await this.unlock();
|
|
55306
|
+
try {
|
|
55307
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
55308
|
+
return await withSecureBytes(decryptedBytes, async (bytes) => {
|
|
55309
|
+
const secretString = textDecoder3.decode(bytes);
|
|
55310
|
+
return await fn(secretString);
|
|
55311
|
+
});
|
|
55312
|
+
} catch (err) {
|
|
55313
|
+
if (err instanceof exports.SecretStorageException) {
|
|
55314
|
+
throw err;
|
|
55315
|
+
}
|
|
55316
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55317
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
55318
|
+
}
|
|
55319
|
+
}
|
|
55320
|
+
async deleteSecret(bundleHash) {
|
|
55321
|
+
const key = `${KEY_PREFIX3}${bundleHash}`;
|
|
55322
|
+
const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
|
|
55323
|
+
const result = await this.backend.removeItem(key);
|
|
55324
|
+
await this.backend.removeItem(recoveryKey);
|
|
55325
|
+
return result !== false;
|
|
55326
|
+
}
|
|
55327
|
+
async hasSecret(bundleHash) {
|
|
55328
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
|
|
55329
|
+
return raw !== null;
|
|
55330
|
+
}
|
|
55331
|
+
async listSecrets() {
|
|
55332
|
+
const keys = await this.backend.keys();
|
|
55333
|
+
const matchingKeys = keys.filter((k2) => k2.startsWith(KEY_PREFIX3) && !k2.startsWith(RECOVERY_KEY_PREFIX));
|
|
55334
|
+
const results = [];
|
|
55335
|
+
for (const key of matchingKeys) {
|
|
55336
|
+
const raw = await this.backend.getItem(key);
|
|
55337
|
+
if (raw) {
|
|
55338
|
+
try {
|
|
55339
|
+
const payload = JSON.parse(raw);
|
|
55340
|
+
if (payload.metadata) {
|
|
55341
|
+
results.push(payload.metadata);
|
|
55342
|
+
}
|
|
55343
|
+
} catch {
|
|
55344
|
+
}
|
|
55345
|
+
}
|
|
55346
|
+
}
|
|
55347
|
+
return results;
|
|
55348
|
+
}
|
|
55349
|
+
/**
|
|
55350
|
+
* Recover a secret using its recovery envelope and re-enroll it under a fresh non-extractable KEK
|
|
55351
|
+
*/
|
|
55352
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
55353
|
+
if (!bundleHash) {
|
|
55354
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
55355
|
+
}
|
|
55356
|
+
if (!recoveryPassphrase) {
|
|
55357
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
55358
|
+
}
|
|
55359
|
+
const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
|
|
55360
|
+
if (!raw) {
|
|
55361
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
55362
|
+
}
|
|
55363
|
+
let payload;
|
|
55364
|
+
try {
|
|
55365
|
+
payload = JSON.parse(raw);
|
|
55366
|
+
} catch {
|
|
55367
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
55368
|
+
}
|
|
55369
|
+
let decryptedBytes;
|
|
55370
|
+
try {
|
|
55371
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
55372
|
+
} catch (err) {
|
|
55373
|
+
if (err instanceof exports.SecretStorageException) {
|
|
55374
|
+
throw err;
|
|
55375
|
+
}
|
|
55376
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55377
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
55378
|
+
}
|
|
55379
|
+
let secretStr;
|
|
55380
|
+
try {
|
|
55381
|
+
secretStr = textDecoder3.decode(decryptedBytes);
|
|
55382
|
+
} finally {
|
|
55383
|
+
zeroizeBytes(decryptedBytes);
|
|
55384
|
+
}
|
|
55385
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
55386
|
+
...options,
|
|
55387
|
+
recoveryPassphrase
|
|
55388
|
+
});
|
|
55389
|
+
}
|
|
54225
55390
|
};
|
|
54226
55391
|
|
|
54227
55392
|
// src/storage/index.ts
|
|
@@ -54232,15 +55397,14 @@ ${operationTypes.join("\n")}
|
|
|
54232
55397
|
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined") {
|
|
54233
55398
|
return new WebCryptoSecretStorageProvider({
|
|
54234
55399
|
backend: options.backend,
|
|
54235
|
-
defaultPassphrase: options.defaultPassphrase
|
|
54236
|
-
hardwareBacked: options.hardwareBacked
|
|
55400
|
+
defaultPassphrase: options.defaultPassphrase
|
|
54237
55401
|
});
|
|
54238
55402
|
}
|
|
54239
55403
|
return new MemorySecretStorageProvider();
|
|
54240
55404
|
}
|
|
54241
55405
|
|
|
54242
55406
|
// src/index.ts
|
|
54243
|
-
var SDK_VERSION = "1.
|
|
55407
|
+
var SDK_VERSION = "1.1.0";
|
|
54244
55408
|
var SDK_NAME = "KnishIO-Client-TS";
|
|
54245
55409
|
var COMPATIBLE_SERVER_VERSIONS = [4, 5];
|
|
54246
55410
|
var SDK_INFO = {
|
|
@@ -54348,9 +55512,12 @@ ${operationTypes.join("\n")}
|
|
|
54348
55512
|
exports.CheckMolecule = CheckMolecule;
|
|
54349
55513
|
exports.DevUtils = DevUtils;
|
|
54350
55514
|
exports.EXTENDED_COMPATIBILITY_TEST_VECTORS = EXTENDED_COMPATIBILITY_TEST_VECTORS;
|
|
55515
|
+
exports.FileStorageBackend = FileStorageBackend;
|
|
54351
55516
|
exports.GraphQLClient = GraphQLClient;
|
|
55517
|
+
exports.IndexedDbKeyStore = IndexedDbKeyStore;
|
|
54352
55518
|
exports.KnishIO = KnishIO;
|
|
54353
55519
|
exports.KnishIOClient = KnishIOClient;
|
|
55520
|
+
exports.MemoryKeyStore = MemoryKeyStore;
|
|
54354
55521
|
exports.MemorySecretStorageProvider = MemorySecretStorageProvider;
|
|
54355
55522
|
exports.MemoryStorageBackend = MemoryStorageBackend;
|
|
54356
55523
|
exports.Meta = Meta;
|
|
@@ -54365,6 +55532,7 @@ ${operationTypes.join("\n")}
|
|
|
54365
55532
|
exports.MutationRequestAuthorization = MutationRequestAuthorization;
|
|
54366
55533
|
exports.MutationRequestTokens = MutationRequestTokens;
|
|
54367
55534
|
exports.MutationTransferTokens = MutationTransferTokens;
|
|
55535
|
+
exports.NonExtractableKeySecretStorageProvider = NonExtractableKeySecretStorageProvider;
|
|
54368
55536
|
exports.PolicyMeta = PolicyMeta;
|
|
54369
55537
|
exports.Query = Query;
|
|
54370
55538
|
exports.QueryAtom = QueryAtom;
|
|
@@ -54376,6 +55544,7 @@ ${operationTypes.join("\n")}
|
|
|
54376
55544
|
exports.QueryMetaTypeViaAtom = QueryMetaTypeViaAtom;
|
|
54377
55545
|
exports.QueryWalletBundle = QueryWalletBundle;
|
|
54378
55546
|
exports.QueryWalletList = QueryWalletList;
|
|
55547
|
+
exports.RECOVERY_KEY_PREFIX = RECOVERY_KEY_PREFIX;
|
|
54379
55548
|
exports.ResponseAppendRequest = ResponseAppendRequest;
|
|
54380
55549
|
exports.ResponseAtom = ResponseAtom;
|
|
54381
55550
|
exports.ResponseBalance = ResponseBalance;
|
|
@@ -54396,9 +55565,12 @@ ${operationTypes.join("\n")}
|
|
|
54396
55565
|
exports.SDK_INFO = SDK_INFO;
|
|
54397
55566
|
exports.SDK_NAME = SDK_NAME;
|
|
54398
55567
|
exports.SDK_VERSION = SDK_VERSION;
|
|
55568
|
+
exports.SECRET_KEY_PREFIX = SECRET_KEY_PREFIX;
|
|
54399
55569
|
exports.TokenUnit = TokenUnit;
|
|
54400
55570
|
exports.Wallet = Wallet;
|
|
55571
|
+
exports.WebAuthnPrfSecretStorageProvider = WebAuthnPrfSecretStorageProvider;
|
|
54401
55572
|
exports.WebCryptoSecretStorageProvider = WebCryptoSecretStorageProvider;
|
|
55573
|
+
exports.WebStorageBackend = WebStorageBackend;
|
|
54402
55574
|
exports.base64ToHex = base64ToHex;
|
|
54403
55575
|
exports.bufferToHexString = bufferToHexString;
|
|
54404
55576
|
exports.capitalize = capitalize;
|
|
@@ -54437,9 +55609,11 @@ ${operationTypes.join("\n")}
|
|
|
54437
55609
|
exports.isPosition = isPosition2;
|
|
54438
55610
|
exports.isWalletAddress = isWalletAddress2;
|
|
54439
55611
|
exports.normalizeMolecularHash = normalizeMolecularHash;
|
|
55612
|
+
exports.openEnvelope = openEnvelope;
|
|
54440
55613
|
exports.randomString = randomString;
|
|
54441
55614
|
exports.runCompatibilityTests = runCompatibilityTests;
|
|
54442
55615
|
exports.runExtendedCompatibilityTests = runExtendedCompatibilityTests;
|
|
55616
|
+
exports.sealEnvelope = sealEnvelope;
|
|
54443
55617
|
exports.shake256 = shake256;
|
|
54444
55618
|
exports.toCamelCase = toCamelCase;
|
|
54445
55619
|
exports.toSnakeCase = toSnakeCase;
|