@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.cjs
CHANGED
|
@@ -572,6 +572,14 @@ var init_SecretStorageException = __esm({
|
|
|
572
572
|
}
|
|
573
573
|
);
|
|
574
574
|
}
|
|
575
|
+
/**
|
|
576
|
+
* Validation error for storage options or parameters
|
|
577
|
+
*/
|
|
578
|
+
static validationError(message) {
|
|
579
|
+
return new _SecretStorageException(message, {
|
|
580
|
+
code: "VALIDATION_ERROR"
|
|
581
|
+
});
|
|
582
|
+
}
|
|
575
583
|
};
|
|
576
584
|
}
|
|
577
585
|
});
|
|
@@ -3661,6 +3669,11 @@ var TokenUnit = class _TokenUnit {
|
|
|
3661
3669
|
};
|
|
3662
3670
|
}
|
|
3663
3671
|
};
|
|
3672
|
+
var ML_KEM_PARAMS = {
|
|
3673
|
+
1024: { kem: mlKem_js.ml_kem1024, pkBytes: 1568, skBytes: 3168, ctBytes: 1568 },
|
|
3674
|
+
768: { kem: mlKem_js.ml_kem768, pkBytes: 1184, skBytes: 2400, ctBytes: 1088 }
|
|
3675
|
+
};
|
|
3676
|
+
var DEFAULT_ML_KEM_PARAMETER_SET = 1024;
|
|
3664
3677
|
var Wallet = class _Wallet {
|
|
3665
3678
|
token;
|
|
3666
3679
|
balance;
|
|
@@ -3675,6 +3688,7 @@ var Wallet = class _Wallet {
|
|
|
3675
3688
|
tokenUnits;
|
|
3676
3689
|
tradeRates;
|
|
3677
3690
|
molecules;
|
|
3691
|
+
mlKemParameterSet;
|
|
3678
3692
|
// Token metadata (populated from query responses)
|
|
3679
3693
|
tokenName;
|
|
3680
3694
|
tokenAmount;
|
|
@@ -3688,8 +3702,14 @@ var Wallet = class _Wallet {
|
|
|
3688
3702
|
address = null,
|
|
3689
3703
|
position = null,
|
|
3690
3704
|
batchId = null,
|
|
3691
|
-
characters = null
|
|
3705
|
+
characters = null,
|
|
3706
|
+
mlKemParameterSet = DEFAULT_ML_KEM_PARAMETER_SET
|
|
3692
3707
|
} = {}) {
|
|
3708
|
+
const paramSetNum = Number(mlKemParameterSet);
|
|
3709
|
+
if (!ML_KEM_PARAMS[paramSetNum]) {
|
|
3710
|
+
throw new Error(`KnishIO: unsupported ML-KEM parameter set ${mlKemParameterSet}; expected 1024 or 768.`);
|
|
3711
|
+
}
|
|
3712
|
+
this.mlKemParameterSet = paramSetNum;
|
|
3693
3713
|
this.token = token;
|
|
3694
3714
|
this.balance = "0";
|
|
3695
3715
|
this.molecules = {};
|
|
@@ -3725,7 +3745,8 @@ var Wallet = class _Wallet {
|
|
|
3725
3745
|
bundle = null,
|
|
3726
3746
|
token = "USER",
|
|
3727
3747
|
batchId = null,
|
|
3728
|
-
characters = null
|
|
3748
|
+
characters = null,
|
|
3749
|
+
mlKemParameterSet = DEFAULT_ML_KEM_PARAMETER_SET
|
|
3729
3750
|
}) {
|
|
3730
3751
|
let position = null;
|
|
3731
3752
|
if (!secret && !bundle) {
|
|
@@ -3741,7 +3762,8 @@ var Wallet = class _Wallet {
|
|
|
3741
3762
|
token,
|
|
3742
3763
|
position,
|
|
3743
3764
|
batchId,
|
|
3744
|
-
characters
|
|
3765
|
+
characters,
|
|
3766
|
+
mlKemParameterSet
|
|
3745
3767
|
});
|
|
3746
3768
|
}
|
|
3747
3769
|
/**
|
|
@@ -3900,20 +3922,80 @@ var Wallet = class _Wallet {
|
|
|
3900
3922
|
return (typeof this.position === "undefined" || this.position === null) && (typeof this.address === "undefined" || this.address === null);
|
|
3901
3923
|
}
|
|
3902
3924
|
// =============================================================================
|
|
3903
|
-
// POST-QUANTUM CRYPTOGRAPHY - ML-
|
|
3925
|
+
// POST-QUANTUM CRYPTOGRAPHY - ML-KEM INTEGRATION
|
|
3904
3926
|
// =============================================================================
|
|
3905
3927
|
/**
|
|
3906
|
-
*
|
|
3928
|
+
* Derive an ML-KEM keypair for an arbitrary parameter set from the wallet's key seed,
|
|
3929
|
+
* without mutating the wallet. The 64-byte `d‖z` seed takes no parameter-set input — only
|
|
3930
|
+
* the final `keygen` call differs — so one KnishIO wallet owns both an ML-KEM-768 and an
|
|
3931
|
+
* ML-KEM-1024 identity and either can be reconstructed on demand.
|
|
3932
|
+
*
|
|
3933
|
+
* Returns `null` when the wallet holds no key — a secret-less wallet, which is what a molecule
|
|
3934
|
+
* deserializer builds for validation context. `generateSecret(null, …)` does NOT throw, so
|
|
3935
|
+
* without this the wallet would derive a plausible-looking identity from a bogus seed and fail
|
|
3936
|
+
* three layers down at AES-GCM instead of at the missing key. The guard lives here rather than
|
|
3937
|
+
* at each call site so a new caller cannot miss it.
|
|
3938
|
+
*
|
|
3939
|
+
* @param parameterSet - 1024 or 768
|
|
3907
3940
|
*/
|
|
3908
|
-
|
|
3941
|
+
deriveMlKemKeypair(parameterSet) {
|
|
3942
|
+
const params = ML_KEM_PARAMS[parameterSet];
|
|
3943
|
+
if (!params) {
|
|
3944
|
+
throw new Error(`KnishIO: unsupported ML-KEM parameter set ${parameterSet}; expected 1024 or 768.`);
|
|
3945
|
+
}
|
|
3946
|
+
if (!this.key) {
|
|
3947
|
+
return null;
|
|
3948
|
+
}
|
|
3909
3949
|
const seedHex = generateSecret(this.key, 128);
|
|
3910
3950
|
const seed = new Uint8Array(64);
|
|
3911
3951
|
for (let i = 0; i < 64; i++) {
|
|
3912
3952
|
seed[i] = parseInt(seedHex.substr(i * 2, 2), 16);
|
|
3913
3953
|
}
|
|
3914
|
-
const { publicKey, secretKey } =
|
|
3915
|
-
|
|
3916
|
-
|
|
3954
|
+
const { publicKey, secretKey } = params.kem.keygen(seed);
|
|
3955
|
+
return {
|
|
3956
|
+
pubkey: this.serializeKey(publicKey),
|
|
3957
|
+
privkey: secretKey,
|
|
3958
|
+
params
|
|
3959
|
+
};
|
|
3960
|
+
}
|
|
3961
|
+
/**
|
|
3962
|
+
* ML-KEM parameter set implied by a serialized public key's raw byte length. FIPS 203's key
|
|
3963
|
+
* lengths are disjoint (1568 bytes → ML-KEM-1024, 1184 bytes → ML-KEM-768), so a stored peer
|
|
3964
|
+
* key recovers the parameter set of the session it belongs to without a wire-format change.
|
|
3965
|
+
* Used by AuthToken.restore to resolve a snapshot that predates the field.
|
|
3966
|
+
*
|
|
3967
|
+
* @param pubkey - Base64-serialized ML-KEM public key
|
|
3968
|
+
* @return 1024, 768, or null when the length matches neither
|
|
3969
|
+
*/
|
|
3970
|
+
static mlKemParameterSetFromPubkey(pubkey) {
|
|
3971
|
+
if (!pubkey) {
|
|
3972
|
+
return null;
|
|
3973
|
+
}
|
|
3974
|
+
let byteLength;
|
|
3975
|
+
try {
|
|
3976
|
+
byteLength = typeof Buffer !== "undefined" ? Buffer.from(pubkey, "base64").length : atob(pubkey).length;
|
|
3977
|
+
} catch {
|
|
3978
|
+
return null;
|
|
3979
|
+
}
|
|
3980
|
+
if (byteLength === ML_KEM_PARAMS[1024].pkBytes) {
|
|
3981
|
+
return 1024;
|
|
3982
|
+
}
|
|
3983
|
+
if (byteLength === ML_KEM_PARAMS[768].pkBytes) {
|
|
3984
|
+
return 768;
|
|
3985
|
+
}
|
|
3986
|
+
return null;
|
|
3987
|
+
}
|
|
3988
|
+
/**
|
|
3989
|
+
* Initializes the ML-KEM key pair (matches JavaScript SDK exactly). Only ever reached from the
|
|
3990
|
+
* constructor's `secret` branch, so the derivation cannot come back empty here.
|
|
3991
|
+
*/
|
|
3992
|
+
initializeMLKEM() {
|
|
3993
|
+
const derived = this.deriveMlKemKeypair(this.mlKemParameterSet);
|
|
3994
|
+
if (!derived) {
|
|
3995
|
+
return;
|
|
3996
|
+
}
|
|
3997
|
+
this.pubkey = derived.pubkey;
|
|
3998
|
+
this.privkey = derived.privkey;
|
|
3917
3999
|
}
|
|
3918
4000
|
// =============================================================================
|
|
3919
4001
|
// HIGH-LEVEL MESSAGE ENCRYPTION (JavaScript SDK Compatibility)
|
|
@@ -3922,13 +4004,13 @@ var Wallet = class _Wallet {
|
|
|
3922
4004
|
const messageString = JSON.stringify(message);
|
|
3923
4005
|
const messageUint8 = new TextEncoder().encode(messageString);
|
|
3924
4006
|
const deserializedPubkey = this.deserializeKey(recipientPubkey);
|
|
3925
|
-
const
|
|
3926
|
-
if (deserializedPubkey.length !==
|
|
4007
|
+
const params = ML_KEM_PARAMS[this.mlKemParameterSet];
|
|
4008
|
+
if (deserializedPubkey.length !== params.pkBytes) {
|
|
3927
4009
|
throw new Error(
|
|
3928
|
-
`KnishIO: cannot ML-KEM-encrypt \u2014 recipient public key is ${deserializedPubkey.length} bytes, expected ${
|
|
4010
|
+
`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.`
|
|
3929
4011
|
);
|
|
3930
4012
|
}
|
|
3931
|
-
const { cipherText, sharedSecret } =
|
|
4013
|
+
const { cipherText, sharedSecret } = params.kem.encapsulate(deserializedPubkey);
|
|
3932
4014
|
const encryptedMessage = await this.encryptWithSharedSecret(messageUint8, sharedSecret);
|
|
3933
4015
|
return {
|
|
3934
4016
|
cipherText: this.serializeKey(cipherText),
|
|
@@ -3940,15 +4022,37 @@ var Wallet = class _Wallet {
|
|
|
3940
4022
|
return decryptedString === null ? null : JSON.parse(decryptedString);
|
|
3941
4023
|
}
|
|
3942
4024
|
/**
|
|
3943
|
-
* ML-
|
|
4025
|
+
* ML-KEM decapsulate + AES-256-GCM decrypt → the RAW decrypted UTF-8 string (no JSON.parse).
|
|
3944
4026
|
* Shared by {@link decryptMessage} (which JSON.parses the result) and the PQ CipherHash transport
|
|
3945
|
-
* ({@link
|
|
4027
|
+
* ({@link decryptMyMessageML}, which needs the raw response JSON text). PQ-transport Phase E.
|
|
3946
4028
|
*/
|
|
3947
4029
|
async _mlkemDecryptToString(encryptedData) {
|
|
3948
4030
|
const { cipherText, encryptedMessage } = encryptedData;
|
|
4031
|
+
const configuredParams = ML_KEM_PARAMS[this.mlKemParameterSet];
|
|
4032
|
+
const otherSet = this.mlKemParameterSet === 1024 ? 768 : 1024;
|
|
4033
|
+
const deserializedCipherText = this.deserializeKey(cipherText);
|
|
4034
|
+
let params = configuredParams;
|
|
4035
|
+
let decapsPrivkey = this.privkey;
|
|
4036
|
+
if (deserializedCipherText.length !== configuredParams.ctBytes) {
|
|
4037
|
+
if (deserializedCipherText.length !== ML_KEM_PARAMS[otherSet].ctBytes) {
|
|
4038
|
+
console.error(
|
|
4039
|
+
`Wallet::decryptMessage() - Ciphertext length mismatch: got ${deserializedCipherText.length}, expected ${configuredParams.ctBytes}`
|
|
4040
|
+
);
|
|
4041
|
+
return null;
|
|
4042
|
+
}
|
|
4043
|
+
const derived = this.deriveMlKemKeypair(otherSet);
|
|
4044
|
+
if (!derived) {
|
|
4045
|
+
console.error(
|
|
4046
|
+
`Wallet::decryptMessage() - cannot derive the ML-KEM-${otherSet} identity: wallet has no key`
|
|
4047
|
+
);
|
|
4048
|
+
return null;
|
|
4049
|
+
}
|
|
4050
|
+
params = derived.params;
|
|
4051
|
+
decapsPrivkey = derived.privkey;
|
|
4052
|
+
}
|
|
3949
4053
|
let sharedSecret;
|
|
3950
4054
|
try {
|
|
3951
|
-
sharedSecret =
|
|
4055
|
+
sharedSecret = params.kem.decapsulate(deserializedCipherText, decapsPrivkey);
|
|
3952
4056
|
} catch (e) {
|
|
3953
4057
|
console.error("Wallet::decryptMessage() - Decapsulation failed", e);
|
|
3954
4058
|
console.info("Wallet::decryptMessage() - my public key", this.pubkey);
|
|
@@ -3998,11 +4102,11 @@ var Wallet = class _Wallet {
|
|
|
3998
4102
|
return this.serializeKey(bytes);
|
|
3999
4103
|
}
|
|
4000
4104
|
/**
|
|
4001
|
-
* Post-quantum (ML-
|
|
4105
|
+
* Post-quantum (ML-KEM) CipherHash request envelope: a stringified single-recipient map
|
|
4002
4106
|
* `{ "<hashShare(recipientPubkey)>": {cipherText, encryptedMessage} }` (object-valued, via
|
|
4003
4107
|
* {@link encryptMessage}). Matches the Rust validator's CipherHash handler. PQ-transport Phase E.
|
|
4004
4108
|
*/
|
|
4005
|
-
async
|
|
4109
|
+
async encryptStringML(message, recipientPubkey) {
|
|
4006
4110
|
const envelope = await this.encryptMessage(message, recipientPubkey);
|
|
4007
4111
|
return JSON.stringify({ [this.hashShare(recipientPubkey)]: envelope });
|
|
4008
4112
|
}
|
|
@@ -4010,9 +4114,21 @@ var Wallet = class _Wallet {
|
|
|
4010
4114
|
* Decrypt a CipherHash response map addressed to THIS wallet's ML-KEM pubkey
|
|
4011
4115
|
* (`hashShare(this.pubkey)`) → the RAW decrypted GraphQL response JSON text (NOT JSON.parsed;
|
|
4012
4116
|
* it replaces the HTTP response body for the normal parser). `null` if no entry / decrypt fails.
|
|
4117
|
+
*
|
|
4118
|
+
* A pre-bump peer addressed its envelope to `hashShare(our_768_pubkey)`, which a wallet
|
|
4119
|
+
* configured at ML-KEM-1024 would never find — so the other identity's share is tried too.
|
|
4120
|
+
* Without this, the permissive length dispatch in {@link _mlkemDecryptToString} is
|
|
4121
|
+
* unreachable on the transport path.
|
|
4013
4122
|
*/
|
|
4014
|
-
async
|
|
4015
|
-
|
|
4123
|
+
async decryptMyMessageML(map) {
|
|
4124
|
+
let envelope = map[this.hashShare(this.pubkey)];
|
|
4125
|
+
if (!envelope) {
|
|
4126
|
+
const otherSet = this.mlKemParameterSet === 1024 ? 768 : 1024;
|
|
4127
|
+
const other = this.deriveMlKemKeypair(otherSet);
|
|
4128
|
+
if (other) {
|
|
4129
|
+
envelope = map[this.hashShare(other.pubkey)];
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4016
4132
|
if (!envelope) {
|
|
4017
4133
|
return null;
|
|
4018
4134
|
}
|
|
@@ -4166,7 +4282,8 @@ zod.z.object({
|
|
|
4166
4282
|
serverSdkVersion: zod.z.number().int().min(1).optional(),
|
|
4167
4283
|
logging: zod.z.boolean().optional(),
|
|
4168
4284
|
defaultRequestPolicy: zod.z.enum(["cache-first", "cache-only", "network-only", "cache-and-network"]).nullable().optional(),
|
|
4169
|
-
secretStorage: zod.z.unknown().optional()
|
|
4285
|
+
secretStorage: zod.z.unknown().optional(),
|
|
4286
|
+
mlKemParameterSet: zod.z.union([zod.z.literal(1024), zod.z.literal(768)]).optional()
|
|
4170
4287
|
}).strict();
|
|
4171
4288
|
zod.z.object({
|
|
4172
4289
|
token: zod.z.string().min(1, "Auth token cannot be empty"),
|
|
@@ -5255,6 +5372,7 @@ var Molecule = class _Molecule {
|
|
|
5255
5372
|
continuIdPosition;
|
|
5256
5373
|
parentHashes;
|
|
5257
5374
|
local;
|
|
5375
|
+
mlKemParameterSet = 1024;
|
|
5258
5376
|
/**
|
|
5259
5377
|
* Create new Molecule instance
|
|
5260
5378
|
* Matches JavaScript SDK constructor signature
|
|
@@ -5266,7 +5384,8 @@ var Molecule = class _Molecule {
|
|
|
5266
5384
|
remainderWallet = null,
|
|
5267
5385
|
cellSlug = null,
|
|
5268
5386
|
version = null,
|
|
5269
|
-
continuIdPosition = null
|
|
5387
|
+
continuIdPosition = null,
|
|
5388
|
+
mlKemParameterSet = null
|
|
5270
5389
|
} = {}) {
|
|
5271
5390
|
this.status = null;
|
|
5272
5391
|
this.molecularHash = null;
|
|
@@ -5278,6 +5397,7 @@ var Molecule = class _Molecule {
|
|
|
5278
5397
|
this.continuIdPosition = continuIdPosition;
|
|
5279
5398
|
this.atoms = [];
|
|
5280
5399
|
this.parentHashes = [];
|
|
5400
|
+
this.mlKemParameterSet = mlKemParameterSet || sourceWallet?.mlKemParameterSet || 1024;
|
|
5281
5401
|
const versionRegistry = versions_default;
|
|
5282
5402
|
if (version !== null && Object.prototype.hasOwnProperty.call(versionRegistry, version)) {
|
|
5283
5403
|
this.version = String(version);
|
|
@@ -5288,7 +5408,8 @@ var Molecule = class _Molecule {
|
|
|
5288
5408
|
bundle,
|
|
5289
5409
|
token: sourceWallet.token,
|
|
5290
5410
|
batchId: sourceWallet.batchId,
|
|
5291
|
-
characters: sourceWallet.characters
|
|
5411
|
+
characters: sourceWallet.characters,
|
|
5412
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
5292
5413
|
});
|
|
5293
5414
|
} else {
|
|
5294
5415
|
this.remainderWallet = null;
|
|
@@ -5349,7 +5470,8 @@ var Molecule = class _Molecule {
|
|
|
5349
5470
|
if (!this.remainderWallet || this.remainderWallet.token !== "USER") {
|
|
5350
5471
|
this.remainderWallet = Wallet.create({
|
|
5351
5472
|
secret: this.secret,
|
|
5352
|
-
bundle: this.bundle
|
|
5473
|
+
bundle: this.bundle,
|
|
5474
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
5353
5475
|
});
|
|
5354
5476
|
}
|
|
5355
5477
|
const continuIdMeta = {};
|
|
@@ -5736,7 +5858,8 @@ var Molecule = class _Molecule {
|
|
|
5736
5858
|
}
|
|
5737
5859
|
const burnWallet = new Wallet({
|
|
5738
5860
|
bundle: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
5739
|
-
token: this.sourceWallet.token
|
|
5861
|
+
token: this.sourceWallet.token,
|
|
5862
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
5740
5863
|
});
|
|
5741
5864
|
this.addAtom(Atom.create({
|
|
5742
5865
|
isotope: "V",
|
|
@@ -6008,7 +6131,8 @@ var Molecule = class _Molecule {
|
|
|
6008
6131
|
position: data.sourceWallet.position,
|
|
6009
6132
|
bundle: data.sourceWallet.bundle,
|
|
6010
6133
|
batchId: data.sourceWallet.batchId,
|
|
6011
|
-
characters: data.sourceWallet.characters
|
|
6134
|
+
characters: data.sourceWallet.characters,
|
|
6135
|
+
mlKemParameterSet: molecule.mlKemParameterSet
|
|
6012
6136
|
});
|
|
6013
6137
|
molecule.sourceWallet.balance = String(data.sourceWallet.balance != null ? data.sourceWallet.balance : 0);
|
|
6014
6138
|
molecule.sourceWallet.address = data.sourceWallet.address;
|
|
@@ -6026,7 +6150,8 @@ var Molecule = class _Molecule {
|
|
|
6026
6150
|
position: data.remainderWallet.position,
|
|
6027
6151
|
bundle: data.remainderWallet.bundle,
|
|
6028
6152
|
batchId: data.remainderWallet.batchId,
|
|
6029
|
-
characters: data.remainderWallet.characters
|
|
6153
|
+
characters: data.remainderWallet.characters,
|
|
6154
|
+
mlKemParameterSet: molecule.mlKemParameterSet
|
|
6030
6155
|
});
|
|
6031
6156
|
molecule.remainderWallet.balance = String(data.remainderWallet.balance != null ? data.remainderWallet.balance : 0);
|
|
6032
6157
|
molecule.remainderWallet.address = data.remainderWallet.address;
|
|
@@ -6124,7 +6249,8 @@ var Molecule = class _Molecule {
|
|
|
6124
6249
|
secret: this.secret,
|
|
6125
6250
|
bundle: this.bundle,
|
|
6126
6251
|
token: this.sourceWallet.token,
|
|
6127
|
-
batchId: this.sourceWallet.batchId
|
|
6252
|
+
batchId: this.sourceWallet.batchId,
|
|
6253
|
+
mlKemParameterSet: this.mlKemParameterSet
|
|
6128
6254
|
});
|
|
6129
6255
|
if (tradeRates) {
|
|
6130
6256
|
bufferWallet.tradeRates = tradeRates;
|
|
@@ -6395,7 +6521,7 @@ var GraphQLClient = class {
|
|
|
6395
6521
|
let encryptedRequest = false;
|
|
6396
6522
|
let requestInit = init;
|
|
6397
6523
|
if (wallet && serverPubkey && init && typeof init.body === "string" && this.shouldEncrypt(init.body)) {
|
|
6398
|
-
const hashVar = await wallet.
|
|
6524
|
+
const hashVar = await wallet.encryptStringML(init.body, serverPubkey);
|
|
6399
6525
|
requestInit = { ...init, body: JSON.stringify({ query: CIPHER_HASH_QUERY, variables: { Hash: hashVar } }) };
|
|
6400
6526
|
encryptedRequest = true;
|
|
6401
6527
|
}
|
|
@@ -6415,7 +6541,7 @@ var GraphQLClient = class {
|
|
|
6415
6541
|
if (typeof hash !== "string") {
|
|
6416
6542
|
return new Response(text, init2);
|
|
6417
6543
|
}
|
|
6418
|
-
const decrypted = await wallet.
|
|
6544
|
+
const decrypted = await wallet.decryptMyMessageML(JSON.parse(hash));
|
|
6419
6545
|
return new Response(decrypted != null ? decrypted : text, init2);
|
|
6420
6546
|
}
|
|
6421
6547
|
setAuthData({
|
|
@@ -6584,6 +6710,22 @@ var AuthToken = class _AuthToken {
|
|
|
6584
6710
|
authToken.setWallet(wallet);
|
|
6585
6711
|
return authToken;
|
|
6586
6712
|
}
|
|
6713
|
+
/**
|
|
6714
|
+
* ML-KEM parameter set a restored session must use, resolved in three tiers:
|
|
6715
|
+
* an explicit snapshot field, then the stored validator key's length, then ML-KEM-768.
|
|
6716
|
+
*
|
|
6717
|
+
* The final tier is deliberately NOT the constructor default. A snapshot with neither an
|
|
6718
|
+
* explicit field nor a recognisable key can only have come from a pre-bump build, and every
|
|
6719
|
+
* pre-bump build was 768-only — defaulting to 1024 would make the restored wallet advertise
|
|
6720
|
+
* a public key the validator never recorded for that token.
|
|
6721
|
+
*/
|
|
6722
|
+
static resolveMlKemParameterSet(snapshot) {
|
|
6723
|
+
const explicit = snapshot.wallet?.mlKemParameterSet;
|
|
6724
|
+
if (explicit) {
|
|
6725
|
+
return Number(explicit) === 768 ? 768 : 1024;
|
|
6726
|
+
}
|
|
6727
|
+
return Wallet.mlKemParameterSetFromPubkey(snapshot.pubkey) ?? 768;
|
|
6728
|
+
}
|
|
6587
6729
|
/**
|
|
6588
6730
|
* Restore AuthToken from snapshot
|
|
6589
6731
|
*/
|
|
@@ -6591,8 +6733,9 @@ var AuthToken = class _AuthToken {
|
|
|
6591
6733
|
const wallet = new Wallet({
|
|
6592
6734
|
secret,
|
|
6593
6735
|
token: "AUTH",
|
|
6594
|
-
position: snapshot.wallet
|
|
6595
|
-
characters: snapshot.wallet
|
|
6736
|
+
position: snapshot.wallet?.position ?? null,
|
|
6737
|
+
characters: snapshot.wallet?.characters ?? null,
|
|
6738
|
+
mlKemParameterSet: _AuthToken.resolveMlKemParameterSet(snapshot)
|
|
6596
6739
|
});
|
|
6597
6740
|
return _AuthToken.create({
|
|
6598
6741
|
token: snapshot.token,
|
|
@@ -6660,7 +6803,9 @@ var AuthToken = class _AuthToken {
|
|
|
6660
6803
|
};
|
|
6661
6804
|
}
|
|
6662
6805
|
/**
|
|
6663
|
-
* Create snapshot for persistence
|
|
6806
|
+
* Create snapshot for persistence. The wallet's ML-KEM parameter set is recorded beside its
|
|
6807
|
+
* position and characters so a stepped-back ML-KEM-768 session restores as 768 rather than
|
|
6808
|
+
* silently taking the constructor default.
|
|
6664
6809
|
*/
|
|
6665
6810
|
toSnapshot() {
|
|
6666
6811
|
return {
|
|
@@ -6671,7 +6816,8 @@ var AuthToken = class _AuthToken {
|
|
|
6671
6816
|
...this.$__wallet ? {
|
|
6672
6817
|
wallet: {
|
|
6673
6818
|
position: this.$__wallet.position,
|
|
6674
|
-
characters: this.$__wallet.characters
|
|
6819
|
+
characters: this.$__wallet.characters,
|
|
6820
|
+
mlKemParameterSet: this.$__wallet.mlKemParameterSet
|
|
6675
6821
|
}
|
|
6676
6822
|
} : {}
|
|
6677
6823
|
};
|
|
@@ -6968,7 +7114,8 @@ var KnishIOClientConfigSchema2 = zod.z.object({
|
|
|
6968
7114
|
// isn't rejected.
|
|
6969
7115
|
defaultRequestPolicy: zod.z.enum(["cache-first", "cache-only", "network-only", "cache-and-network"]).nullable().optional(),
|
|
6970
7116
|
// Pluggable hardware envelope encryption secret storage provider
|
|
6971
|
-
secretStorage: zod.z.unknown().optional()
|
|
7117
|
+
secretStorage: zod.z.unknown().optional(),
|
|
7118
|
+
mlKemParameterSet: zod.z.union([zod.z.literal(1024), zod.z.literal(768)]).optional()
|
|
6972
7119
|
}).strict();
|
|
6973
7120
|
var EnvironmentConfigSchema = zod.z.object({
|
|
6974
7121
|
NODE_ENV: zod.z.enum(["development", "production", "test"]).optional(),
|
|
@@ -10558,10 +10705,121 @@ function constantTimeCompare(a, b) {
|
|
|
10558
10705
|
return result === 0;
|
|
10559
10706
|
}
|
|
10560
10707
|
|
|
10708
|
+
// src/storage/secretEnvelope.ts
|
|
10709
|
+
init_SecretStorageException();
|
|
10710
|
+
var ENVELOPE_ALGORITHM = "AES-GCM";
|
|
10711
|
+
var DEFAULT_ITERATIONS = 1e5;
|
|
10712
|
+
var SECRET_KEY_PREFIX = "knishio:secret:";
|
|
10713
|
+
var RECOVERY_KEY_PREFIX = "knishio:recovery:";
|
|
10714
|
+
var GCM_IV_LENGTH = 12;
|
|
10715
|
+
var SALT_LENGTH = 16;
|
|
10716
|
+
var textEncoder2 = new TextEncoder();
|
|
10717
|
+
function uint8ArrayToBase64(bytes) {
|
|
10718
|
+
let binary = "";
|
|
10719
|
+
const len = bytes.byteLength;
|
|
10720
|
+
for (let i = 0; i < len; i++) {
|
|
10721
|
+
const byte = bytes[i];
|
|
10722
|
+
if (byte !== void 0) {
|
|
10723
|
+
binary += String.fromCharCode(byte);
|
|
10724
|
+
}
|
|
10725
|
+
}
|
|
10726
|
+
return btoa(binary);
|
|
10727
|
+
}
|
|
10728
|
+
function base64ToUint8Array(base64) {
|
|
10729
|
+
const binary = atob(base64);
|
|
10730
|
+
const len = binary.length;
|
|
10731
|
+
const bytes = new Uint8Array(len);
|
|
10732
|
+
for (let i = 0; i < len; i++) {
|
|
10733
|
+
bytes[i] = binary.charCodeAt(i);
|
|
10734
|
+
}
|
|
10735
|
+
return bytes;
|
|
10736
|
+
}
|
|
10737
|
+
async function deriveEnvelopeKey(passphrase, salt, iterations = DEFAULT_ITERATIONS) {
|
|
10738
|
+
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
10739
|
+
throw new exports.SecretStorageException("WebCrypto API is not available");
|
|
10740
|
+
}
|
|
10741
|
+
const passphraseBytes = textEncoder2.encode(passphrase);
|
|
10742
|
+
try {
|
|
10743
|
+
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
10744
|
+
"raw",
|
|
10745
|
+
passphraseBytes,
|
|
10746
|
+
"PBKDF2",
|
|
10747
|
+
false,
|
|
10748
|
+
["deriveKey"]
|
|
10749
|
+
);
|
|
10750
|
+
return await globalThis.crypto.subtle.deriveKey(
|
|
10751
|
+
{
|
|
10752
|
+
name: "PBKDF2",
|
|
10753
|
+
salt,
|
|
10754
|
+
iterations,
|
|
10755
|
+
hash: "SHA-256"
|
|
10756
|
+
},
|
|
10757
|
+
baseKey,
|
|
10758
|
+
{ name: "AES-GCM", length: 256 },
|
|
10759
|
+
false,
|
|
10760
|
+
["encrypt", "decrypt"]
|
|
10761
|
+
);
|
|
10762
|
+
} finally {
|
|
10763
|
+
zeroizeBytes(passphraseBytes);
|
|
10764
|
+
}
|
|
10765
|
+
}
|
|
10766
|
+
async function sealEnvelope(secret, passphrase, metadata) {
|
|
10767
|
+
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
10768
|
+
throw new exports.SecretStorageException("WebCrypto API is not available");
|
|
10769
|
+
}
|
|
10770
|
+
const salt = new Uint8Array(SALT_LENGTH);
|
|
10771
|
+
const iv = new Uint8Array(GCM_IV_LENGTH);
|
|
10772
|
+
globalThis.crypto.getRandomValues(salt);
|
|
10773
|
+
globalThis.crypto.getRandomValues(iv);
|
|
10774
|
+
const key = await deriveEnvelopeKey(passphrase, salt, DEFAULT_ITERATIONS);
|
|
10775
|
+
const secretBytes = textEncoder2.encode(secret);
|
|
10776
|
+
try {
|
|
10777
|
+
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
10778
|
+
{
|
|
10779
|
+
name: ENVELOPE_ALGORITHM,
|
|
10780
|
+
iv
|
|
10781
|
+
},
|
|
10782
|
+
key,
|
|
10783
|
+
secretBytes
|
|
10784
|
+
);
|
|
10785
|
+
const ciphertext = uint8ArrayToBase64(new Uint8Array(encryptedBuffer));
|
|
10786
|
+
return {
|
|
10787
|
+
version: 1,
|
|
10788
|
+
ciphertext,
|
|
10789
|
+
iv: uint8ArrayToBase64(iv),
|
|
10790
|
+
salt: uint8ArrayToBase64(salt),
|
|
10791
|
+
algorithm: ENVELOPE_ALGORITHM,
|
|
10792
|
+
iterations: DEFAULT_ITERATIONS,
|
|
10793
|
+
metadata
|
|
10794
|
+
};
|
|
10795
|
+
} finally {
|
|
10796
|
+
zeroizeBytes(secretBytes);
|
|
10797
|
+
}
|
|
10798
|
+
}
|
|
10799
|
+
async function openEnvelope(payload, passphrase) {
|
|
10800
|
+
if (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.subtle === "undefined") {
|
|
10801
|
+
throw new exports.SecretStorageException("WebCrypto API is not available");
|
|
10802
|
+
}
|
|
10803
|
+
const salt = base64ToUint8Array(payload.salt);
|
|
10804
|
+
const iv = base64ToUint8Array(payload.iv);
|
|
10805
|
+
const ciphertext = base64ToUint8Array(payload.ciphertext);
|
|
10806
|
+
const key = await deriveEnvelopeKey(passphrase, salt, payload.iterations ?? DEFAULT_ITERATIONS);
|
|
10807
|
+
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
10808
|
+
{
|
|
10809
|
+
name: ENVELOPE_ALGORITHM,
|
|
10810
|
+
iv
|
|
10811
|
+
},
|
|
10812
|
+
key,
|
|
10813
|
+
ciphertext
|
|
10814
|
+
);
|
|
10815
|
+
return new Uint8Array(decryptedBuffer);
|
|
10816
|
+
}
|
|
10817
|
+
|
|
10561
10818
|
// src/storage/MemorySecretStorageProvider.ts
|
|
10562
10819
|
var MemorySecretStorageProvider = class {
|
|
10563
10820
|
providerType = "memory";
|
|
10564
10821
|
secrets = /* @__PURE__ */ new Map();
|
|
10822
|
+
recoverySecrets = /* @__PURE__ */ new Map();
|
|
10565
10823
|
/**
|
|
10566
10824
|
* Memory storage is not hardware backed
|
|
10567
10825
|
*/
|
|
@@ -10592,6 +10850,17 @@ var MemorySecretStorageProvider = class {
|
|
|
10592
10850
|
providerType: this.providerType
|
|
10593
10851
|
};
|
|
10594
10852
|
this.secrets.set(bundleHash, { secret, metadata });
|
|
10853
|
+
if (options?.recoveryPassphrase) {
|
|
10854
|
+
const recoveryMetadata = {
|
|
10855
|
+
bundleHash,
|
|
10856
|
+
label: options?.label,
|
|
10857
|
+
createdAt: Date.now(),
|
|
10858
|
+
hardwareBacked: false,
|
|
10859
|
+
providerType: "webcrypto-aes-gcm"
|
|
10860
|
+
};
|
|
10861
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
10862
|
+
this.recoverySecrets.set(bundleHash, JSON.stringify(recoveryPayload));
|
|
10863
|
+
}
|
|
10595
10864
|
}
|
|
10596
10865
|
/**
|
|
10597
10866
|
* Retrieve a secret from memory
|
|
@@ -10604,6 +10873,7 @@ var MemorySecretStorageProvider = class {
|
|
|
10604
10873
|
* Delete a stored secret
|
|
10605
10874
|
*/
|
|
10606
10875
|
async deleteSecret(bundleHash) {
|
|
10876
|
+
this.recoverySecrets.delete(bundleHash);
|
|
10607
10877
|
return this.secrets.delete(bundleHash);
|
|
10608
10878
|
}
|
|
10609
10879
|
/**
|
|
@@ -10633,6 +10903,48 @@ var MemorySecretStorageProvider = class {
|
|
|
10633
10903
|
*/
|
|
10634
10904
|
clear() {
|
|
10635
10905
|
this.secrets.clear();
|
|
10906
|
+
this.recoverySecrets.clear();
|
|
10907
|
+
}
|
|
10908
|
+
/**
|
|
10909
|
+
* Recover a secret using its recovery envelope and restore it
|
|
10910
|
+
*/
|
|
10911
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
10912
|
+
if (!bundleHash) {
|
|
10913
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
10914
|
+
}
|
|
10915
|
+
if (!recoveryPassphrase) {
|
|
10916
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
10917
|
+
}
|
|
10918
|
+
const raw = this.recoverySecrets.get(bundleHash);
|
|
10919
|
+
if (!raw) {
|
|
10920
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
10921
|
+
}
|
|
10922
|
+
let payload;
|
|
10923
|
+
try {
|
|
10924
|
+
payload = JSON.parse(raw);
|
|
10925
|
+
} catch {
|
|
10926
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
10927
|
+
}
|
|
10928
|
+
let decryptedBytes;
|
|
10929
|
+
try {
|
|
10930
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
10931
|
+
} catch (err) {
|
|
10932
|
+
if (err instanceof exports.SecretStorageException) {
|
|
10933
|
+
throw err;
|
|
10934
|
+
}
|
|
10935
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10936
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
10937
|
+
}
|
|
10938
|
+
let secretStr;
|
|
10939
|
+
try {
|
|
10940
|
+
secretStr = new TextDecoder().decode(decryptedBytes);
|
|
10941
|
+
} finally {
|
|
10942
|
+
zeroizeBytes(decryptedBytes);
|
|
10943
|
+
}
|
|
10944
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
10945
|
+
...options,
|
|
10946
|
+
recoveryPassphrase
|
|
10947
|
+
});
|
|
10636
10948
|
}
|
|
10637
10949
|
};
|
|
10638
10950
|
|
|
@@ -10654,6 +10966,7 @@ var KnishIOClient = class {
|
|
|
10654
10966
|
$__authTokenObjects = {};
|
|
10655
10967
|
$__authToken = null;
|
|
10656
10968
|
$__authInProcess = false;
|
|
10969
|
+
$__mlKemParameterSet = 1024;
|
|
10657
10970
|
$__remainderWallet = null;
|
|
10658
10971
|
lastMoleculeQuery = null;
|
|
10659
10972
|
abortControllers = /* @__PURE__ */ new Map();
|
|
@@ -10698,7 +11011,8 @@ var KnishIOClient = class {
|
|
|
10698
11011
|
client,
|
|
10699
11012
|
serverSdkVersion,
|
|
10700
11013
|
logging,
|
|
10701
|
-
defaultRequestPolicy
|
|
11014
|
+
defaultRequestPolicy,
|
|
11015
|
+
mlKemParameterSet: config.mlKemParameterSet ?? 1024
|
|
10702
11016
|
});
|
|
10703
11017
|
if (config.secretStorage) {
|
|
10704
11018
|
this.$__secretStorage = config.secretStorage;
|
|
@@ -10714,10 +11028,12 @@ var KnishIOClient = class {
|
|
|
10714
11028
|
client = null,
|
|
10715
11029
|
serverSdkVersion = 3,
|
|
10716
11030
|
logging = false,
|
|
10717
|
-
defaultRequestPolicy = null
|
|
11031
|
+
defaultRequestPolicy = null,
|
|
11032
|
+
mlKemParameterSet = 1024
|
|
10718
11033
|
}) {
|
|
10719
11034
|
this.reset();
|
|
10720
11035
|
this.$__logging = logging;
|
|
11036
|
+
this.setMlKemParameterSet(mlKemParameterSet);
|
|
10721
11037
|
this.$__authTokenObjects = {};
|
|
10722
11038
|
this.setUri(uri);
|
|
10723
11039
|
if (cellSlug) {
|
|
@@ -10738,6 +11054,17 @@ var KnishIOClient = class {
|
|
|
10738
11054
|
this.$__serverSdkVersion = serverSdkVersion;
|
|
10739
11055
|
this.$__defaultRequestPolicy = defaultRequestPolicy;
|
|
10740
11056
|
}
|
|
11057
|
+
getMlKemParameterSet() {
|
|
11058
|
+
return this.$__mlKemParameterSet || 1024;
|
|
11059
|
+
}
|
|
11060
|
+
setMlKemParameterSet(parameterSet) {
|
|
11061
|
+
const paramNum = Number(parameterSet);
|
|
11062
|
+
if (![1024, 768].includes(paramNum)) {
|
|
11063
|
+
throw new Error(`KnishIO: unsupported ML-KEM parameter set ${parameterSet}; expected 1024 or 768.`);
|
|
11064
|
+
}
|
|
11065
|
+
this.$__mlKemParameterSet = paramNum;
|
|
11066
|
+
return this;
|
|
11067
|
+
}
|
|
10741
11068
|
/**
|
|
10742
11069
|
* Get random uri from specified this.$__uris
|
|
10743
11070
|
*/
|
|
@@ -10974,7 +11301,8 @@ var KnishIOClient = class {
|
|
|
10974
11301
|
bundle,
|
|
10975
11302
|
token: "USER",
|
|
10976
11303
|
batchId: sourceWallet.batchId,
|
|
10977
|
-
characters: sourceWallet.characters
|
|
11304
|
+
characters: sourceWallet.characters,
|
|
11305
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
10978
11306
|
}));
|
|
10979
11307
|
return new Molecule({
|
|
10980
11308
|
secret,
|
|
@@ -10983,7 +11311,8 @@ var KnishIOClient = class {
|
|
|
10983
11311
|
remainderWallet: this.getRemainderWallet(),
|
|
10984
11312
|
cellSlug: this.getCellSlug(),
|
|
10985
11313
|
version: this.getServerSdkVersion(),
|
|
10986
|
-
continuIdPosition
|
|
11314
|
+
continuIdPosition,
|
|
11315
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
10987
11316
|
});
|
|
10988
11317
|
}
|
|
10989
11318
|
/**
|
|
@@ -11095,7 +11424,8 @@ var KnishIOClient = class {
|
|
|
11095
11424
|
}))?.payload();
|
|
11096
11425
|
if (!sourceWallet) {
|
|
11097
11426
|
sourceWallet = new Wallet({
|
|
11098
|
-
secret: this.getSecret()
|
|
11427
|
+
secret: this.getSecret(),
|
|
11428
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11099
11429
|
});
|
|
11100
11430
|
} else {
|
|
11101
11431
|
sourceWallet.key = Wallet.generateKey({
|
|
@@ -11138,7 +11468,8 @@ var KnishIOClient = class {
|
|
|
11138
11468
|
}
|
|
11139
11469
|
const recipientWallet = Wallet.create({
|
|
11140
11470
|
bundle: bundleHash,
|
|
11141
|
-
token
|
|
11471
|
+
token,
|
|
11472
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11142
11473
|
});
|
|
11143
11474
|
if (batchId !== null) {
|
|
11144
11475
|
recipientWallet.batchId = batchId;
|
|
@@ -11205,7 +11536,8 @@ var KnishIOClient = class {
|
|
|
11205
11536
|
const recipientWallets = recipients.map((recipient) => {
|
|
11206
11537
|
const recipientWallet = Wallet.create({
|
|
11207
11538
|
bundle: recipient.bundleHash,
|
|
11208
|
-
token
|
|
11539
|
+
token,
|
|
11540
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11209
11541
|
});
|
|
11210
11542
|
if (recipient.batchId !== null && recipient.batchId !== void 0) {
|
|
11211
11543
|
recipientWallet.batchId = recipient.batchId;
|
|
@@ -11829,7 +12161,8 @@ var KnishIOClient = class {
|
|
|
11829
12161
|
secret: this.getSecret(),
|
|
11830
12162
|
bundle: this.getBundle(),
|
|
11831
12163
|
token,
|
|
11832
|
-
batchId
|
|
12164
|
+
batchId,
|
|
12165
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11833
12166
|
});
|
|
11834
12167
|
await mutation.fillMolecule({
|
|
11835
12168
|
recipientWallet,
|
|
@@ -11897,7 +12230,8 @@ var KnishIOClient = class {
|
|
|
11897
12230
|
const recipientWallet = new Wallet({
|
|
11898
12231
|
secret: this.getSecret(),
|
|
11899
12232
|
bundle: "0000000000000000000000000000000000000000000000000000000000000000",
|
|
11900
|
-
token
|
|
12233
|
+
token,
|
|
12234
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11901
12235
|
});
|
|
11902
12236
|
await mutation.fillMolecule({
|
|
11903
12237
|
recipientWallet,
|
|
@@ -11974,7 +12308,8 @@ var KnishIOClient = class {
|
|
|
11974
12308
|
const newWallet = new Wallet({
|
|
11975
12309
|
secret: this.getSecret(),
|
|
11976
12310
|
bundle: this.getBundle(),
|
|
11977
|
-
token
|
|
12311
|
+
token,
|
|
12312
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
11978
12313
|
});
|
|
11979
12314
|
await mutation.fillMolecule(newWallet);
|
|
11980
12315
|
const response = await this.executeQuery(mutation);
|
|
@@ -12302,7 +12637,8 @@ var KnishIOClient = class {
|
|
|
12302
12637
|
this.setSecret(secret);
|
|
12303
12638
|
const wallet = new Wallet({
|
|
12304
12639
|
secret,
|
|
12305
|
-
token: "AUTH"
|
|
12640
|
+
token: "AUTH",
|
|
12641
|
+
mlKemParameterSet: this.getMlKemParameterSet()
|
|
12306
12642
|
});
|
|
12307
12643
|
const molecule = await this.createMolecule({
|
|
12308
12644
|
secret,
|
|
@@ -12430,45 +12766,25 @@ var MemoryStorageBackend = class {
|
|
|
12430
12766
|
return Array.from(this.store.keys());
|
|
12431
12767
|
}
|
|
12432
12768
|
};
|
|
12433
|
-
function uint8ArrayToBase64(bytes) {
|
|
12434
|
-
let binary = "";
|
|
12435
|
-
const len = bytes.byteLength;
|
|
12436
|
-
for (let i = 0; i < len; i++) {
|
|
12437
|
-
const byte = bytes[i];
|
|
12438
|
-
if (byte !== void 0) {
|
|
12439
|
-
binary += String.fromCharCode(byte);
|
|
12440
|
-
}
|
|
12441
|
-
}
|
|
12442
|
-
return btoa(binary);
|
|
12443
|
-
}
|
|
12444
|
-
function base64ToUint8Array(base64) {
|
|
12445
|
-
const binary = atob(base64);
|
|
12446
|
-
const len = binary.length;
|
|
12447
|
-
const bytes = new Uint8Array(len);
|
|
12448
|
-
for (let i = 0; i < len; i++) {
|
|
12449
|
-
bytes[i] = binary.charCodeAt(i);
|
|
12450
|
-
}
|
|
12451
|
-
return bytes;
|
|
12452
|
-
}
|
|
12453
|
-
var textEncoder2 = new TextEncoder();
|
|
12454
12769
|
var textDecoder = new TextDecoder();
|
|
12455
|
-
var KEY_PREFIX =
|
|
12456
|
-
var DEFAULT_ITERATIONS = 1e5;
|
|
12770
|
+
var KEY_PREFIX = SECRET_KEY_PREFIX;
|
|
12457
12771
|
var WebCryptoSecretStorageProvider = class {
|
|
12458
12772
|
providerType = "webcrypto-aes-gcm";
|
|
12459
12773
|
backend;
|
|
12460
12774
|
defaultPassphrase;
|
|
12461
|
-
hardwareBacked;
|
|
12462
12775
|
constructor(options = {}) {
|
|
12463
12776
|
this.backend = options.backend ?? new MemoryStorageBackend();
|
|
12464
12777
|
this.defaultPassphrase = options.defaultPassphrase;
|
|
12465
|
-
this.hardwareBacked = options.hardwareBacked ?? false;
|
|
12466
12778
|
}
|
|
12467
12779
|
/**
|
|
12468
|
-
*
|
|
12780
|
+
* True only when this provider holds a non-exportable key inside platform-secure
|
|
12781
|
+
* hardware (Android TEE/StrongBox, Secure Enclave, TPM) and learned that from the
|
|
12782
|
+
* platform itself — never from a caller argument. Software envelope providers
|
|
12783
|
+
* return false. The value is persisted as `metadata.hardwareBacked` in every
|
|
12784
|
+
* envelope this provider writes.
|
|
12469
12785
|
*/
|
|
12470
12786
|
isHardwareBacked() {
|
|
12471
|
-
return
|
|
12787
|
+
return false;
|
|
12472
12788
|
}
|
|
12473
12789
|
/**
|
|
12474
12790
|
* Check if WebCrypto subtle API is available
|
|
@@ -12476,38 +12792,6 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12476
12792
|
async isAvailable() {
|
|
12477
12793
|
return typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
|
|
12478
12794
|
}
|
|
12479
|
-
/**
|
|
12480
|
-
* Derive an AES-GCM CryptoKey from a passphrase and salt using PBKDF2
|
|
12481
|
-
*/
|
|
12482
|
-
async deriveKey(passphrase, salt, iterations = DEFAULT_ITERATIONS) {
|
|
12483
|
-
if (!await this.isAvailable()) {
|
|
12484
|
-
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12485
|
-
}
|
|
12486
|
-
const passphraseBytes = textEncoder2.encode(passphrase);
|
|
12487
|
-
try {
|
|
12488
|
-
const baseKey = await globalThis.crypto.subtle.importKey(
|
|
12489
|
-
"raw",
|
|
12490
|
-
passphraseBytes,
|
|
12491
|
-
"PBKDF2",
|
|
12492
|
-
false,
|
|
12493
|
-
["deriveKey"]
|
|
12494
|
-
);
|
|
12495
|
-
return await globalThis.crypto.subtle.deriveKey(
|
|
12496
|
-
{
|
|
12497
|
-
name: "PBKDF2",
|
|
12498
|
-
salt,
|
|
12499
|
-
iterations,
|
|
12500
|
-
hash: "SHA-256"
|
|
12501
|
-
},
|
|
12502
|
-
baseKey,
|
|
12503
|
-
{ name: "AES-GCM", length: 256 },
|
|
12504
|
-
false,
|
|
12505
|
-
["encrypt", "decrypt"]
|
|
12506
|
-
);
|
|
12507
|
-
} finally {
|
|
12508
|
-
zeroizeBytes(passphraseBytes);
|
|
12509
|
-
}
|
|
12510
|
-
}
|
|
12511
12795
|
/**
|
|
12512
12796
|
* Store and encrypt a master secret
|
|
12513
12797
|
*/
|
|
@@ -12522,44 +12806,36 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12522
12806
|
if (!passphrase) {
|
|
12523
12807
|
throw new exports.SecretStorageException("Passphrase required for envelope encryption");
|
|
12524
12808
|
}
|
|
12525
|
-
|
|
12526
|
-
|
|
12527
|
-
|
|
12528
|
-
globalThis.crypto.getRandomValues(iv);
|
|
12529
|
-
const key = await this.deriveKey(passphrase, salt, DEFAULT_ITERATIONS);
|
|
12530
|
-
const secretBytes = textEncoder2.encode(secret);
|
|
12809
|
+
if (!await this.isAvailable()) {
|
|
12810
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12811
|
+
}
|
|
12531
12812
|
try {
|
|
12532
|
-
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
12533
|
-
{
|
|
12534
|
-
name: "AES-GCM",
|
|
12535
|
-
iv
|
|
12536
|
-
},
|
|
12537
|
-
key,
|
|
12538
|
-
secretBytes
|
|
12539
|
-
);
|
|
12540
|
-
const ciphertext = uint8ArrayToBase64(new Uint8Array(encryptedBuffer));
|
|
12541
12813
|
const metadata = {
|
|
12542
12814
|
bundleHash,
|
|
12543
12815
|
label: options?.label,
|
|
12544
12816
|
createdAt: Date.now(),
|
|
12545
|
-
hardwareBacked:
|
|
12817
|
+
hardwareBacked: false,
|
|
12546
12818
|
providerType: this.providerType
|
|
12547
12819
|
};
|
|
12548
|
-
const payload =
|
|
12549
|
-
version: 1,
|
|
12550
|
-
ciphertext,
|
|
12551
|
-
iv: uint8ArrayToBase64(iv),
|
|
12552
|
-
salt: uint8ArrayToBase64(salt),
|
|
12553
|
-
algorithm: "AES-GCM",
|
|
12554
|
-
iterations: DEFAULT_ITERATIONS,
|
|
12555
|
-
metadata
|
|
12556
|
-
};
|
|
12820
|
+
const payload = await sealEnvelope(secret, passphrase, metadata);
|
|
12557
12821
|
await this.backend.setItem(`${KEY_PREFIX}${bundleHash}`, JSON.stringify(payload));
|
|
12822
|
+
if (options?.recoveryPassphrase) {
|
|
12823
|
+
const recoveryMetadata = {
|
|
12824
|
+
bundleHash,
|
|
12825
|
+
label: options?.label,
|
|
12826
|
+
createdAt: Date.now(),
|
|
12827
|
+
hardwareBacked: false,
|
|
12828
|
+
providerType: "webcrypto-aes-gcm"
|
|
12829
|
+
};
|
|
12830
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
12831
|
+
await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
|
|
12832
|
+
}
|
|
12558
12833
|
} catch (err) {
|
|
12834
|
+
if (err instanceof exports.SecretStorageException) {
|
|
12835
|
+
throw err;
|
|
12836
|
+
}
|
|
12559
12837
|
const msg = err instanceof Error ? err.message : String(err);
|
|
12560
12838
|
throw new exports.SecretStorageException(`Encryption failed: ${msg}`);
|
|
12561
|
-
} finally {
|
|
12562
|
-
zeroizeBytes(secretBytes);
|
|
12563
12839
|
}
|
|
12564
12840
|
}
|
|
12565
12841
|
/**
|
|
@@ -12580,26 +12856,20 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12580
12856
|
if (!passphrase) {
|
|
12581
12857
|
throw new exports.SecretStorageException("Passphrase required for secret decryption");
|
|
12582
12858
|
}
|
|
12583
|
-
|
|
12584
|
-
|
|
12585
|
-
|
|
12859
|
+
if (!await this.isAvailable()) {
|
|
12860
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12861
|
+
}
|
|
12586
12862
|
try {
|
|
12587
|
-
const
|
|
12588
|
-
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
12589
|
-
{
|
|
12590
|
-
name: "AES-GCM",
|
|
12591
|
-
iv
|
|
12592
|
-
},
|
|
12593
|
-
key,
|
|
12594
|
-
ciphertext
|
|
12595
|
-
);
|
|
12596
|
-
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
12863
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
12597
12864
|
try {
|
|
12598
12865
|
return textDecoder.decode(decryptedBytes);
|
|
12599
12866
|
} finally {
|
|
12600
12867
|
zeroizeBytes(decryptedBytes);
|
|
12601
12868
|
}
|
|
12602
12869
|
} catch (err) {
|
|
12870
|
+
if (err instanceof exports.SecretStorageException) {
|
|
12871
|
+
throw err;
|
|
12872
|
+
}
|
|
12603
12873
|
const msg = err instanceof Error ? err.message : String(err);
|
|
12604
12874
|
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
12605
12875
|
}
|
|
@@ -12609,7 +12879,9 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12609
12879
|
*/
|
|
12610
12880
|
async deleteSecret(bundleHash) {
|
|
12611
12881
|
const key = `${KEY_PREFIX}${bundleHash}`;
|
|
12882
|
+
const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
|
|
12612
12883
|
const result = await this.backend.removeItem(key);
|
|
12884
|
+
await this.backend.removeItem(recoveryKey);
|
|
12613
12885
|
return result !== false;
|
|
12614
12886
|
}
|
|
12615
12887
|
/**
|
|
@@ -12624,7 +12896,7 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12624
12896
|
*/
|
|
12625
12897
|
async listSecrets() {
|
|
12626
12898
|
const keys = await this.backend.keys();
|
|
12627
|
-
const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX));
|
|
12899
|
+
const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX) && !k.startsWith(RECOVERY_KEY_PREFIX));
|
|
12628
12900
|
const results = [];
|
|
12629
12901
|
for (const key of matchingKeys) {
|
|
12630
12902
|
const raw = await this.backend.getItem(key);
|
|
@@ -12658,20 +12930,11 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12658
12930
|
if (!passphrase) {
|
|
12659
12931
|
throw new exports.SecretStorageException("Passphrase required for secret decryption");
|
|
12660
12932
|
}
|
|
12661
|
-
|
|
12662
|
-
|
|
12663
|
-
|
|
12933
|
+
if (!await this.isAvailable()) {
|
|
12934
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebCrypto API is not available");
|
|
12935
|
+
}
|
|
12664
12936
|
try {
|
|
12665
|
-
const
|
|
12666
|
-
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
12667
|
-
{
|
|
12668
|
-
name: "AES-GCM",
|
|
12669
|
-
iv
|
|
12670
|
-
},
|
|
12671
|
-
key,
|
|
12672
|
-
ciphertext
|
|
12673
|
-
);
|
|
12674
|
-
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
12937
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
12675
12938
|
return await withSecureBytes(decryptedBytes, async (bytes) => {
|
|
12676
12939
|
const secretString = textDecoder.decode(bytes);
|
|
12677
12940
|
return await fn(secretString);
|
|
@@ -12684,6 +12947,1071 @@ var WebCryptoSecretStorageProvider = class {
|
|
|
12684
12947
|
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
12685
12948
|
}
|
|
12686
12949
|
}
|
|
12950
|
+
/**
|
|
12951
|
+
* Recover a secret using its recovery envelope and re-enroll it
|
|
12952
|
+
*/
|
|
12953
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
12954
|
+
if (!bundleHash) {
|
|
12955
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
12956
|
+
}
|
|
12957
|
+
if (!recoveryPassphrase) {
|
|
12958
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
12959
|
+
}
|
|
12960
|
+
const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
|
|
12961
|
+
if (!raw) {
|
|
12962
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
12963
|
+
}
|
|
12964
|
+
let payload;
|
|
12965
|
+
try {
|
|
12966
|
+
payload = JSON.parse(raw);
|
|
12967
|
+
} catch {
|
|
12968
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
12969
|
+
}
|
|
12970
|
+
let decryptedBytes;
|
|
12971
|
+
try {
|
|
12972
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
12973
|
+
} catch (err) {
|
|
12974
|
+
if (err instanceof exports.SecretStorageException) {
|
|
12975
|
+
throw err;
|
|
12976
|
+
}
|
|
12977
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
12978
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
12979
|
+
}
|
|
12980
|
+
let secretStr;
|
|
12981
|
+
try {
|
|
12982
|
+
secretStr = textDecoder.decode(decryptedBytes);
|
|
12983
|
+
} finally {
|
|
12984
|
+
zeroizeBytes(decryptedBytes);
|
|
12985
|
+
}
|
|
12986
|
+
const storePassphrase = options?.passphrase ?? this.defaultPassphrase ?? recoveryPassphrase;
|
|
12987
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
12988
|
+
...options,
|
|
12989
|
+
passphrase: storePassphrase,
|
|
12990
|
+
recoveryPassphrase
|
|
12991
|
+
});
|
|
12992
|
+
}
|
|
12993
|
+
};
|
|
12994
|
+
|
|
12995
|
+
// src/storage/FileStorageBackend.ts
|
|
12996
|
+
init_SecretStorageException();
|
|
12997
|
+
var FileStorageBackend = class {
|
|
12998
|
+
filePath;
|
|
12999
|
+
store = /* @__PURE__ */ new Map();
|
|
13000
|
+
loaded = false;
|
|
13001
|
+
constructor(filePath) {
|
|
13002
|
+
if (!filePath) {
|
|
13003
|
+
throw new exports.SecretStorageException("Storage file path cannot be empty");
|
|
13004
|
+
}
|
|
13005
|
+
this.filePath = filePath;
|
|
13006
|
+
}
|
|
13007
|
+
async getFs() {
|
|
13008
|
+
try {
|
|
13009
|
+
const fs = await import('fs/promises');
|
|
13010
|
+
const path = await import('path');
|
|
13011
|
+
return { fs, path };
|
|
13012
|
+
} catch {
|
|
13013
|
+
throw exports.SecretStorageException.unavailable(
|
|
13014
|
+
"file-storage",
|
|
13015
|
+
"FileStorageBackend is only supported in Node.js environments with node:fs access"
|
|
13016
|
+
);
|
|
13017
|
+
}
|
|
13018
|
+
}
|
|
13019
|
+
async ensureLoaded() {
|
|
13020
|
+
if (this.loaded) {
|
|
13021
|
+
return this.store;
|
|
13022
|
+
}
|
|
13023
|
+
const { fs } = await this.getFs();
|
|
13024
|
+
try {
|
|
13025
|
+
const content = await fs.readFile(this.filePath, "utf8");
|
|
13026
|
+
let parsed;
|
|
13027
|
+
try {
|
|
13028
|
+
parsed = JSON.parse(content);
|
|
13029
|
+
} catch {
|
|
13030
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted storage file format");
|
|
13031
|
+
}
|
|
13032
|
+
if (parsed && typeof parsed === "object") {
|
|
13033
|
+
this.store = new Map(Object.entries(parsed).map(([k, v]) => [k, String(v)]));
|
|
13034
|
+
}
|
|
13035
|
+
} catch (err) {
|
|
13036
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13037
|
+
throw err;
|
|
13038
|
+
}
|
|
13039
|
+
const nodeErr = err;
|
|
13040
|
+
if (nodeErr?.code !== "ENOENT") {
|
|
13041
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13042
|
+
throw new exports.SecretStorageException(`Failed to read storage file: ${msg}`);
|
|
13043
|
+
}
|
|
13044
|
+
this.store = /* @__PURE__ */ new Map();
|
|
13045
|
+
}
|
|
13046
|
+
this.loaded = true;
|
|
13047
|
+
return this.store;
|
|
13048
|
+
}
|
|
13049
|
+
async persist() {
|
|
13050
|
+
const { fs, path } = await this.getFs();
|
|
13051
|
+
const dir = path.dirname(this.filePath);
|
|
13052
|
+
if (dir && dir !== ".") {
|
|
13053
|
+
await fs.mkdir(dir, { recursive: true });
|
|
13054
|
+
}
|
|
13055
|
+
const tmpPath = `${this.filePath}.tmp.${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
13056
|
+
const data = JSON.stringify(Object.fromEntries(this.store), null, 2);
|
|
13057
|
+
try {
|
|
13058
|
+
await fs.writeFile(tmpPath, data, { mode: 384, encoding: "utf8" });
|
|
13059
|
+
if (typeof process !== "undefined" && process.platform !== "win32") {
|
|
13060
|
+
try {
|
|
13061
|
+
await fs.chmod(tmpPath, 384);
|
|
13062
|
+
} catch {
|
|
13063
|
+
}
|
|
13064
|
+
}
|
|
13065
|
+
await fs.rename(tmpPath, this.filePath);
|
|
13066
|
+
} catch (err) {
|
|
13067
|
+
try {
|
|
13068
|
+
await fs.unlink(tmpPath);
|
|
13069
|
+
} catch {
|
|
13070
|
+
}
|
|
13071
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13072
|
+
throw new exports.SecretStorageException(`Failed to persist storage file: ${msg}`);
|
|
13073
|
+
}
|
|
13074
|
+
}
|
|
13075
|
+
async getItem(key) {
|
|
13076
|
+
await this.ensureLoaded();
|
|
13077
|
+
return this.store.get(key) ?? null;
|
|
13078
|
+
}
|
|
13079
|
+
async setItem(key, value) {
|
|
13080
|
+
await this.ensureLoaded();
|
|
13081
|
+
this.store.set(key, value);
|
|
13082
|
+
await this.persist();
|
|
13083
|
+
}
|
|
13084
|
+
async removeItem(key) {
|
|
13085
|
+
await this.ensureLoaded();
|
|
13086
|
+
const existed = this.store.delete(key);
|
|
13087
|
+
if (existed) {
|
|
13088
|
+
await this.persist();
|
|
13089
|
+
}
|
|
13090
|
+
return existed;
|
|
13091
|
+
}
|
|
13092
|
+
async keys() {
|
|
13093
|
+
await this.ensureLoaded();
|
|
13094
|
+
return Array.from(this.store.keys());
|
|
13095
|
+
}
|
|
13096
|
+
};
|
|
13097
|
+
|
|
13098
|
+
// src/storage/WebStorageBackend.ts
|
|
13099
|
+
init_SecretStorageException();
|
|
13100
|
+
var WebStorageBackend = class {
|
|
13101
|
+
storage;
|
|
13102
|
+
prefix;
|
|
13103
|
+
constructor(storage, prefix = "knishio:") {
|
|
13104
|
+
if (storage) {
|
|
13105
|
+
this.storage = storage;
|
|
13106
|
+
} else if (typeof globalThis !== "undefined" && globalThis.localStorage) {
|
|
13107
|
+
this.storage = globalThis.localStorage;
|
|
13108
|
+
} else {
|
|
13109
|
+
throw exports.SecretStorageException.unavailable(
|
|
13110
|
+
"web-storage",
|
|
13111
|
+
"WebStorageBackend requires a Storage object or global localStorage"
|
|
13112
|
+
);
|
|
13113
|
+
}
|
|
13114
|
+
this.prefix = prefix;
|
|
13115
|
+
}
|
|
13116
|
+
getItem(key) {
|
|
13117
|
+
return this.storage.getItem(key);
|
|
13118
|
+
}
|
|
13119
|
+
setItem(key, value) {
|
|
13120
|
+
this.storage.setItem(key, value);
|
|
13121
|
+
}
|
|
13122
|
+
removeItem(key) {
|
|
13123
|
+
const existed = this.storage.getItem(key) !== null;
|
|
13124
|
+
this.storage.removeItem(key);
|
|
13125
|
+
return existed;
|
|
13126
|
+
}
|
|
13127
|
+
keys() {
|
|
13128
|
+
const result = [];
|
|
13129
|
+
const len = this.storage.length;
|
|
13130
|
+
for (let i = 0; i < len; i++) {
|
|
13131
|
+
const k = this.storage.key(i);
|
|
13132
|
+
if (k !== null) {
|
|
13133
|
+
if (!this.prefix || k.startsWith(this.prefix)) {
|
|
13134
|
+
result.push(k);
|
|
13135
|
+
}
|
|
13136
|
+
}
|
|
13137
|
+
}
|
|
13138
|
+
return result;
|
|
13139
|
+
}
|
|
13140
|
+
};
|
|
13141
|
+
|
|
13142
|
+
// src/storage/WebAuthnPrfSecretStorageProvider.ts
|
|
13143
|
+
init_SecretStorageException();
|
|
13144
|
+
var PRF_SALT_LABEL = "knishio:secret-storage:webauthn-prf:v1";
|
|
13145
|
+
var KEK_INFO = "knishio:secret-storage:kek:v1";
|
|
13146
|
+
var KEY_PREFIX2 = SECRET_KEY_PREFIX;
|
|
13147
|
+
var GCM_IV_LENGTH2 = 12;
|
|
13148
|
+
var textEncoder3 = new TextEncoder();
|
|
13149
|
+
var textDecoder2 = new TextDecoder();
|
|
13150
|
+
function base64UrlEncode(bytes) {
|
|
13151
|
+
return uint8ArrayToBase64(bytes).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
13152
|
+
}
|
|
13153
|
+
function base64UrlDecode(str) {
|
|
13154
|
+
let base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
13155
|
+
while (base64.length % 4 !== 0) {
|
|
13156
|
+
base64 += "=";
|
|
13157
|
+
}
|
|
13158
|
+
return base64ToUint8Array(base64);
|
|
13159
|
+
}
|
|
13160
|
+
function toUint8Array(buf) {
|
|
13161
|
+
if (buf instanceof Uint8Array) {
|
|
13162
|
+
return buf;
|
|
13163
|
+
}
|
|
13164
|
+
if (ArrayBuffer.isView(buf)) {
|
|
13165
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
13166
|
+
}
|
|
13167
|
+
return new Uint8Array(buf);
|
|
13168
|
+
}
|
|
13169
|
+
async function computePrfSalt() {
|
|
13170
|
+
const hash = await globalThis.crypto.subtle.digest("SHA-256", textEncoder3.encode(PRF_SALT_LABEL));
|
|
13171
|
+
return new Uint8Array(hash);
|
|
13172
|
+
}
|
|
13173
|
+
async function deriveKekFromPrf(prfOutput, prfSalt) {
|
|
13174
|
+
const hkdfKey = await globalThis.crypto.subtle.importKey(
|
|
13175
|
+
"raw",
|
|
13176
|
+
prfOutput,
|
|
13177
|
+
"HKDF",
|
|
13178
|
+
false,
|
|
13179
|
+
["deriveKey"]
|
|
13180
|
+
);
|
|
13181
|
+
return await globalThis.crypto.subtle.deriveKey(
|
|
13182
|
+
{
|
|
13183
|
+
name: "HKDF",
|
|
13184
|
+
hash: "SHA-256",
|
|
13185
|
+
salt: prfSalt,
|
|
13186
|
+
info: textEncoder3.encode(KEK_INFO)
|
|
13187
|
+
},
|
|
13188
|
+
hkdfKey,
|
|
13189
|
+
{ name: "AES-GCM", length: 256 },
|
|
13190
|
+
false,
|
|
13191
|
+
["encrypt", "decrypt"]
|
|
13192
|
+
);
|
|
13193
|
+
}
|
|
13194
|
+
var WebAuthnPrfSecretStorageProvider = class {
|
|
13195
|
+
providerType = "webauthn-prf";
|
|
13196
|
+
backend;
|
|
13197
|
+
rp;
|
|
13198
|
+
user;
|
|
13199
|
+
credentialsContainer;
|
|
13200
|
+
alias;
|
|
13201
|
+
cachedPassphrase;
|
|
13202
|
+
constructor(options) {
|
|
13203
|
+
this.backend = options.backend;
|
|
13204
|
+
this.rp = options.rp;
|
|
13205
|
+
this.user = options.user;
|
|
13206
|
+
this.credentialsContainer = options.credentials;
|
|
13207
|
+
this.alias = options.alias ?? "default";
|
|
13208
|
+
}
|
|
13209
|
+
get credentials() {
|
|
13210
|
+
if (this.credentialsContainer) {
|
|
13211
|
+
return this.credentialsContainer;
|
|
13212
|
+
}
|
|
13213
|
+
if (typeof globalThis.navigator !== "undefined" && globalThis.navigator.credentials) {
|
|
13214
|
+
return globalThis.navigator.credentials;
|
|
13215
|
+
}
|
|
13216
|
+
throw exports.SecretStorageException.unavailable(
|
|
13217
|
+
this.providerType,
|
|
13218
|
+
"WebAuthn credentials container is not available"
|
|
13219
|
+
);
|
|
13220
|
+
}
|
|
13221
|
+
get recordKey() {
|
|
13222
|
+
return `knishio:webauthn-prf:${this.alias}`;
|
|
13223
|
+
}
|
|
13224
|
+
isHardwareBacked() {
|
|
13225
|
+
return false;
|
|
13226
|
+
}
|
|
13227
|
+
async isAvailable() {
|
|
13228
|
+
const hasWebCrypto = typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
|
|
13229
|
+
const hasCredentials = Boolean(
|
|
13230
|
+
this.credentialsContainer || typeof globalThis.navigator !== "undefined" && globalThis.navigator.credentials && typeof globalThis.PublicKeyCredential !== "undefined"
|
|
13231
|
+
);
|
|
13232
|
+
return hasWebCrypto && hasCredentials;
|
|
13233
|
+
}
|
|
13234
|
+
/**
|
|
13235
|
+
* Enroll a new passkey credential with PRF support and wrap a random device passphrase
|
|
13236
|
+
*/
|
|
13237
|
+
async enroll() {
|
|
13238
|
+
const existing = await this.backend.getItem(this.recordKey);
|
|
13239
|
+
if (existing) {
|
|
13240
|
+
return;
|
|
13241
|
+
}
|
|
13242
|
+
if (!await this.isAvailable()) {
|
|
13243
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "WebAuthn PRF is not available");
|
|
13244
|
+
}
|
|
13245
|
+
const challenge = new Uint8Array(32);
|
|
13246
|
+
globalThis.crypto.getRandomValues(challenge);
|
|
13247
|
+
const credential = await this.credentials.create({
|
|
13248
|
+
publicKey: {
|
|
13249
|
+
rp: this.rp,
|
|
13250
|
+
user: {
|
|
13251
|
+
id: this.user.id,
|
|
13252
|
+
name: this.user.name,
|
|
13253
|
+
displayName: this.user.displayName
|
|
13254
|
+
},
|
|
13255
|
+
challenge,
|
|
13256
|
+
pubKeyCredParams: [
|
|
13257
|
+
{ type: "public-key", alg: -7 },
|
|
13258
|
+
{ type: "public-key", alg: -257 }
|
|
13259
|
+
],
|
|
13260
|
+
authenticatorSelection: {
|
|
13261
|
+
residentKey: "required",
|
|
13262
|
+
userVerification: "required"
|
|
13263
|
+
},
|
|
13264
|
+
extensions: {
|
|
13265
|
+
prf: {}
|
|
13266
|
+
}
|
|
13267
|
+
}
|
|
13268
|
+
});
|
|
13269
|
+
if (!credential) {
|
|
13270
|
+
throw exports.SecretStorageException.unavailable(this.providerType, "Authenticator creation returned null");
|
|
13271
|
+
}
|
|
13272
|
+
const extResults = credential.getClientExtensionResults?.();
|
|
13273
|
+
if (extResults?.prf?.enabled !== true) {
|
|
13274
|
+
throw exports.SecretStorageException.unavailable(
|
|
13275
|
+
this.providerType,
|
|
13276
|
+
"authenticator does not support the PRF extension"
|
|
13277
|
+
);
|
|
13278
|
+
}
|
|
13279
|
+
const credentialIdBytes = new Uint8Array(credential.rawId);
|
|
13280
|
+
const prfSalt = await computePrfSalt();
|
|
13281
|
+
const getChallenge = new Uint8Array(32);
|
|
13282
|
+
globalThis.crypto.getRandomValues(getChallenge);
|
|
13283
|
+
let assertion;
|
|
13284
|
+
try {
|
|
13285
|
+
assertion = await this.credentials.get({
|
|
13286
|
+
publicKey: {
|
|
13287
|
+
challenge: getChallenge,
|
|
13288
|
+
rpId: this.rp.id,
|
|
13289
|
+
allowCredentials: [
|
|
13290
|
+
{
|
|
13291
|
+
type: "public-key",
|
|
13292
|
+
id: credentialIdBytes
|
|
13293
|
+
}
|
|
13294
|
+
],
|
|
13295
|
+
userVerification: "required",
|
|
13296
|
+
extensions: {
|
|
13297
|
+
prf: {
|
|
13298
|
+
eval: {
|
|
13299
|
+
first: prfSalt
|
|
13300
|
+
}
|
|
13301
|
+
}
|
|
13302
|
+
}
|
|
13303
|
+
}
|
|
13304
|
+
});
|
|
13305
|
+
} catch (err) {
|
|
13306
|
+
const isNotAllowed = err instanceof Error && err.name === "NotAllowedError" || err?.name === "NotAllowedError";
|
|
13307
|
+
if (isNotAllowed) {
|
|
13308
|
+
throw exports.SecretStorageException.unavailable(
|
|
13309
|
+
this.providerType,
|
|
13310
|
+
"authenticator refused or credential missing"
|
|
13311
|
+
);
|
|
13312
|
+
}
|
|
13313
|
+
throw err;
|
|
13314
|
+
}
|
|
13315
|
+
if (!assertion) {
|
|
13316
|
+
throw exports.SecretStorageException.unavailable(
|
|
13317
|
+
this.providerType,
|
|
13318
|
+
"authenticator refused or credential missing"
|
|
13319
|
+
);
|
|
13320
|
+
}
|
|
13321
|
+
const getExtResults = assertion?.getClientExtensionResults?.();
|
|
13322
|
+
const firstOutput = getExtResults?.prf?.results?.first;
|
|
13323
|
+
if (!firstOutput) {
|
|
13324
|
+
throw exports.SecretStorageException.unavailable(
|
|
13325
|
+
this.providerType,
|
|
13326
|
+
"authenticator returned no PRF result"
|
|
13327
|
+
);
|
|
13328
|
+
}
|
|
13329
|
+
const prfBytes = toUint8Array(firstOutput);
|
|
13330
|
+
const kek = await deriveKekFromPrf(prfBytes, prfSalt);
|
|
13331
|
+
const devicePassphraseBytes = new Uint8Array(32);
|
|
13332
|
+
globalThis.crypto.getRandomValues(devicePassphraseBytes);
|
|
13333
|
+
const devicePassphrase = uint8ArrayToBase64(devicePassphraseBytes);
|
|
13334
|
+
const iv = new Uint8Array(GCM_IV_LENGTH2);
|
|
13335
|
+
globalThis.crypto.getRandomValues(iv);
|
|
13336
|
+
const passphraseBytes = textEncoder3.encode(devicePassphrase);
|
|
13337
|
+
try {
|
|
13338
|
+
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
13339
|
+
{
|
|
13340
|
+
name: "AES-GCM",
|
|
13341
|
+
iv
|
|
13342
|
+
},
|
|
13343
|
+
kek,
|
|
13344
|
+
passphraseBytes
|
|
13345
|
+
);
|
|
13346
|
+
const record = {
|
|
13347
|
+
version: 1,
|
|
13348
|
+
credentialId: base64UrlEncode(credentialIdBytes),
|
|
13349
|
+
iv: uint8ArrayToBase64(iv),
|
|
13350
|
+
ciphertext: uint8ArrayToBase64(new Uint8Array(encryptedBuffer))
|
|
13351
|
+
};
|
|
13352
|
+
await this.backend.setItem(this.recordKey, JSON.stringify(record));
|
|
13353
|
+
this.cachedPassphrase = devicePassphrase;
|
|
13354
|
+
} finally {
|
|
13355
|
+
zeroizeBytes(passphraseBytes);
|
|
13356
|
+
zeroizeBytes(devicePassphraseBytes);
|
|
13357
|
+
}
|
|
13358
|
+
}
|
|
13359
|
+
/**
|
|
13360
|
+
* Unlock the device passphrase using the enrolled WebAuthn PRF credential
|
|
13361
|
+
*/
|
|
13362
|
+
async unlock() {
|
|
13363
|
+
if (this.cachedPassphrase) {
|
|
13364
|
+
return this.cachedPassphrase;
|
|
13365
|
+
}
|
|
13366
|
+
const rawRecord = await this.backend.getItem(this.recordKey);
|
|
13367
|
+
if (!rawRecord) {
|
|
13368
|
+
throw exports.SecretStorageException.unavailable(
|
|
13369
|
+
this.providerType,
|
|
13370
|
+
"no enrolled credential; call enroll() first"
|
|
13371
|
+
);
|
|
13372
|
+
}
|
|
13373
|
+
let record;
|
|
13374
|
+
try {
|
|
13375
|
+
record = JSON.parse(rawRecord);
|
|
13376
|
+
} catch {
|
|
13377
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted PRF record format");
|
|
13378
|
+
}
|
|
13379
|
+
const credentialIdBytes = base64UrlDecode(record.credentialId);
|
|
13380
|
+
const prfSalt = await computePrfSalt();
|
|
13381
|
+
const challenge = new Uint8Array(32);
|
|
13382
|
+
globalThis.crypto.getRandomValues(challenge);
|
|
13383
|
+
let assertion;
|
|
13384
|
+
try {
|
|
13385
|
+
assertion = await this.credentials.get({
|
|
13386
|
+
publicKey: {
|
|
13387
|
+
challenge,
|
|
13388
|
+
rpId: this.rp.id,
|
|
13389
|
+
allowCredentials: [
|
|
13390
|
+
{
|
|
13391
|
+
type: "public-key",
|
|
13392
|
+
id: credentialIdBytes
|
|
13393
|
+
}
|
|
13394
|
+
],
|
|
13395
|
+
userVerification: "required",
|
|
13396
|
+
extensions: {
|
|
13397
|
+
prf: {
|
|
13398
|
+
eval: {
|
|
13399
|
+
first: prfSalt
|
|
13400
|
+
}
|
|
13401
|
+
}
|
|
13402
|
+
}
|
|
13403
|
+
}
|
|
13404
|
+
});
|
|
13405
|
+
} catch (err) {
|
|
13406
|
+
const isNotAllowed = err instanceof Error && err.name === "NotAllowedError" || err?.name === "NotAllowedError";
|
|
13407
|
+
if (isNotAllowed) {
|
|
13408
|
+
throw exports.SecretStorageException.unavailable(
|
|
13409
|
+
this.providerType,
|
|
13410
|
+
"authenticator refused or credential missing"
|
|
13411
|
+
);
|
|
13412
|
+
}
|
|
13413
|
+
throw err;
|
|
13414
|
+
}
|
|
13415
|
+
if (!assertion) {
|
|
13416
|
+
throw exports.SecretStorageException.unavailable(
|
|
13417
|
+
this.providerType,
|
|
13418
|
+
"authenticator refused or credential missing"
|
|
13419
|
+
);
|
|
13420
|
+
}
|
|
13421
|
+
const extResults = assertion?.getClientExtensionResults?.();
|
|
13422
|
+
const firstOutput = extResults?.prf?.results?.first;
|
|
13423
|
+
if (!firstOutput) {
|
|
13424
|
+
throw exports.SecretStorageException.unavailable(
|
|
13425
|
+
this.providerType,
|
|
13426
|
+
"authenticator returned no PRF result"
|
|
13427
|
+
);
|
|
13428
|
+
}
|
|
13429
|
+
const prfBytes = toUint8Array(firstOutput);
|
|
13430
|
+
const kek = await deriveKekFromPrf(prfBytes, prfSalt);
|
|
13431
|
+
const iv = base64ToUint8Array(record.iv);
|
|
13432
|
+
const ciphertext = base64ToUint8Array(record.ciphertext);
|
|
13433
|
+
try {
|
|
13434
|
+
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
13435
|
+
{
|
|
13436
|
+
name: "AES-GCM",
|
|
13437
|
+
iv
|
|
13438
|
+
},
|
|
13439
|
+
kek,
|
|
13440
|
+
ciphertext
|
|
13441
|
+
);
|
|
13442
|
+
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
13443
|
+
try {
|
|
13444
|
+
this.cachedPassphrase = textDecoder2.decode(decryptedBytes);
|
|
13445
|
+
return this.cachedPassphrase;
|
|
13446
|
+
} finally {
|
|
13447
|
+
zeroizeBytes(decryptedBytes);
|
|
13448
|
+
}
|
|
13449
|
+
} catch {
|
|
13450
|
+
throw exports.SecretStorageException.decryptionFailed(
|
|
13451
|
+
"wrapped device passphrase failed authentication under the enrolled credential"
|
|
13452
|
+
);
|
|
13453
|
+
}
|
|
13454
|
+
}
|
|
13455
|
+
/**
|
|
13456
|
+
* Lock the provider by clearing cached passphrase material
|
|
13457
|
+
*/
|
|
13458
|
+
lock() {
|
|
13459
|
+
this.cachedPassphrase = void 0;
|
|
13460
|
+
}
|
|
13461
|
+
/**
|
|
13462
|
+
* Unenroll the current credential, removing the stored PRF record and clearing cached passphrase
|
|
13463
|
+
*/
|
|
13464
|
+
async unenroll() {
|
|
13465
|
+
this.lock();
|
|
13466
|
+
await this.backend.removeItem(this.recordKey);
|
|
13467
|
+
}
|
|
13468
|
+
async storeSecret(bundleHash, secret, options) {
|
|
13469
|
+
if (!bundleHash) {
|
|
13470
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
13471
|
+
}
|
|
13472
|
+
if (!secret) {
|
|
13473
|
+
throw new exports.SecretStorageException("Secret cannot be empty");
|
|
13474
|
+
}
|
|
13475
|
+
if (options?.passphrase) {
|
|
13476
|
+
throw new exports.SecretStorageException(
|
|
13477
|
+
"WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
|
|
13478
|
+
);
|
|
13479
|
+
}
|
|
13480
|
+
if (!options?.recoveryPassphrase && !options?.allowUnrecoverable) {
|
|
13481
|
+
throw exports.SecretStorageException.validationError(
|
|
13482
|
+
"Recovery passphrase required for non-exportable hardware key unless allowUnrecoverable is true"
|
|
13483
|
+
);
|
|
13484
|
+
}
|
|
13485
|
+
const passphrase = await this.unlock();
|
|
13486
|
+
const metadata = {
|
|
13487
|
+
bundleHash,
|
|
13488
|
+
label: options?.label,
|
|
13489
|
+
createdAt: Date.now(),
|
|
13490
|
+
hardwareBacked: false,
|
|
13491
|
+
providerType: this.providerType
|
|
13492
|
+
};
|
|
13493
|
+
const payload = await sealEnvelope(secret, passphrase, metadata);
|
|
13494
|
+
await this.backend.setItem(`${KEY_PREFIX2}${bundleHash}`, JSON.stringify(payload));
|
|
13495
|
+
if (options?.recoveryPassphrase) {
|
|
13496
|
+
const recoveryMetadata = {
|
|
13497
|
+
bundleHash,
|
|
13498
|
+
label: options?.label,
|
|
13499
|
+
createdAt: Date.now(),
|
|
13500
|
+
hardwareBacked: false,
|
|
13501
|
+
providerType: "webcrypto-aes-gcm"
|
|
13502
|
+
};
|
|
13503
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
13504
|
+
await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
|
|
13505
|
+
}
|
|
13506
|
+
}
|
|
13507
|
+
async retrieveSecret(bundleHash, options) {
|
|
13508
|
+
if (options?.passphrase) {
|
|
13509
|
+
throw new exports.SecretStorageException(
|
|
13510
|
+
"WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
|
|
13511
|
+
);
|
|
13512
|
+
}
|
|
13513
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
|
|
13514
|
+
if (!raw) {
|
|
13515
|
+
return null;
|
|
13516
|
+
}
|
|
13517
|
+
let payload;
|
|
13518
|
+
try {
|
|
13519
|
+
payload = JSON.parse(raw);
|
|
13520
|
+
} catch {
|
|
13521
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
13522
|
+
}
|
|
13523
|
+
const passphrase = await this.unlock();
|
|
13524
|
+
try {
|
|
13525
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
13526
|
+
try {
|
|
13527
|
+
return textDecoder2.decode(decryptedBytes);
|
|
13528
|
+
} finally {
|
|
13529
|
+
zeroizeBytes(decryptedBytes);
|
|
13530
|
+
}
|
|
13531
|
+
} catch (err) {
|
|
13532
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13533
|
+
throw err;
|
|
13534
|
+
}
|
|
13535
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13536
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
13537
|
+
}
|
|
13538
|
+
}
|
|
13539
|
+
async withSecret(bundleHash, fn, options) {
|
|
13540
|
+
if (options?.passphrase) {
|
|
13541
|
+
throw new exports.SecretStorageException(
|
|
13542
|
+
"WebAuthnPrfSecretStorageProvider derives its passphrase from the authenticator; options.passphrase is not accepted"
|
|
13543
|
+
);
|
|
13544
|
+
}
|
|
13545
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
|
|
13546
|
+
if (!raw) {
|
|
13547
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
13548
|
+
}
|
|
13549
|
+
let payload;
|
|
13550
|
+
try {
|
|
13551
|
+
payload = JSON.parse(raw);
|
|
13552
|
+
} catch {
|
|
13553
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
13554
|
+
}
|
|
13555
|
+
const passphrase = await this.unlock();
|
|
13556
|
+
try {
|
|
13557
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
13558
|
+
return await withSecureBytes(decryptedBytes, async (bytes) => {
|
|
13559
|
+
const secretString = textDecoder2.decode(bytes);
|
|
13560
|
+
return await fn(secretString);
|
|
13561
|
+
});
|
|
13562
|
+
} catch (err) {
|
|
13563
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13564
|
+
throw err;
|
|
13565
|
+
}
|
|
13566
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13567
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
13568
|
+
}
|
|
13569
|
+
}
|
|
13570
|
+
async deleteSecret(bundleHash) {
|
|
13571
|
+
const key = `${KEY_PREFIX2}${bundleHash}`;
|
|
13572
|
+
const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
|
|
13573
|
+
const result = await this.backend.removeItem(key);
|
|
13574
|
+
await this.backend.removeItem(recoveryKey);
|
|
13575
|
+
return result !== false;
|
|
13576
|
+
}
|
|
13577
|
+
async hasSecret(bundleHash) {
|
|
13578
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX2}${bundleHash}`);
|
|
13579
|
+
return raw !== null;
|
|
13580
|
+
}
|
|
13581
|
+
async listSecrets() {
|
|
13582
|
+
const keys = await this.backend.keys();
|
|
13583
|
+
const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX2) && !k.startsWith(RECOVERY_KEY_PREFIX));
|
|
13584
|
+
const results = [];
|
|
13585
|
+
for (const key of matchingKeys) {
|
|
13586
|
+
const raw = await this.backend.getItem(key);
|
|
13587
|
+
if (raw) {
|
|
13588
|
+
try {
|
|
13589
|
+
const payload = JSON.parse(raw);
|
|
13590
|
+
if (payload.metadata) {
|
|
13591
|
+
results.push(payload.metadata);
|
|
13592
|
+
}
|
|
13593
|
+
} catch {
|
|
13594
|
+
}
|
|
13595
|
+
}
|
|
13596
|
+
}
|
|
13597
|
+
return results;
|
|
13598
|
+
}
|
|
13599
|
+
/**
|
|
13600
|
+
* Recover a secret using its recovery envelope and re-enroll it under a fresh WebAuthn PRF credential
|
|
13601
|
+
*/
|
|
13602
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
13603
|
+
if (!bundleHash) {
|
|
13604
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
13605
|
+
}
|
|
13606
|
+
if (!recoveryPassphrase) {
|
|
13607
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
13608
|
+
}
|
|
13609
|
+
const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
|
|
13610
|
+
if (!raw) {
|
|
13611
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
13612
|
+
}
|
|
13613
|
+
let payload;
|
|
13614
|
+
try {
|
|
13615
|
+
payload = JSON.parse(raw);
|
|
13616
|
+
} catch {
|
|
13617
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
13618
|
+
}
|
|
13619
|
+
let decryptedBytes;
|
|
13620
|
+
try {
|
|
13621
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
13622
|
+
} catch (err) {
|
|
13623
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13624
|
+
throw err;
|
|
13625
|
+
}
|
|
13626
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13627
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
13628
|
+
}
|
|
13629
|
+
let secretStr;
|
|
13630
|
+
try {
|
|
13631
|
+
secretStr = textDecoder2.decode(decryptedBytes);
|
|
13632
|
+
} finally {
|
|
13633
|
+
zeroizeBytes(decryptedBytes);
|
|
13634
|
+
}
|
|
13635
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
13636
|
+
...options,
|
|
13637
|
+
recoveryPassphrase
|
|
13638
|
+
});
|
|
13639
|
+
}
|
|
13640
|
+
};
|
|
13641
|
+
|
|
13642
|
+
// src/storage/NonExtractableKeySecretStorageProvider.ts
|
|
13643
|
+
init_SecretStorageException();
|
|
13644
|
+
var KEY_PREFIX3 = SECRET_KEY_PREFIX;
|
|
13645
|
+
var GCM_IV_LENGTH3 = 12;
|
|
13646
|
+
var textEncoder4 = new TextEncoder();
|
|
13647
|
+
var textDecoder3 = new TextDecoder();
|
|
13648
|
+
var MemoryKeyStore = class {
|
|
13649
|
+
keys = /* @__PURE__ */ new Map();
|
|
13650
|
+
async get(name) {
|
|
13651
|
+
return this.keys.get(name);
|
|
13652
|
+
}
|
|
13653
|
+
async put(name, key) {
|
|
13654
|
+
this.keys.set(name, key);
|
|
13655
|
+
}
|
|
13656
|
+
async delete(name) {
|
|
13657
|
+
return this.keys.delete(name);
|
|
13658
|
+
}
|
|
13659
|
+
};
|
|
13660
|
+
var IndexedDbKeyStore = class {
|
|
13661
|
+
dbName;
|
|
13662
|
+
storeName = "keys";
|
|
13663
|
+
constructor(dbName = "knishio-secret-storage") {
|
|
13664
|
+
this.dbName = dbName;
|
|
13665
|
+
}
|
|
13666
|
+
// Executor form intentionally retained for browser runtime compatibility with ES2022 / browsers without Promise.withResolvers polyfill
|
|
13667
|
+
async getDb() {
|
|
13668
|
+
if (typeof globalThis.indexedDB === "undefined") {
|
|
13669
|
+
throw exports.SecretStorageException.unavailable(
|
|
13670
|
+
"webcrypto-nonextractable",
|
|
13671
|
+
"IndexedDB is not available"
|
|
13672
|
+
);
|
|
13673
|
+
}
|
|
13674
|
+
return new Promise((resolve, reject) => {
|
|
13675
|
+
const request = globalThis.indexedDB.open(this.dbName, 1);
|
|
13676
|
+
request.onupgradeneeded = () => {
|
|
13677
|
+
const db = request.result;
|
|
13678
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
13679
|
+
db.createObjectStore(this.storeName);
|
|
13680
|
+
}
|
|
13681
|
+
};
|
|
13682
|
+
request.onsuccess = () => resolve(request.result);
|
|
13683
|
+
request.onerror = () => reject(request.error);
|
|
13684
|
+
});
|
|
13685
|
+
}
|
|
13686
|
+
async get(name) {
|
|
13687
|
+
const db = await this.getDb();
|
|
13688
|
+
return new Promise((resolve, reject) => {
|
|
13689
|
+
const tx = db.transaction(this.storeName, "readonly");
|
|
13690
|
+
const store = tx.objectStore(this.storeName);
|
|
13691
|
+
const request = store.get(name);
|
|
13692
|
+
request.onsuccess = () => resolve(request.result);
|
|
13693
|
+
request.onerror = () => reject(request.error);
|
|
13694
|
+
});
|
|
13695
|
+
}
|
|
13696
|
+
async put(name, key) {
|
|
13697
|
+
const db = await this.getDb();
|
|
13698
|
+
return new Promise((resolve, reject) => {
|
|
13699
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
13700
|
+
const store = tx.objectStore(this.storeName);
|
|
13701
|
+
const request = store.put(key, name);
|
|
13702
|
+
request.onsuccess = () => resolve();
|
|
13703
|
+
request.onerror = () => reject(request.error);
|
|
13704
|
+
});
|
|
13705
|
+
}
|
|
13706
|
+
async delete(name) {
|
|
13707
|
+
const db = await this.getDb();
|
|
13708
|
+
return new Promise((resolve, reject) => {
|
|
13709
|
+
const tx = db.transaction(this.storeName, "readwrite");
|
|
13710
|
+
const store = tx.objectStore(this.storeName);
|
|
13711
|
+
const request = store.delete(name);
|
|
13712
|
+
request.onsuccess = () => resolve(true);
|
|
13713
|
+
request.onerror = () => reject(request.error);
|
|
13714
|
+
});
|
|
13715
|
+
}
|
|
13716
|
+
};
|
|
13717
|
+
var NonExtractableKeySecretStorageProvider = class {
|
|
13718
|
+
providerType = "webcrypto-nonextractable";
|
|
13719
|
+
backend;
|
|
13720
|
+
keyStore;
|
|
13721
|
+
alias;
|
|
13722
|
+
cachedPassphrase;
|
|
13723
|
+
constructor(options) {
|
|
13724
|
+
this.backend = options.backend;
|
|
13725
|
+
this.keyStore = options.keyStore ?? new IndexedDbKeyStore();
|
|
13726
|
+
this.alias = options.alias ?? "default";
|
|
13727
|
+
}
|
|
13728
|
+
get recordKey() {
|
|
13729
|
+
return `knishio:kek:webcrypto-nonextractable:${this.alias}`;
|
|
13730
|
+
}
|
|
13731
|
+
get kekStoreKey() {
|
|
13732
|
+
return `knishio:kek:${this.alias}`;
|
|
13733
|
+
}
|
|
13734
|
+
isHardwareBacked() {
|
|
13735
|
+
return false;
|
|
13736
|
+
}
|
|
13737
|
+
async isAvailable() {
|
|
13738
|
+
return typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined";
|
|
13739
|
+
}
|
|
13740
|
+
/**
|
|
13741
|
+
* Unlock or initialize the device passphrase using the non-extractable KEK
|
|
13742
|
+
*/
|
|
13743
|
+
async unlock() {
|
|
13744
|
+
if (this.cachedPassphrase) {
|
|
13745
|
+
return this.cachedPassphrase;
|
|
13746
|
+
}
|
|
13747
|
+
if (!await this.isAvailable()) {
|
|
13748
|
+
throw exports.SecretStorageException.unavailable(
|
|
13749
|
+
this.providerType,
|
|
13750
|
+
"WebCrypto API is not available"
|
|
13751
|
+
);
|
|
13752
|
+
}
|
|
13753
|
+
const rawRecord = await this.backend.getItem(this.recordKey);
|
|
13754
|
+
if (!rawRecord) {
|
|
13755
|
+
let kek2 = await this.keyStore.get(this.kekStoreKey);
|
|
13756
|
+
if (!kek2) {
|
|
13757
|
+
kek2 = await globalThis.crypto.subtle.generateKey(
|
|
13758
|
+
{ name: "AES-GCM", length: 256 },
|
|
13759
|
+
false,
|
|
13760
|
+
["encrypt", "decrypt"]
|
|
13761
|
+
);
|
|
13762
|
+
await this.keyStore.put(this.kekStoreKey, kek2);
|
|
13763
|
+
}
|
|
13764
|
+
const devicePassphraseBytes = new Uint8Array(32);
|
|
13765
|
+
globalThis.crypto.getRandomValues(devicePassphraseBytes);
|
|
13766
|
+
const devicePassphrase = uint8ArrayToBase64(devicePassphraseBytes);
|
|
13767
|
+
const iv2 = new Uint8Array(GCM_IV_LENGTH3);
|
|
13768
|
+
globalThis.crypto.getRandomValues(iv2);
|
|
13769
|
+
const passphraseBytes = textEncoder4.encode(devicePassphrase);
|
|
13770
|
+
try {
|
|
13771
|
+
const encryptedBuffer = await globalThis.crypto.subtle.encrypt(
|
|
13772
|
+
{
|
|
13773
|
+
name: "AES-GCM",
|
|
13774
|
+
iv: iv2
|
|
13775
|
+
},
|
|
13776
|
+
kek2,
|
|
13777
|
+
passphraseBytes
|
|
13778
|
+
);
|
|
13779
|
+
const record2 = {
|
|
13780
|
+
version: 1,
|
|
13781
|
+
iv: uint8ArrayToBase64(iv2),
|
|
13782
|
+
ciphertext: uint8ArrayToBase64(new Uint8Array(encryptedBuffer))
|
|
13783
|
+
};
|
|
13784
|
+
await this.backend.setItem(this.recordKey, JSON.stringify(record2));
|
|
13785
|
+
this.cachedPassphrase = devicePassphrase;
|
|
13786
|
+
return devicePassphrase;
|
|
13787
|
+
} finally {
|
|
13788
|
+
zeroizeBytes(passphraseBytes);
|
|
13789
|
+
zeroizeBytes(devicePassphraseBytes);
|
|
13790
|
+
}
|
|
13791
|
+
}
|
|
13792
|
+
let record;
|
|
13793
|
+
try {
|
|
13794
|
+
record = JSON.parse(rawRecord);
|
|
13795
|
+
} catch {
|
|
13796
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted key record format");
|
|
13797
|
+
}
|
|
13798
|
+
const kek = await this.keyStore.get(this.kekStoreKey);
|
|
13799
|
+
if (!kek) {
|
|
13800
|
+
throw exports.SecretStorageException.unavailable(
|
|
13801
|
+
this.providerType,
|
|
13802
|
+
`no non-extractable key found for alias '${this.alias}'`
|
|
13803
|
+
);
|
|
13804
|
+
}
|
|
13805
|
+
const iv = base64ToUint8Array(record.iv);
|
|
13806
|
+
const ciphertext = base64ToUint8Array(record.ciphertext);
|
|
13807
|
+
try {
|
|
13808
|
+
const decryptedBuffer = await globalThis.crypto.subtle.decrypt(
|
|
13809
|
+
{
|
|
13810
|
+
name: "AES-GCM",
|
|
13811
|
+
iv
|
|
13812
|
+
},
|
|
13813
|
+
kek,
|
|
13814
|
+
ciphertext
|
|
13815
|
+
);
|
|
13816
|
+
const decryptedBytes = new Uint8Array(decryptedBuffer);
|
|
13817
|
+
try {
|
|
13818
|
+
this.cachedPassphrase = textDecoder3.decode(decryptedBytes);
|
|
13819
|
+
return this.cachedPassphrase;
|
|
13820
|
+
} finally {
|
|
13821
|
+
zeroizeBytes(decryptedBytes);
|
|
13822
|
+
}
|
|
13823
|
+
} catch {
|
|
13824
|
+
throw exports.SecretStorageException.decryptionFailed(
|
|
13825
|
+
"wrapped device passphrase failed authentication under non-extractable key"
|
|
13826
|
+
);
|
|
13827
|
+
}
|
|
13828
|
+
}
|
|
13829
|
+
/**
|
|
13830
|
+
* Lock the provider by clearing cached passphrase material
|
|
13831
|
+
*/
|
|
13832
|
+
lock() {
|
|
13833
|
+
this.cachedPassphrase = void 0;
|
|
13834
|
+
}
|
|
13835
|
+
/**
|
|
13836
|
+
* Unenroll the non-extractable key, removing the stored wrapped record and KEK
|
|
13837
|
+
*/
|
|
13838
|
+
async unenroll() {
|
|
13839
|
+
this.lock();
|
|
13840
|
+
await this.backend.removeItem(this.recordKey);
|
|
13841
|
+
await this.keyStore.delete(this.kekStoreKey);
|
|
13842
|
+
}
|
|
13843
|
+
async storeSecret(bundleHash, secret, options) {
|
|
13844
|
+
if (!bundleHash) {
|
|
13845
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
13846
|
+
}
|
|
13847
|
+
if (!secret) {
|
|
13848
|
+
throw new exports.SecretStorageException("Secret cannot be empty");
|
|
13849
|
+
}
|
|
13850
|
+
if (options?.passphrase) {
|
|
13851
|
+
throw new exports.SecretStorageException(
|
|
13852
|
+
"NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
|
|
13853
|
+
);
|
|
13854
|
+
}
|
|
13855
|
+
if (!options?.recoveryPassphrase && !options?.allowUnrecoverable) {
|
|
13856
|
+
throw exports.SecretStorageException.validationError(
|
|
13857
|
+
"Recovery passphrase required for non-exportable hardware key unless allowUnrecoverable is true"
|
|
13858
|
+
);
|
|
13859
|
+
}
|
|
13860
|
+
const passphrase = await this.unlock();
|
|
13861
|
+
const metadata = {
|
|
13862
|
+
bundleHash,
|
|
13863
|
+
label: options?.label,
|
|
13864
|
+
createdAt: Date.now(),
|
|
13865
|
+
hardwareBacked: false,
|
|
13866
|
+
providerType: this.providerType
|
|
13867
|
+
};
|
|
13868
|
+
const payload = await sealEnvelope(secret, passphrase, metadata);
|
|
13869
|
+
await this.backend.setItem(`${KEY_PREFIX3}${bundleHash}`, JSON.stringify(payload));
|
|
13870
|
+
if (options?.recoveryPassphrase) {
|
|
13871
|
+
const recoveryMetadata = {
|
|
13872
|
+
bundleHash,
|
|
13873
|
+
label: options?.label,
|
|
13874
|
+
createdAt: Date.now(),
|
|
13875
|
+
hardwareBacked: false,
|
|
13876
|
+
providerType: "webcrypto-aes-gcm"
|
|
13877
|
+
};
|
|
13878
|
+
const recoveryPayload = await sealEnvelope(secret, options.recoveryPassphrase, recoveryMetadata);
|
|
13879
|
+
await this.backend.setItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`, JSON.stringify(recoveryPayload));
|
|
13880
|
+
}
|
|
13881
|
+
}
|
|
13882
|
+
async retrieveSecret(bundleHash, options) {
|
|
13883
|
+
if (options?.passphrase) {
|
|
13884
|
+
throw new exports.SecretStorageException(
|
|
13885
|
+
"NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
|
|
13886
|
+
);
|
|
13887
|
+
}
|
|
13888
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
|
|
13889
|
+
if (!raw) {
|
|
13890
|
+
return null;
|
|
13891
|
+
}
|
|
13892
|
+
let payload;
|
|
13893
|
+
try {
|
|
13894
|
+
payload = JSON.parse(raw);
|
|
13895
|
+
} catch {
|
|
13896
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
13897
|
+
}
|
|
13898
|
+
const passphrase = await this.unlock();
|
|
13899
|
+
try {
|
|
13900
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
13901
|
+
try {
|
|
13902
|
+
return textDecoder3.decode(decryptedBytes);
|
|
13903
|
+
} finally {
|
|
13904
|
+
zeroizeBytes(decryptedBytes);
|
|
13905
|
+
}
|
|
13906
|
+
} catch (err) {
|
|
13907
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13908
|
+
throw err;
|
|
13909
|
+
}
|
|
13910
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13911
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
13912
|
+
}
|
|
13913
|
+
}
|
|
13914
|
+
async withSecret(bundleHash, fn, options) {
|
|
13915
|
+
if (options?.passphrase) {
|
|
13916
|
+
throw new exports.SecretStorageException(
|
|
13917
|
+
"NonExtractableKeySecretStorageProvider derives its passphrase from the non-extractable device key; options.passphrase is not accepted"
|
|
13918
|
+
);
|
|
13919
|
+
}
|
|
13920
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
|
|
13921
|
+
if (!raw) {
|
|
13922
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
13923
|
+
}
|
|
13924
|
+
let payload;
|
|
13925
|
+
try {
|
|
13926
|
+
payload = JSON.parse(raw);
|
|
13927
|
+
} catch {
|
|
13928
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted payload format");
|
|
13929
|
+
}
|
|
13930
|
+
const passphrase = await this.unlock();
|
|
13931
|
+
try {
|
|
13932
|
+
const decryptedBytes = await openEnvelope(payload, passphrase);
|
|
13933
|
+
return await withSecureBytes(decryptedBytes, async (bytes) => {
|
|
13934
|
+
const secretString = textDecoder3.decode(bytes);
|
|
13935
|
+
return await fn(secretString);
|
|
13936
|
+
});
|
|
13937
|
+
} catch (err) {
|
|
13938
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13939
|
+
throw err;
|
|
13940
|
+
}
|
|
13941
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
13942
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
13943
|
+
}
|
|
13944
|
+
}
|
|
13945
|
+
async deleteSecret(bundleHash) {
|
|
13946
|
+
const key = `${KEY_PREFIX3}${bundleHash}`;
|
|
13947
|
+
const recoveryKey = `${RECOVERY_KEY_PREFIX}${bundleHash}`;
|
|
13948
|
+
const result = await this.backend.removeItem(key);
|
|
13949
|
+
await this.backend.removeItem(recoveryKey);
|
|
13950
|
+
return result !== false;
|
|
13951
|
+
}
|
|
13952
|
+
async hasSecret(bundleHash) {
|
|
13953
|
+
const raw = await this.backend.getItem(`${KEY_PREFIX3}${bundleHash}`);
|
|
13954
|
+
return raw !== null;
|
|
13955
|
+
}
|
|
13956
|
+
async listSecrets() {
|
|
13957
|
+
const keys = await this.backend.keys();
|
|
13958
|
+
const matchingKeys = keys.filter((k) => k.startsWith(KEY_PREFIX3) && !k.startsWith(RECOVERY_KEY_PREFIX));
|
|
13959
|
+
const results = [];
|
|
13960
|
+
for (const key of matchingKeys) {
|
|
13961
|
+
const raw = await this.backend.getItem(key);
|
|
13962
|
+
if (raw) {
|
|
13963
|
+
try {
|
|
13964
|
+
const payload = JSON.parse(raw);
|
|
13965
|
+
if (payload.metadata) {
|
|
13966
|
+
results.push(payload.metadata);
|
|
13967
|
+
}
|
|
13968
|
+
} catch {
|
|
13969
|
+
}
|
|
13970
|
+
}
|
|
13971
|
+
}
|
|
13972
|
+
return results;
|
|
13973
|
+
}
|
|
13974
|
+
/**
|
|
13975
|
+
* Recover a secret using its recovery envelope and re-enroll it under a fresh non-extractable KEK
|
|
13976
|
+
*/
|
|
13977
|
+
async recoverSecret(bundleHash, recoveryPassphrase, options) {
|
|
13978
|
+
if (!bundleHash) {
|
|
13979
|
+
throw new exports.SecretStorageException("Bundle hash cannot be empty");
|
|
13980
|
+
}
|
|
13981
|
+
if (!recoveryPassphrase) {
|
|
13982
|
+
throw new exports.SecretStorageException("Recovery passphrase cannot be empty");
|
|
13983
|
+
}
|
|
13984
|
+
const raw = await this.backend.getItem(`${RECOVERY_KEY_PREFIX}${bundleHash}`);
|
|
13985
|
+
if (!raw) {
|
|
13986
|
+
throw exports.SecretStorageException.notFound(bundleHash);
|
|
13987
|
+
}
|
|
13988
|
+
let payload;
|
|
13989
|
+
try {
|
|
13990
|
+
payload = JSON.parse(raw);
|
|
13991
|
+
} catch {
|
|
13992
|
+
throw exports.SecretStorageException.decryptionFailed("Corrupted recovery payload format");
|
|
13993
|
+
}
|
|
13994
|
+
let decryptedBytes;
|
|
13995
|
+
try {
|
|
13996
|
+
decryptedBytes = await openEnvelope(payload, recoveryPassphrase);
|
|
13997
|
+
} catch (err) {
|
|
13998
|
+
if (err instanceof exports.SecretStorageException) {
|
|
13999
|
+
throw err;
|
|
14000
|
+
}
|
|
14001
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
14002
|
+
throw exports.SecretStorageException.decryptionFailed(msg);
|
|
14003
|
+
}
|
|
14004
|
+
let secretStr;
|
|
14005
|
+
try {
|
|
14006
|
+
secretStr = textDecoder3.decode(decryptedBytes);
|
|
14007
|
+
} finally {
|
|
14008
|
+
zeroizeBytes(decryptedBytes);
|
|
14009
|
+
}
|
|
14010
|
+
await this.storeSecret(bundleHash, secretStr, {
|
|
14011
|
+
...options,
|
|
14012
|
+
recoveryPassphrase
|
|
14013
|
+
});
|
|
14014
|
+
}
|
|
12687
14015
|
};
|
|
12688
14016
|
|
|
12689
14017
|
// src/storage/index.ts
|
|
@@ -12694,15 +14022,14 @@ function createDefaultSecretStorage(options = {}) {
|
|
|
12694
14022
|
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined") {
|
|
12695
14023
|
return new WebCryptoSecretStorageProvider({
|
|
12696
14024
|
backend: options.backend,
|
|
12697
|
-
defaultPassphrase: options.defaultPassphrase
|
|
12698
|
-
hardwareBacked: options.hardwareBacked
|
|
14025
|
+
defaultPassphrase: options.defaultPassphrase
|
|
12699
14026
|
});
|
|
12700
14027
|
}
|
|
12701
14028
|
return new MemorySecretStorageProvider();
|
|
12702
14029
|
}
|
|
12703
14030
|
|
|
12704
14031
|
// src/index.ts
|
|
12705
|
-
var SDK_VERSION = "
|
|
14032
|
+
var SDK_VERSION = "1.1.0";
|
|
12706
14033
|
var SDK_NAME = "KnishIO-Client-TS";
|
|
12707
14034
|
var COMPATIBLE_SERVER_VERSIONS = [4, 5];
|
|
12708
14035
|
var SDK_INFO = {
|
|
@@ -12711,7 +14038,7 @@ var SDK_INFO = {
|
|
|
12711
14038
|
description: "TypeScript SDK for Knish.IO post-blockchain distributed ledger",
|
|
12712
14039
|
compatibleServerVersions: COMPATIBLE_SERVER_VERSIONS,
|
|
12713
14040
|
features: [
|
|
12714
|
-
"Post-quantum cryptography (XMSS, ML-
|
|
14041
|
+
"Post-quantum cryptography (XMSS, ML-KEM-1024)",
|
|
12715
14042
|
"Cross-platform compatibility",
|
|
12716
14043
|
"Type-safe APIs",
|
|
12717
14044
|
"DAG-based transaction processing",
|
|
@@ -12800,9 +14127,12 @@ exports.CRYPTO_CONSTANTS = CRYPTO_CONSTANTS;
|
|
|
12800
14127
|
exports.CheckMolecule = CheckMolecule;
|
|
12801
14128
|
exports.DevUtils = DevUtils;
|
|
12802
14129
|
exports.EXTENDED_COMPATIBILITY_TEST_VECTORS = EXTENDED_COMPATIBILITY_TEST_VECTORS;
|
|
14130
|
+
exports.FileStorageBackend = FileStorageBackend;
|
|
12803
14131
|
exports.GraphQLClient = GraphQLClient;
|
|
14132
|
+
exports.IndexedDbKeyStore = IndexedDbKeyStore;
|
|
12804
14133
|
exports.KnishIO = KnishIO;
|
|
12805
14134
|
exports.KnishIOClient = KnishIOClient;
|
|
14135
|
+
exports.MemoryKeyStore = MemoryKeyStore;
|
|
12806
14136
|
exports.MemorySecretStorageProvider = MemorySecretStorageProvider;
|
|
12807
14137
|
exports.MemoryStorageBackend = MemoryStorageBackend;
|
|
12808
14138
|
exports.Meta = Meta;
|
|
@@ -12817,6 +14147,7 @@ exports.MutationProposeMolecule = MutationProposeMolecule;
|
|
|
12817
14147
|
exports.MutationRequestAuthorization = MutationRequestAuthorization;
|
|
12818
14148
|
exports.MutationRequestTokens = MutationRequestTokens;
|
|
12819
14149
|
exports.MutationTransferTokens = MutationTransferTokens;
|
|
14150
|
+
exports.NonExtractableKeySecretStorageProvider = NonExtractableKeySecretStorageProvider;
|
|
12820
14151
|
exports.PolicyMeta = PolicyMeta;
|
|
12821
14152
|
exports.Query = Query;
|
|
12822
14153
|
exports.QueryAtom = QueryAtom;
|
|
@@ -12828,6 +14159,7 @@ exports.QueryMetaType = QueryMetaType;
|
|
|
12828
14159
|
exports.QueryMetaTypeViaAtom = QueryMetaTypeViaAtom;
|
|
12829
14160
|
exports.QueryWalletBundle = QueryWalletBundle;
|
|
12830
14161
|
exports.QueryWalletList = QueryWalletList;
|
|
14162
|
+
exports.RECOVERY_KEY_PREFIX = RECOVERY_KEY_PREFIX;
|
|
12831
14163
|
exports.ResponseAppendRequest = ResponseAppendRequest;
|
|
12832
14164
|
exports.ResponseAtom = ResponseAtom;
|
|
12833
14165
|
exports.ResponseBalance = ResponseBalance;
|
|
@@ -12848,9 +14180,12 @@ exports.ResponseWalletList = ResponseWalletList;
|
|
|
12848
14180
|
exports.SDK_INFO = SDK_INFO;
|
|
12849
14181
|
exports.SDK_NAME = SDK_NAME;
|
|
12850
14182
|
exports.SDK_VERSION = SDK_VERSION;
|
|
14183
|
+
exports.SECRET_KEY_PREFIX = SECRET_KEY_PREFIX;
|
|
12851
14184
|
exports.TokenUnit = TokenUnit;
|
|
12852
14185
|
exports.Wallet = Wallet;
|
|
14186
|
+
exports.WebAuthnPrfSecretStorageProvider = WebAuthnPrfSecretStorageProvider;
|
|
12853
14187
|
exports.WebCryptoSecretStorageProvider = WebCryptoSecretStorageProvider;
|
|
14188
|
+
exports.WebStorageBackend = WebStorageBackend;
|
|
12854
14189
|
exports.base64ToHex = base64ToHex;
|
|
12855
14190
|
exports.bufferToHexString = bufferToHexString;
|
|
12856
14191
|
exports.capitalize = capitalize;
|
|
@@ -12889,9 +14224,11 @@ exports.isNumeric = isNumeric;
|
|
|
12889
14224
|
exports.isPosition = isPosition2;
|
|
12890
14225
|
exports.isWalletAddress = isWalletAddress2;
|
|
12891
14226
|
exports.normalizeMolecularHash = normalizeMolecularHash;
|
|
14227
|
+
exports.openEnvelope = openEnvelope;
|
|
12892
14228
|
exports.randomString = randomString;
|
|
12893
14229
|
exports.runCompatibilityTests = runCompatibilityTests;
|
|
12894
14230
|
exports.runExtendedCompatibilityTests = runExtendedCompatibilityTests;
|
|
14231
|
+
exports.sealEnvelope = sealEnvelope;
|
|
12895
14232
|
exports.shake256 = shake256;
|
|
12896
14233
|
exports.toCamelCase = toCamelCase;
|
|
12897
14234
|
exports.toSnakeCase = toSnakeCase;
|