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