ns-auth-sdk 1.12.2 → 1.12.4
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/dist/index.cjs +50 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +5 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +52 -117
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { finalizeEvent, getPublicKey } from "applesauce-core/helpers";
|
|
2
2
|
import * as secp256k1 from "@noble/secp256k1";
|
|
3
|
+
import { sha256 } from "@noble/hashes/sha2";
|
|
4
|
+
import { pbkdf2 } from "@noble/hashes/pbkdf2";
|
|
5
|
+
import { aesGcm } from "@noble/ciphers/aes";
|
|
6
|
+
import { randomBytes } from "@noble/secp256k1/utils";
|
|
3
7
|
|
|
4
8
|
export * from "applesauce-core"
|
|
5
9
|
|
|
@@ -171,8 +175,6 @@ const PRF_EVAL_INPUT = new TextEncoder().encode("nostr-pwk");
|
|
|
171
175
|
*/
|
|
172
176
|
async function isPrfSupported() {
|
|
173
177
|
try {
|
|
174
|
-
const cryptoObj = typeof window !== "undefined" && window.crypto || globalThis.crypto;
|
|
175
|
-
if (!cryptoObj || !cryptoObj.subtle) return false;
|
|
176
178
|
const response = await navigator.credentials.get({ publicKey: {
|
|
177
179
|
challenge: crypto.getRandomValues(new Uint8Array(32)),
|
|
178
180
|
allowCredentials: [],
|
|
@@ -256,32 +258,29 @@ async function getPrfSecret(credentialId, options) {
|
|
|
256
258
|
* password-protected private key. The private key is wrapped with a password-
|
|
257
259
|
* derived AES-GCM key and stored alongside the public key (SPKI).
|
|
258
260
|
*
|
|
259
|
-
* This
|
|
260
|
-
*
|
|
261
|
+
* This implementation uses @noble libraries for cryptographic operations,
|
|
262
|
+
* ensuring functionality even when Web Crypto API is unavailable.
|
|
261
263
|
*/
|
|
262
264
|
function toBase64(bytes) {
|
|
263
|
-
const arr = new Uint8Array(bytes);
|
|
264
265
|
let binary = "";
|
|
265
|
-
for (let i = 0; i <
|
|
266
|
+
for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]);
|
|
266
267
|
if (typeof window !== "undefined" && window.btoa) return window.btoa(binary);
|
|
267
|
-
return
|
|
268
|
+
return btoa(binary);
|
|
268
269
|
}
|
|
269
270
|
function fromBase64(b64) {
|
|
270
271
|
if (typeof window !== "undefined" && window.atob) {
|
|
271
|
-
const bin = window.atob(b64);
|
|
272
|
-
const bytes = new Uint8Array(bin.length);
|
|
273
|
-
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
274
|
-
return bytes;
|
|
275
|
-
}
|
|
276
|
-
|
|
272
|
+
const bin$1 = window.atob(b64);
|
|
273
|
+
const bytes$1 = new Uint8Array(bin$1.length);
|
|
274
|
+
for (let i = 0; i < bin$1.length; i++) bytes$1[i] = bin$1.charCodeAt(i);
|
|
275
|
+
return bytes$1;
|
|
276
|
+
}
|
|
277
|
+
const bin = atob(b64);
|
|
278
|
+
const bytes = new Uint8Array(bin.length);
|
|
279
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
|
|
280
|
+
return bytes;
|
|
277
281
|
}
|
|
278
282
|
async function checkPRFSupport() {
|
|
279
283
|
try {
|
|
280
|
-
const cryptoObj = typeof window !== "undefined" && window.crypto || globalThis.crypto;
|
|
281
|
-
if (!cryptoObj || !cryptoObj.subtle) {
|
|
282
|
-
console.log("Web Crypto API not available, falling back to password-protected key");
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
284
|
const supported = (typeof PublicKeyCredential !== "undefined" ? await PublicKeyCredential.getClientCapabilities() : null)?.["extension:prf"] === true;
|
|
286
285
|
if (supported) console.log("PRF extension is supported.");
|
|
287
286
|
else console.log("PRF extension is not supported.");
|
|
@@ -292,107 +291,43 @@ async function checkPRFSupport() {
|
|
|
292
291
|
}
|
|
293
292
|
}
|
|
294
293
|
async function generatePasswordProtectedKey(password) {
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
const publicKeySpkiBase64 = toBase64(await cryptoObj.subtle.exportKey("spki", publicKey));
|
|
304
|
-
const privateKeyJwk = await cryptoObj.subtle.exportKey("jwk", privateKey);
|
|
305
|
-
const jwkStr = JSON.stringify(privateKeyJwk);
|
|
306
|
-
const salt = cryptoObj.getRandomValues(new Uint8Array(16));
|
|
307
|
-
const pbKey = await cryptoObj.subtle.importKey("raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"]);
|
|
308
|
-
const aesKey = await cryptoObj.subtle.deriveKey({
|
|
309
|
-
name: "PBKDF2",
|
|
310
|
-
salt: salt.slice().buffer,
|
|
311
|
-
iterations: 1e5,
|
|
312
|
-
hash: "SHA-256"
|
|
313
|
-
}, pbKey, {
|
|
314
|
-
name: "AES-GCM",
|
|
315
|
-
length: 256
|
|
316
|
-
}, false, ["encrypt"]);
|
|
317
|
-
const iv = cryptoObj.getRandomValues(new Uint8Array(12));
|
|
318
|
-
const enc = new TextEncoder().encode(jwkStr);
|
|
294
|
+
const privateKey = secp256k1.utils.randomPrivateKey();
|
|
295
|
+
const publicKeySpkiBase64 = toBase64(secp256k1.getPublicKey(privateKey, true));
|
|
296
|
+
const salt = randomBytes(16);
|
|
297
|
+
const derivedKey = pbkdf2(sha256, new TextEncoder().encode(password), salt, {
|
|
298
|
+
c: 1e5,
|
|
299
|
+
dkLen: 32
|
|
300
|
+
});
|
|
301
|
+
const iv = randomBytes(12);
|
|
319
302
|
return {
|
|
320
303
|
publicKeySpkiBase64,
|
|
321
|
-
wrappedPrivateKeyBase64: toBase64(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}, aesKey, enc)),
|
|
325
|
-
saltBase64: toBase64(salt.buffer),
|
|
326
|
-
ivBase64: toBase64(iv.buffer)
|
|
304
|
+
wrappedPrivateKeyBase64: toBase64(aesGcm(derivedKey, iv).encrypt(privateKey)),
|
|
305
|
+
saltBase64: toBase64(salt),
|
|
306
|
+
ivBase64: toBase64(iv)
|
|
327
307
|
};
|
|
328
308
|
}
|
|
329
309
|
async function unwrapPasswordProtectedPrivateKey(bundle, password) {
|
|
330
|
-
const cryptoObj = typeof window !== "undefined" && window.crypto || globalThis.crypto;
|
|
331
|
-
if (!cryptoObj || !cryptoObj.subtle) throw new Error("Web Crypto API not available");
|
|
332
310
|
const salt = fromBase64(bundle.saltBase64);
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
iterations: 1e5,
|
|
338
|
-
hash: "SHA-256"
|
|
339
|
-
}, pbKey, {
|
|
340
|
-
name: "AES-GCM",
|
|
341
|
-
length: 256
|
|
342
|
-
}, false, ["decrypt"]);
|
|
311
|
+
const derivedKey = pbkdf2(sha256, new TextEncoder().encode(password), salt, {
|
|
312
|
+
c: 1e5,
|
|
313
|
+
dkLen: 32
|
|
314
|
+
});
|
|
343
315
|
const iv = fromBase64(bundle.ivBase64);
|
|
344
316
|
const ct = fromBase64(bundle.wrappedPrivateKeyBase64);
|
|
345
|
-
|
|
346
|
-
name: "AES-GCM",
|
|
347
|
-
iv: iv.slice()
|
|
348
|
-
}, aesKey, ct.slice());
|
|
349
|
-
const jwkStr = new TextDecoder().decode(jwkBytes);
|
|
350
|
-
const privateKeyJwk = JSON.parse(jwkStr);
|
|
351
|
-
await cryptoObj.subtle.importKey("jwk", privateKeyJwk, {
|
|
352
|
-
name: "ECDSA",
|
|
353
|
-
namedCurve: "P-256"
|
|
354
|
-
}, true, ["sign"]);
|
|
355
|
-
const d = privateKeyJwk.d;
|
|
356
|
-
if (!d) throw new Error("Missing private scalar in JWK");
|
|
357
|
-
return base64UrlToBytes(d);
|
|
358
|
-
}
|
|
359
|
-
function base64UrlToBytes(base64url) {
|
|
360
|
-
let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
361
|
-
const pad = base64.length % 4;
|
|
362
|
-
if (pad) base64 += "=".repeat(4 - pad);
|
|
363
|
-
if (typeof window !== "undefined" && window.atob) {
|
|
364
|
-
const binary = window.atob(base64);
|
|
365
|
-
const bytes = new Uint8Array(binary.length);
|
|
366
|
-
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
367
|
-
return bytes;
|
|
368
|
-
}
|
|
369
|
-
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
317
|
+
return aesGcm(derivedKey, iv).decrypt(ct);
|
|
370
318
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (!cryptoObj || !cryptoObj.subtle) throw new Error("Web Crypto API not available");
|
|
374
|
-
const spki = fromBase64(bundle.publicKeySpkiBase64);
|
|
375
|
-
return await cryptoObj.subtle.importKey("spki", spki, {
|
|
376
|
-
name: "ECDSA",
|
|
377
|
-
namedCurve: "P-256"
|
|
378
|
-
}, true, ["verify"]);
|
|
319
|
+
function importPublicKeyFromBundle(bundle) {
|
|
320
|
+
return fromBase64(bundle.publicKeySpkiBase64);
|
|
379
321
|
}
|
|
380
322
|
const DEFAULT_SALT = "nostr-key-derivation";
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const derivedBits = await cryptoObj.subtle.deriveBits({
|
|
387
|
-
name: "PBKDF2",
|
|
388
|
-
salt: encoder.encode(salt),
|
|
389
|
-
iterations: 1e5,
|
|
390
|
-
hash: "SHA-256"
|
|
391
|
-
}, passwordKey, 256);
|
|
392
|
-
return new Uint8Array(derivedBits);
|
|
323
|
+
function deriveNostrPrivateKey(password, salt = DEFAULT_SALT) {
|
|
324
|
+
return pbkdf2(sha256, new TextEncoder().encode(password), new TextEncoder().encode(salt), {
|
|
325
|
+
c: 1e5,
|
|
326
|
+
dkLen: 32
|
|
327
|
+
});
|
|
393
328
|
}
|
|
394
|
-
|
|
395
|
-
return getPublicKey(
|
|
329
|
+
function getPublicKeyFromPassword(password, salt = DEFAULT_SALT) {
|
|
330
|
+
return getPublicKey(deriveNostrPrivateKey(password, salt));
|
|
396
331
|
}
|
|
397
332
|
|
|
398
333
|
//#endregion
|
|
@@ -439,7 +374,7 @@ async function verifyRecoverySignature(kind0) {
|
|
|
439
374
|
if (!parseRecoveryTag(kind0.tags || [])) return false;
|
|
440
375
|
const signature = getRecoverySignature(kind0);
|
|
441
376
|
if (!signature || !kind0.pubkey) return false;
|
|
442
|
-
const messageHash = await sha256(kind0.pubkey);
|
|
377
|
+
const messageHash = await sha256$1(kind0.pubkey);
|
|
443
378
|
const signatureBytes = hexToBytes(signature);
|
|
444
379
|
const pubkeyBytes = hexToBytes(kind0.pubkey);
|
|
445
380
|
return secp256k1.verify(signatureBytes, messageHash, pubkeyBytes);
|
|
@@ -447,7 +382,7 @@ async function verifyRecoverySignature(kind0) {
|
|
|
447
382
|
return false;
|
|
448
383
|
}
|
|
449
384
|
}
|
|
450
|
-
async function sha256(message) {
|
|
385
|
+
async function sha256$1(message) {
|
|
451
386
|
const msgBuffer = new TextEncoder().encode(message);
|
|
452
387
|
const subtle = globalThis.crypto?.subtle;
|
|
453
388
|
if (!subtle) throw new Error("Web Crypto API not available");
|
|
@@ -675,7 +610,7 @@ var NosskeyManager = class {
|
|
|
675
610
|
*/
|
|
676
611
|
async createPasswordProtectedNostrKey(password, options = {}) {
|
|
677
612
|
const salt = await deriveSaltFromUsername(options.username);
|
|
678
|
-
const pubkey =
|
|
613
|
+
const pubkey = getPublicKeyFromPassword(password, salt);
|
|
679
614
|
return {
|
|
680
615
|
credentialId: bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16))),
|
|
681
616
|
pubkey,
|
|
@@ -696,7 +631,7 @@ var NosskeyManager = class {
|
|
|
696
631
|
if (isPasswordDerived) {
|
|
697
632
|
if (shouldUseCache) sk = this.#keyCache.getKey(keyInfo.credentialId);
|
|
698
633
|
if (!sk && password) {
|
|
699
|
-
sk =
|
|
634
|
+
sk = deriveNostrPrivateKey(password, keyInfo.salt);
|
|
700
635
|
if (shouldUseCache) this.#keyCache.setKey(keyInfo.credentialId, sk);
|
|
701
636
|
}
|
|
702
637
|
if (!sk) throw new Error("Password required - key not in cache. Provide password to sign.");
|
|
@@ -737,7 +672,7 @@ var NosskeyManager = class {
|
|
|
737
672
|
}
|
|
738
673
|
if (!keyInfo.passwordProtectedBundle && keyInfo.salt) {
|
|
739
674
|
if (!options.password) throw new Error("Password is required for password-derived keys");
|
|
740
|
-
return bytesToHex(
|
|
675
|
+
return bytesToHex(deriveNostrPrivateKey(options.password, keyInfo.salt));
|
|
741
676
|
}
|
|
742
677
|
let usedCredentialId = credentialId;
|
|
743
678
|
if (!usedCredentialId && keyInfo.credentialId) usedCredentialId = hexToBytes(keyInfo.credentialId);
|
|
@@ -762,8 +697,8 @@ var NosskeyManager = class {
|
|
|
762
697
|
if (keyInfo.recovery) throw new Error("Recovery already configured");
|
|
763
698
|
if (!(currentCredentialId || (keyInfo.credentialId ? hexToBytes(keyInfo.credentialId) : void 0))) throw new Error("Credential ID required");
|
|
764
699
|
const recoverySalt = bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16)));
|
|
765
|
-
const recoveryPubkey =
|
|
766
|
-
const recoverySk =
|
|
700
|
+
const recoveryPubkey = getPublicKeyFromPassword(password, recoverySalt);
|
|
701
|
+
const recoverySk = deriveNostrPrivateKey(password, recoverySalt);
|
|
767
702
|
const signature = this.#signWithKey(recoverySk, keyInfo.pubkey);
|
|
768
703
|
this.#clearKey(recoverySk);
|
|
769
704
|
const updatedKeyInfo = {
|
|
@@ -798,13 +733,13 @@ var NosskeyManager = class {
|
|
|
798
733
|
if (!keyInfo.recovery) throw new Error("No recovery key configured");
|
|
799
734
|
if (!newCredentialId) throw new Error("New credential ID is required for recovery");
|
|
800
735
|
const { recoveryPubkey, recoverySalt } = keyInfo.recovery;
|
|
801
|
-
if (
|
|
736
|
+
if (getPublicKeyFromPassword(password, recoverySalt) !== recoveryPubkey) throw new Error("Invalid recovery password");
|
|
802
737
|
const { secret: newSk } = await getPrfSecret(newCredentialId, this.#prfOptions);
|
|
803
738
|
const newPubkey = getPublicKey(newSk);
|
|
804
739
|
this.#clearKey(newSk);
|
|
805
740
|
const newRecoverySalt = bytesToHex((typeof window !== "undefined" ? window.crypto : globalThis.crypto).getRandomValues(new Uint8Array(16)));
|
|
806
|
-
const newRecoveryPubkey =
|
|
807
|
-
const recoverySk =
|
|
741
|
+
const newRecoveryPubkey = getPublicKeyFromPassword(password, newRecoverySalt);
|
|
742
|
+
const recoverySk = deriveNostrPrivateKey(password, recoverySalt);
|
|
808
743
|
const signature = this.#signWithKey(recoverySk, newPubkey);
|
|
809
744
|
this.#clearKey(recoverySk);
|
|
810
745
|
const updatedKeyInfo = {
|