@neuraiproject/neurai-message 0.8.0 → 0.9.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 +40 -8
- package/dist/NeuraiMessage.global.js +12837 -0
- package/dist/browser.mjs +12810 -0
- package/dist/index.cjs +2901 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +2890 -0
- package/dist/main.js +10421 -16548
- package/dist/module.js +10783 -16914
- package/dist/src/browser-shims.d.ts +1 -0
- package/dist/src/browser.d.ts +1 -0
- package/dist/src/core.d.ts +6 -0
- package/dist/src/global.d.ts +8 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/legacy-message.d.ts +6 -0
- package/index.ts +1 -256
- package/package.json +33 -19
- package/src/browser-shims.ts +9 -0
- package/src/browser.ts +1 -0
- package/src/core.ts +257 -0
- package/src/global.ts +15 -0
- package/src/index.ts +1 -0
- package/src/legacy-message.ts +140 -0
- package/{test.js → test.spec.js} +5 -7
- package/tsconfig.json +6 -3
- package/vitest.config.mjs +7 -0
- package/dist/main.js.map +0 -1
- package/dist/module.js.map +0 -1
- package/dist/types.d.ts +0 -8
- package/dist/types.d.ts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./core";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** returns a base64 encoded string representation of the legacy signature */
|
|
2
|
+
export declare function sign(message: string, privateKey: Uint8Array, compressed?: boolean): string;
|
|
3
|
+
export declare function signPQMessage(message: string, privateKey: Uint8Array, publicKey: Uint8Array): string;
|
|
4
|
+
export declare function verifyLegacyMessage(message: string, address: string, signature: string | Uint8Array): any;
|
|
5
|
+
export declare function verifyPQMessage(message: string, address: string, signature: string | Uint8Array): any;
|
|
6
|
+
export declare function verifyMessage(message: string, address: string, signature: string | Uint8Array): any;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./core";
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { Buffer } from "buffer";
|
|
4
|
+
export declare function magicHash(message: string | Buffer, messagePrefix: string | Buffer): any;
|
|
5
|
+
export declare function signLegacyMessage(message: string, privateKey: Uint8Array, compressed: boolean, messagePrefix: string | Buffer): Buffer;
|
|
6
|
+
export declare function verifyLegacyCompactMessage(message: string, address: string, signature: Uint8Array, messagePrefix: string | Buffer): any;
|
package/index.ts
CHANGED
|
@@ -1,256 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { bech32m } from "bech32";
|
|
3
|
-
import * as bitcoinMessage from "bitcoinjs-message";
|
|
4
|
-
import { ml_dsa44 } from "@noble/post-quantum/ml-dsa.js";
|
|
5
|
-
|
|
6
|
-
const MESSAGE_MAGIC = "Neurai Signed Message:\n";
|
|
7
|
-
const PQ_MESSAGE_SIGNATURE_PREFIX = 0x35;
|
|
8
|
-
const PQ_SERIALIZED_PUBKEY_PREFIX = 0x05;
|
|
9
|
-
const PQ_PUBLIC_KEY_LENGTH = 1312;
|
|
10
|
-
const PQ_SERIALIZED_PUBKEY_LENGTH = 1 + PQ_PUBLIC_KEY_LENGTH;
|
|
11
|
-
const PQ_SIGNATURE_LENGTH = 2420;
|
|
12
|
-
const LEGACY_MESSAGE_PREFIX =
|
|
13
|
-
String.fromCharCode(Buffer.byteLength(MESSAGE_MAGIC, "utf8")) +
|
|
14
|
-
MESSAGE_MAGIC;
|
|
15
|
-
|
|
16
|
-
function encodeCompactSize(value: number): Buffer {
|
|
17
|
-
if (!Number.isInteger(value) || value < 0) {
|
|
18
|
-
throw new Error("CompactSize value must be a non-negative integer");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (value < 253) {
|
|
22
|
-
return Buffer.from([value]);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (value <= 0xffff) {
|
|
26
|
-
const buffer = Buffer.alloc(3);
|
|
27
|
-
buffer[0] = 0xfd;
|
|
28
|
-
buffer.writeUInt16LE(value, 1);
|
|
29
|
-
return buffer;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (value <= 0xffffffff) {
|
|
33
|
-
const buffer = Buffer.alloc(5);
|
|
34
|
-
buffer[0] = 0xfe;
|
|
35
|
-
buffer.writeUInt32LE(value, 1);
|
|
36
|
-
return buffer;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
throw new Error("CompactSize values above uint32 are not supported");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function decodeCompactSize(buffer: Buffer, offset: number) {
|
|
43
|
-
if (offset >= buffer.length) {
|
|
44
|
-
throw new Error("Unexpected end of CompactSize data");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const first = buffer[offset];
|
|
48
|
-
if (first < 253) {
|
|
49
|
-
return { value: first, offset: offset + 1 };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (first === 0xfd) {
|
|
53
|
-
if (offset + 3 > buffer.length) {
|
|
54
|
-
throw new Error("Unexpected end of CompactSize uint16 data");
|
|
55
|
-
}
|
|
56
|
-
return { value: buffer.readUInt16LE(offset + 1), offset: offset + 3 };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (first === 0xfe) {
|
|
60
|
-
if (offset + 5 > buffer.length) {
|
|
61
|
-
throw new Error("Unexpected end of CompactSize uint32 data");
|
|
62
|
-
}
|
|
63
|
-
return { value: buffer.readUInt32LE(offset + 1), offset: offset + 5 };
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (first === 0xff) {
|
|
67
|
-
throw new Error("CompactSize uint64 is not supported");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
throw new Error("Invalid CompactSize prefix");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function sha256(bytes: Uint8Array) {
|
|
74
|
-
return createHash("sha256").update(bytes).digest();
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function hash256(bytes: Uint8Array) {
|
|
78
|
-
return sha256(sha256(bytes));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function hash160(bytes: Uint8Array) {
|
|
82
|
-
return createHash("ripemd160").update(sha256(bytes)).digest();
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function encodeMessageHash(message: string) {
|
|
86
|
-
const messageBytes = Buffer.from(message, "utf8");
|
|
87
|
-
const magicBytes = Buffer.from(MESSAGE_MAGIC, "utf8");
|
|
88
|
-
const payload = Buffer.concat([
|
|
89
|
-
encodeCompactSize(magicBytes.length),
|
|
90
|
-
magicBytes,
|
|
91
|
-
encodeCompactSize(messageBytes.length),
|
|
92
|
-
messageBytes,
|
|
93
|
-
]);
|
|
94
|
-
|
|
95
|
-
return hash256(payload);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function toSignatureBuffer(signature: string | Uint8Array) {
|
|
99
|
-
return typeof signature === "string"
|
|
100
|
-
? Buffer.from(signature, "base64")
|
|
101
|
-
: Buffer.from(signature);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function normalizePQPublicKey(publicKey: Uint8Array) {
|
|
105
|
-
const buffer = Buffer.from(publicKey);
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
buffer.length === PQ_SERIALIZED_PUBKEY_LENGTH &&
|
|
109
|
-
buffer[0] === PQ_SERIALIZED_PUBKEY_PREFIX
|
|
110
|
-
) {
|
|
111
|
-
return buffer;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (buffer.length === PQ_PUBLIC_KEY_LENGTH) {
|
|
115
|
-
return Buffer.concat([Buffer.from([PQ_SERIALIZED_PUBKEY_PREFIX]), buffer]);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
throw new Error("Invalid PQ public key length");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function isPQMessageSignature(signature: string | Uint8Array) {
|
|
122
|
-
const buffer = toSignatureBuffer(signature);
|
|
123
|
-
return buffer.length > 0 && buffer[0] === PQ_MESSAGE_SIGNATURE_PREFIX;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function decodePQAddress(address: string) {
|
|
127
|
-
const decoded = bech32m.decode(address);
|
|
128
|
-
if (decoded.words.length === 0) {
|
|
129
|
-
throw new Error("Invalid bech32m address");
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
prefix: decoded.prefix,
|
|
134
|
-
version: decoded.words[0],
|
|
135
|
-
program: Buffer.from(bech32m.fromWords(decoded.words.slice(1))),
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** returns a base64 encoded string representation of the legacy signature */
|
|
140
|
-
export function sign(message: string, privateKey: Uint8Array, compressed = true) {
|
|
141
|
-
const signature = bitcoinMessage.sign(
|
|
142
|
-
message,
|
|
143
|
-
Buffer.from(privateKey),
|
|
144
|
-
compressed,
|
|
145
|
-
LEGACY_MESSAGE_PREFIX
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
return signature.toString("base64");
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function signPQMessage(
|
|
152
|
-
message: string,
|
|
153
|
-
privateKey: Uint8Array,
|
|
154
|
-
publicKey: Uint8Array
|
|
155
|
-
) {
|
|
156
|
-
const serializedPublicKey = normalizePQPublicKey(publicKey);
|
|
157
|
-
const hash = encodeMessageHash(message);
|
|
158
|
-
const pqSignature = Buffer.from(ml_dsa44.sign(hash, Buffer.from(privateKey)));
|
|
159
|
-
|
|
160
|
-
const payload = Buffer.concat([
|
|
161
|
-
Buffer.from([PQ_MESSAGE_SIGNATURE_PREFIX]),
|
|
162
|
-
encodeCompactSize(serializedPublicKey.length),
|
|
163
|
-
serializedPublicKey,
|
|
164
|
-
encodeCompactSize(pqSignature.length),
|
|
165
|
-
pqSignature,
|
|
166
|
-
]);
|
|
167
|
-
|
|
168
|
-
return payload.toString("base64");
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export function verifyLegacyMessage(
|
|
172
|
-
message: string,
|
|
173
|
-
address: string,
|
|
174
|
-
signature: string | Uint8Array
|
|
175
|
-
) {
|
|
176
|
-
try {
|
|
177
|
-
return bitcoinMessage.verify(
|
|
178
|
-
message,
|
|
179
|
-
address,
|
|
180
|
-
toSignatureBuffer(signature),
|
|
181
|
-
LEGACY_MESSAGE_PREFIX
|
|
182
|
-
);
|
|
183
|
-
} catch {
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
export function verifyPQMessage(
|
|
189
|
-
message: string,
|
|
190
|
-
address: string,
|
|
191
|
-
signature: string | Uint8Array
|
|
192
|
-
) {
|
|
193
|
-
try {
|
|
194
|
-
const payload = toSignatureBuffer(signature);
|
|
195
|
-
let offset = 0;
|
|
196
|
-
|
|
197
|
-
if (payload[offset++] !== PQ_MESSAGE_SIGNATURE_PREFIX) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const publicKeyLength = decodeCompactSize(payload, offset);
|
|
202
|
-
offset = publicKeyLength.offset;
|
|
203
|
-
|
|
204
|
-
const serializedPublicKey = payload.subarray(
|
|
205
|
-
offset,
|
|
206
|
-
offset + publicKeyLength.value
|
|
207
|
-
);
|
|
208
|
-
offset += publicKeyLength.value;
|
|
209
|
-
|
|
210
|
-
const signatureLength = decodeCompactSize(payload, offset);
|
|
211
|
-
offset = signatureLength.offset;
|
|
212
|
-
|
|
213
|
-
const pqSignature = payload.subarray(offset, offset + signatureLength.value);
|
|
214
|
-
offset += signatureLength.value;
|
|
215
|
-
|
|
216
|
-
if (offset !== payload.length) {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
serializedPublicKey.length !== PQ_SERIALIZED_PUBKEY_LENGTH ||
|
|
222
|
-
serializedPublicKey[0] !== PQ_SERIALIZED_PUBKEY_PREFIX ||
|
|
223
|
-
pqSignature.length !== PQ_SIGNATURE_LENGTH
|
|
224
|
-
) {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const decodedAddress = decodePQAddress(address);
|
|
229
|
-
if (decodedAddress.version !== 1 || decodedAddress.program.length !== 20) {
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
const expectedProgram = hash160(serializedPublicKey);
|
|
234
|
-
if (!expectedProgram.equals(decodedAddress.program)) {
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return ml_dsa44.verify(
|
|
239
|
-
pqSignature,
|
|
240
|
-
encodeMessageHash(message),
|
|
241
|
-
serializedPublicKey.subarray(1)
|
|
242
|
-
);
|
|
243
|
-
} catch {
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
export function verifyMessage(
|
|
249
|
-
message: string,
|
|
250
|
-
address: string,
|
|
251
|
-
signature: string | Uint8Array
|
|
252
|
-
) {
|
|
253
|
-
return isPQMessageSignature(signature)
|
|
254
|
-
? verifyPQMessage(message, address, signature)
|
|
255
|
-
: verifyLegacyMessage(message, address, signature);
|
|
256
|
-
}
|
|
1
|
+
export * from "./src/index";
|
package/package.json
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neuraiproject/neurai-message",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Sign and Verify messages in Neurai",
|
|
5
5
|
"source": "index.ts",
|
|
6
|
-
"main": "dist/
|
|
7
|
-
"module": "dist/
|
|
8
|
-
"types": "dist/
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
6
|
+
"main": "dist/index.cjs",
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"browser": {
|
|
10
|
+
"stream": "stream-browserify",
|
|
11
|
+
"buffer": "buffer/",
|
|
12
|
+
"process": "process/browser"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.mjs",
|
|
18
|
+
"require": "./dist/index.cjs"
|
|
19
|
+
},
|
|
20
|
+
"./browser": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/browser.mjs"
|
|
12
23
|
},
|
|
13
|
-
"
|
|
14
|
-
"includeNodeModules": true
|
|
15
|
-
}
|
|
24
|
+
"./global": "./dist/NeuraiMessage.global.js"
|
|
16
25
|
},
|
|
17
26
|
"scripts": {
|
|
18
|
-
"test": "npm run build &&
|
|
19
|
-
"build": "
|
|
27
|
+
"test": "npm run build && vitest run",
|
|
28
|
+
"build": "esbuild src/index.ts --bundle --platform=node --target=es2020 --format=cjs --outfile=dist/index.cjs && esbuild src/index.ts --bundle --platform=node --target=es2020 --format=esm --outfile=dist/index.mjs && esbuild src/browser.ts --bundle --platform=browser --target=es2020 --format=esm --main-fields=browser,module,main --outfile=dist/browser.mjs --inject:./src/browser-shims.ts && esbuild src/global.ts --bundle --platform=browser --target=es2020 --format=iife --global-name=NeuraiMessage --main-fields=browser,module,main --outfile=dist/NeuraiMessage.global.js --inject:./src/browser-shims.ts && tsc --emitDeclarationOnly --declaration --outDir dist"
|
|
20
29
|
},
|
|
21
30
|
"repository": {
|
|
22
31
|
"type": "git",
|
|
@@ -33,17 +42,22 @@
|
|
|
33
42
|
},
|
|
34
43
|
"homepage": "https://github.com/neuraiproject/neurai-message#readme",
|
|
35
44
|
"dependencies": {
|
|
45
|
+
"@noble/hashes": "^2.0.1",
|
|
36
46
|
"@noble/post-quantum": "^0.6.0",
|
|
47
|
+
"@noble/secp256k1": "^3.0.0",
|
|
37
48
|
"bech32": "^2.0.0",
|
|
38
|
-
"
|
|
49
|
+
"bs58check": "^2.1.2",
|
|
50
|
+
"create-hash": "^1.2.0",
|
|
51
|
+
"varuint-bitcoin": "^1.1.2"
|
|
39
52
|
},
|
|
40
53
|
"devDependencies": {
|
|
41
|
-
"@parcel/packager-ts": "^2.8.3",
|
|
42
|
-
"@parcel/transformer-typescript-types": "2.16.4",
|
|
43
|
-
"@types/jest": "^28.1.4",
|
|
44
54
|
"@types/node": "^18.0.0",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"
|
|
55
|
+
"buffer": "^6.0.3",
|
|
56
|
+
"esbuild": "^0.25.0",
|
|
57
|
+
"events": "^3.3.0",
|
|
58
|
+
"process": "^0.11.10",
|
|
59
|
+
"stream-browserify": "^3.0.0",
|
|
60
|
+
"typescript": "^4.9.4",
|
|
61
|
+
"vitest": "^3.2.4"
|
|
48
62
|
}
|
|
49
63
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Buffer } from "buffer";
|
|
2
|
+
import process from "process/browser";
|
|
3
|
+
|
|
4
|
+
if (typeof globalThis !== "undefined") {
|
|
5
|
+
(globalThis as typeof globalThis & { Buffer?: typeof Buffer }).Buffer ??=
|
|
6
|
+
Buffer;
|
|
7
|
+
(globalThis as typeof globalThis & { process?: typeof process }).process ??=
|
|
8
|
+
process;
|
|
9
|
+
}
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./core";
|
package/src/core.ts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { Buffer } from "buffer";
|
|
2
|
+
import createHash from "create-hash";
|
|
3
|
+
import { bech32m } from "bech32";
|
|
4
|
+
import { ml_dsa44 } from "@noble/post-quantum/ml-dsa.js";
|
|
5
|
+
import { signLegacyMessage, verifyLegacyCompactMessage } from "./legacy-message";
|
|
6
|
+
|
|
7
|
+
const MESSAGE_MAGIC = "Neurai Signed Message:\n";
|
|
8
|
+
const PQ_MESSAGE_SIGNATURE_PREFIX = 0x35;
|
|
9
|
+
const PQ_SERIALIZED_PUBKEY_PREFIX = 0x05;
|
|
10
|
+
const PQ_PUBLIC_KEY_LENGTH = 1312;
|
|
11
|
+
const PQ_SERIALIZED_PUBKEY_LENGTH = 1 + PQ_PUBLIC_KEY_LENGTH;
|
|
12
|
+
const PQ_SIGNATURE_LENGTH = 2420;
|
|
13
|
+
const LEGACY_MESSAGE_PREFIX =
|
|
14
|
+
String.fromCharCode(Buffer.byteLength(MESSAGE_MAGIC, "utf8")) +
|
|
15
|
+
MESSAGE_MAGIC;
|
|
16
|
+
|
|
17
|
+
function encodeCompactSize(value: number): Buffer {
|
|
18
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
19
|
+
throw new Error("CompactSize value must be a non-negative integer");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (value < 253) {
|
|
23
|
+
return Buffer.from([value]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (value <= 0xffff) {
|
|
27
|
+
const buffer = Buffer.alloc(3);
|
|
28
|
+
buffer[0] = 0xfd;
|
|
29
|
+
buffer.writeUInt16LE(value, 1);
|
|
30
|
+
return buffer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (value <= 0xffffffff) {
|
|
34
|
+
const buffer = Buffer.alloc(5);
|
|
35
|
+
buffer[0] = 0xfe;
|
|
36
|
+
buffer.writeUInt32LE(value, 1);
|
|
37
|
+
return buffer;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw new Error("CompactSize values above uint32 are not supported");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function decodeCompactSize(buffer: Buffer, offset: number) {
|
|
44
|
+
if (offset >= buffer.length) {
|
|
45
|
+
throw new Error("Unexpected end of CompactSize data");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const first = buffer[offset];
|
|
49
|
+
if (first < 253) {
|
|
50
|
+
return { value: first, offset: offset + 1 };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (first === 0xfd) {
|
|
54
|
+
if (offset + 3 > buffer.length) {
|
|
55
|
+
throw new Error("Unexpected end of CompactSize uint16 data");
|
|
56
|
+
}
|
|
57
|
+
return { value: buffer.readUInt16LE(offset + 1), offset: offset + 3 };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (first === 0xfe) {
|
|
61
|
+
if (offset + 5 > buffer.length) {
|
|
62
|
+
throw new Error("Unexpected end of CompactSize uint32 data");
|
|
63
|
+
}
|
|
64
|
+
return { value: buffer.readUInt32LE(offset + 1), offset: offset + 5 };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (first === 0xff) {
|
|
68
|
+
throw new Error("CompactSize uint64 is not supported");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
throw new Error("Invalid CompactSize prefix");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function sha256(bytes: Uint8Array) {
|
|
75
|
+
return createHash("sha256").update(bytes).digest();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function hash256(bytes: Uint8Array) {
|
|
79
|
+
return sha256(sha256(bytes));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hash160(bytes: Uint8Array) {
|
|
83
|
+
return createHash("ripemd160").update(sha256(bytes)).digest();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function encodeMessageHash(message: string) {
|
|
87
|
+
const messageBytes = Buffer.from(message, "utf8");
|
|
88
|
+
const magicBytes = Buffer.from(MESSAGE_MAGIC, "utf8");
|
|
89
|
+
const payload = Buffer.concat([
|
|
90
|
+
encodeCompactSize(magicBytes.length),
|
|
91
|
+
magicBytes,
|
|
92
|
+
encodeCompactSize(messageBytes.length),
|
|
93
|
+
messageBytes,
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
return hash256(payload);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function toSignatureBuffer(signature: string | Uint8Array) {
|
|
100
|
+
return typeof signature === "string"
|
|
101
|
+
? Buffer.from(signature, "base64")
|
|
102
|
+
: Buffer.from(signature);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizePQPublicKey(publicKey: Uint8Array) {
|
|
106
|
+
const buffer = Buffer.from(publicKey);
|
|
107
|
+
|
|
108
|
+
if (
|
|
109
|
+
buffer.length === PQ_SERIALIZED_PUBKEY_LENGTH &&
|
|
110
|
+
buffer[0] === PQ_SERIALIZED_PUBKEY_PREFIX
|
|
111
|
+
) {
|
|
112
|
+
return buffer;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (buffer.length === PQ_PUBLIC_KEY_LENGTH) {
|
|
116
|
+
return Buffer.concat([Buffer.from([PQ_SERIALIZED_PUBKEY_PREFIX]), buffer]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new Error("Invalid PQ public key length");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isPQMessageSignature(signature: string | Uint8Array) {
|
|
123
|
+
const buffer = toSignatureBuffer(signature);
|
|
124
|
+
return buffer.length > 0 && buffer[0] === PQ_MESSAGE_SIGNATURE_PREFIX;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function decodePQAddress(address: string) {
|
|
128
|
+
const decoded = bech32m.decode(address);
|
|
129
|
+
if (decoded.words.length === 0) {
|
|
130
|
+
throw new Error("Invalid bech32m address");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
prefix: decoded.prefix,
|
|
135
|
+
version: decoded.words[0],
|
|
136
|
+
program: Buffer.from(bech32m.fromWords(decoded.words.slice(1))),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** returns a base64 encoded string representation of the legacy signature */
|
|
141
|
+
export function sign(message: string, privateKey: Uint8Array, compressed = true) {
|
|
142
|
+
const signature = signLegacyMessage(
|
|
143
|
+
message,
|
|
144
|
+
Buffer.from(privateKey),
|
|
145
|
+
compressed,
|
|
146
|
+
LEGACY_MESSAGE_PREFIX
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return signature.toString("base64");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function signPQMessage(
|
|
153
|
+
message: string,
|
|
154
|
+
privateKey: Uint8Array,
|
|
155
|
+
publicKey: Uint8Array
|
|
156
|
+
) {
|
|
157
|
+
const serializedPublicKey = normalizePQPublicKey(publicKey);
|
|
158
|
+
const hash = encodeMessageHash(message);
|
|
159
|
+
const pqSignature = Buffer.from(ml_dsa44.sign(hash, Buffer.from(privateKey)));
|
|
160
|
+
|
|
161
|
+
const payload = Buffer.concat([
|
|
162
|
+
Buffer.from([PQ_MESSAGE_SIGNATURE_PREFIX]),
|
|
163
|
+
encodeCompactSize(serializedPublicKey.length),
|
|
164
|
+
serializedPublicKey,
|
|
165
|
+
encodeCompactSize(pqSignature.length),
|
|
166
|
+
pqSignature,
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
return payload.toString("base64");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function verifyLegacyMessage(
|
|
173
|
+
message: string,
|
|
174
|
+
address: string,
|
|
175
|
+
signature: string | Uint8Array
|
|
176
|
+
) {
|
|
177
|
+
try {
|
|
178
|
+
return verifyLegacyCompactMessage(
|
|
179
|
+
message,
|
|
180
|
+
address,
|
|
181
|
+
toSignatureBuffer(signature),
|
|
182
|
+
LEGACY_MESSAGE_PREFIX
|
|
183
|
+
);
|
|
184
|
+
} catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function verifyPQMessage(
|
|
190
|
+
message: string,
|
|
191
|
+
address: string,
|
|
192
|
+
signature: string | Uint8Array
|
|
193
|
+
) {
|
|
194
|
+
try {
|
|
195
|
+
const payload = toSignatureBuffer(signature);
|
|
196
|
+
let offset = 0;
|
|
197
|
+
|
|
198
|
+
if (payload[offset++] !== PQ_MESSAGE_SIGNATURE_PREFIX) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const publicKeyLength = decodeCompactSize(payload, offset);
|
|
203
|
+
offset = publicKeyLength.offset;
|
|
204
|
+
|
|
205
|
+
const serializedPublicKey = payload.subarray(
|
|
206
|
+
offset,
|
|
207
|
+
offset + publicKeyLength.value
|
|
208
|
+
);
|
|
209
|
+
offset += publicKeyLength.value;
|
|
210
|
+
|
|
211
|
+
const signatureLength = decodeCompactSize(payload, offset);
|
|
212
|
+
offset = signatureLength.offset;
|
|
213
|
+
|
|
214
|
+
const pqSignature = payload.subarray(offset, offset + signatureLength.value);
|
|
215
|
+
offset += signatureLength.value;
|
|
216
|
+
|
|
217
|
+
if (offset !== payload.length) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
serializedPublicKey.length !== PQ_SERIALIZED_PUBKEY_LENGTH ||
|
|
223
|
+
serializedPublicKey[0] !== PQ_SERIALIZED_PUBKEY_PREFIX ||
|
|
224
|
+
pqSignature.length !== PQ_SIGNATURE_LENGTH
|
|
225
|
+
) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const decodedAddress = decodePQAddress(address);
|
|
230
|
+
if (decodedAddress.version !== 1 || decodedAddress.program.length !== 20) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const expectedProgram = hash160(serializedPublicKey);
|
|
235
|
+
if (!expectedProgram.equals(decodedAddress.program)) {
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return ml_dsa44.verify(
|
|
240
|
+
pqSignature,
|
|
241
|
+
encodeMessageHash(message),
|
|
242
|
+
serializedPublicKey.subarray(1)
|
|
243
|
+
);
|
|
244
|
+
} catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function verifyMessage(
|
|
250
|
+
message: string,
|
|
251
|
+
address: string,
|
|
252
|
+
signature: string | Uint8Array
|
|
253
|
+
) {
|
|
254
|
+
return isPQMessageSignature(signature)
|
|
255
|
+
? verifyPQMessage(message, address, signature)
|
|
256
|
+
: verifyLegacyMessage(message, address, signature);
|
|
257
|
+
}
|
package/src/global.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as api from "./browser";
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
interface Window {
|
|
5
|
+
NeuraiMessage: typeof api;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (typeof globalThis !== "undefined") {
|
|
10
|
+
(globalThis as typeof globalThis & { NeuraiMessage: typeof api }).NeuraiMessage =
|
|
11
|
+
api;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export * from "./browser";
|
|
15
|
+
export default api;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./core";
|