@wishknish/knishio-client-ts 0.9.8 → 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 +1500 -163
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +188 -48
- package/dist/index.d.ts +188 -48
- package/dist/index.iife.js +1494 -156
- package/dist/index.iife.js.map +1 -1
- package/dist/index.js +1492 -165
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/AuthToken.ts +47 -25
- package/src/KnishIOClient.ts +40 -12
- package/src/core/Molecule.ts +17 -7
- package/src/core/Wallet.ts +161 -32
- package/src/exception/SecretStorageException.ts +9 -0
- package/src/index.ts +17 -3
- package/src/libraries/GraphQLClient.ts +2 -2
- package/src/schemas/index.ts +2 -1
- 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/crypto.ts +9 -4
- package/src/types/storage.ts +22 -2
- package/src/validation/schemas.ts +2 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import JsSHA from 'jssha';
|
|
3
|
-
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
3
|
+
import { ml_kem768, ml_kem1024 } from '@noble/post-quantum/ml-kem.js';
|
|
4
4
|
import { cacheExchange, fetchExchange, subscriptionExchange, createClient as createClient$1, gql } from '@urql/core';
|
|
5
5
|
import { createClient } from 'graphql-ws';
|
|
6
6
|
import { pipe, subscribe } from 'wonka';
|
|
@@ -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
|
});
|
|
@@ -3655,6 +3663,11 @@ var TokenUnit = class _TokenUnit {
|
|
|
3655
3663
|
};
|
|
3656
3664
|
}
|
|
3657
3665
|
};
|
|
3666
|
+
var ML_KEM_PARAMS = {
|
|
3667
|
+
1024: { kem: ml_kem1024, pkBytes: 1568, skBytes: 3168, ctBytes: 1568 },
|
|
3668
|
+
768: { kem: ml_kem768, pkBytes: 1184, skBytes: 2400, ctBytes: 1088 }
|
|
3669
|
+
};
|
|
3670
|
+
var DEFAULT_ML_KEM_PARAMETER_SET = 1024;
|
|
3658
3671
|
var Wallet = class _Wallet {
|
|
3659
3672
|
token;
|
|
3660
3673
|
balance;
|
|
@@ -3669,6 +3682,7 @@ var Wallet = class _Wallet {
|
|
|
3669
3682
|
tokenUnits;
|
|
3670
3683
|
tradeRates;
|
|
3671
3684
|
molecules;
|
|
3685
|
+
mlKemParameterSet;
|
|
3672
3686
|
// Token metadata (populated from query responses)
|
|
3673
3687
|
tokenName;
|
|
3674
3688
|
tokenAmount;
|
|
@@ -3682,8 +3696,14 @@ var Wallet = class _Wallet {
|
|
|
3682
3696
|
address = null,
|
|
3683
3697
|
position = null,
|
|
3684
3698
|
batchId = null,
|
|
3685
|
-
characters = null
|
|
3699
|
+
characters = null,
|
|
3700
|
+
mlKemParameterSet = DEFAULT_ML_KEM_PARAMETER_SET
|
|
3686
3701
|
} = {}) {
|
|
3702
|
+
const paramSetNum = Number(mlKemParameterSet);
|
|
3703
|
+
if (!ML_KEM_PARAMS[paramSetNum]) {
|
|
3704
|
+
throw new Error(`KnishIO: unsupported ML-KEM parameter set ${mlKemParameterSet}; expected 1024 or 768.`);
|
|
3705
|
+
}
|
|
3706
|
+
this.mlKemParameterSet = paramSetNum;
|
|
3687
3707
|
this.token = token;
|
|
3688
3708
|
this.balance = "0";
|
|
3689
3709
|
this.molecules = {};
|
|
@@ -3719,7 +3739,8 @@ var Wallet = class _Wallet {
|
|
|
3719
3739
|
bundle = null,
|
|
3720
3740
|
token = "USER",
|
|
3721
3741
|
batchId = null,
|
|
3722
|
-
characters = null
|
|
3742
|
+
characters = null,
|
|
3743
|
+
mlKemParameterSet = DEFAULT_ML_KEM_PARAMETER_SET
|
|
3723
3744
|
}) {
|
|
3724
3745
|
let position = null;
|
|
3725
3746
|
if (!secret && !bundle) {
|
|
@@ -3735,7 +3756,8 @@ var Wallet = class _Wallet {
|
|
|
3735
3756
|
token,
|
|
3736
3757
|
position,
|
|
3737
3758
|
batchId,
|
|
3738
|
-
characters
|
|
3759
|
+
characters,
|
|
3760
|
+
mlKemParameterSet
|
|
3739
3761
|
});
|
|
3740
3762
|
}
|
|
3741
3763
|
/**
|
|
@@ -3894,20 +3916,80 @@ var Wallet = class _Wallet {
|
|
|
3894
3916
|
return (typeof this.position === "undefined" || this.position === null) && (typeof this.address === "undefined" || this.address === null);
|
|
3895
3917
|
}
|
|
3896
3918
|
// =============================================================================
|
|
3897
|
-
// POST-QUANTUM CRYPTOGRAPHY - ML-
|
|
3919
|
+
// POST-QUANTUM CRYPTOGRAPHY - ML-KEM INTEGRATION
|
|
3898
3920
|
// =============================================================================
|
|
3899
3921
|
/**
|
|
3900
|
-
*
|
|
3922
|
+
* Derive an ML-KEM keypair for an arbitrary parameter set from the wallet's key seed,
|
|
3923
|
+
* without mutating the wallet. The 64-byte `d‖z` seed takes no parameter-set input — only
|
|
3924
|
+
* the final `keygen` call differs — so one KnishIO wallet owns both an ML-KEM-768 and an
|
|
3925
|
+
* ML-KEM-1024 identity and either can be reconstructed on demand.
|
|
3926
|
+
*
|
|
3927
|
+
* Returns `null` when the wallet holds no key — a secret-less wallet, which is what a molecule
|
|
3928
|
+
* deserializer builds for validation context. `generateSecret(null, …)` does NOT throw, so
|
|
3929
|
+
* without this the wallet would derive a plausible-looking identity from a bogus seed and fail
|
|
3930
|
+
* three layers down at AES-GCM instead of at the missing key. The guard lives here rather than
|
|
3931
|
+
* at each call site so a new caller cannot miss it.
|
|
3932
|
+
*
|
|
3933
|
+
* @param parameterSet - 1024 or 768
|
|
3901
3934
|
*/
|
|
3902
|
-
|
|
3935
|
+
deriveMlKemKeypair(parameterSet) {
|
|
3936
|
+
const params = ML_KEM_PARAMS[parameterSet];
|
|
3937
|
+
if (!params) {
|
|
3938
|
+
throw new Error(`KnishIO: unsupported ML-KEM parameter set ${parameterSet}; expected 1024 or 768.`);
|
|
3939
|
+
}
|
|
3940
|
+
if (!this.key) {
|
|
3941
|
+
return null;
|
|
3942
|
+
}
|
|
3903
3943
|
const seedHex = generateSecret(this.key, 128);
|
|
3904
3944
|
const seed = new Uint8Array(64);
|
|
3905
3945
|
for (let i = 0; i < 64; i++) {
|
|
3906
3946
|
seed[i] = parseInt(seedHex.substr(i * 2, 2), 16);
|
|
3907
3947
|
}
|
|
3908
|
-
const { publicKey, secretKey } =
|
|
3909
|
-
|
|
3910
|
-
|
|
3948
|
+
const { publicKey, secretKey } = params.kem.keygen(seed);
|
|
3949
|
+
return {
|
|
3950
|
+
pubkey: this.serializeKey(publicKey),
|
|
3951
|
+
privkey: secretKey,
|
|
3952
|
+
params
|
|
3953
|
+
};
|
|
3954
|
+
}
|
|
3955
|
+
/**
|
|
3956
|
+
* ML-KEM parameter set implied by a serialized public key's raw byte length. FIPS 203's key
|
|
3957
|
+
* lengths are disjoint (1568 bytes → ML-KEM-1024, 1184 bytes → ML-KEM-768), so a stored peer
|
|
3958
|
+
* key recovers the parameter set of the session it belongs to without a wire-format change.
|
|
3959
|
+
* Used by AuthToken.restore to resolve a snapshot that predates the field.
|
|
3960
|
+
*
|
|
3961
|
+
* @param pubkey - Base64-serialized ML-KEM public key
|
|
3962
|
+
* @return 1024, 768, or null when the length matches neither
|
|
3963
|
+
*/
|
|
3964
|
+
static mlKemParameterSetFromPubkey(pubkey) {
|
|
3965
|
+
if (!pubkey) {
|
|
3966
|
+
return null;
|
|
3967
|
+
}
|
|
3968
|
+
let byteLength;
|
|
3969
|
+
try {
|
|
3970
|
+
byteLength = typeof Buffer !== "undefined" ? Buffer.from(pubkey, "base64").length : atob(pubkey).length;
|
|
3971
|
+
} catch {
|
|
3972
|
+
return null;
|
|
3973
|
+
}
|
|
3974
|
+
if (byteLength === ML_KEM_PARAMS[1024].pkBytes) {
|
|
3975
|
+
return 1024;
|
|
3976
|
+
}
|
|
3977
|
+
if (byteLength === ML_KEM_PARAMS[768].pkBytes) {
|
|
3978
|
+
return 768;
|
|
3979
|
+
}
|
|
3980
|
+
return null;
|
|
3981
|
+
}
|
|
3982
|
+
/**
|
|
3983
|
+
* Initializes the ML-KEM key pair (matches JavaScript SDK exactly). Only ever reached from the
|
|
3984
|
+
* constructor's `secret` branch, so the derivation cannot come back empty here.
|
|
3985
|
+
*/
|
|
3986
|
+
initializeMLKEM() {
|
|
3987
|
+
const derived = this.deriveMlKemKeypair(this.mlKemParameterSet);
|
|
3988
|
+
if (!derived) {
|
|
3989
|
+
return;
|
|
3990
|
+
}
|
|
3991
|
+
this.pubkey = derived.pubkey;
|
|
3992
|
+
this.privkey = derived.privkey;
|
|
3911
3993
|
}
|
|
3912
3994
|
// =============================================================================
|
|
3913
3995
|
// HIGH-LEVEL MESSAGE ENCRYPTION (JavaScript SDK Compatibility)
|
|
@@ -3916,13 +3998,13 @@ var Wallet = class _Wallet {
|
|
|
3916
3998
|
const messageString = JSON.stringify(message);
|
|
3917
3999
|
const messageUint8 = new TextEncoder().encode(messageString);
|
|
3918
4000
|
const deserializedPubkey = this.deserializeKey(recipientPubkey);
|
|
3919
|
-
const
|
|
3920
|
-
if (deserializedPubkey.length !==
|
|
4001
|
+
const params = ML_KEM_PARAMS[this.mlKemParameterSet];
|
|
4002
|
+
if (deserializedPubkey.length !== params.pkBytes) {
|
|
3921
4003
|
throw new Error(
|
|
3922
|
-
`KnishIO: cannot ML-KEM-encrypt \u2014 recipient public key is ${deserializedPubkey.length} bytes, expected ${
|
|
4004
|
+
`KnishIO: cannot ML-KEM-encrypt \u2014 recipient public key is ${deserializedPubkey.length} bytes, expected ${params.pkBytes} (ML-KEM-${this.mlKemParameterSet}). The peer is not running ML-KEM-${this.mlKemParameterSet}; upgrade the peer, or step this client back to the other parameter set.`
|
|
3923
4005
|
);
|
|
3924
4006
|
}
|
|
3925
|
-
const { cipherText, sharedSecret } =
|
|
4007
|
+
const { cipherText, sharedSecret } = params.kem.encapsulate(deserializedPubkey);
|
|
3926
4008
|
const encryptedMessage = await this.encryptWithSharedSecret(messageUint8, sharedSecret);
|
|
3927
4009
|
return {
|
|
3928
4010
|
cipherText: this.serializeKey(cipherText),
|
|
@@ -3934,15 +4016,37 @@ var Wallet = class _Wallet {
|
|
|
3934
4016
|
return decryptedString === null ? null : JSON.parse(decryptedString);
|
|
3935
4017
|
}
|
|
3936
4018
|
/**
|
|
3937
|
-
* ML-
|
|
4019
|
+
* ML-KEM decapsulate + AES-256-GCM decrypt → the RAW decrypted UTF-8 string (no JSON.parse).
|
|
3938
4020
|
* Shared by {@link decryptMessage} (which JSON.parses the result) and the PQ CipherHash transport
|
|
3939
|
-
* ({@link
|
|
4021
|
+
* ({@link decryptMyMessageML}, which needs the raw response JSON text). PQ-transport Phase E.
|
|
3940
4022
|
*/
|
|
3941
4023
|
async _mlkemDecryptToString(encryptedData) {
|
|
3942
4024
|
const { cipherText, encryptedMessage } = encryptedData;
|
|
4025
|
+
const configuredParams = ML_KEM_PARAMS[this.mlKemParameterSet];
|
|
4026
|
+
const otherSet = this.mlKemParameterSet === 1024 ? 768 : 1024;
|
|
4027
|
+
const deserializedCipherText = this.deserializeKey(cipherText);
|
|
4028
|
+
let params = configuredParams;
|
|
4029
|
+
let decapsPrivkey = this.privkey;
|
|
4030
|
+
if (deserializedCipherText.length !== configuredParams.ctBytes) {
|
|
4031
|
+
if (deserializedCipherText.length !== ML_KEM_PARAMS[otherSet].ctBytes) {
|
|
4032
|
+
console.error(
|
|
4033
|
+
`Wallet::decryptMessage() - Ciphertext length mismatch: got ${deserializedCipherText.length}, expected ${configuredParams.ctBytes}`
|
|
4034
|
+
);
|
|
4035
|
+
return null;
|
|
4036
|
+
}
|
|
4037
|
+
const derived = this.deriveMlKemKeypair(otherSet);
|
|
4038
|
+
if (!derived) {
|
|
4039
|
+
console.error(
|
|
4040
|
+
`Wallet::decryptMessage() - cannot derive the ML-KEM-${otherSet} identity: wallet has no key`
|
|
4041
|
+
);
|
|
4042
|
+
return null;
|
|
4043
|
+
}
|
|
4044
|
+
params = derived.params;
|
|
4045
|
+
decapsPrivkey = derived.privkey;
|
|
4046
|
+
}
|
|
3943
4047
|
let sharedSecret;
|
|
3944
4048
|
try {
|
|
3945
|
-
sharedSecret =
|
|
4049
|
+
sharedSecret = params.kem.decapsulate(deserializedCipherText, decapsPrivkey);
|
|
3946
4050
|
} catch (e) {
|
|
3947
4051
|
console.error("Wallet::decryptMessage() - Decapsulation failed", e);
|
|
3948
4052
|
console.info("Wallet::decryptMessage() - my public key", this.pubkey);
|
|
@@ -3992,11 +4096,11 @@ var Wallet = class _Wallet {
|
|
|
3992
4096
|
return this.serializeKey(bytes);
|
|
3993
4097
|
}
|
|
3994
4098
|
/**
|
|
3995
|
-
* Post-quantum (ML-
|
|
4099
|
+
* Post-quantum (ML-KEM) CipherHash request envelope: a stringified single-recipient map
|
|
3996
4100
|
* `{ "<hashShare(recipientPubkey)>": {cipherText, encryptedMessage} }` (object-valued, via
|
|
3997
4101
|
* {@link encryptMessage}). Matches the Rust validator's CipherHash handler. PQ-transport Phase E.
|
|
3998
4102
|
*/
|
|
3999
|
-
async
|
|
4103
|
+
async encryptStringML(message, recipientPubkey) {
|
|
4000
4104
|
const envelope = await this.encryptMessage(message, recipientPubkey);
|
|
4001
4105
|
return JSON.stringify({ [this.hashShare(recipientPubkey)]: envelope });
|
|
4002
4106
|
}
|
|
@@ -4004,9 +4108,21 @@ var Wallet = class _Wallet {
|
|
|
4004
4108
|
* Decrypt a CipherHash response map addressed to THIS wallet's ML-KEM pubkey
|
|
4005
4109
|
* (`hashShare(this.pubkey)`) → the RAW decrypted GraphQL response JSON text (NOT JSON.parsed;
|
|
4006
4110
|
* it replaces the HTTP response body for the normal parser). `null` if no entry / decrypt fails.
|
|
4111
|
+
*
|
|
4112
|
+
* A pre-bump peer addressed its envelope to `hashShare(our_768_pubkey)`, which a wallet
|
|
4113
|
+
* configured at ML-KEM-1024 would never find — so the other identity's share is tried too.
|
|
4114
|
+
* Without this, the permissive length dispatch in {@link _mlkemDecryptToString} is
|
|
4115
|
+
* unreachable on the transport path.
|
|
4007
4116
|
*/
|
|
4008
|
-
async
|
|
4009
|
-
|
|
4117
|
+
async decryptMyMessageML(map) {
|
|
4118
|
+
let envelope = map[this.hashShare(this.pubkey)];
|
|
4119
|
+
if (!envelope) {
|
|
4120
|
+
const otherSet = this.mlKemParameterSet === 1024 ? 768 : 1024;
|
|
4121
|
+
const other = this.deriveMlKemKeypair(otherSet);
|
|
4122
|
+
if (other) {
|
|
4123
|
+
envelope = map[this.hashShare(other.pubkey)];
|
|
4124
|
+
}
|
|
4125
|
+
}
|
|
4010
4126
|
if (!envelope) {
|
|
4011
4127
|
return null;
|
|
4012
4128
|
}
|
|
@@ -4160,7 +4276,8 @@ z.object({
|
|
|
4160
4276
|
serverSdkVersion: z.number().int().min(1).optional(),
|
|
4161
4277
|
logging: z.boolean().optional(),
|
|
4162
4278
|
defaultRequestPolicy: z.enum(["cache-first", "cache-only", "network-only", "cache-and-network"]).nullable().optional(),
|
|
4163
|
-
secretStorage: z.unknown().optional()
|
|
4279
|
+
secretStorage: z.unknown().optional(),
|
|
4280
|
+
mlKemParameterSet: z.union([z.literal(1024), z.literal(768)]).optional()
|
|
4164
4281
|
}).strict();
|
|
4165
4282
|
z.object({
|
|
4166
4283
|
token: z.string().min(1, "Auth token cannot be empty"),
|
|
@@ -5249,6 +5366,7 @@ var Molecule = class _Molecule {
|
|
|
5249
5366
|
continuIdPosition;
|
|
5250
5367
|
parentHashes;
|
|
5251
5368
|
local;
|
|
5369
|
+
mlKemParameterSet = 1024;
|
|
5252
5370
|
/**
|
|
5253
5371
|
* Create new Molecule instance
|
|
5254
5372
|
* Matches JavaScript SDK constructor signature
|
|
@@ -5260,7 +5378,8 @@ var Molecule = class _Molecule {
|
|
|
5260
5378
|
remainderWallet = null,
|
|
5261
5379
|
cellSlug = null,
|
|
5262
5380
|
version = null,
|
|
5263
|
-
continuIdPosition = null
|
|
5381
|
+
continuIdPosition = null,
|
|
5382
|
+
mlKemParameterSet = null
|
|
5264
5383
|
} = {}) {
|
|
5265
5384
|
this.status = null;
|
|
5266
5385
|
this.molecularHash = null;
|
|
@@ -5272,6 +5391,7 @@ var Molecule = class _Molecule {
|
|
|
5272
5391
|
this.continuIdPosition = continuIdPosition;
|
|
5273
5392
|
this.atoms = [];
|
|
5274
5393
|
this.parentHashes = [];
|
|
5394
|
+
this.mlKemParameterSet = mlKemParameterSet || sourceWallet?.mlKemParameterSet || 1024;
|
|
5275
5395
|
const versionRegistry = versions_default;
|
|
5276
5396
|
if (version !== null && Object.prototype.hasOwnProperty.call(versionRegistry, version)) {
|
|
5277
5397
|
this.version = String(version);
|
|
@@ -5282,7 +5402,8 @@ var Molecule = class _Molecule {
|
|
|
5282
5402
|
bundle,
|
|
5283
5403
|
token: sourceWallet.token,
|
|
5284
5404
|
batchId: sourceWallet.batchId,
|
|
5285
|
-
characters: sourceWallet.characters
|
|
5405
|
+
characters: sourceWallet.characters,
|
|
5406
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
5286
5407
|
});
|
|
5287
5408
|
} else {
|
|
5288
5409
|
this.remainderWallet = null;
|
|
@@ -5343,7 +5464,8 @@ var Molecule = class _Molecule {
|
|
|
5343
5464
|
if (!this.remainderWallet || this.remainderWallet.token !== "USER") {
|
|
5344
5465
|
this.remainderWallet = Wallet.create({
|
|
5345
5466
|
secret: this.secret,
|
|
5346
|
-
bundle: this.bundle
|
|
5467
|
+
bundle: this.bundle,
|
|
5468
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
5347
5469
|
});
|
|
5348
5470
|
}
|
|
5349
5471
|
const continuIdMeta = {};
|
|
@@ -5730,7 +5852,8 @@ var Molecule = class _Molecule {
|
|
|
5730
5852
|
}
|
|
5731
5853
|
const burnWallet = new Wallet({
|
|
5732
5854
|
bundle: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
5733
|
-
token: this.sourceWallet.token
|
|
5855
|
+
token: this.sourceWallet.token,
|
|
5856
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
5734
5857
|
});
|
|
5735
5858
|
this.addAtom(Atom.create({
|
|
5736
5859
|
isotope: "V",
|
|
@@ -6002,7 +6125,8 @@ var Molecule = class _Molecule {
|
|
|
6002
6125
|
position: data.sourceWallet.position,
|
|
6003
6126
|
bundle: data.sourceWallet.bundle,
|
|
6004
6127
|
batchId: data.sourceWallet.batchId,
|
|
6005
|
-
characters: data.sourceWallet.characters
|
|
6128
|
+
characters: data.sourceWallet.characters,
|
|
6129
|
+
mlKemParameterSet: molecule.mlKemParameterSet
|
|
6006
6130
|
});
|
|
6007
6131
|
molecule.sourceWallet.balance = String(data.sourceWallet.balance != null ? data.sourceWallet.balance : 0);
|
|
6008
6132
|
molecule.sourceWallet.address = data.sourceWallet.address;
|
|
@@ -6020,7 +6144,8 @@ var Molecule = class _Molecule {
|
|
|
6020
6144
|
position: data.remainderWallet.position,
|
|
6021
6145
|
bundle: data.remainderWallet.bundle,
|
|
6022
6146
|
batchId: data.remainderWallet.batchId,
|
|
6023
|
-
characters: data.remainderWallet.characters
|
|
6147
|
+
characters: data.remainderWallet.characters,
|
|
6148
|
+
mlKemParameterSet: molecule.mlKemParameterSet
|
|
6024
6149
|
});
|
|
6025
6150
|
molecule.remainderWallet.balance = String(data.remainderWallet.balance != null ? data.remainderWallet.balance : 0);
|
|
6026
6151
|
molecule.remainderWallet.address = data.remainderWallet.address;
|
|
@@ -6118,7 +6243,8 @@ var Molecule = class _Molecule {
|
|
|
6118
6243
|
secret: this.secret,
|
|
6119
6244
|
bundle: this.bundle,
|
|
6120
6245
|
token: this.sourceWallet.token,
|
|
6121
|
-
batchId: this.sourceWallet.batchId
|
|
6246
|
+
batchId: this.sourceWallet.batchId,
|
|
6247
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
6122
6248
|
});
|
|
6123
6249
|
if (tradeRates) {
|
|
6124
6250
|
bufferWallet.tradeRates = tradeRates;
|
|
@@ -6389,7 +6515,7 @@ var GraphQLClient = class {
|
|
|
6389
6515
|
let encryptedRequest = false;
|
|
6390
6516
|
let requestInit = init;
|
|
6391
6517
|
if (wallet && serverPubkey && init && typeof init.body === "string" && this.shouldEncrypt(init.body)) {
|
|
6392
|
-
const hashVar = await wallet.
|
|
6518
|
+
const hashVar = await wallet.encryptStringML(init.body, serverPubkey);
|
|
6393
6519
|
requestInit = { ...init, body: JSON.stringify({ query: CIPHER_HASH_QUERY, variables: { Hash: hashVar } }) };
|
|
6394
6520
|
encryptedRequest = true;
|
|
6395
6521
|
}
|
|
@@ -6409,7 +6535,7 @@ var GraphQLClient = class {
|
|
|
6409
6535
|
if (typeof hash !== "string") {
|
|
6410
6536
|
return new Response(text, init2);
|
|
6411
6537
|
}
|
|
6412
|
-
const decrypted = await wallet.
|
|
6538
|
+
const decrypted = await wallet.decryptMyMessageML(JSON.parse(hash));
|
|
6413
6539
|
return new Response(decrypted != null ? decrypted : text, init2);
|
|
6414
6540
|
}
|
|
6415
6541
|
setAuthData({
|
|
@@ -6578,6 +6704,22 @@ var AuthToken = class _AuthToken {
|
|
|
6578
6704
|
authToken.setWallet(wallet);
|
|
6579
6705
|
return authToken;
|
|
6580
6706
|
}
|
|
6707
|
+
/**
|
|
6708
|
+
* ML-KEM parameter set a restored session must use, resolved in three tiers:
|
|
6709
|
+
* an explicit snapshot field, then the stored validator key's length, then ML-KEM-768.
|
|
6710
|
+
*
|
|
6711
|
+
* The final tier is deliberately NOT the constructor default. A snapshot with neither an
|
|
6712
|
+
* explicit field nor a recognisable key can only have come from a pre-bump build, and every
|
|
6713
|
+
* pre-bump build was 768-only — defaulting to 1024 would make the restored wallet advertise
|
|
6714
|
+
* a public key the validator never recorded for that token.
|
|
6715
|
+
*/
|
|
6716
|
+
static resolveMlKemParameterSet(snapshot) {
|
|
6717
|
+
const explicit = snapshot.wallet?.mlKemParameterSet;
|
|
6718
|
+
if (explicit) {
|
|
6719
|
+
return Number(explicit) === 768 ? 768 : 1024;
|
|
6720
|
+
}
|
|
6721
|
+
return Wallet.mlKemParameterSetFromPubkey(snapshot.pubkey) ?? 768;
|
|
6722
|
+
}
|
|
6581
6723
|
/**
|
|
6582
6724
|
* Restore AuthToken from snapshot
|
|
6583
6725
|
*/
|
|
@@ -6585,8 +6727,9 @@ var AuthToken = class _AuthToken {
|
|
|
6585
6727
|
const wallet = new Wallet({
|
|
6586
6728
|
secret,
|
|
6587
6729
|
token: "AUTH",
|
|
6588
|
-
position: snapshot.wallet
|
|
6589
|
-
characters: snapshot.wallet
|
|
6730
|
+
position: snapshot.wallet?.position ?? null,
|
|
6731
|
+
characters: snapshot.wallet?.characters ?? null,
|
|
6732
|
+
mlKemParameterSet: _AuthToken.resolveMlKemParameterSet(snapshot)
|
|
6590
6733
|
});
|
|
6591
6734
|
return _AuthToken.create({
|
|
6592
6735
|
token: snapshot.token,
|
|
@@ -6654,7 +6797,9 @@ var AuthToken = class _AuthToken {
|
|
|
6654
6797
|
};
|
|
6655
6798
|
}
|
|
6656
6799
|
/**
|
|
6657
|
-
* Create snapshot for persistence
|
|
6800
|
+
* Create snapshot for persistence. The wallet's ML-KEM parameter set is recorded beside its
|
|
6801
|
+
* position and characters so a stepped-back ML-KEM-768 session restores as 768 rather than
|
|
6802
|
+
* silently taking the constructor default.
|
|
6658
6803
|
*/
|
|
6659
6804
|
toSnapshot() {
|
|
6660
6805
|
return {
|
|
@@ -6665,7 +6810,8 @@ var AuthToken = class _AuthToken {
|
|
|
6665
6810
|
...this.$__wallet ? {
|
|
6666
6811
|
wallet: {
|
|
6667
6812
|
position: this.$__wallet.position,
|
|
6668
|
-
characters: this.$__wallet.characters
|
|
6813
|
+
characters: this.$__wallet.characters,
|
|
6814
|
+
mlKemParameterSet: this.$__wallet.mlKemParameterSet
|
|
6669
6815
|
}
|
|
6670
6816
|
} : {}
|
|
6671
6817
|
};
|
|
@@ -6962,7 +7108,8 @@ var KnishIOClientConfigSchema2 = z.object({
|
|
|
6962
7108
|
// isn't rejected.
|
|
6963
7109
|
defaultRequestPolicy: z.enum(["cache-first", "cache-only", "network-only", "cache-and-network"]).nullable().optional(),
|
|
6964
7110
|
// Pluggable hardware envelope encryption secret storage provider
|
|
6965
|
-
secretStorage: z.unknown().optional()
|
|
7111
|
+
secretStorage: z.unknown().optional(),
|
|
7112
|
+
mlKemParameterSet: z.union([z.literal(1024), z.literal(768)]).optional()
|
|
6966
7113
|
}).strict();
|
|
6967
7114
|
var EnvironmentConfigSchema = z.object({
|
|
6968
7115
|
NODE_ENV: z.enum(["development", "production", "test"]).optional(),
|
|
@@ -10552,10 +10699,121 @@ function constantTimeCompare(a, b) {
|
|
|
10552
10699
|
return result === 0;
|
|
10553
10700
|
}
|
|
10554
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
|
+
|
|
10555
10812
|
// src/storage/MemorySecretStorageProvider.ts
|
|
10556
10813
|
var MemorySecretStorageProvider = class {
|
|
10557
10814
|
providerType = "memory";
|
|
10558
10815
|
secrets = /* @__PURE__ */ new Map();
|
|
10816
|
+
recoverySecrets = /* @__PURE__ */ new Map();
|
|
10559
10817
|
/**
|
|
10560
10818
|
* Memory storage is not hardware backed
|
|
10561
10819
|
*/
|
|
@@ -10586,6 +10844,17 @@ var MemorySecretStorageProvider = class {
|
|
|
10586
10844
|
providerType: this.providerType
|
|
10587
10845
|
};
|
|
10588
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
|
+
}
|
|
10589
10858
|
}
|
|
10590
10859
|
/**
|
|
10591
10860
|
* Retrieve a secret from memory
|
|
@@ -10598,6 +10867,7 @@ var MemorySecretStorageProvider = class {
|
|
|
10598
10867
|
* Delete a stored secret
|
|
10599
10868
|
*/
|
|
10600
10869
|
async deleteSecret(bundleHash) {
|
|
10870
|
+
this.recoverySecrets.delete(bundleHash);
|
|
10601
10871
|
return this.secrets.delete(bundleHash);
|
|
10602
10872
|
}
|
|
10603
10873
|
/**
|
|
@@ -10627,6 +10897,48 @@ var MemorySecretStorageProvider = class {
|
|
|
10627
10897
|
*/
|
|
10628
10898
|
clear() {
|
|
10629
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
|
+
});
|
|
10630
10942
|
}
|
|
10631
10943
|
};
|
|
10632
10944
|
|
|
@@ -10648,6 +10960,7 @@ var KnishIOClient = class {
|
|
|
10648
10960
|
$__authTokenObjects = {};
|
|
10649
10961
|
$__authToken = null;
|
|
10650
10962
|
$__authInProcess = false;
|
|
10963
|
+
$__mlKemParameterSet = 1024;
|
|
10651
10964
|
$__remainderWallet = null;
|
|
10652
10965
|
lastMoleculeQuery = null;
|
|
10653
10966
|
abortControllers = /* @__PURE__ */ new Map();
|
|
@@ -10692,7 +11005,8 @@ var KnishIOClient = class {
|
|
|
10692
11005
|
client,
|
|
10693
11006
|
serverSdkVersion,
|
|
10694
11007
|
logging,
|
|
10695
|
-
defaultRequestPolicy
|
|
11008
|
+
defaultRequestPolicy,
|
|
11009
|
+
mlKemParameterSet: config.mlKemParameterSet ?? 1024
|
|
10696
11010
|
});
|
|
10697
11011
|
if (config.secretStorage) {
|
|
10698
11012
|
this.$__secretStorage = config.secretStorage;
|
|
@@ -10708,10 +11022,12 @@ var KnishIOClient = class {
|
|
|
10708
11022
|
client = null,
|
|
10709
11023
|
serverSdkVersion = 3,
|
|
10710
11024
|
logging = false,
|
|
10711
|
-
defaultRequestPolicy = null
|
|
11025
|
+
defaultRequestPolicy = null,
|
|
11026
|
+
mlKemParameterSet = 1024
|
|
10712
11027
|
}) {
|
|
10713
11028
|
this.reset();
|
|
10714
11029
|
this.$__logging = logging;
|
|
11030
|
+
this.setMlKemParameterSet(mlKemParameterSet);
|
|
10715
11031
|
this.$__authTokenObjects = {};
|
|
10716
11032
|
this.setUri(uri);
|
|
10717
11033
|
if (cellSlug) {
|
|
@@ -10732,6 +11048,17 @@ var KnishIOClient = class {
|
|
|
10732
11048
|
this.$__serverSdkVersion = serverSdkVersion;
|
|
10733
11049
|
this.$__defaultRequestPolicy = defaultRequestPolicy;
|
|
10734
11050
|
}
|
|
11051
|
+
getMlKemParameterSet() {
|
|
11052
|
+
return this.$__mlKemParameterSet || 1024;
|
|
11053
|
+
}
|
|
11054
|
+
setMlKemParameterSet(parameterSet) {
|
|
11055
|
+
const paramNum = Number(parameterSet);
|
|
11056
|
+
if (![1024, 768].includes(paramNum)) {
|
|
11057
|
+
throw new Error(`KnishIO: unsupported ML-KEM parameter set ${parameterSet}; expected 1024 or 768.`);
|
|
11058
|
+
}
|
|
11059
|
+
this.$__mlKemParameterSet = paramNum;
|
|
11060
|
+
return this;
|
|
11061
|
+
}
|
|
10735
11062
|
/**
|
|
10736
11063
|
* Get random uri from specified this.$__uris
|
|
10737
11064
|
*/
|
|
@@ -10968,7 +11295,8 @@ var KnishIOClient = class {
|
|
|
10968
11295
|
bundle,
|
|
10969
11296
|
token: "USER",
|
|
10970
11297
|
batchId: sourceWallet.batchId,
|
|
10971
|
-
characters: sourceWallet.characters
|
|
11298
|
+
characters: sourceWallet.characters,
|
|
11299
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
10972
11300
|
}));
|
|
10973
11301
|
return new Molecule({
|
|
10974
11302
|
secret,
|
|
@@ -10977,7 +11305,8 @@ var KnishIOClient = class {
|
|
|
10977
11305
|
remainderWallet: this.getRemainderWallet(),
|
|
10978
11306
|
cellSlug: this.getCellSlug(),
|
|
10979
11307
|
version: this.getServerSdkVersion(),
|
|
10980
|
-
continuIdPosition
|
|
11308
|
+
continuIdPosition,
|
|
11309
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
10981
11310
|
});
|
|
10982
11311
|
}
|
|
10983
11312
|
/**
|
|
@@ -11089,7 +11418,8 @@ var KnishIOClient = class {
|
|
|
11089
11418
|
}))?.payload();
|
|
11090
11419
|
if (!sourceWallet) {
|
|
11091
11420
|
sourceWallet = new Wallet({
|
|
11092
|
-
secret: this.getSecret()
|
|
11421
|
+
secret: this.getSecret(),
|
|
11422
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11093
11423
|
});
|
|
11094
11424
|
} else {
|
|
11095
11425
|
sourceWallet.key = Wallet.generateKey({
|
|
@@ -11132,7 +11462,8 @@ var KnishIOClient = class {
|
|
|
11132
11462
|
}
|
|
11133
11463
|
const recipientWallet = Wallet.create({
|
|
11134
11464
|
bundle: bundleHash,
|
|
11135
|
-
token
|
|
11465
|
+
token,
|
|
11466
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11136
11467
|
});
|
|
11137
11468
|
if (batchId !== null) {
|
|
11138
11469
|
recipientWallet.batchId = batchId;
|
|
@@ -11199,7 +11530,8 @@ var KnishIOClient = class {
|
|
|
11199
11530
|
const recipientWallets = recipients.map((recipient) => {
|
|
11200
11531
|
const recipientWallet = Wallet.create({
|
|
11201
11532
|
bundle: recipient.bundleHash,
|
|
11202
|
-
token
|
|
11533
|
+
token,
|
|
11534
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11203
11535
|
});
|
|
11204
11536
|
if (recipient.batchId !== null && recipient.batchId !== void 0) {
|
|
11205
11537
|
recipientWallet.batchId = recipient.batchId;
|
|
@@ -11823,7 +12155,8 @@ var KnishIOClient = class {
|
|
|
11823
12155
|
secret: this.getSecret(),
|
|
11824
12156
|
bundle: this.getBundle(),
|
|
11825
12157
|
token,
|
|
11826
|
-
batchId
|
|
12158
|
+
batchId,
|
|
12159
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11827
12160
|
});
|
|
11828
12161
|
await mutation.fillMolecule({
|
|
11829
12162
|
recipientWallet,
|
|
@@ -11891,7 +12224,8 @@ var KnishIOClient = class {
|
|
|
11891
12224
|
const recipientWallet = new Wallet({
|
|
11892
12225
|
secret: this.getSecret(),
|
|
11893
12226
|
bundle: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
11894
|
-
token
|
|
12227
|
+
token,
|
|
12228
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11895
12229
|
});
|
|
11896
12230
|
await mutation.fillMolecule({
|
|
11897
12231
|
recipientWallet,
|
|
@@ -11968,7 +12302,8 @@ var KnishIOClient = class {
|
|
|
11968
12302
|
const newWallet = new Wallet({
|
|
11969
12303
|
secret: this.getSecret(),
|
|
11970
12304
|
bundle: this.getBundle(),
|
|
11971
|
-
token
|
|
12305
|
+
token,
|
|
12306
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11972
12307
|
});
|
|
11973
12308
|
await mutation.fillMolecule(newWallet);
|
|
11974
12309
|
const response = await this.executeQuery(mutation);
|
|
@@ -12296,7 +12631,8 @@ var KnishIOClient = class {
|
|
|
12296
12631
|
this.setSecret(secret);
|
|
12297
12632
|
const wallet = new Wallet({
|
|
12298
12633
|
secret,
|
|
12299
|
-
token: "AUTH"
|
|
12634
|
+
token: "AUTH",
|
|
12635
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
12300
12636
|
});
|
|
12301
12637
|
const molecule = await this.createMolecule({
|
|
12302
12638
|
secret,
|
|
@@ -12424,45 +12760,25 @@ var MemoryStorageBackend = class {
|
|
|
12424
12760
|
return Array.from(this.store.keys());
|
|
12425
12761
|
}
|
|
12426
12762
|
};
|
|
12427
|
-
function uint8ArrayToBase64(bytes) {
|
|
12428
|
-
let binary = "";
|
|
12429
|
-
const len = bytes.byteLength;
|
|
12430
|
-
for (let i = 0; i < len; i++) {
|
|
12431
|
-
const byte = bytes[i];
|
|
12432
|
-
if (byte !== void 0) {
|
|
12433
|
-
binary += String.fromCharCode(byte);
|
|
12434
|
-
}
|
|
12435
|
-
}
|
|
12436
|
-
return btoa(binary);
|
|
12437
|
-
}
|
|
12438
|
-
function base64ToUint8Array(base64) {
|
|
12439
|
-
const binary = atob(base64);
|
|
12440
|
-
const len = binary.length;
|
|
12441
|
-
const bytes = new Uint8Array(len);
|
|
12442
|
-
for (let i = 0; i < len; i++) {
|
|
12443
|
-
bytes[i] = binary.charCodeAt(i);
|
|
12444
|
-
}
|
|
12445
|
-
return bytes;
|
|
12446
|
-
}
|
|
12447
|
-
var textEncoder2 = new TextEncoder();
|
|
12448
12763
|
var textDecoder = new TextDecoder();
|
|
12449
|
-
var KEY_PREFIX =
|
|
12450
|
-
var DEFAULT_ITERATIONS = 1e5;
|
|
12764
|
+
var KEY_PREFIX = SECRET_KEY_PREFIX;
|
|
12451
12765
|
var WebCryptoSecretStorageProvider = class {
|
|
12452
12766
|
providerType = "webcrypto-aes-gcm";
|
|
12453
12767
|
backend;
|
|
12454
12768
|
defaultPassphrase;
|
|
12455
|
-
hardwareBacked;
|
|
12456
12769
|
constructor(options = {}) {
|
|
12457
12770
|
this.backend = options.backend ?? new MemoryStorageBackend();
|
|
12458
12771
|
this.defaultPassphrase = options.defaultPassphrase;
|
|
12459
|
-
this.hardwareBacked = options.hardwareBacked ?? false;
|
|
12460
12772
|
}
|
|
12461
12773
|
/**
|
|
12462
|
-
*
|
|
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.
|
|
12463
12779
|
*/
|
|
12464
12780
|
isHardwareBacked() {
|
|
12465
|
-
return
|
|
12781
|
+
return false;
|
|
12466
12782
|
}
|
|
12467
12783
|
/**
|
|
12468
12784
|
* Check if WebCrypto subtle API is available
|
|
@@ -12470,38 +12786,6 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12470
12786
|
async isAvailable() {
|
|
12471
12787
|
return typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
|
|
12472
12788
|
}
|
|
12473
|
-
/**
|
|
12474
|
-
* Derive an AES-GCM CryptoKey from a passphrase and salt using PBKDF2
|
|
12475
|
-
*/
|
|
12476
|
-
async deriveKey(passphrase, salt, iterations = DEFAULT_ITERATIONS) {
|
|
12477
|
-
if (!await this.isAvailable()) {
|
|
12478
|
-
throw SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12479
|
-
}
|
|
12480
|
-
const passphraseBytes = textEncoder2.encode(passphrase);
|
|
12481
|
-
try {
|
|
12482
|
-
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
12483
|
-
"raw",
|
|
12484
|
-
passphraseBytes,
|
|
12485
|
-
"PBKDF2",
|
|
12486
|
-
false,
|
|
12487
|
-
["deriveKey"]
|
|
12488
|
-
);
|
|
12489
|
-
return await globalThis.crypto.subtle.deriveKey(
|
|
12490
|
-
{
|
|
12491
|
-
name: "PBKDF2",
|
|
12492
|
-
salt,
|
|
12493
|
-
iterations,
|
|
12494
|
-
hash: "SHA-256"
|
|
12495
|
-
},
|
|
12496
|
-
baseKey,
|
|
12497
|
-
{ name: "AES-GCM", length: 256 },
|
|
12498
|
-
false,
|
|
12499
|
-
["encrypt", "decrypt"]
|
|
12500
|
-
);
|
|
12501
|
-
} finally {
|
|
12502
|
-
zeroizeBytes(passphraseBytes);
|
|
12503
|
-
}
|
|
12504
|
-
}
|
|
12505
12789
|
/**
|
|
12506
12790
|
* Store and encrypt a master secret
|
|
12507
12791
|
*/
|
|
@@ -12516,44 +12800,36 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12516
12800
|
if (!passphrase) {
|
|
12517
12801
|
throw new SecretStorageException("Passphrase required for envelope encryption");
|
|
12518
12802
|
}
|
|
12519
|
-
|
|
12520
|
-
|
|
12521
|
-
|
|
12522
|
-
globalThis.crypto.getRandomValues(iv);
|
|
12523
|
-
const key = await this.deriveKey(passphrase, salt, DEFAULT_ITERATIONS);
|
|
12524
|
-
const secretBytes = textEncoder2.encode(secret);
|
|
12803
|
+
if (!await this.isAvailable()) {
|
|
12804
|
+
throw SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12805
|
+
}
|
|
12525
12806
|
try {
|
|
12526
|
-
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
12527
|
-
{
|
|
12528
|
-
name: "AES-GCM",
|
|
12529
|
-
iv
|
|
12530
|
-
},
|
|
12531
|
-
key,
|
|
12532
|
-
secretBytes
|
|
12533
|
-
);
|
|
12534
|
-
const ciphertext = uint8ArrayToBase64(new Uint8Array(encryptedBuffer));
|
|
12535
12807
|
const metadata = {
|
|
12536
12808
|
bundleHash,
|
|
12537
12809
|
label: options?.label,
|
|
12538
12810
|
createdAt: Date.now(),
|
|
12539
|
-
hardwareBacked:
|
|
12811
|
+
hardwareBacked: false,
|
|
12540
12812
|
providerType: this.providerType
|
|
12541
12813
|
};
|
|
12542
|
-
const payload =
|
|
12543
|
-
version: 1,
|
|
12544
|
-
ciphertext,
|
|
12545
|
-
iv: uint8ArrayToBase64(iv),
|
|
12546
|
-
salt: uint8ArrayToBase64(salt),
|
|
12547
|
-
algorithm: "AES-GCM",
|
|
12548
|
-
iterations: DEFAULT_ITERATIONS,
|
|
12549
|
-
metadata
|
|
12550
|
-
};
|
|
12814
|
+
const payload = await sealEnvelope(secret, passphrase, metadata);
|
|
12551
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
|
+
}
|
|
12552
12827
|
} catch (err) {
|
|
12828
|
+
if (err instanceof SecretStorageException) {
|
|
12829
|
+
throw err;
|
|
12830
|
+
}
|
|
12553
12831
|
const msg = err instanceof Error ? err.message : String(err);
|
|
12554
12832
|
throw new SecretStorageException(`Encryption failed: ${msg}`);
|
|
12555
|
-
} finally {
|
|
12556
|
-
zeroizeBytes(secretBytes);
|
|
12557
12833
|
}
|
|
12558
12834
|
}
|
|
12559
12835
|
/**
|
|
@@ -12574,26 +12850,20 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12574
12850
|
if (!passphrase) {
|
|
12575
12851
|
throw new SecretStorageException("Passphrase required for secret decryption");
|
|
12576
12852
|
}
|
|
12577
|
-
|
|
12578
|
-
|
|
12579
|
-
|
|
12853
|
+
if (!await this.isAvailable()) {
|
|
12854
|
+
throw SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12855
|
+
}
|
|
12580
12856
|
try {
|
|
12581
|
-
const
|
|
12582
|
-
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
12583
|
-
{
|
|
12584
|
-
name: "AES-GCM",
|
|
12585
|
-
iv
|
|
12586
|
-
},
|
|
12587
|
-
key,
|
|
12588
|
-
ciphertext
|
|
12589
|
-
);
|
|
12590
|
-
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
12857
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
12591
12858
|
try {
|
|
12592
12859
|
return textDecoder.decode(decryptedBytes);
|
|
12593
12860
|
} finally {
|
|
12594
12861
|
zeroizeBytes(decryptedBytes);
|
|
12595
12862
|
}
|
|
12596
12863
|
} catch (err) {
|
|
12864
|
+
if (err instanceof SecretStorageException) {
|
|
12865
|
+
throw err;
|
|
12866
|
+
}
|
|
12597
12867
|
const msg = err instanceof Error ? err.message : String(err);
|
|
12598
12868
|
throw SecretStorageException.decryptionFailed(msg);
|
|
12599
12869
|
}
|
|
@@ -12603,7 +12873,9 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12603
12873
|
*/
|
|
12604
12874
|
async deleteSecret(bundleHash) {
|
|
12605
12875
|
const key = `${KEY_PREFIX}${bundleHash}`;
|
|
12876
|
+
const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
|
|
12606
12877
|
const result = await this.backend.removeItem(key);
|
|
12878
|
+
await this.backend.removeItem(recoveryKey);
|
|
12607
12879
|
return result !== false;
|
|
12608
12880
|
}
|
|
12609
12881
|
/**
|
|
@@ -12618,7 +12890,7 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12618
12890
|
*/
|
|
12619
12891
|
async listSecrets() {
|
|
12620
12892
|
const keys = await this.backend.keys();
|
|
12621
|
-
const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX));
|
|
12893
|
+
const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX) && !k.startsWith(RECOVERY_KEY_PREFIX));
|
|
12622
12894
|
const results = [];
|
|
12623
12895
|
for (const key of matchingKeys) {
|
|
12624
12896
|
const raw = await this.backend.getItem(key);
|
|
@@ -12652,20 +12924,11 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12652
12924
|
if (!passphrase) {
|
|
12653
12925
|
throw new SecretStorageException("Passphrase required for secret decryption");
|
|
12654
12926
|
}
|
|
12655
|
-
|
|
12656
|
-
|
|
12657
|
-
|
|
12927
|
+
if (!await this.isAvailable()) {
|
|
12928
|
+
throw SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12929
|
+
}
|
|
12658
12930
|
try {
|
|
12659
|
-
const
|
|
12660
|
-
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
12661
|
-
{
|
|
12662
|
-
name: "AES-GCM",
|
|
12663
|
-
iv
|
|
12664
|
-
},
|
|
12665
|
-
key,
|
|
12666
|
-
ciphertext
|
|
12667
|
-
);
|
|
12668
|
-
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
12931
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
12669
12932
|
return await withSecureBytes(decryptedBytes, async (bytes) => {
|
|
12670
12933
|
const secretString = textDecoder.decode(bytes);
|
|
12671
12934
|
return await fn(secretString);
|
|
@@ -12678,6 +12941,1071 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12678
12941
|
throw SecretStorageException.decryptionFailed(msg);
|
|
12679
12942
|
}
|
|
12680
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
|
+
}
|
|
12681
14009
|
};
|
|
12682
14010
|
|
|
12683
14011
|
// src/storage/index.ts
|
|
@@ -12688,15 +14016,14 @@ function createDefaultSecretStorage(options = {}) {
|
|
|
12688
14016
|
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined") {
|
|
12689
14017
|
return new WebCryptoSecretStorageProvider({
|
|
12690
14018
|
backend: options.backend,
|
|
12691
|
-
defaultPassphrase: options.defaultPassphrase
|
|
12692
|
-
hardwareBacked: options.hardwareBacked
|
|
14019
|
+
defaultPassphrase: options.defaultPassphrase
|
|
12693
14020
|
});
|
|
12694
14021
|
}
|
|
12695
14022
|
return new MemorySecretStorageProvider();
|
|
12696
14023
|
}
|
|
12697
14024
|
|
|
12698
14025
|
// src/index.ts
|
|
12699
|
-
var SDK_VERSION = "
|
|
14026
|
+
var SDK_VERSION = "1.1.0";
|
|
12700
14027
|
var SDK_NAME = "KnishIO-Client-TS";
|
|
12701
14028
|
var COMPATIBLE_SERVER_VERSIONS = [4, 5];
|
|
12702
14029
|
var SDK_INFO = {
|
|
@@ -12705,7 +14032,7 @@ var SDK_INFO = {
|
|
|
12705
14032
|
description: "TypeScript SDK for Knish.IO post-blockchain distributed ledger",
|
|
12706
14033
|
compatibleServerVersions: COMPATIBLE_SERVER_VERSIONS,
|
|
12707
14034
|
features: [
|
|
12708
|
-
"Post-quantum cryptography (XMSS, ML-
|
|
14035
|
+
"Post-quantum cryptography (XMSS, ML-KEM-1024)",
|
|
12709
14036
|
"Cross-platform compatibility",
|
|
12710
14037
|
"Type-safe APIs",
|
|
12711
14038
|
"DAG-based transaction processing",
|
|
@@ -12786,6 +14113,6 @@ var KnishIO = {
|
|
|
12786
14113
|
SDK_INFO
|
|
12787
14114
|
};
|
|
12788
14115
|
|
|
12789
|
-
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 };
|
|
12790
14117
|
//# sourceMappingURL=index.js.map
|
|
12791
14118
|
//# sourceMappingURL=index.js.map
|