@wishknish/knishio-client-ts 1.0.0 → 1.2.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 +1304 -121
- 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 +3044 -1049
- package/dist/index.iife.js.map +1 -1
- package/dist/index.js +1295 -122
- 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/libraries/GraphQLClient.ts +18 -5
- 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.cjs
CHANGED
|
@@ -572,6 +572,14 @@ var init_SecretStorageException = __esm({
|
|
|
572
572
|
}
|
|
573
573
|
);
|
|
574
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* Validation error for storage options or parameters
|
|
577
|
+
*/
|
|
578
|
+
static validationError(message) {
|
|
579
|
+
return new _SecretStorageException(message, {
|
|
580
|
+
code: "VALIDATION_ERROR"
|
|
581
|
+
});
|
|
582
|
+
}
|
|
575
583
|
};
|
|
576
584
|
}
|
|
577
585
|
});
|
|
@@ -6403,6 +6411,9 @@ var Decimal = class _Decimal {
|
|
|
6403
6411
|
return _Decimal.cmp(value1, value2) === 0;
|
|
6404
6412
|
}
|
|
6405
6413
|
};
|
|
6414
|
+
|
|
6415
|
+
// src/libraries/GraphQLClient.ts
|
|
6416
|
+
init_exception();
|
|
6406
6417
|
var CIPHER_HASH_QUERY = "query ( $Hash: String! ) { CipherHash ( Hash: $Hash ) { hash } }";
|
|
6407
6418
|
function parseOperation(query) {
|
|
6408
6419
|
const typeMatch = (query || "").match(/\b(query|mutation|subscription)\b/i);
|
|
@@ -6510,15 +6521,21 @@ var GraphQLClient = class {
|
|
|
6510
6521
|
async cipherFetch(input, init) {
|
|
6511
6522
|
const wallet = this.$__wallet;
|
|
6512
6523
|
const serverPubkey = this.$__pubkey;
|
|
6513
|
-
let
|
|
6524
|
+
let cipherWallet = null;
|
|
6514
6525
|
let requestInit = init;
|
|
6515
|
-
if (
|
|
6526
|
+
if (init && typeof init.body === "string" && this.shouldEncrypt(init.body)) {
|
|
6527
|
+
if (!wallet) {
|
|
6528
|
+
throw new CodeException("Authorized wallet missing.");
|
|
6529
|
+
}
|
|
6530
|
+
if (!serverPubkey) {
|
|
6531
|
+
throw new CodeException("Server public key missing.");
|
|
6532
|
+
}
|
|
6516
6533
|
const hashVar = await wallet.encryptStringML(init.body, serverPubkey);
|
|
6517
6534
|
requestInit = { ...init, body: JSON.stringify({ query: CIPHER_HASH_QUERY, variables: { Hash: hashVar } }) };
|
|
6518
|
-
|
|
6535
|
+
cipherWallet = wallet;
|
|
6519
6536
|
}
|
|
6520
6537
|
const response = await fetch(input, requestInit);
|
|
6521
|
-
if (!
|
|
6538
|
+
if (!cipherWallet) {
|
|
6522
6539
|
return response;
|
|
6523
6540
|
}
|
|
6524
6541
|
const text = await response.text();
|
|
@@ -6533,7 +6550,7 @@ var GraphQLClient = class {
|
|
|
6533
6550
|
if (typeof hash !== "string") {
|
|
6534
6551
|
return new Response(text, init2);
|
|
6535
6552
|
}
|
|
6536
|
-
const decrypted = await
|
|
6553
|
+
const decrypted = await cipherWallet.decryptMyMessageML(JSON.parse(hash));
|
|
6537
6554
|
return new Response(decrypted != null ? decrypted : text, init2);
|
|
6538
6555
|
}
|
|
6539
6556
|
setAuthData({
|
|
@@ -10697,10 +10714,121 @@ function constantTimeCompare(a, b) {
|
|
|
10697
10714
|
return result === 0;
|
|
10698
10715
|
}
|
|
10699
10716
|
|
|
10717
|
+
// src/storage/secretEnvelope.ts
|
|
10718
|
+
init_SecretStorageException();
|
|
10719
|
+
var ENVELOPE_ALGORITHM = "AES-GCM";
|
|
10720
|
+
var DEFAULT_ITERATIONS = 1e5;
|
|
10721
|
+
var SECRET_KEY_PREFIX = "knishio:secret:";
|
|
10722
|
+
var RECOVERY_KEY_PREFIX = "knishio:recovery:";
|
|
10723
|
+
var GCM_IV_LENGTH = 12;
|
|
10724
|
+
var SALT_LENGTH = 16;
|
|
10725
|
+
var textEncoder2 = new TextEncoder();
|
|
10726
|
+
function uint8ArrayToBase64(bytes) {
|
|
10727
|
+
let binary = "";
|
|
10728
|
+
const len = bytes.byteLength;
|
|
10729
|
+
for (let i = 0; i < len; i++) {
|
|
10730
|
+
const byte = bytes[i];
|
|
10731
|
+
if (byte !== void 0) {
|
|
10732
|
+
binary += String.fromCharCode(byte);
|
|
10733
|
+
}
|
|
10734
|
+
}
|
|
10735
|
+
return btoa(binary);
|
|
10736
|
+
}
|
|
10737
|
+
function base64ToUint8Array(base64) {
|
|
10738
|
+
const binary = atob(base64);
|
|
10739
|
+
const len = binary.length;
|
|
10740
|
+
const bytes = new Uint8Array(len);
|
|
10741
|
+
for (let i = 0; i < len; i++) {
|
|
10742
|
+
bytes[i] = binary.charCodeAt(i);
|
|
10743
|
+
}
|
|
10744
|
+
return bytes;
|
|
10745
|
+
}
|
|
10746
|
+
async function deriveEnvelopeKey(passphrase, salt, iterations = DEFAULT_ITERATIONS) {
|
|
10747
|
+
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
10748
|
+
throw new exports.SecretStorageException("WebCrypto API is not available");
|
|
10749
|
+
}
|
|
10750
|
+
const passphraseBytes = textEncoder2.encode(passphrase);
|
|
10751
|
+
try {
|
|
10752
|
+
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
10753
|
+
"raw",
|
|
10754
|
+
passphraseBytes,
|
|
10755
|
+
"PBKDF2",
|
|
10756
|
+
false,
|
|
10757
|
+
["deriveKey"]
|
|
10758
|
+
);
|
|
10759
|
+
return await globalThis.crypto.subtle.deriveKey(
|
|
10760
|
+
{
|
|
10761
|
+
name: "PBKDF2",
|
|
10762
|
+
salt,
|
|
10763
|
+
iterations,
|
|
10764
|
+
hash: "SHA-256"
|
|
10765
|
+
},
|
|
10766
|
+
baseKey,
|
|
10767
|
+
{ name: "AES-GCM", length: 256 },
|
|
10768
|
+
false,
|
|
10769
|
+
["encrypt", "decrypt"]
|
|
10770
|
+
);
|
|
10771
|
+
} finally {
|
|
10772
|
+
zeroizeBytes(passphraseBytes);
|
|
10773
|
+
}
|
|
10774
|
+
}
|
|
10775
|
+
async function sealEnvelope(secret, passphrase, metadata) {
|
|
10776
|
+
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
10777
|
+
throw new exports.SecretStorageException("WebCrypto API is not available");
|
|
10778
|
+
}
|
|
10779
|
+
const salt = new Uint8Array(SALT_LENGTH);
|
|
10780
|
+
const iv = new Uint8Array(GCM_IV_LENGTH);
|
|
10781
|
+
globalThis.crypto.getRandomValues(salt);
|
|
10782
|
+
globalThis.crypto.getRandomValues(iv);
|
|
10783
|
+
const key = await deriveEnvelopeKey(passphrase, salt, DEFAULT_ITERATIONS);
|
|
10784
|
+
const secretBytes = textEncoder2.encode(secret);
|
|
10785
|
+
try {
|
|
10786
|
+
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
10787
|
+
{
|
|
10788
|
+
name: ENVELOPE_ALGORITHM,
|
|
10789
|
+
iv
|
|
10790
|
+
},
|
|
10791
|
+
key,
|
|
10792
|
+
secretBytes
|
|
10793
|
+
);
|
|
10794
|
+
const ciphertext = uint8ArrayToBase64(new Uint8Array(encryptedBuffer));
|
|
10795
|
+
return {
|
|
10796
|
+
version: 1,
|
|
10797
|
+
ciphertext,
|
|
10798
|
+
iv: uint8ArrayToBase64(iv),
|
|
10799
|
+
salt: uint8ArrayToBase64(salt),
|
|
10800
|
+
algorithm: ENVELOPE_ALGORITHM,
|
|
10801
|
+
iterations: DEFAULT_ITERATIONS,
|
|
10802
|
+
metadata
|
|
10803
|
+
};
|
|
10804
|
+
} finally {
|
|
10805
|
+
zeroizeBytes(secretBytes);
|
|
10806
|
+
}
|
|
10807
|
+
}
|
|
10808
|
+
async function openEnvelope(payload, passphrase) {
|
|
10809
|
+
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
10810
|
+
throw new exports.SecretStorageException("WebCrypto API is not available");
|
|
10811
|
+
}
|
|
10812
|
+
const salt = base64ToUint8Array(payload.salt);
|
|
10813
|
+
const iv = base64ToUint8Array(payload.iv);
|
|
10814
|
+
const ciphertext = base64ToUint8Array(payload.ciphertext);
|
|
10815
|
+
const key = await deriveEnvelopeKey(passphrase, salt, payload.iterations ?? DEFAULT_ITERATIONS);
|
|
10816
|
+
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
10817
|
+
{
|
|
10818
|
+
name: ENVELOPE_ALGORITHM,
|
|
10819
|
+
iv
|
|
10820
|
+
},
|
|
10821
|
+
key,
|
|
10822
|
+
ciphertext
|
|
10823
|
+
);
|
|
10824
|
+
return new Uint8Array(decryptedBuffer);
|
|
10825
|
+
}
|
|
10826
|
+
|
|
10700
10827
|
// src/storage/MemorySecretStorageProvider.ts
|
|
10701
10828
|
var MemorySecretStorageProvider = class {
|
|
10702
10829
|
providerType = "memory";
|
|
10703
10830
|
secrets = /* @__PURE__ */ new Map();
|
|
10831
|
+
recoverySecrets = /* @__PURE__ */ new Map();
|
|
10704
10832
|
/**
|
|
10705
10833
|
* Memory storage is not hardware backed
|
|
10706
10834
|
*/
|
|
@@ -10731,6 +10859,17 @@ var MemorySecretStorageProvider = class {
|
|
|
10731
10859
|
providerType: this.providerType
|
|
10732
10860
|
};
|
|
10733
10861
|
this.secrets.set(bundleHash, { secret, metadata });
|
|
10862
|
+
if (options?.recoveryPassphrase) {
|
|
10863
|
+
const recoveryMetadata = {
|
|
10864
|
+
bundleHash,
|
|
10865
|
+
label: options?.label,
|
|
10866
|
+
createdAt: Date.now(),
|
|
10867
|
+
hardwareBacked: false,
|
|
10868
|
+
providerType: "webcrypto-aes-gcm"
|
|
10869
|
+
};
|
|
10870
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
10871
|
+
this.recoverySecrets.set(bundleHash, JSON.stringify(recoveryPayload));
|
|
10872
|
+
}
|
|
10734
10873
|
}
|
|
10735
10874
|
/**
|
|
10736
10875
|
* Retrieve a secret from memory
|
|
@@ -10743,6 +10882,7 @@ var MemorySecretStorageProvider = class {
|
|
|
10743
10882
|
* Delete a stored secret
|
|
10744
10883
|
*/
|
|
10745
10884
|
async deleteSecret(bundleHash) {
|
|
10885
|
+
this.recoverySecrets.delete(bundleHash);
|
|
10746
10886
|
return this.secrets.delete(bundleHash);
|
|
10747
10887
|
}
|
|
10748
10888
|
/**
|
|
@@ -10772,6 +10912,48 @@ var MemorySecretStorageProvider = class {
|
|
|
10772
10912
|
*/
|
|
10773
10913
|
clear() {
|
|
10774
10914
|
this.secrets.clear();
|
|
10915
|
+
this.recoverySecrets.clear();
|
|
10916
|
+
}
|
|
10917
|
+
/**
|
|
10918
|
+
* Recover a secret using its recovery envelope and restore it
|
|
10919
|
+
*/
|
|
10920
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
10921
|
+
if (!bundleHash) {
|
|
10922
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
10923
|
+
}
|
|
10924
|
+
if (!recoveryPassphrase) {
|
|
10925
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
10926
|
+
}
|
|
10927
|
+
const raw = this.recoverySecrets.get(bundleHash);
|
|
10928
|
+
if (!raw) {
|
|
10929
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
10930
|
+
}
|
|
10931
|
+
let payload;
|
|
10932
|
+
try {
|
|
10933
|
+
payload = JSON.parse(raw);
|
|
10934
|
+
} catch {
|
|
10935
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
10936
|
+
}
|
|
10937
|
+
let decryptedBytes;
|
|
10938
|
+
try {
|
|
10939
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
10940
|
+
} catch (err) {
|
|
10941
|
+
if (err instanceof exports.SecretStorageException) {
|
|
10942
|
+
throw err;
|
|
10943
|
+
}
|
|
10944
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10945
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
10946
|
+
}
|
|
10947
|
+
let secretStr;
|
|
10948
|
+
try {
|
|
10949
|
+
secretStr = new TextDecoder().decode(decryptedBytes);
|
|
10950
|
+
} finally {
|
|
10951
|
+
zeroizeBytes(decryptedBytes);
|
|
10952
|
+
}
|
|
10953
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
10954
|
+
...options,
|
|
10955
|
+
recoveryPassphrase
|
|
10956
|
+
});
|
|
10775
10957
|
}
|
|
10776
10958
|
};
|
|
10777
10959
|
|
|
@@ -12593,45 +12775,25 @@ var MemoryStorageBackend = class {
|
|
|
12593
12775
|
return Array.from(this.store.keys());
|
|
12594
12776
|
}
|
|
12595
12777
|
};
|
|
12596
|
-
function uint8ArrayToBase64(bytes) {
|
|
12597
|
-
let binary = "";
|
|
12598
|
-
const len = bytes.byteLength;
|
|
12599
|
-
for (let i = 0; i < len; i++) {
|
|
12600
|
-
const byte = bytes[i];
|
|
12601
|
-
if (byte !== void 0) {
|
|
12602
|
-
binary += String.fromCharCode(byte);
|
|
12603
|
-
}
|
|
12604
|
-
}
|
|
12605
|
-
return btoa(binary);
|
|
12606
|
-
}
|
|
12607
|
-
function base64ToUint8Array(base64) {
|
|
12608
|
-
const binary = atob(base64);
|
|
12609
|
-
const len = binary.length;
|
|
12610
|
-
const bytes = new Uint8Array(len);
|
|
12611
|
-
for (let i = 0; i < len; i++) {
|
|
12612
|
-
bytes[i] = binary.charCodeAt(i);
|
|
12613
|
-
}
|
|
12614
|
-
return bytes;
|
|
12615
|
-
}
|
|
12616
|
-
var textEncoder2 = new TextEncoder();
|
|
12617
12778
|
var textDecoder = new TextDecoder();
|
|
12618
|
-
var KEY_PREFIX =
|
|
12619
|
-
var DEFAULT_ITERATIONS = 1e5;
|
|
12779
|
+
var KEY_PREFIX = SECRET_KEY_PREFIX;
|
|
12620
12780
|
var WebCryptoSecretStorageProvider = class {
|
|
12621
12781
|
providerType = "webcrypto-aes-gcm";
|
|
12622
12782
|
backend;
|
|
12623
12783
|
defaultPassphrase;
|
|
12624
|
-
hardwareBacked;
|
|
12625
12784
|
constructor(options = {}) {
|
|
12626
12785
|
this.backend = options.backend ?? new MemoryStorageBackend();
|
|
12627
12786
|
this.defaultPassphrase = options.defaultPassphrase;
|
|
12628
|
-
this.hardwareBacked = options.hardwareBacked ?? false;
|
|
12629
12787
|
}
|
|
12630
12788
|
/**
|
|
12631
|
-
*
|
|
12789
|
+
* True only when this provider holds a non-exportable key inside platform-secure
|
|
12790
|
+
* hardware (Android TEE/StrongBox, Secure Enclave, TPM) and learned that from the
|
|
12791
|
+
* platform itself — never from a caller argument. Software envelope providers
|
|
12792
|
+
* return false. The value is persisted as `metadata.hardwareBacked` in every
|
|
12793
|
+
* envelope this provider writes.
|
|
12632
12794
|
*/
|
|
12633
12795
|
isHardwareBacked() {
|
|
12634
|
-
return
|
|
12796
|
+
return false;
|
|
12635
12797
|
}
|
|
12636
12798
|
/**
|
|
12637
12799
|
* Check if WebCrypto subtle API is available
|
|
@@ -12639,38 +12801,6 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12639
12801
|
async isAvailable() {
|
|
12640
12802
|
return typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
|
|
12641
12803
|
}
|
|
12642
|
-
/**
|
|
12643
|
-
* Derive an AES-GCM CryptoKey from a passphrase and salt using PBKDF2
|
|
12644
|
-
*/
|
|
12645
|
-
async deriveKey(passphrase, salt, iterations = DEFAULT_ITERATIONS) {
|
|
12646
|
-
if (!await this.isAvailable()) {
|
|
12647
|
-
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12648
|
-
}
|
|
12649
|
-
const passphraseBytes = textEncoder2.encode(passphrase);
|
|
12650
|
-
try {
|
|
12651
|
-
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
12652
|
-
"raw",
|
|
12653
|
-
passphraseBytes,
|
|
12654
|
-
"PBKDF2",
|
|
12655
|
-
false,
|
|
12656
|
-
["deriveKey"]
|
|
12657
|
-
);
|
|
12658
|
-
return await globalThis.crypto.subtle.deriveKey(
|
|
12659
|
-
{
|
|
12660
|
-
name: "PBKDF2",
|
|
12661
|
-
salt,
|
|
12662
|
-
iterations,
|
|
12663
|
-
hash: "SHA-256"
|
|
12664
|
-
},
|
|
12665
|
-
baseKey,
|
|
12666
|
-
{ name: "AES-GCM", length: 256 },
|
|
12667
|
-
false,
|
|
12668
|
-
["encrypt", "decrypt"]
|
|
12669
|
-
);
|
|
12670
|
-
} finally {
|
|
12671
|
-
zeroizeBytes(passphraseBytes);
|
|
12672
|
-
}
|
|
12673
|
-
}
|
|
12674
12804
|
/**
|
|
12675
12805
|
* Store and encrypt a master secret
|
|
12676
12806
|
*/
|
|
@@ -12685,44 +12815,36 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12685
12815
|
if (!passphrase) {
|
|
12686
12816
|
throw new exports.SecretStorageException("Passphrase required for envelope encryption");
|
|
12687
12817
|
}
|
|
12688
|
-
|
|
12689
|
-
|
|
12690
|
-
|
|
12691
|
-
globalThis.crypto.getRandomValues(iv);
|
|
12692
|
-
const key = await this.deriveKey(passphrase, salt, DEFAULT_ITERATIONS);
|
|
12693
|
-
const secretBytes = textEncoder2.encode(secret);
|
|
12818
|
+
if (!await this.isAvailable()) {
|
|
12819
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12820
|
+
}
|
|
12694
12821
|
try {
|
|
12695
|
-
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
12696
|
-
{
|
|
12697
|
-
name: "AES-GCM",
|
|
12698
|
-
iv
|
|
12699
|
-
},
|
|
12700
|
-
key,
|
|
12701
|
-
secretBytes
|
|
12702
|
-
);
|
|
12703
|
-
const ciphertext = uint8ArrayToBase64(new Uint8Array(encryptedBuffer));
|
|
12704
12822
|
const metadata = {
|
|
12705
12823
|
bundleHash,
|
|
12706
12824
|
label: options?.label,
|
|
12707
12825
|
createdAt: Date.now(),
|
|
12708
|
-
hardwareBacked:
|
|
12826
|
+
hardwareBacked: false,
|
|
12709
12827
|
providerType: this.providerType
|
|
12710
12828
|
};
|
|
12711
|
-
const payload =
|
|
12712
|
-
version: 1,
|
|
12713
|
-
ciphertext,
|
|
12714
|
-
iv: uint8ArrayToBase64(iv),
|
|
12715
|
-
salt: uint8ArrayToBase64(salt),
|
|
12716
|
-
algorithm: "AES-GCM",
|
|
12717
|
-
iterations: DEFAULT_ITERATIONS,
|
|
12718
|
-
metadata
|
|
12719
|
-
};
|
|
12829
|
+
const payload = await sealEnvelope(secret, passphrase, metadata);
|
|
12720
12830
|
await this.backend.setItem(`${KEY_PREFIX}${bundleHash}`, JSON.stringify(payload));
|
|
12831
|
+
if (options?.recoveryPassphrase) {
|
|
12832
|
+
const recoveryMetadata = {
|
|
12833
|
+
bundleHash,
|
|
12834
|
+
label: options?.label,
|
|
12835
|
+
createdAt: Date.now(),
|
|
12836
|
+
hardwareBacked: false,
|
|
12837
|
+
providerType: "webcrypto-aes-gcm"
|
|
12838
|
+
};
|
|
12839
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
12840
|
+
await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
|
|
12841
|
+
}
|
|
12721
12842
|
} catch (err) {
|
|
12843
|
+
if (err instanceof exports.SecretStorageException) {
|
|
12844
|
+
throw err;
|
|
12845
|
+
}
|
|
12722
12846
|
const msg = err instanceof Error ? err.message : String(err);
|
|
12723
12847
|
throw new exports.SecretStorageException(`Encryption failed: ${msg}`);
|
|
12724
|
-
} finally {
|
|
12725
|
-
zeroizeBytes(secretBytes);
|
|
12726
12848
|
}
|
|
12727
12849
|
}
|
|
12728
12850
|
/**
|
|
@@ -12743,26 +12865,20 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12743
12865
|
if (!passphrase) {
|
|
12744
12866
|
throw new exports.SecretStorageException("Passphrase required for secret decryption");
|
|
12745
12867
|
}
|
|
12746
|
-
|
|
12747
|
-
|
|
12748
|
-
|
|
12868
|
+
if (!await this.isAvailable()) {
|
|
12869
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12870
|
+
}
|
|
12749
12871
|
try {
|
|
12750
|
-
const
|
|
12751
|
-
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
12752
|
-
{
|
|
12753
|
-
name: "AES-GCM",
|
|
12754
|
-
iv
|
|
12755
|
-
},
|
|
12756
|
-
key,
|
|
12757
|
-
ciphertext
|
|
12758
|
-
);
|
|
12759
|
-
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
12872
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
12760
12873
|
try {
|
|
12761
12874
|
return textDecoder.decode(decryptedBytes);
|
|
12762
12875
|
} finally {
|
|
12763
12876
|
zeroizeBytes(decryptedBytes);
|
|
12764
12877
|
}
|
|
12765
12878
|
} catch (err) {
|
|
12879
|
+
if (err instanceof exports.SecretStorageException) {
|
|
12880
|
+
throw err;
|
|
12881
|
+
}
|
|
12766
12882
|
const msg = err instanceof Error ? err.message : String(err);
|
|
12767
12883
|
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
12768
12884
|
}
|
|
@@ -12772,7 +12888,9 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12772
12888
|
*/
|
|
12773
12889
|
async deleteSecret(bundleHash) {
|
|
12774
12890
|
const key = `${KEY_PREFIX}${bundleHash}`;
|
|
12891
|
+
const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
|
|
12775
12892
|
const result = await this.backend.removeItem(key);
|
|
12893
|
+
await this.backend.removeItem(recoveryKey);
|
|
12776
12894
|
return result !== false;
|
|
12777
12895
|
}
|
|
12778
12896
|
/**
|
|
@@ -12787,7 +12905,7 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12787
12905
|
*/
|
|
12788
12906
|
async listSecrets() {
|
|
12789
12907
|
const keys = await this.backend.keys();
|
|
12790
|
-
const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX));
|
|
12908
|
+
const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX) && !k.startsWith(RECOVERY_KEY_PREFIX));
|
|
12791
12909
|
const results = [];
|
|
12792
12910
|
for (const key of matchingKeys) {
|
|
12793
12911
|
const raw = await this.backend.getItem(key);
|
|
@@ -12821,20 +12939,11 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12821
12939
|
if (!passphrase) {
|
|
12822
12940
|
throw new exports.SecretStorageException("Passphrase required for secret decryption");
|
|
12823
12941
|
}
|
|
12824
|
-
|
|
12825
|
-
|
|
12826
|
-
|
|
12942
|
+
if (!await this.isAvailable()) {
|
|
12943
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12944
|
+
}
|
|
12827
12945
|
try {
|
|
12828
|
-
const
|
|
12829
|
-
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
12830
|
-
{
|
|
12831
|
-
name: "AES-GCM",
|
|
12832
|
-
iv
|
|
12833
|
-
},
|
|
12834
|
-
key,
|
|
12835
|
-
ciphertext
|
|
12836
|
-
);
|
|
12837
|
-
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
12946
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
12838
12947
|
return await withSecureBytes(decryptedBytes, async (bytes) => {
|
|
12839
12948
|
const secretString = textDecoder.decode(bytes);
|
|
12840
12949
|
return await fn(secretString);
|
|
@@ -12847,6 +12956,1071 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12847
12956
|
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
12848
12957
|
}
|
|
12849
12958
|
}
|
|
12959
|
+
/**
|
|
12960
|
+
* Recover a secret using its recovery envelope and re-enroll it
|
|
12961
|
+
*/
|
|
12962
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
12963
|
+
if (!bundleHash) {
|
|
12964
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
12965
|
+
}
|
|
12966
|
+
if (!recoveryPassphrase) {
|
|
12967
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
12968
|
+
}
|
|
12969
|
+
const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
|
|
12970
|
+
if (!raw) {
|
|
12971
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
12972
|
+
}
|
|
12973
|
+
let payload;
|
|
12974
|
+
try {
|
|
12975
|
+
payload = JSON.parse(raw);
|
|
12976
|
+
} catch {
|
|
12977
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
12978
|
+
}
|
|
12979
|
+
let decryptedBytes;
|
|
12980
|
+
try {
|
|
12981
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
12982
|
+
} catch (err) {
|
|
12983
|
+
if (err instanceof exports.SecretStorageException) {
|
|
12984
|
+
throw err;
|
|
12985
|
+
}
|
|
12986
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12987
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
12988
|
+
}
|
|
12989
|
+
let secretStr;
|
|
12990
|
+
try {
|
|
12991
|
+
secretStr = textDecoder.decode(decryptedBytes);
|
|
12992
|
+
} finally {
|
|
12993
|
+
zeroizeBytes(decryptedBytes);
|
|
12994
|
+
}
|
|
12995
|
+
const storePassphrase = options?.passphrase ?? this.defaultPassphrase ?? recoveryPassphrase;
|
|
12996
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
12997
|
+
...options,
|
|
12998
|
+
passphrase: storePassphrase,
|
|
12999
|
+
recoveryPassphrase
|
|
13000
|
+
});
|
|
13001
|
+
}
|
|
13002
|
+
};
|
|
13003
|
+
|
|
13004
|
+
// src/storage/FileStorageBackend.ts
|
|
13005
|
+
init_SecretStorageException();
|
|
13006
|
+
var FileStorageBackend = class {
|
|
13007
|
+
filePath;
|
|
13008
|
+
store = /* @__PURE__ */ new Map();
|
|
13009
|
+
loaded = false;
|
|
13010
|
+
constructor(filePath) {
|
|
13011
|
+
if (!filePath) {
|
|
13012
|
+
throw new exports.SecretStorageException("Storage file path cannot be empty");
|
|
13013
|
+
}
|
|
13014
|
+
this.filePath = filePath;
|
|
13015
|
+
}
|
|
13016
|
+
async getFs() {
|
|
13017
|
+
try {
|
|
13018
|
+
const fs = await import('fs/promises');
|
|
13019
|
+
const path = await import('path');
|
|
13020
|
+
return { fs, path };
|
|
13021
|
+
} catch {
|
|
13022
|
+
throw exports.SecretStorageException.unavailable(
|
|
13023
|
+
"file-storage",
|
|
13024
|
+
"FileStorageBackend is only supported in Node.js environments with node:fs access"
|
|
13025
|
+
);
|
|
13026
|
+
}
|
|
13027
|
+
}
|
|
13028
|
+
async ensureLoaded() {
|
|
13029
|
+
if (this.loaded) {
|
|
13030
|
+
return this.store;
|
|
13031
|
+
}
|
|
13032
|
+
const { fs } = await this.getFs();
|
|
13033
|
+
try {
|
|
13034
|
+
const content = await fs.readFile(this.filePath, "utf8");
|
|
13035
|
+
let parsed;
|
|
13036
|
+
try {
|
|
13037
|
+
parsed = JSON.parse(content);
|
|
13038
|
+
} catch {
|
|
13039
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted storage file format");
|
|
13040
|
+
}
|
|
13041
|
+
if (parsed && typeof parsed === "object") {
|
|
13042
|
+
this.store = new Map(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
13043
|
+
}
|
|
13044
|
+
} catch (err) {
|
|
13045
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13046
|
+
throw err;
|
|
13047
|
+
}
|
|
13048
|
+
const nodeErr = err;
|
|
13049
|
+
if (nodeErr?.code !== "ENOENT") {
|
|
13050
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13051
|
+
throw new exports.SecretStorageException(`Failed to read storage file: ${msg}`);
|
|
13052
|
+
}
|
|
13053
|
+
this.store = /* @__PURE__ */ new Map();
|
|
13054
|
+
}
|
|
13055
|
+
this.loaded = true;
|
|
13056
|
+
return this.store;
|
|
13057
|
+
}
|
|
13058
|
+
async persist() {
|
|
13059
|
+
const { fs, path } = await this.getFs();
|
|
13060
|
+
const dir = path.dirname(this.filePath);
|
|
13061
|
+
if (dir && dir !== ".") {
|
|
13062
|
+
await fs.mkdir(dir, { recursive: true });
|
|
13063
|
+
}
|
|
13064
|
+
const tmpPath = `${this.filePath}.tmp.${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
13065
|
+
const data = JSON.stringify(Object.fromEntries(this.store), null, 2);
|
|
13066
|
+
try {
|
|
13067
|
+
await fs.writeFile(tmpPath, data, { mode: 384, encoding: "utf8" });
|
|
13068
|
+
if (typeof process !== "undefined" && process.platform !== "win32") {
|
|
13069
|
+
try {
|
|
13070
|
+
await fs.chmod(tmpPath, 384);
|
|
13071
|
+
} catch {
|
|
13072
|
+
}
|
|
13073
|
+
}
|
|
13074
|
+
await fs.rename(tmpPath, this.filePath);
|
|
13075
|
+
} catch (err) {
|
|
13076
|
+
try {
|
|
13077
|
+
await fs.unlink(tmpPath);
|
|
13078
|
+
} catch {
|
|
13079
|
+
}
|
|
13080
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13081
|
+
throw new exports.SecretStorageException(`Failed to persist storage file: ${msg}`);
|
|
13082
|
+
}
|
|
13083
|
+
}
|
|
13084
|
+
async getItem(key) {
|
|
13085
|
+
await this.ensureLoaded();
|
|
13086
|
+
return this.store.get(key) ?? null;
|
|
13087
|
+
}
|
|
13088
|
+
async setItem(key, value) {
|
|
13089
|
+
await this.ensureLoaded();
|
|
13090
|
+
this.store.set(key, value);
|
|
13091
|
+
await this.persist();
|
|
13092
|
+
}
|
|
13093
|
+
async removeItem(key) {
|
|
13094
|
+
await this.ensureLoaded();
|
|
13095
|
+
const existed = this.store.delete(key);
|
|
13096
|
+
if (existed) {
|
|
13097
|
+
await this.persist();
|
|
13098
|
+
}
|
|
13099
|
+
return existed;
|
|
13100
|
+
}
|
|
13101
|
+
async keys() {
|
|
13102
|
+
await this.ensureLoaded();
|
|
13103
|
+
return Array.from(this.store.keys());
|
|
13104
|
+
}
|
|
13105
|
+
};
|
|
13106
|
+
|
|
13107
|
+
// src/storage/WebStorageBackend.ts
|
|
13108
|
+
init_SecretStorageException();
|
|
13109
|
+
var WebStorageBackend = class {
|
|
13110
|
+
storage;
|
|
13111
|
+
prefix;
|
|
13112
|
+
constructor(storage, prefix = "knishio:") {
|
|
13113
|
+
if (storage) {
|
|
13114
|
+
this.storage = storage;
|
|
13115
|
+
} else if (typeof globalThis !== "undefined" && globalThis.localStorage) {
|
|
13116
|
+
this.storage = globalThis.localStorage;
|
|
13117
|
+
} else {
|
|
13118
|
+
throw exports.SecretStorageException.unavailable(
|
|
13119
|
+
"web-storage",
|
|
13120
|
+
"WebStorageBackend requires a Storage object or global localStorage"
|
|
13121
|
+
);
|
|
13122
|
+
}
|
|
13123
|
+
this.prefix = prefix;
|
|
13124
|
+
}
|
|
13125
|
+
getItem(key) {
|
|
13126
|
+
return this.storage.getItem(key);
|
|
13127
|
+
}
|
|
13128
|
+
setItem(key, value) {
|
|
13129
|
+
this.storage.setItem(key, value);
|
|
13130
|
+
}
|
|
13131
|
+
removeItem(key) {
|
|
13132
|
+
const existed = this.storage.getItem(key) !== null;
|
|
13133
|
+
this.storage.removeItem(key);
|
|
13134
|
+
return existed;
|
|
13135
|
+
}
|
|
13136
|
+
keys() {
|
|
13137
|
+
const result = [];
|
|
13138
|
+
const len = this.storage.length;
|
|
13139
|
+
for (let i = 0; i < len; i++) {
|
|
13140
|
+
const k = this.storage.key(i);
|
|
13141
|
+
if (k !== null) {
|
|
13142
|
+
if (!this.prefix || k.startsWith(this.prefix)) {
|
|
13143
|
+
result.push(k);
|
|
13144
|
+
}
|
|
13145
|
+
}
|
|
13146
|
+
}
|
|
13147
|
+
return result;
|
|
13148
|
+
}
|
|
13149
|
+
};
|
|
13150
|
+
|
|
13151
|
+
// src/storage/WebAuthnPrfSecretStorageProvider.ts
|
|
13152
|
+
init_SecretStorageException();
|
|
13153
|
+
var PRF_SALT_LABEL = "knishio:secret-storage:webauthn-prf:v1";
|
|
13154
|
+
var KEK_INFO = "knishio:secret-storage:kek:v1";
|
|
13155
|
+
var KEY_PREFIX2 = SECRET_KEY_PREFIX;
|
|
13156
|
+
var GCM_IV_LENGTH2 = 12;
|
|
13157
|
+
var textEncoder3 = new TextEncoder();
|
|
13158
|
+
var textDecoder2 = new TextDecoder();
|
|
13159
|
+
function base64UrlEncode(bytes) {
|
|
13160
|
+
return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
13161
|
+
}
|
|
13162
|
+
function base64UrlDecode(str) {
|
|
13163
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
13164
|
+
while (base64.length % 4 !== 0) {
|
|
13165
|
+
base64 += "=";
|
|
13166
|
+
}
|
|
13167
|
+
return base64ToUint8Array(base64);
|
|
13168
|
+
}
|
|
13169
|
+
function toUint8Array(buf) {
|
|
13170
|
+
if (buf instanceof Uint8Array) {
|
|
13171
|
+
return buf;
|
|
13172
|
+
}
|
|
13173
|
+
if (ArrayBuffer.isView(buf)) {
|
|
13174
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
13175
|
+
}
|
|
13176
|
+
return new Uint8Array(buf);
|
|
13177
|
+
}
|
|
13178
|
+
async function computePrfSalt() {
|
|
13179
|
+
const hash = await globalThis.crypto.subtle.digest("SHA-256", textEncoder3.encode(PRF_SALT_LABEL));
|
|
13180
|
+
return new Uint8Array(hash);
|
|
13181
|
+
}
|
|
13182
|
+
async function deriveKekFromPrf(prfOutput, prfSalt) {
|
|
13183
|
+
const hkdfKey = await globalThis.crypto.subtle.importKey(
|
|
13184
|
+
"raw",
|
|
13185
|
+
prfOutput,
|
|
13186
|
+
"HKDF",
|
|
13187
|
+
false,
|
|
13188
|
+
["deriveKey"]
|
|
13189
|
+
);
|
|
13190
|
+
return await globalThis.crypto.subtle.deriveKey(
|
|
13191
|
+
{
|
|
13192
|
+
name: "HKDF",
|
|
13193
|
+
hash: "SHA-256",
|
|
13194
|
+
salt: prfSalt,
|
|
13195
|
+
info: textEncoder3.encode(KEK_INFO)
|
|
13196
|
+
},
|
|
13197
|
+
hkdfKey,
|
|
13198
|
+
{ name: "AES-GCM", length: 256 },
|
|
13199
|
+
false,
|
|
13200
|
+
["encrypt", "decrypt"]
|
|
13201
|
+
);
|
|
13202
|
+
}
|
|
13203
|
+
var WebAuthnPrfSecretStorageProvider = class {
|
|
13204
|
+
providerType = "webauthn-prf";
|
|
13205
|
+
backend;
|
|
13206
|
+
rp;
|
|
13207
|
+
user;
|
|
13208
|
+
credentialsContainer;
|
|
13209
|
+
alias;
|
|
13210
|
+
cachedPassphrase;
|
|
13211
|
+
constructor(options) {
|
|
13212
|
+
this.backend = options.backend;
|
|
13213
|
+
this.rp = options.rp;
|
|
13214
|
+
this.user = options.user;
|
|
13215
|
+
this.credentialsContainer = options.credentials;
|
|
13216
|
+
this.alias = options.alias ?? "default";
|
|
13217
|
+
}
|
|
13218
|
+
get credentials() {
|
|
13219
|
+
if (this.credentialsContainer) {
|
|
13220
|
+
return this.credentialsContainer;
|
|
13221
|
+
}
|
|
13222
|
+
if (typeof globalThis.navigator !== "undefined" && globalThis.navigator.credentials) {
|
|
13223
|
+
return globalThis.navigator.credentials;
|
|
13224
|
+
}
|
|
13225
|
+
throw exports.SecretStorageException.unavailable(
|
|
13226
|
+
this.providerType,
|
|
13227
|
+
"WebAuthn credentials container is not available"
|
|
13228
|
+
);
|
|
13229
|
+
}
|
|
13230
|
+
get recordKey() {
|
|
13231
|
+
return `knishio:webauthn-prf:${this.alias}`;
|
|
13232
|
+
}
|
|
13233
|
+
isHardwareBacked() {
|
|
13234
|
+
return false;
|
|
13235
|
+
}
|
|
13236
|
+
async isAvailable() {
|
|
13237
|
+
const hasWebCrypto = typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
|
|
13238
|
+
const hasCredentials = Boolean(
|
|
13239
|
+
this.credentialsContainer || typeof globalThis.navigator !== "undefined" && globalThis.navigator.credentials && typeof globalThis.PublicKeyCredential !== "undefined"
|
|
13240
|
+
);
|
|
13241
|
+
return hasWebCrypto && hasCredentials;
|
|
13242
|
+
}
|
|
13243
|
+
/**
|
|
13244
|
+
* Enroll a new passkey credential with PRF support and wrap a random device passphrase
|
|
13245
|
+
*/
|
|
13246
|
+
async enroll() {
|
|
13247
|
+
const existing = await this.backend.getItem(this.recordKey);
|
|
13248
|
+
if (existing) {
|
|
13249
|
+
return;
|
|
13250
|
+
}
|
|
13251
|
+
if (!await this.isAvailable()) {
|
|
13252
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebAuthn PRF is not available");
|
|
13253
|
+
}
|
|
13254
|
+
const challenge = new Uint8Array(32);
|
|
13255
|
+
globalThis.crypto.getRandomValues(challenge);
|
|
13256
|
+
const credential = await this.credentials.create({
|
|
13257
|
+
publicKey: {
|
|
13258
|
+
rp: this.rp,
|
|
13259
|
+
user: {
|
|
13260
|
+
id: this.user.id,
|
|
13261
|
+
name: this.user.name,
|
|
13262
|
+
displayName: this.user.displayName
|
|
13263
|
+
},
|
|
13264
|
+
challenge,
|
|
13265
|
+
pubKeyCredParams: [
|
|
13266
|
+
{ type: "public-key", alg: -7 },
|
|
13267
|
+
{ type: "public-key", alg: -257 }
|
|
13268
|
+
],
|
|
13269
|
+
authenticatorSelection: {
|
|
13270
|
+
residentKey: "required",
|
|
13271
|
+
userVerification: "required"
|
|
13272
|
+
},
|
|
13273
|
+
extensions: {
|
|
13274
|
+
prf: {}
|
|
13275
|
+
}
|
|
13276
|
+
}
|
|
13277
|
+
});
|
|
13278
|
+
if (!credential) {
|
|
13279
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "Authenticator creation returned null");
|
|
13280
|
+
}
|
|
13281
|
+
const extResults = credential.getClientExtensionResults?.();
|
|
13282
|
+
if (extResults?.prf?.enabled !== true) {
|
|
13283
|
+
throw exports.SecretStorageException.unavailable(
|
|
13284
|
+
this.providerType,
|
|
13285
|
+
"authenticator does not support the PRF extension"
|
|
13286
|
+
);
|
|
13287
|
+
}
|
|
13288
|
+
const credentialIdBytes = new Uint8Array(credential.rawId);
|
|
13289
|
+
const prfSalt = await computePrfSalt();
|
|
13290
|
+
const getChallenge = new Uint8Array(32);
|
|
13291
|
+
globalThis.crypto.getRandomValues(getChallenge);
|
|
13292
|
+
let assertion;
|
|
13293
|
+
try {
|
|
13294
|
+
assertion = await this.credentials.get({
|
|
13295
|
+
publicKey: {
|
|
13296
|
+
challenge: getChallenge,
|
|
13297
|
+
rpId: this.rp.id,
|
|
13298
|
+
allowCredentials: [
|
|
13299
|
+
{
|
|
13300
|
+
type: "public-key",
|
|
13301
|
+
id: credentialIdBytes
|
|
13302
|
+
}
|
|
13303
|
+
],
|
|
13304
|
+
userVerification: "required",
|
|
13305
|
+
extensions: {
|
|
13306
|
+
prf: {
|
|
13307
|
+
eval: {
|
|
13308
|
+
first: prfSalt
|
|
13309
|
+
}
|
|
13310
|
+
}
|
|
13311
|
+
}
|
|
13312
|
+
}
|
|
13313
|
+
});
|
|
13314
|
+
} catch (err) {
|
|
13315
|
+
const isNotAllowed = err instanceof Error && err.name === "NotAllowedError" || err?.name === "NotAllowedError";
|
|
13316
|
+
if (isNotAllowed) {
|
|
13317
|
+
throw exports.SecretStorageException.unavailable(
|
|
13318
|
+
this.providerType,
|
|
13319
|
+
"authenticator refused or credential missing"
|
|
13320
|
+
);
|
|
13321
|
+
}
|
|
13322
|
+
throw err;
|
|
13323
|
+
}
|
|
13324
|
+
if (!assertion) {
|
|
13325
|
+
throw exports.SecretStorageException.unavailable(
|
|
13326
|
+
this.providerType,
|
|
13327
|
+
"authenticator refused or credential missing"
|
|
13328
|
+
);
|
|
13329
|
+
}
|
|
13330
|
+
const getExtResults = assertion?.getClientExtensionResults?.();
|
|
13331
|
+
const firstOutput = getExtResults?.prf?.results?.first;
|
|
13332
|
+
if (!firstOutput) {
|
|
13333
|
+
throw exports.SecretStorageException.unavailable(
|
|
13334
|
+
this.providerType,
|
|
13335
|
+
"authenticator returned no PRF result"
|
|
13336
|
+
);
|
|
13337
|
+
}
|
|
13338
|
+
const prfBytes = toUint8Array(firstOutput);
|
|
13339
|
+
const kek = await deriveKekFromPrf(prfBytes, prfSalt);
|
|
13340
|
+
const devicePassphraseBytes = new Uint8Array(32);
|
|
13341
|
+
globalThis.crypto.getRandomValues(devicePassphraseBytes);
|
|
13342
|
+
const devicePassphrase = uint8ArrayToBase64(devicePassphraseBytes);
|
|
13343
|
+
const iv = new Uint8Array(GCM_IV_LENGTH2);
|
|
13344
|
+
globalThis.crypto.getRandomValues(iv);
|
|
13345
|
+
const passphraseBytes = textEncoder3.encode(devicePassphrase);
|
|
13346
|
+
try {
|
|
13347
|
+
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
13348
|
+
{
|
|
13349
|
+
name: "AES-GCM",
|
|
13350
|
+
iv
|
|
13351
|
+
},
|
|
13352
|
+
kek,
|
|
13353
|
+
passphraseBytes
|
|
13354
|
+
);
|
|
13355
|
+
const record = {
|
|
13356
|
+
version: 1,
|
|
13357
|
+
credentialId: base64UrlEncode(credentialIdBytes),
|
|
13358
|
+
iv: uint8ArrayToBase64(iv),
|
|
13359
|
+
ciphertext: uint8ArrayToBase64(new Uint8Array(encryptedBuffer))
|
|
13360
|
+
};
|
|
13361
|
+
await this.backend.setItem(this.recordKey, JSON.stringify(record));
|
|
13362
|
+
this.cachedPassphrase = devicePassphrase;
|
|
13363
|
+
} finally {
|
|
13364
|
+
zeroizeBytes(passphraseBytes);
|
|
13365
|
+
zeroizeBytes(devicePassphraseBytes);
|
|
13366
|
+
}
|
|
13367
|
+
}
|
|
13368
|
+
/**
|
|
13369
|
+
* Unlock the device passphrase using the enrolled WebAuthn PRF credential
|
|
13370
|
+
*/
|
|
13371
|
+
async unlock() {
|
|
13372
|
+
if (this.cachedPassphrase) {
|
|
13373
|
+
return this.cachedPassphrase;
|
|
13374
|
+
}
|
|
13375
|
+
const rawRecord = await this.backend.getItem(this.recordKey);
|
|
13376
|
+
if (!rawRecord) {
|
|
13377
|
+
throw exports.SecretStorageException.unavailable(
|
|
13378
|
+
this.providerType,
|
|
13379
|
+
"no enrolled credential; call enroll() first"
|
|
13380
|
+
);
|
|
13381
|
+
}
|
|
13382
|
+
let record;
|
|
13383
|
+
try {
|
|
13384
|
+
record = JSON.parse(rawRecord);
|
|
13385
|
+
} catch {
|
|
13386
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted PRF record format");
|
|
13387
|
+
}
|
|
13388
|
+
const credentialIdBytes = base64UrlDecode(record.credentialId);
|
|
13389
|
+
const prfSalt = await computePrfSalt();
|
|
13390
|
+
const challenge = new Uint8Array(32);
|
|
13391
|
+
globalThis.crypto.getRandomValues(challenge);
|
|
13392
|
+
let assertion;
|
|
13393
|
+
try {
|
|
13394
|
+
assertion = await this.credentials.get({
|
|
13395
|
+
publicKey: {
|
|
13396
|
+
challenge,
|
|
13397
|
+
rpId: this.rp.id,
|
|
13398
|
+
allowCredentials: [
|
|
13399
|
+
{
|
|
13400
|
+
type: "public-key",
|
|
13401
|
+
id: credentialIdBytes
|
|
13402
|
+
}
|
|
13403
|
+
],
|
|
13404
|
+
userVerification: "required",
|
|
13405
|
+
extensions: {
|
|
13406
|
+
prf: {
|
|
13407
|
+
eval: {
|
|
13408
|
+
first: prfSalt
|
|
13409
|
+
}
|
|
13410
|
+
}
|
|
13411
|
+
}
|
|
13412
|
+
}
|
|
13413
|
+
});
|
|
13414
|
+
} catch (err) {
|
|
13415
|
+
const isNotAllowed = err instanceof Error && err.name === "NotAllowedError" || err?.name === "NotAllowedError";
|
|
13416
|
+
if (isNotAllowed) {
|
|
13417
|
+
throw exports.SecretStorageException.unavailable(
|
|
13418
|
+
this.providerType,
|
|
13419
|
+
"authenticator refused or credential missing"
|
|
13420
|
+
);
|
|
13421
|
+
}
|
|
13422
|
+
throw err;
|
|
13423
|
+
}
|
|
13424
|
+
if (!assertion) {
|
|
13425
|
+
throw exports.SecretStorageException.unavailable(
|
|
13426
|
+
this.providerType,
|
|
13427
|
+
"authenticator refused or credential missing"
|
|
13428
|
+
);
|
|
13429
|
+
}
|
|
13430
|
+
const extResults = assertion?.getClientExtensionResults?.();
|
|
13431
|
+
const firstOutput = extResults?.prf?.results?.first;
|
|
13432
|
+
if (!firstOutput) {
|
|
13433
|
+
throw exports.SecretStorageException.unavailable(
|
|
13434
|
+
this.providerType,
|
|
13435
|
+
"authenticator returned no PRF result"
|
|
13436
|
+
);
|
|
13437
|
+
}
|
|
13438
|
+
const prfBytes = toUint8Array(firstOutput);
|
|
13439
|
+
const kek = await deriveKekFromPrf(prfBytes, prfSalt);
|
|
13440
|
+
const iv = base64ToUint8Array(record.iv);
|
|
13441
|
+
const ciphertext = base64ToUint8Array(record.ciphertext);
|
|
13442
|
+
try {
|
|
13443
|
+
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
13444
|
+
{
|
|
13445
|
+
name: "AES-GCM",
|
|
13446
|
+
iv
|
|
13447
|
+
},
|
|
13448
|
+
kek,
|
|
13449
|
+
ciphertext
|
|
13450
|
+
);
|
|
13451
|
+
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
13452
|
+
try {
|
|
13453
|
+
this.cachedPassphrase = textDecoder2.decode(decryptedBytes);
|
|
13454
|
+
return this.cachedPassphrase;
|
|
13455
|
+
} finally {
|
|
13456
|
+
zeroizeBytes(decryptedBytes);
|
|
13457
|
+
}
|
|
13458
|
+
} catch {
|
|
13459
|
+
throw exports.SecretStorageException.decryptionFailed(
|
|
13460
|
+
"wrapped device passphrase failed authentication under the enrolled credential"
|
|
13461
|
+
);
|
|
13462
|
+
}
|
|
13463
|
+
}
|
|
13464
|
+
/**
|
|
13465
|
+
* Lock the provider by clearing cached passphrase material
|
|
13466
|
+
*/
|
|
13467
|
+
lock() {
|
|
13468
|
+
this.cachedPassphrase = void 0;
|
|
13469
|
+
}
|
|
13470
|
+
/**
|
|
13471
|
+
* Unenroll the current credential, removing the stored PRF record and clearing cached passphrase
|
|
13472
|
+
*/
|
|
13473
|
+
async unenroll() {
|
|
13474
|
+
this.lock();
|
|
13475
|
+
await this.backend.removeItem(this.recordKey);
|
|
13476
|
+
}
|
|
13477
|
+
async storeSecret(bundleHash, secret, options) {
|
|
13478
|
+
if (!bundleHash) {
|
|
13479
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
13480
|
+
}
|
|
13481
|
+
if (!secret) {
|
|
13482
|
+
throw new exports.SecretStorageException("Secret cannot be empty");
|
|
13483
|
+
}
|
|
13484
|
+
if (options?.passphrase) {
|
|
13485
|
+
throw new exports.SecretStorageException(
|
|
13486
|
+
"WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
|
|
13487
|
+
);
|
|
13488
|
+
}
|
|
13489
|
+
if (!options?.recoveryPassphrase && !options?.allowUnrecoverable) {
|
|
13490
|
+
throw exports.SecretStorageException.validationError(
|
|
13491
|
+
"Recovery passphrase required for non-exportable hardware key unless allowUnrecoverable is true"
|
|
13492
|
+
);
|
|
13493
|
+
}
|
|
13494
|
+
const passphrase = await this.unlock();
|
|
13495
|
+
const metadata = {
|
|
13496
|
+
bundleHash,
|
|
13497
|
+
label: options?.label,
|
|
13498
|
+
createdAt: Date.now(),
|
|
13499
|
+
hardwareBacked: false,
|
|
13500
|
+
providerType: this.providerType
|
|
13501
|
+
};
|
|
13502
|
+
const payload = await sealEnvelope(secret, passphrase, metadata);
|
|
13503
|
+
await this.backend.setItem(`${KEY_PREFIX2}${bundleHash}`, JSON.stringify(payload));
|
|
13504
|
+
if (options?.recoveryPassphrase) {
|
|
13505
|
+
const recoveryMetadata = {
|
|
13506
|
+
bundleHash,
|
|
13507
|
+
label: options?.label,
|
|
13508
|
+
createdAt: Date.now(),
|
|
13509
|
+
hardwareBacked: false,
|
|
13510
|
+
providerType: "webcrypto-aes-gcm"
|
|
13511
|
+
};
|
|
13512
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
13513
|
+
await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
|
|
13514
|
+
}
|
|
13515
|
+
}
|
|
13516
|
+
async retrieveSecret(bundleHash, options) {
|
|
13517
|
+
if (options?.passphrase) {
|
|
13518
|
+
throw new exports.SecretStorageException(
|
|
13519
|
+
"WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
|
|
13520
|
+
);
|
|
13521
|
+
}
|
|
13522
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
|
|
13523
|
+
if (!raw) {
|
|
13524
|
+
return null;
|
|
13525
|
+
}
|
|
13526
|
+
let payload;
|
|
13527
|
+
try {
|
|
13528
|
+
payload = JSON.parse(raw);
|
|
13529
|
+
} catch {
|
|
13530
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
13531
|
+
}
|
|
13532
|
+
const passphrase = await this.unlock();
|
|
13533
|
+
try {
|
|
13534
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
13535
|
+
try {
|
|
13536
|
+
return textDecoder2.decode(decryptedBytes);
|
|
13537
|
+
} finally {
|
|
13538
|
+
zeroizeBytes(decryptedBytes);
|
|
13539
|
+
}
|
|
13540
|
+
} catch (err) {
|
|
13541
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13542
|
+
throw err;
|
|
13543
|
+
}
|
|
13544
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13545
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
13546
|
+
}
|
|
13547
|
+
}
|
|
13548
|
+
async withSecret(bundleHash, fn, options) {
|
|
13549
|
+
if (options?.passphrase) {
|
|
13550
|
+
throw new exports.SecretStorageException(
|
|
13551
|
+
"WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
|
|
13552
|
+
);
|
|
13553
|
+
}
|
|
13554
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
|
|
13555
|
+
if (!raw) {
|
|
13556
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
13557
|
+
}
|
|
13558
|
+
let payload;
|
|
13559
|
+
try {
|
|
13560
|
+
payload = JSON.parse(raw);
|
|
13561
|
+
} catch {
|
|
13562
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
13563
|
+
}
|
|
13564
|
+
const passphrase = await this.unlock();
|
|
13565
|
+
try {
|
|
13566
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
13567
|
+
return await withSecureBytes(decryptedBytes, async (bytes) => {
|
|
13568
|
+
const secretString = textDecoder2.decode(bytes);
|
|
13569
|
+
return await fn(secretString);
|
|
13570
|
+
});
|
|
13571
|
+
} catch (err) {
|
|
13572
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13573
|
+
throw err;
|
|
13574
|
+
}
|
|
13575
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13576
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
13577
|
+
}
|
|
13578
|
+
}
|
|
13579
|
+
async deleteSecret(bundleHash) {
|
|
13580
|
+
const key = `${KEY_PREFIX2}${bundleHash}`;
|
|
13581
|
+
const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
|
|
13582
|
+
const result = await this.backend.removeItem(key);
|
|
13583
|
+
await this.backend.removeItem(recoveryKey);
|
|
13584
|
+
return result !== false;
|
|
13585
|
+
}
|
|
13586
|
+
async hasSecret(bundleHash) {
|
|
13587
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
|
|
13588
|
+
return raw !== null;
|
|
13589
|
+
}
|
|
13590
|
+
async listSecrets() {
|
|
13591
|
+
const keys = await this.backend.keys();
|
|
13592
|
+
const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX2) && !k.startsWith(RECOVERY_KEY_PREFIX));
|
|
13593
|
+
const results = [];
|
|
13594
|
+
for (const key of matchingKeys) {
|
|
13595
|
+
const raw = await this.backend.getItem(key);
|
|
13596
|
+
if (raw) {
|
|
13597
|
+
try {
|
|
13598
|
+
const payload = JSON.parse(raw);
|
|
13599
|
+
if (payload.metadata) {
|
|
13600
|
+
results.push(payload.metadata);
|
|
13601
|
+
}
|
|
13602
|
+
} catch {
|
|
13603
|
+
}
|
|
13604
|
+
}
|
|
13605
|
+
}
|
|
13606
|
+
return results;
|
|
13607
|
+
}
|
|
13608
|
+
/**
|
|
13609
|
+
* Recover a secret using its recovery envelope and re-enroll it under a fresh WebAuthn PRF credential
|
|
13610
|
+
*/
|
|
13611
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
13612
|
+
if (!bundleHash) {
|
|
13613
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
13614
|
+
}
|
|
13615
|
+
if (!recoveryPassphrase) {
|
|
13616
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
13617
|
+
}
|
|
13618
|
+
const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
|
|
13619
|
+
if (!raw) {
|
|
13620
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
13621
|
+
}
|
|
13622
|
+
let payload;
|
|
13623
|
+
try {
|
|
13624
|
+
payload = JSON.parse(raw);
|
|
13625
|
+
} catch {
|
|
13626
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
13627
|
+
}
|
|
13628
|
+
let decryptedBytes;
|
|
13629
|
+
try {
|
|
13630
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
13631
|
+
} catch (err) {
|
|
13632
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13633
|
+
throw err;
|
|
13634
|
+
}
|
|
13635
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13636
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
13637
|
+
}
|
|
13638
|
+
let secretStr;
|
|
13639
|
+
try {
|
|
13640
|
+
secretStr = textDecoder2.decode(decryptedBytes);
|
|
13641
|
+
} finally {
|
|
13642
|
+
zeroizeBytes(decryptedBytes);
|
|
13643
|
+
}
|
|
13644
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
13645
|
+
...options,
|
|
13646
|
+
recoveryPassphrase
|
|
13647
|
+
});
|
|
13648
|
+
}
|
|
13649
|
+
};
|
|
13650
|
+
|
|
13651
|
+
// src/storage/NonExtractableKeySecretStorageProvider.ts
|
|
13652
|
+
init_SecretStorageException();
|
|
13653
|
+
var KEY_PREFIX3 = SECRET_KEY_PREFIX;
|
|
13654
|
+
var GCM_IV_LENGTH3 = 12;
|
|
13655
|
+
var textEncoder4 = new TextEncoder();
|
|
13656
|
+
var textDecoder3 = new TextDecoder();
|
|
13657
|
+
var MemoryKeyStore = class {
|
|
13658
|
+
keys = /* @__PURE__ */ new Map();
|
|
13659
|
+
async get(name) {
|
|
13660
|
+
return this.keys.get(name);
|
|
13661
|
+
}
|
|
13662
|
+
async put(name, key) {
|
|
13663
|
+
this.keys.set(name, key);
|
|
13664
|
+
}
|
|
13665
|
+
async delete(name) {
|
|
13666
|
+
return this.keys.delete(name);
|
|
13667
|
+
}
|
|
13668
|
+
};
|
|
13669
|
+
var IndexedDbKeyStore = class {
|
|
13670
|
+
dbName;
|
|
13671
|
+
storeName = "keys";
|
|
13672
|
+
constructor(dbName = "knishio-secret-storage") {
|
|
13673
|
+
this.dbName = dbName;
|
|
13674
|
+
}
|
|
13675
|
+
// Executor form intentionally retained for browser runtime compatibility with ES2022 / browsers without Promise.withResolvers polyfill
|
|
13676
|
+
async getDb() {
|
|
13677
|
+
if (typeof globalThis.indexedDB === "undefined") {
|
|
13678
|
+
throw exports.SecretStorageException.unavailable(
|
|
13679
|
+
"webcrypto-nonextractable",
|
|
13680
|
+
"IndexedDB is not available"
|
|
13681
|
+
);
|
|
13682
|
+
}
|
|
13683
|
+
return new Promise((resolve, reject) => {
|
|
13684
|
+
const request = globalThis.indexedDB.open(this.dbName, 1);
|
|
13685
|
+
request.onupgradeneeded = () => {
|
|
13686
|
+
const db = request.result;
|
|
13687
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
13688
|
+
db.createObjectStore(this.storeName);
|
|
13689
|
+
}
|
|
13690
|
+
};
|
|
13691
|
+
request.onsuccess = () => resolve(request.result);
|
|
13692
|
+
request.onerror = () => reject(request.error);
|
|
13693
|
+
});
|
|
13694
|
+
}
|
|
13695
|
+
async get(name) {
|
|
13696
|
+
const db = await this.getDb();
|
|
13697
|
+
return new Promise((resolve, reject) => {
|
|
13698
|
+
const tx = db.transaction(this.storeName, "readonly");
|
|
13699
|
+
const store = tx.objectStore(this.storeName);
|
|
13700
|
+
const request = store.get(name);
|
|
13701
|
+
request.onsuccess = () => resolve(request.result);
|
|
13702
|
+
request.onerror = () => reject(request.error);
|
|
13703
|
+
});
|
|
13704
|
+
}
|
|
13705
|
+
async put(name, key) {
|
|
13706
|
+
const db = await this.getDb();
|
|
13707
|
+
return new Promise((resolve, reject) => {
|
|
13708
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
13709
|
+
const store = tx.objectStore(this.storeName);
|
|
13710
|
+
const request = store.put(key, name);
|
|
13711
|
+
request.onsuccess = () => resolve();
|
|
13712
|
+
request.onerror = () => reject(request.error);
|
|
13713
|
+
});
|
|
13714
|
+
}
|
|
13715
|
+
async delete(name) {
|
|
13716
|
+
const db = await this.getDb();
|
|
13717
|
+
return new Promise((resolve, reject) => {
|
|
13718
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
13719
|
+
const store = tx.objectStore(this.storeName);
|
|
13720
|
+
const request = store.delete(name);
|
|
13721
|
+
request.onsuccess = () => resolve(true);
|
|
13722
|
+
request.onerror = () => reject(request.error);
|
|
13723
|
+
});
|
|
13724
|
+
}
|
|
13725
|
+
};
|
|
13726
|
+
var NonExtractableKeySecretStorageProvider = class {
|
|
13727
|
+
providerType = "webcrypto-nonextractable";
|
|
13728
|
+
backend;
|
|
13729
|
+
keyStore;
|
|
13730
|
+
alias;
|
|
13731
|
+
cachedPassphrase;
|
|
13732
|
+
constructor(options) {
|
|
13733
|
+
this.backend = options.backend;
|
|
13734
|
+
this.keyStore = options.keyStore ?? new IndexedDbKeyStore();
|
|
13735
|
+
this.alias = options.alias ?? "default";
|
|
13736
|
+
}
|
|
13737
|
+
get recordKey() {
|
|
13738
|
+
return `knishio:kek:webcrypto-nonextractable:${this.alias}`;
|
|
13739
|
+
}
|
|
13740
|
+
get kekStoreKey() {
|
|
13741
|
+
return `knishio:kek:${this.alias}`;
|
|
13742
|
+
}
|
|
13743
|
+
isHardwareBacked() {
|
|
13744
|
+
return false;
|
|
13745
|
+
}
|
|
13746
|
+
async isAvailable() {
|
|
13747
|
+
return typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
|
|
13748
|
+
}
|
|
13749
|
+
/**
|
|
13750
|
+
* Unlock or initialize the device passphrase using the non-extractable KEK
|
|
13751
|
+
*/
|
|
13752
|
+
async unlock() {
|
|
13753
|
+
if (this.cachedPassphrase) {
|
|
13754
|
+
return this.cachedPassphrase;
|
|
13755
|
+
}
|
|
13756
|
+
if (!await this.isAvailable()) {
|
|
13757
|
+
throw exports.SecretStorageException.unavailable(
|
|
13758
|
+
this.providerType,
|
|
13759
|
+
"WebCrypto API is not available"
|
|
13760
|
+
);
|
|
13761
|
+
}
|
|
13762
|
+
const rawRecord = await this.backend.getItem(this.recordKey);
|
|
13763
|
+
if (!rawRecord) {
|
|
13764
|
+
let kek2 = await this.keyStore.get(this.kekStoreKey);
|
|
13765
|
+
if (!kek2) {
|
|
13766
|
+
kek2 = await globalThis.crypto.subtle.generateKey(
|
|
13767
|
+
{ name: "AES-GCM", length: 256 },
|
|
13768
|
+
false,
|
|
13769
|
+
["encrypt", "decrypt"]
|
|
13770
|
+
);
|
|
13771
|
+
await this.keyStore.put(this.kekStoreKey, kek2);
|
|
13772
|
+
}
|
|
13773
|
+
const devicePassphraseBytes = new Uint8Array(32);
|
|
13774
|
+
globalThis.crypto.getRandomValues(devicePassphraseBytes);
|
|
13775
|
+
const devicePassphrase = uint8ArrayToBase64(devicePassphraseBytes);
|
|
13776
|
+
const iv2 = new Uint8Array(GCM_IV_LENGTH3);
|
|
13777
|
+
globalThis.crypto.getRandomValues(iv2);
|
|
13778
|
+
const passphraseBytes = textEncoder4.encode(devicePassphrase);
|
|
13779
|
+
try {
|
|
13780
|
+
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
13781
|
+
{
|
|
13782
|
+
name: "AES-GCM",
|
|
13783
|
+
iv: iv2
|
|
13784
|
+
},
|
|
13785
|
+
kek2,
|
|
13786
|
+
passphraseBytes
|
|
13787
|
+
);
|
|
13788
|
+
const record2 = {
|
|
13789
|
+
version: 1,
|
|
13790
|
+
iv: uint8ArrayToBase64(iv2),
|
|
13791
|
+
ciphertext: uint8ArrayToBase64(new Uint8Array(encryptedBuffer))
|
|
13792
|
+
};
|
|
13793
|
+
await this.backend.setItem(this.recordKey, JSON.stringify(record2));
|
|
13794
|
+
this.cachedPassphrase = devicePassphrase;
|
|
13795
|
+
return devicePassphrase;
|
|
13796
|
+
} finally {
|
|
13797
|
+
zeroizeBytes(passphraseBytes);
|
|
13798
|
+
zeroizeBytes(devicePassphraseBytes);
|
|
13799
|
+
}
|
|
13800
|
+
}
|
|
13801
|
+
let record;
|
|
13802
|
+
try {
|
|
13803
|
+
record = JSON.parse(rawRecord);
|
|
13804
|
+
} catch {
|
|
13805
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted key record format");
|
|
13806
|
+
}
|
|
13807
|
+
const kek = await this.keyStore.get(this.kekStoreKey);
|
|
13808
|
+
if (!kek) {
|
|
13809
|
+
throw exports.SecretStorageException.unavailable(
|
|
13810
|
+
this.providerType,
|
|
13811
|
+
`no non-extractable key found for alias '${this.alias}'`
|
|
13812
|
+
);
|
|
13813
|
+
}
|
|
13814
|
+
const iv = base64ToUint8Array(record.iv);
|
|
13815
|
+
const ciphertext = base64ToUint8Array(record.ciphertext);
|
|
13816
|
+
try {
|
|
13817
|
+
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
13818
|
+
{
|
|
13819
|
+
name: "AES-GCM",
|
|
13820
|
+
iv
|
|
13821
|
+
},
|
|
13822
|
+
kek,
|
|
13823
|
+
ciphertext
|
|
13824
|
+
);
|
|
13825
|
+
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
13826
|
+
try {
|
|
13827
|
+
this.cachedPassphrase = textDecoder3.decode(decryptedBytes);
|
|
13828
|
+
return this.cachedPassphrase;
|
|
13829
|
+
} finally {
|
|
13830
|
+
zeroizeBytes(decryptedBytes);
|
|
13831
|
+
}
|
|
13832
|
+
} catch {
|
|
13833
|
+
throw exports.SecretStorageException.decryptionFailed(
|
|
13834
|
+
"wrapped device passphrase failed authentication under non-extractable key"
|
|
13835
|
+
);
|
|
13836
|
+
}
|
|
13837
|
+
}
|
|
13838
|
+
/**
|
|
13839
|
+
* Lock the provider by clearing cached passphrase material
|
|
13840
|
+
*/
|
|
13841
|
+
lock() {
|
|
13842
|
+
this.cachedPassphrase = void 0;
|
|
13843
|
+
}
|
|
13844
|
+
/**
|
|
13845
|
+
* Unenroll the non-extractable key, removing the stored wrapped record and KEK
|
|
13846
|
+
*/
|
|
13847
|
+
async unenroll() {
|
|
13848
|
+
this.lock();
|
|
13849
|
+
await this.backend.removeItem(this.recordKey);
|
|
13850
|
+
await this.keyStore.delete(this.kekStoreKey);
|
|
13851
|
+
}
|
|
13852
|
+
async storeSecret(bundleHash, secret, options) {
|
|
13853
|
+
if (!bundleHash) {
|
|
13854
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
13855
|
+
}
|
|
13856
|
+
if (!secret) {
|
|
13857
|
+
throw new exports.SecretStorageException("Secret cannot be empty");
|
|
13858
|
+
}
|
|
13859
|
+
if (options?.passphrase) {
|
|
13860
|
+
throw new exports.SecretStorageException(
|
|
13861
|
+
"NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
|
|
13862
|
+
);
|
|
13863
|
+
}
|
|
13864
|
+
if (!options?.recoveryPassphrase && !options?.allowUnrecoverable) {
|
|
13865
|
+
throw exports.SecretStorageException.validationError(
|
|
13866
|
+
"Recovery passphrase required for non-exportable hardware key unless allowUnrecoverable is true"
|
|
13867
|
+
);
|
|
13868
|
+
}
|
|
13869
|
+
const passphrase = await this.unlock();
|
|
13870
|
+
const metadata = {
|
|
13871
|
+
bundleHash,
|
|
13872
|
+
label: options?.label,
|
|
13873
|
+
createdAt: Date.now(),
|
|
13874
|
+
hardwareBacked: false,
|
|
13875
|
+
providerType: this.providerType
|
|
13876
|
+
};
|
|
13877
|
+
const payload = await sealEnvelope(secret, passphrase, metadata);
|
|
13878
|
+
await this.backend.setItem(`${KEY_PREFIX3}${bundleHash}`, JSON.stringify(payload));
|
|
13879
|
+
if (options?.recoveryPassphrase) {
|
|
13880
|
+
const recoveryMetadata = {
|
|
13881
|
+
bundleHash,
|
|
13882
|
+
label: options?.label,
|
|
13883
|
+
createdAt: Date.now(),
|
|
13884
|
+
hardwareBacked: false,
|
|
13885
|
+
providerType: "webcrypto-aes-gcm"
|
|
13886
|
+
};
|
|
13887
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
13888
|
+
await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
|
|
13889
|
+
}
|
|
13890
|
+
}
|
|
13891
|
+
async retrieveSecret(bundleHash, options) {
|
|
13892
|
+
if (options?.passphrase) {
|
|
13893
|
+
throw new exports.SecretStorageException(
|
|
13894
|
+
"NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
|
|
13895
|
+
);
|
|
13896
|
+
}
|
|
13897
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
|
|
13898
|
+
if (!raw) {
|
|
13899
|
+
return null;
|
|
13900
|
+
}
|
|
13901
|
+
let payload;
|
|
13902
|
+
try {
|
|
13903
|
+
payload = JSON.parse(raw);
|
|
13904
|
+
} catch {
|
|
13905
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
13906
|
+
}
|
|
13907
|
+
const passphrase = await this.unlock();
|
|
13908
|
+
try {
|
|
13909
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
13910
|
+
try {
|
|
13911
|
+
return textDecoder3.decode(decryptedBytes);
|
|
13912
|
+
} finally {
|
|
13913
|
+
zeroizeBytes(decryptedBytes);
|
|
13914
|
+
}
|
|
13915
|
+
} catch (err) {
|
|
13916
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13917
|
+
throw err;
|
|
13918
|
+
}
|
|
13919
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13920
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
13921
|
+
}
|
|
13922
|
+
}
|
|
13923
|
+
async withSecret(bundleHash, fn, options) {
|
|
13924
|
+
if (options?.passphrase) {
|
|
13925
|
+
throw new exports.SecretStorageException(
|
|
13926
|
+
"NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
|
|
13927
|
+
);
|
|
13928
|
+
}
|
|
13929
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
|
|
13930
|
+
if (!raw) {
|
|
13931
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
13932
|
+
}
|
|
13933
|
+
let payload;
|
|
13934
|
+
try {
|
|
13935
|
+
payload = JSON.parse(raw);
|
|
13936
|
+
} catch {
|
|
13937
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
13938
|
+
}
|
|
13939
|
+
const passphrase = await this.unlock();
|
|
13940
|
+
try {
|
|
13941
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
13942
|
+
return await withSecureBytes(decryptedBytes, async (bytes) => {
|
|
13943
|
+
const secretString = textDecoder3.decode(bytes);
|
|
13944
|
+
return await fn(secretString);
|
|
13945
|
+
});
|
|
13946
|
+
} catch (err) {
|
|
13947
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13948
|
+
throw err;
|
|
13949
|
+
}
|
|
13950
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13951
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
13952
|
+
}
|
|
13953
|
+
}
|
|
13954
|
+
async deleteSecret(bundleHash) {
|
|
13955
|
+
const key = `${KEY_PREFIX3}${bundleHash}`;
|
|
13956
|
+
const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
|
|
13957
|
+
const result = await this.backend.removeItem(key);
|
|
13958
|
+
await this.backend.removeItem(recoveryKey);
|
|
13959
|
+
return result !== false;
|
|
13960
|
+
}
|
|
13961
|
+
async hasSecret(bundleHash) {
|
|
13962
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
|
|
13963
|
+
return raw !== null;
|
|
13964
|
+
}
|
|
13965
|
+
async listSecrets() {
|
|
13966
|
+
const keys = await this.backend.keys();
|
|
13967
|
+
const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX3) && !k.startsWith(RECOVERY_KEY_PREFIX));
|
|
13968
|
+
const results = [];
|
|
13969
|
+
for (const key of matchingKeys) {
|
|
13970
|
+
const raw = await this.backend.getItem(key);
|
|
13971
|
+
if (raw) {
|
|
13972
|
+
try {
|
|
13973
|
+
const payload = JSON.parse(raw);
|
|
13974
|
+
if (payload.metadata) {
|
|
13975
|
+
results.push(payload.metadata);
|
|
13976
|
+
}
|
|
13977
|
+
} catch {
|
|
13978
|
+
}
|
|
13979
|
+
}
|
|
13980
|
+
}
|
|
13981
|
+
return results;
|
|
13982
|
+
}
|
|
13983
|
+
/**
|
|
13984
|
+
* Recover a secret using its recovery envelope and re-enroll it under a fresh non-extractable KEK
|
|
13985
|
+
*/
|
|
13986
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
13987
|
+
if (!bundleHash) {
|
|
13988
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
13989
|
+
}
|
|
13990
|
+
if (!recoveryPassphrase) {
|
|
13991
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
13992
|
+
}
|
|
13993
|
+
const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
|
|
13994
|
+
if (!raw) {
|
|
13995
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
13996
|
+
}
|
|
13997
|
+
let payload;
|
|
13998
|
+
try {
|
|
13999
|
+
payload = JSON.parse(raw);
|
|
14000
|
+
} catch {
|
|
14001
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
14002
|
+
}
|
|
14003
|
+
let decryptedBytes;
|
|
14004
|
+
try {
|
|
14005
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
14006
|
+
} catch (err) {
|
|
14007
|
+
if (err instanceof exports.SecretStorageException) {
|
|
14008
|
+
throw err;
|
|
14009
|
+
}
|
|
14010
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14011
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
14012
|
+
}
|
|
14013
|
+
let secretStr;
|
|
14014
|
+
try {
|
|
14015
|
+
secretStr = textDecoder3.decode(decryptedBytes);
|
|
14016
|
+
} finally {
|
|
14017
|
+
zeroizeBytes(decryptedBytes);
|
|
14018
|
+
}
|
|
14019
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
14020
|
+
...options,
|
|
14021
|
+
recoveryPassphrase
|
|
14022
|
+
});
|
|
14023
|
+
}
|
|
12850
14024
|
};
|
|
12851
14025
|
|
|
12852
14026
|
// src/storage/index.ts
|
|
@@ -12857,15 +14031,14 @@ function createDefaultSecretStorage(options = {}) {
|
|
|
12857
14031
|
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined") {
|
|
12858
14032
|
return new WebCryptoSecretStorageProvider({
|
|
12859
14033
|
backend: options.backend,
|
|
12860
|
-
defaultPassphrase: options.defaultPassphrase
|
|
12861
|
-
hardwareBacked: options.hardwareBacked
|
|
14034
|
+
defaultPassphrase: options.defaultPassphrase
|
|
12862
14035
|
});
|
|
12863
14036
|
}
|
|
12864
14037
|
return new MemorySecretStorageProvider();
|
|
12865
14038
|
}
|
|
12866
14039
|
|
|
12867
14040
|
// src/index.ts
|
|
12868
|
-
var SDK_VERSION = "1.
|
|
14041
|
+
var SDK_VERSION = "1.2.0";
|
|
12869
14042
|
var SDK_NAME = "KnishIO-Client-TS";
|
|
12870
14043
|
var COMPATIBLE_SERVER_VERSIONS = [4, 5];
|
|
12871
14044
|
var SDK_INFO = {
|
|
@@ -12963,9 +14136,12 @@ exports.CRYPTO_CONSTANTS = CRYPTO_CONSTANTS;
|
|
|
12963
14136
|
exports.CheckMolecule = CheckMolecule;
|
|
12964
14137
|
exports.DevUtils = DevUtils;
|
|
12965
14138
|
exports.EXTENDED_COMPATIBILITY_TEST_VECTORS = EXTENDED_COMPATIBILITY_TEST_VECTORS;
|
|
14139
|
+
exports.FileStorageBackend = FileStorageBackend;
|
|
12966
14140
|
exports.GraphQLClient = GraphQLClient;
|
|
14141
|
+
exports.IndexedDbKeyStore = IndexedDbKeyStore;
|
|
12967
14142
|
exports.KnishIO = KnishIO;
|
|
12968
14143
|
exports.KnishIOClient = KnishIOClient;
|
|
14144
|
+
exports.MemoryKeyStore = MemoryKeyStore;
|
|
12969
14145
|
exports.MemorySecretStorageProvider = MemorySecretStorageProvider;
|
|
12970
14146
|
exports.MemoryStorageBackend = MemoryStorageBackend;
|
|
12971
14147
|
exports.Meta = Meta;
|
|
@@ -12980,6 +14156,7 @@ exports.MutationProposeMolecule = MutationProposeMolecule;
|
|
|
12980
14156
|
exports.MutationRequestAuthorization = MutationRequestAuthorization;
|
|
12981
14157
|
exports.MutationRequestTokens = MutationRequestTokens;
|
|
12982
14158
|
exports.MutationTransferTokens = MutationTransferTokens;
|
|
14159
|
+
exports.NonExtractableKeySecretStorageProvider = NonExtractableKeySecretStorageProvider;
|
|
12983
14160
|
exports.PolicyMeta = PolicyMeta;
|
|
12984
14161
|
exports.Query = Query;
|
|
12985
14162
|
exports.QueryAtom = QueryAtom;
|
|
@@ -12991,6 +14168,7 @@ exports.QueryMetaType = QueryMetaType;
|
|
|
12991
14168
|
exports.QueryMetaTypeViaAtom = QueryMetaTypeViaAtom;
|
|
12992
14169
|
exports.QueryWalletBundle = QueryWalletBundle;
|
|
12993
14170
|
exports.QueryWalletList = QueryWalletList;
|
|
14171
|
+
exports.RECOVERY_KEY_PREFIX = RECOVERY_KEY_PREFIX;
|
|
12994
14172
|
exports.ResponseAppendRequest = ResponseAppendRequest;
|
|
12995
14173
|
exports.ResponseAtom = ResponseAtom;
|
|
12996
14174
|
exports.ResponseBalance = ResponseBalance;
|
|
@@ -13011,9 +14189,12 @@ exports.ResponseWalletList = ResponseWalletList;
|
|
|
13011
14189
|
exports.SDK_INFO = SDK_INFO;
|
|
13012
14190
|
exports.SDK_NAME = SDK_NAME;
|
|
13013
14191
|
exports.SDK_VERSION = SDK_VERSION;
|
|
14192
|
+
exports.SECRET_KEY_PREFIX = SECRET_KEY_PREFIX;
|
|
13014
14193
|
exports.TokenUnit = TokenUnit;
|
|
13015
14194
|
exports.Wallet = Wallet;
|
|
14195
|
+
exports.WebAuthnPrfSecretStorageProvider = WebAuthnPrfSecretStorageProvider;
|
|
13016
14196
|
exports.WebCryptoSecretStorageProvider = WebCryptoSecretStorageProvider;
|
|
14197
|
+
exports.WebStorageBackend = WebStorageBackend;
|
|
13017
14198
|
exports.base64ToHex = base64ToHex;
|
|
13018
14199
|
exports.bufferToHexString = bufferToHexString;
|
|
13019
14200
|
exports.capitalize = capitalize;
|
|
@@ -13052,9 +14233,11 @@ exports.isNumeric = isNumeric;
|
|
|
13052
14233
|
exports.isPosition = isPosition2;
|
|
13053
14234
|
exports.isWalletAddress = isWalletAddress2;
|
|
13054
14235
|
exports.normalizeMolecularHash = normalizeMolecularHash;
|
|
14236
|
+
exports.openEnvelope = openEnvelope;
|
|
13055
14237
|
exports.randomString = randomString;
|
|
13056
14238
|
exports.runCompatibilityTests = runCompatibilityTests;
|
|
13057
14239
|
exports.runExtendedCompatibilityTests = runExtendedCompatibilityTests;
|
|
14240
|
+
exports.sealEnvelope = sealEnvelope;
|
|
13058
14241
|
exports.shake256 = shake256;
|
|
13059
14242
|
exports.toCamelCase = toCamelCase;
|
|
13060
14243
|
exports.toSnakeCase = toSnakeCase;
|