Nostr + Web Crypto API #1045
Replies: 2 comments
-
Found it: w3c/webcrypto#82 Chrome is opposed to adding it, so the W3C closed the issue. 🤦 And of course @paulmillr is ten steps ahead because his comment is there. |
Beta Was this translation helpful? Give feedback.
-
Update: It's possible in JavaScript to overwrite global APIs at runtime. /** Lock a key from being accessed by `localStorage` and `sessionStorage`. */
function lockStorageKey(key: string): void {
const proto = Object.getPrototypeOf(localStorage ?? sessionStorage);
const _getItem = proto.getItem;
proto.getItem = function(_key: string) {
if (_key === key) {
throw new Error(`${_key} is locked`);
} else {
return _getItem.bind(this)(_key);
}
};
} call like this: localStorage.getItem('nostr:keys'); // returns data
lockStorageKey('nostr:keys');
localStorage.getItem('nostr:keys'); // throws Error This enabled me to create a class that reads data from the storage, locks the storage key, and then exposes a NIP-07 API. import { getPublicKey, nip19 } from 'nostr-tools';
import { NSchema as n, NostrSigner, NSecSigner } from 'nspec';
import { lockStorageKey } from 'soapbox/utils/storage';
/**
* Gets Nostr keypairs from storage and returns a `Map`-like object of signers.
* When instantiated, it will lock the storage key to prevent tampering.
* Changes to the object will sync to storage.
*/
export class NKeyStorage implements ReadonlyMap<string, NostrSigner> {
#keypairs = new Map<string, Uint8Array>();
#storage: Storage;
#storageKey: string;
constructor(storage: Storage, storageKey: string) {
this.#storage = storage;
this.#storageKey = storageKey;
const data = this.#storage.getItem(storageKey);
lockStorageKey(storageKey);
try {
const nsecs = new Set(this.#dataSchema().parse(data));
for (const nsec of nsecs) {
const { data: secretKey } = nip19.decode(nsec);
const pubkey = getPublicKey(secretKey);
this.#keypairs.set(pubkey, secretKey);
}
} catch (e) {
this.clear();
}
}
#dataSchema() {
return n.json().pipe(n.bech32('nsec').array());
}
#syncStorage() {
const secretKeys = [...this.#keypairs.values()].map(nip19.nsecEncode);
this.#storage.setItem(this.#storageKey, JSON.stringify(secretKeys));
}
get size(): number {
return this.#keypairs.size;
}
clear(): void {
this.#keypairs.clear();
this.#syncStorage();
}
delete(pubkey: string): boolean {
const result = this.#keypairs.delete(pubkey);
this.#syncStorage();
return result;
}
forEach(callbackfn: (signer: NostrSigner, pubkey: string, map: typeof this) => void, thisArg?: any): void {
for (const [pubkey] of this.#keypairs) {
const signer = this.get(pubkey);
if (signer) {
callbackfn.call(thisArg, signer, pubkey, this);
}
}
}
get(pubkey: string): NostrSigner | undefined {
const secretKey = this.#keypairs.get(pubkey);
if (secretKey) {
return new NSecSigner(secretKey);
}
}
has(pubkey: string): boolean {
return this.#keypairs.has(pubkey);
}
add(secretKey: Uint8Array): NostrSigner {
const pubkey = getPublicKey(secretKey);
this.#keypairs.set(pubkey, secretKey);
this.#syncStorage();
return this.get(pubkey)!;
}
*entries(): IterableIterator<[string, NostrSigner]> {
for (const [pubkey] of this.#keypairs) {
yield [pubkey, this.get(pubkey)!];
}
}
*keys(): IterableIterator<string> {
for (const pubkey of this.#keypairs.keys()) {
yield pubkey;
}
}
*values(): IterableIterator<NostrSigner> {
for (const pubkey of this.#keypairs.keys()) {
yield this.get(pubkey)!;
}
}
[Symbol.iterator](): IterableIterator<[string, NostrSigner]> {
return this.entries();
}
[Symbol.toStringTag] = 'NKeyStorage';
} Tested working in FireFox and Brave. You want to instantiate this class as soon as possible in your code. If you load it before any user-generated-content can be displayed, it should prevent against most XSS attacks. There's no way to circumvent it in the main thread that I can tell. I bet an attacker could still create a new WebWorker to get around it. So you would want to use a similar method to lock the WebWorker and ServiceWorker APIs after you've instantiated all the workers your application expects to create (or even whitelist worker filenames). |
Beta Was this translation helpful? Give feedback.
-
I've been exploring the best way to deal with keys in a PWA. NIP-07 browser extension is a barrier of entry, especially on mobile devices. Meanwhile,
localStorage
is not inherently secure from XSS.Enter
CryptoKey
. This is (almost) the perfect solution, because you can sign events with it AND prevent JavaScript code from accessing the key data, and you can drop it straight into IndexedDB to preserve between reloads!The flow looks something like this:
extractable
set totrue
nsec
to the user exactly once, during the key's creation.extractable
tofalse
, and throw out the old oneNow on subsequent page reloads, you have access to the
CryptoKey
object for signing events, but the data is tamper-proof. Great!Only one problem... it doesn't support secp256k1
How do we convince the W3C to add it to the spec and get browsers to implement it? It would make Bitcoin wallets in the browser viable, too.
Beta Was this translation helpful? Give feedback.
All reactions