applesauce-signers 0.0.0-next-20250128152442

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.
@@ -0,0 +1,229 @@
1
+ /// <reference types="@types/dom-serial" />
2
+ import { getEventHash, verifyEvent } from "nostr-tools";
3
+ import { base64 } from "@scure/base";
4
+ import { randomBytes, hexToBytes, bytesToHex } from "@noble/hashes/utils";
5
+ import { Point } from "@noble/secp256k1";
6
+ import { createDefer } from "applesauce-core/promise";
7
+ import { logger } from "../logger.js";
8
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
9
+ function xOnlyToXY(p) {
10
+ return Point.fromHex(p).toHex().substring(2);
11
+ }
12
+ const utf8Decoder = new TextDecoder("utf-8");
13
+ const utf8Encoder = new TextEncoder();
14
+ /** A signer that works with [nostr-signing-device](https://github.com/lnbits/nostr-signing-device) */
15
+ export class SerialPortSigner {
16
+ log = logger.extend("SerialPortSigner");
17
+ writer = null;
18
+ pubkey;
19
+ get isConnected() {
20
+ return !!this.writer;
21
+ }
22
+ verifyEvent = verifyEvent;
23
+ nip04;
24
+ constructor() {
25
+ this.nip04 = {
26
+ encrypt: this.nip04Encrypt.bind(this),
27
+ decrypt: this.nip04Decrypt.bind(this),
28
+ };
29
+ }
30
+ lastCommand = null;
31
+ async callMethodOnDevice(method, params, opts = {}) {
32
+ if (!SerialPortSigner.SUPPORTED)
33
+ throw new Error("Serial devices are not supported");
34
+ if (!this.writer)
35
+ await this.connectToDevice(opts);
36
+ // only one command can be pending at any time
37
+ // but each will only wait 6 seconds
38
+ if (this.lastCommand)
39
+ throw new Error("Previous command to device still pending!");
40
+ const command = createDefer();
41
+ this.lastCommand = command;
42
+ // send actual command
43
+ this.sendCommand(method, params);
44
+ setTimeout(() => {
45
+ command.reject(new Error("Device timeout"));
46
+ if (this.lastCommand === command)
47
+ this.lastCommand = null;
48
+ }, 6000);
49
+ return this.lastCommand;
50
+ }
51
+ async connectToDevice({ onConnect, onDisconnect, onError, onDone }) {
52
+ let port = await window.navigator.serial.requestPort();
53
+ let reader;
54
+ const startSerialPortReading = async () => {
55
+ // reading responses
56
+ while (port && port.readable) {
57
+ const textDecoder = new window.TextDecoderStream();
58
+ port.readable.pipeTo(textDecoder.writable);
59
+ reader = textDecoder.readable.getReader();
60
+ const readStringUntil = this.readFromSerialPort(reader);
61
+ try {
62
+ while (true) {
63
+ const { value, done } = await readStringUntil("\n");
64
+ if (value) {
65
+ const { method, data } = this.parseResponse(value);
66
+ // if (method === "/log") deviceLog(data);
67
+ if (method === "/ping")
68
+ this.log("Pong");
69
+ if (SerialPortSigner.PUBLIC_METHODS.indexOf(method) === -1) {
70
+ // ignore /ping, /log responses
71
+ continue;
72
+ }
73
+ this.log("Received: ", method, data);
74
+ if (this.lastCommand) {
75
+ this.lastCommand.resolve(data);
76
+ this.lastCommand = null;
77
+ }
78
+ }
79
+ if (done) {
80
+ this.lastCommand = null;
81
+ this.writer = null;
82
+ if (onDone)
83
+ onDone();
84
+ return;
85
+ }
86
+ }
87
+ }
88
+ catch (error) {
89
+ if (error instanceof Error) {
90
+ this.writer = null;
91
+ if (onError)
92
+ onError(error);
93
+ if (this.lastCommand) {
94
+ this.lastCommand.reject(error);
95
+ this.lastCommand = null;
96
+ }
97
+ throw error;
98
+ }
99
+ }
100
+ }
101
+ };
102
+ await port.open({ baudRate: 9600 });
103
+ // this `sleep()` is a hack, I know!
104
+ // but `port.onconnect` is never called. I don't know why!
105
+ await sleep(1000);
106
+ startSerialPortReading();
107
+ const textEncoder = new window.TextEncoderStream();
108
+ textEncoder.readable.pipeTo(port.writable);
109
+ this.writer = textEncoder.writable.getWriter();
110
+ // send ping first
111
+ await this.sendCommand(SerialPortSigner.METHOD_PING);
112
+ await this.sendCommand(SerialPortSigner.METHOD_PING, [window.location.host]);
113
+ if (onConnect)
114
+ onConnect();
115
+ port.addEventListener("disconnect", () => {
116
+ this.log("Disconnected");
117
+ this.lastCommand = null;
118
+ this.writer = null;
119
+ if (onDisconnect)
120
+ onDisconnect();
121
+ });
122
+ }
123
+ async sendCommand(method, params = []) {
124
+ if (!this.writer)
125
+ return;
126
+ this.log("Send command", method, params);
127
+ const message = [method].concat(params).join(" ");
128
+ await this.writer.write(message + "\n");
129
+ }
130
+ readFromSerialPort(reader) {
131
+ let partialChunk;
132
+ let fulliness = [];
133
+ const readStringUntil = async (separator = "\n") => {
134
+ if (fulliness.length)
135
+ return { value: fulliness.shift().trim(), done: false };
136
+ const chunks = [];
137
+ if (partialChunk) {
138
+ // leftovers from previous read
139
+ chunks.push(partialChunk);
140
+ partialChunk = undefined;
141
+ }
142
+ while (true) {
143
+ const { value, done } = await reader.read();
144
+ if (value) {
145
+ const values = value.split(separator);
146
+ // found one or more separators
147
+ if (values.length > 1) {
148
+ chunks.push(values.shift()); // first element
149
+ partialChunk = values.pop(); // last element
150
+ fulliness = values; // full lines
151
+ return { value: chunks.join("").trim(), done: false };
152
+ }
153
+ chunks.push(value);
154
+ }
155
+ if (done)
156
+ return { value: chunks.join("").trim(), done: true };
157
+ }
158
+ };
159
+ return readStringUntil;
160
+ }
161
+ parseResponse(value) {
162
+ const method = value.split(" ")[0];
163
+ const data = value.substring(method.length).trim();
164
+ return { method, data };
165
+ }
166
+ // NIP-04
167
+ async nip04Encrypt(pubkey, text) {
168
+ const sharedSecretStr = await this.callMethodOnDevice(SerialPortSigner.METHOD_SHARED_SECRET, [xOnlyToXY(pubkey)]);
169
+ const sharedSecret = hexToBytes(sharedSecretStr);
170
+ let iv = Uint8Array.from(randomBytes(16));
171
+ let plaintext = utf8Encoder.encode(text);
172
+ let cryptoKey = await crypto.subtle.importKey("raw", sharedSecret, { name: "AES-CBC" }, false, ["encrypt"]);
173
+ let ciphertext = await crypto.subtle.encrypt({ name: "AES-CBC", iv }, cryptoKey, plaintext);
174
+ let ctb64 = base64.encode(new Uint8Array(ciphertext));
175
+ let ivb64 = base64.encode(new Uint8Array(iv.buffer));
176
+ return `${ctb64}?iv=${ivb64}`;
177
+ }
178
+ async nip04Decrypt(pubkey, data) {
179
+ let [ctb64, ivb64] = data.split("?iv=");
180
+ const sharedSecretStr = await this.callMethodOnDevice(SerialPortSigner.METHOD_SHARED_SECRET, [xOnlyToXY(pubkey)]);
181
+ const sharedSecret = hexToBytes(sharedSecretStr);
182
+ let cryptoKey = await crypto.subtle.importKey("raw", sharedSecret, { name: "AES-CBC" }, false, ["decrypt"]);
183
+ let ciphertext = base64.decode(ctb64);
184
+ let iv = base64.decode(ivb64);
185
+ let plaintext = await crypto.subtle.decrypt({ name: "AES-CBC", iv }, cryptoKey, ciphertext);
186
+ let text = utf8Decoder.decode(plaintext);
187
+ return text;
188
+ }
189
+ /** Returns the public key on the device */
190
+ async getPublicKey() {
191
+ const pubkey = await this.callMethodOnDevice(SerialPortSigner.METHOD_PUBLIC_KEY, []);
192
+ this.pubkey = pubkey;
193
+ return pubkey;
194
+ }
195
+ /** Sets the secret key used on the device */
196
+ async restore(secretKey) {
197
+ await this.callMethodOnDevice(SerialPortSigner.METHOD_RESTORE, [bytesToHex(secretKey)]);
198
+ }
199
+ /** Requires the device to sign an event */
200
+ async signEvent(draft) {
201
+ const pubkey = draft.pubkey || this.pubkey;
202
+ if (!pubkey)
203
+ throw new Error("Unknown signer pubkey");
204
+ const draftWithId = { ...draft, id: getEventHash({ ...draft, pubkey }) };
205
+ const sig = await this.callMethodOnDevice(SerialPortSigner.METHOD_SIGN_MESSAGE, [draftWithId.id]);
206
+ const event = { ...draftWithId, sig, pubkey };
207
+ if (!this.verifyEvent(event))
208
+ throw new Error("Invalid signature");
209
+ return event;
210
+ }
211
+ /** Pings to device to see if the connection is open */
212
+ ping() {
213
+ this.sendCommand(SerialPortSigner.METHOD_PING, [window.location.host]);
214
+ }
215
+ // static const
216
+ static SUPPORTED = "navigator" in globalThis && !!navigator.serial;
217
+ static METHOD_PING = "/ping";
218
+ static METHOD_LOG = "/log";
219
+ static METHOD_SIGN_MESSAGE = "/sign-message";
220
+ static METHOD_SHARED_SECRET = "/shared-secret";
221
+ static METHOD_PUBLIC_KEY = "/public-key";
222
+ static METHOD_RESTORE = "/restore";
223
+ static PUBLIC_METHODS = [
224
+ SerialPortSigner.METHOD_PUBLIC_KEY,
225
+ SerialPortSigner.METHOD_SIGN_MESSAGE,
226
+ SerialPortSigner.METHOD_SHARED_SECRET,
227
+ SerialPortSigner.METHOD_RESTORE,
228
+ ];
229
+ }
@@ -0,0 +1,16 @@
1
+ import { EventTemplate } from "nostr-tools";
2
+ /** A Simple NIP-07 signer class */
3
+ export declare class SimpleSigner {
4
+ key: Uint8Array;
5
+ constructor(key?: Uint8Array);
6
+ getPublicKey(): Promise<string>;
7
+ signEvent(event: EventTemplate): Promise<import("nostr-tools").VerifiedEvent>;
8
+ nip04: {
9
+ encrypt: (pubkey: string, plaintext: string) => Promise<string>;
10
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
11
+ };
12
+ nip44: {
13
+ encrypt: (pubkey: string, plaintext: string) => Promise<string>;
14
+ decrypt: (pubkey: string, ciphertext: string) => Promise<string>;
15
+ };
16
+ }
@@ -0,0 +1,22 @@
1
+ import { finalizeEvent, generateSecretKey, getPublicKey, nip04, nip44 } from "nostr-tools";
2
+ /** A Simple NIP-07 signer class */
3
+ export class SimpleSigner {
4
+ key;
5
+ constructor(key) {
6
+ this.key = key || generateSecretKey();
7
+ }
8
+ async getPublicKey() {
9
+ return getPublicKey(this.key);
10
+ }
11
+ async signEvent(event) {
12
+ return finalizeEvent(event, this.key);
13
+ }
14
+ nip04 = {
15
+ encrypt: async (pubkey, plaintext) => nip04.encrypt(this.key, pubkey, plaintext),
16
+ decrypt: async (pubkey, ciphertext) => nip04.decrypt(this.key, pubkey, ciphertext),
17
+ };
18
+ nip44 = {
19
+ encrypt: async (pubkey, plaintext) => nip44.v2.encrypt(plaintext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
20
+ decrypt: async (pubkey, ciphertext) => nip44.v2.decrypt(ciphertext, nip44.v2.utils.getConversationKey(this.key, pubkey)),
21
+ };
22
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "applesauce-signers",
3
+ "version": "0.0.0-next-20250128152442",
4
+ "description": "Signer classes for applesauce",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "keywords": [
9
+ "nostr"
10
+ ],
11
+ "author": "hzrd149",
12
+ "license": "MIT",
13
+ "files": [
14
+ "dist",
15
+ "applesauce"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.js",
21
+ "types": "./dist/index.d.ts"
22
+ },
23
+ "./signers": {
24
+ "import": "./dist/signers/index.js",
25
+ "require": "./dist/signers/index.js",
26
+ "types": "./dist/signers/index.d.ts"
27
+ },
28
+ "./signers/*": {
29
+ "import": "./dist/signers/*.js",
30
+ "require": "./dist/signers/*.js",
31
+ "types": "./dist/signers/*.d.ts"
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "@noble/hashes": "^1.5.0",
36
+ "@noble/secp256k1": "^1.7.1",
37
+ "@scure/base": "^1.1.9",
38
+ "applesauce-core": "0.0.0-next-20250128152442",
39
+ "debug": "^4.3.7",
40
+ "nanoid": "^5.0.7",
41
+ "nostr-tools": "^2.10.3"
42
+ },
43
+ "devDependencies": {
44
+ "@types/dom-serial": "^1.0.6",
45
+ "@types/debug": "^4.1.12",
46
+ "typescript": "^5.6.3",
47
+ "vitest": "^2.1.8"
48
+ },
49
+ "funding": {
50
+ "type": "lightning",
51
+ "url": "lightning:nostrudel@geyser.fund"
52
+ },
53
+ "scripts": {
54
+ "build": "tsc",
55
+ "watch:build": "tsc --watch > /dev/null",
56
+ "test": "vitest run --passWithNoTests",
57
+ "watch:test": "vitest"
58
+ }
59
+ }