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