@vex-chat/crypto 1.1.1 → 2.0.1
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 +88 -12
- package/dist/index.d.ts +107 -89
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +207 -171
- package/dist/index.js.map +1 -1
- package/package.json +67 -34
- package/src/__tests__/XKeyConvert.ts +68 -0
- package/src/__tests__/XUtils/bytesEqual.ts +16 -0
- package/src/__tests__/XUtils/emptyHeader.ts +12 -0
- package/src/__tests__/XUtils/numberToUint8Arr.ts +22 -0
- package/src/__tests__/XUtils/packMessage.ts +23 -0
- package/src/__tests__/XUtils/stringEncoding.ts +68 -0
- package/src/__tests__/XUtils/uint8ArrToNumber.ts +19 -0
- package/src/__tests__/XUtils/unpackMessage.ts +25 -0
- package/src/__tests__/xConcat.ts +18 -0
- package/src/__tests__/xDH.ts +16 -0
- package/src/__tests__/xEncode.ts +11 -0
- package/src/__tests__/xHMAC.ts +26 -0
- package/src/__tests__/xMnemonic.ts +12 -0
- package/src/index.ts +540 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import type { BaseMsg } from "@vex-chat/types";
|
|
2
|
+
|
|
3
|
+
import { hkdf } from "@noble/hashes/hkdf.js";
|
|
4
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
5
|
+
import { pbkdf2 as noblePbkdf2 } from "@noble/hashes/pbkdf2.js";
|
|
6
|
+
import { sha256, sha512 } from "@noble/hashes/sha2.js";
|
|
7
|
+
import {
|
|
8
|
+
decode as decodeBase64,
|
|
9
|
+
encode as encodeBase64,
|
|
10
|
+
} from "@stablelib/base64";
|
|
11
|
+
import { encode as decodeUTF8, decode as encodeUTF8 } from "@stablelib/utf8";
|
|
12
|
+
import * as bip39 from "bip39";
|
|
13
|
+
import ed2curve from "ed2curve";
|
|
14
|
+
import { Packr } from "msgpackr";
|
|
15
|
+
import nacl from "tweetnacl";
|
|
16
|
+
import { z } from "zod/v4";
|
|
17
|
+
|
|
18
|
+
// msgpackr with useRecords:false emits standard msgpack (no nonstandard record extension).
|
|
19
|
+
// moreTypes:false keeps the extension set to only what other decoders understand.
|
|
20
|
+
// pack() returns Node Buffer (tight view) so consumers like axios send the correct bytes.
|
|
21
|
+
const packer = new Packr({ moreTypes: false, useRecords: false });
|
|
22
|
+
const msgpackEncode = packer.pack.bind(packer);
|
|
23
|
+
const msgpackDecode = packer.unpack.bind(packer);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provides an interface that can map an ed25519 keypair to its equivalent
|
|
27
|
+
* X25519 keypair.
|
|
28
|
+
*/
|
|
29
|
+
export const XKeyConvert = ed2curve;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Provides several methods that are useful in working with bytes and
|
|
33
|
+
* vex messages.
|
|
34
|
+
*/
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-extraneous-class -- intentional static utility namespace (public API surface)
|
|
36
|
+
export class XUtils {
|
|
37
|
+
public static decodeBase64 = decodeBase64;
|
|
38
|
+
|
|
39
|
+
public static decodeUTF8 = decodeUTF8;
|
|
40
|
+
|
|
41
|
+
public static encodeBase64 = encodeBase64;
|
|
42
|
+
|
|
43
|
+
public static encodeUTF8 = encodeUTF8;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Checks if two buffer-like objects are equal.
|
|
47
|
+
* When lengths match, comparison is constant-time in the inputs (no early exit on first differing byte).
|
|
48
|
+
*
|
|
49
|
+
* @param buf1
|
|
50
|
+
* @param buf2
|
|
51
|
+
*
|
|
52
|
+
* @returns True if equal, else false.
|
|
53
|
+
*/
|
|
54
|
+
public static bytesEqual(
|
|
55
|
+
buf1: ArrayBuffer | Uint8Array,
|
|
56
|
+
buf2: ArrayBuffer | Uint8Array,
|
|
57
|
+
) {
|
|
58
|
+
const a = buf1 instanceof Uint8Array ? buf1 : new Uint8Array(buf1);
|
|
59
|
+
const b = buf2 instanceof Uint8Array ? buf2 : new Uint8Array(buf2);
|
|
60
|
+
if (a.byteLength !== b.byteLength) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
let diff = 0;
|
|
64
|
+
for (let i = 0; i !== a.byteLength; i++) {
|
|
65
|
+
const x = a[i];
|
|
66
|
+
const y = b[i];
|
|
67
|
+
if (x === undefined || y === undefined) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
diff |= x ^ y;
|
|
71
|
+
}
|
|
72
|
+
return diff === 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Decodes a hex string into a Uint8Array.
|
|
77
|
+
*
|
|
78
|
+
* @returns The Uint8Array.
|
|
79
|
+
*/
|
|
80
|
+
public static decodeHex(hexString: string): Uint8Array {
|
|
81
|
+
if (hexString.length === 0) {
|
|
82
|
+
return new Uint8Array();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const matches = hexString.match(/.{1,2}/g) ?? [];
|
|
86
|
+
return new Uint8Array(matches.map((byte) => parseInt(byte, 16)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Decrypts a secret key from the binary format produced by encryptKeyData().
|
|
91
|
+
* No I/O — the caller handles reading the data.
|
|
92
|
+
*
|
|
93
|
+
* @param keyData The encrypted key data as a Uint8Array.
|
|
94
|
+
* @param password The password used to encrypt.
|
|
95
|
+
* @returns The hex-encoded secret key.
|
|
96
|
+
*/
|
|
97
|
+
public static decryptKeyData = (
|
|
98
|
+
keyData: Uint8Array,
|
|
99
|
+
password: string,
|
|
100
|
+
): string => {
|
|
101
|
+
const ITERATIONS = XUtils.uint8ArrToNumber(keyData.slice(0, 6));
|
|
102
|
+
const PKBDF_SALT = keyData.slice(6, 30);
|
|
103
|
+
const ENCRYPTION_NONCE = keyData.slice(30, 54);
|
|
104
|
+
const ENCRYPTED_KEY = keyData.slice(54);
|
|
105
|
+
const DERIVED_KEY = noblePbkdf2(sha512, password, PKBDF_SALT, {
|
|
106
|
+
c: ITERATIONS,
|
|
107
|
+
dkLen: 32,
|
|
108
|
+
});
|
|
109
|
+
const DECRYPTED_SIGNKEY = nacl.secretbox.open(
|
|
110
|
+
ENCRYPTED_KEY,
|
|
111
|
+
ENCRYPTION_NONCE,
|
|
112
|
+
DERIVED_KEY,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
if (DECRYPTED_SIGNKEY === null) {
|
|
116
|
+
throw new Error("Decryption failed. Wrong password?");
|
|
117
|
+
}
|
|
118
|
+
return XUtils.encodeHex(DECRYPTED_SIGNKEY);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Returns the empty header (32 0's)
|
|
123
|
+
*
|
|
124
|
+
* @returns The empty header.
|
|
125
|
+
*/
|
|
126
|
+
public static emptyHeader() {
|
|
127
|
+
return new Uint8Array(xConstants.HEADER_SIZE);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Encodes a Uint8Array to a hex string.
|
|
132
|
+
*
|
|
133
|
+
* @returns The hex string.
|
|
134
|
+
*/
|
|
135
|
+
public static encodeHex(bytes: Uint8Array): string {
|
|
136
|
+
return bytes.reduce(
|
|
137
|
+
(str, byte) => str + byte.toString(16).padStart(2, "0"),
|
|
138
|
+
"",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Encrypts a secret key into a portable binary format.
|
|
144
|
+
* The result can be written to disk, sent over the network, etc.
|
|
145
|
+
* No I/O — the caller handles persistence.
|
|
146
|
+
*
|
|
147
|
+
* Format: [iterations(6)|salt(24)|nonce(24)|ciphertext(N)]
|
|
148
|
+
*
|
|
149
|
+
* @param password The password to derive the encryption key from.
|
|
150
|
+
* @param keyToSave The hex-encoded secret key to encrypt.
|
|
151
|
+
* @param iterationOverride Optional PBKDF2 iteration count (random if omitted).
|
|
152
|
+
* @returns The encrypted key data as a Uint8Array.
|
|
153
|
+
*/
|
|
154
|
+
public static encryptKeyData = (
|
|
155
|
+
password: string,
|
|
156
|
+
keyToSave: string,
|
|
157
|
+
iterationOverride?: number,
|
|
158
|
+
): Uint8Array => {
|
|
159
|
+
const UNENCRYPTED_SIGNKEY = XUtils.decodeHex(keyToSave);
|
|
160
|
+
const OFFSET = 1000;
|
|
161
|
+
const rand = nacl.randomBytes(2);
|
|
162
|
+
const [N1 = 0, N2 = 0] = rand;
|
|
163
|
+
const iterations =
|
|
164
|
+
iterationOverride !== undefined && iterationOverride !== 0
|
|
165
|
+
? iterationOverride
|
|
166
|
+
: N1 * N2 + OFFSET;
|
|
167
|
+
const ITERATIONS = XUtils.numberToUint8Arr(iterations);
|
|
168
|
+
const PKBDF_SALT = xMakeNonce();
|
|
169
|
+
const ENCRYPTION_KEY = noblePbkdf2(sha512, password, PKBDF_SALT, {
|
|
170
|
+
c: iterations,
|
|
171
|
+
dkLen: 32,
|
|
172
|
+
});
|
|
173
|
+
const NONCE = xMakeNonce();
|
|
174
|
+
const ENCRYPTED_SIGNKEY = nacl.secretbox(
|
|
175
|
+
UNENCRYPTED_SIGNKEY,
|
|
176
|
+
NONCE,
|
|
177
|
+
ENCRYPTION_KEY,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const result = new Uint8Array(
|
|
181
|
+
ITERATIONS.length +
|
|
182
|
+
PKBDF_SALT.length +
|
|
183
|
+
NONCE.length +
|
|
184
|
+
ENCRYPTED_SIGNKEY.length,
|
|
185
|
+
);
|
|
186
|
+
let offset = 0;
|
|
187
|
+
result.set(ITERATIONS, offset);
|
|
188
|
+
offset += ITERATIONS.length;
|
|
189
|
+
result.set(PKBDF_SALT, offset);
|
|
190
|
+
offset += PKBDF_SALT.length;
|
|
191
|
+
result.set(NONCE, offset);
|
|
192
|
+
offset += NONCE.length;
|
|
193
|
+
result.set(ENCRYPTED_SIGNKEY, offset);
|
|
194
|
+
return result;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Returns a six bit Uint8Array representation of an integer.
|
|
199
|
+
* The integer must be positive, and it must be able to be stored
|
|
200
|
+
* in six bytes.
|
|
201
|
+
*
|
|
202
|
+
* @param n The number to convert.
|
|
203
|
+
* @returns The Uint8Array representation of n.
|
|
204
|
+
*/
|
|
205
|
+
public static numberToUint8Arr(n: number): Uint8Array {
|
|
206
|
+
if (n < 0 || n > 281474976710655) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
"Expected integer 0 < n < 281474976710655, received " +
|
|
209
|
+
String(n),
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let str = n.toString(16);
|
|
214
|
+
while (str.length < 12) {
|
|
215
|
+
str = "0" + str;
|
|
216
|
+
}
|
|
217
|
+
return XUtils.decodeHex(str);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Packs a javascript object and a 32 byte header into a vex message.
|
|
222
|
+
*
|
|
223
|
+
* @param msg Message body (msgpack-serialized).
|
|
224
|
+
* @param header Optional 32-byte header; defaults to an empty header.
|
|
225
|
+
* @returns the packed message.
|
|
226
|
+
*/
|
|
227
|
+
public static packMessage(msg: unknown, header?: Uint8Array) {
|
|
228
|
+
const msgb = msgpackEncode(msg);
|
|
229
|
+
const msgh = header ?? XUtils.emptyHeader();
|
|
230
|
+
return xConcat(msgh, msgb);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Converts a Uint8Array representation of an integer back into a number.
|
|
235
|
+
*
|
|
236
|
+
* @param arr The array to convert.
|
|
237
|
+
* @returns the number representation of arr.
|
|
238
|
+
*/
|
|
239
|
+
public static uint8ArrToNumber(arr: Uint8Array) {
|
|
240
|
+
let n = 0;
|
|
241
|
+
for (const byte of arr) {
|
|
242
|
+
n = n * 256 + byte;
|
|
243
|
+
}
|
|
244
|
+
return n;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Takes a vex message and unpacks it into its header and a javascript object
|
|
249
|
+
* representation of its body.
|
|
250
|
+
*
|
|
251
|
+
* @param msg Full wire message (32-byte header + msgpack body).
|
|
252
|
+
* @returns [32 byte header, message body]
|
|
253
|
+
*/
|
|
254
|
+
public static unpackMessage(
|
|
255
|
+
msg: Buffer | Uint8Array,
|
|
256
|
+
): [Uint8Array, BaseMsg] {
|
|
257
|
+
const msgp = Uint8Array.from(msg);
|
|
258
|
+
const msgh = msgp.slice(0, xConstants.HEADER_SIZE);
|
|
259
|
+
// Validate base fields exist, keep all extra fields for the caller to narrow
|
|
260
|
+
const raw: unknown = msgpackDecode(msgp.slice(xConstants.HEADER_SIZE));
|
|
261
|
+
const msgb = z
|
|
262
|
+
.object({
|
|
263
|
+
transmissionID: z.string(),
|
|
264
|
+
type: z.string(),
|
|
265
|
+
})
|
|
266
|
+
.loose()
|
|
267
|
+
.parse(raw) as BaseMsg;
|
|
268
|
+
|
|
269
|
+
return [msgh, msgb];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Returns a 32 byte HMAC of a javscript object.
|
|
275
|
+
*
|
|
276
|
+
* @param msg the message to create the HMAC of
|
|
277
|
+
* @param SK the secret key to create the HMAC with
|
|
278
|
+
*/
|
|
279
|
+
export function xHMAC(msg: unknown, SK: Uint8Array) {
|
|
280
|
+
const packedMsg = msgpackEncode(msg);
|
|
281
|
+
return hmac(sha256, SK, packedMsg);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Gets a word list representation of a byte sequence.
|
|
286
|
+
*
|
|
287
|
+
* @param entropy The bytes to derive the wordlist from.
|
|
288
|
+
* @param wordList Optional, override the wordlist. See bip39 docs for details.
|
|
289
|
+
*/
|
|
290
|
+
export function xMnemonic(entropy: Uint8Array, wordList?: string[]) {
|
|
291
|
+
return bip39.entropyToMnemonic(XUtils.encodeHex(entropy), wordList);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Constants for vex.
|
|
296
|
+
*/
|
|
297
|
+
export const xConstants: XConstants = {
|
|
298
|
+
CURVE: "X25519",
|
|
299
|
+
HASH: "SHA-512",
|
|
300
|
+
HEADER_SIZE: 32,
|
|
301
|
+
INFO: "xchat",
|
|
302
|
+
KEY_LENGTH: 32,
|
|
303
|
+
MIN_OTK_SUPPLY: 100,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/** Ed25519 or X25519 key pair. Structurally identical to nacl.SignKeyPair / nacl.BoxKeyPair. */
|
|
307
|
+
export interface KeyPair {
|
|
308
|
+
publicKey: Uint8Array;
|
|
309
|
+
secretKey: Uint8Array;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Derives a 32 byte secret key from some initial key material.
|
|
314
|
+
*
|
|
315
|
+
* @param IKM the initial key material.
|
|
316
|
+
* @returns The generated key.
|
|
317
|
+
*/
|
|
318
|
+
// export function xKDF(IKM: Uint8Array): Uint8Array {
|
|
319
|
+
// return Uint8Array.from(
|
|
320
|
+
// hkdf(Buffer.from(IKM), xConstants.KEY_LENGTH, {
|
|
321
|
+
// salt: Buffer.from(xMakeSalt(xConstants.CURVE)),
|
|
322
|
+
// info: xConstants.INFO,
|
|
323
|
+
// hash: xConstants.HASH,
|
|
324
|
+
// })
|
|
325
|
+
// );
|
|
326
|
+
// }
|
|
327
|
+
|
|
328
|
+
/** Shape of the {@link xConstants} runtime object. */
|
|
329
|
+
export interface XConstants {
|
|
330
|
+
CURVE: "X25519";
|
|
331
|
+
HASH: "SHA-512";
|
|
332
|
+
HEADER_SIZE: 32;
|
|
333
|
+
INFO: string;
|
|
334
|
+
KEY_LENGTH: 32 | 57;
|
|
335
|
+
MIN_OTK_SUPPLY: number;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Generate a fresh X25519 box key pair. */
|
|
339
|
+
export function xBoxKeyPair(): KeyPair {
|
|
340
|
+
return nacl.box.keyPair();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Restore an X25519 box key pair from a 32-byte secret key. */
|
|
344
|
+
export function xBoxKeyPairFromSecret(secretKey: Uint8Array): KeyPair {
|
|
345
|
+
return nacl.box.keyPair.fromSecretKey(secretKey);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Concatanates multiple Uint8Arrays.
|
|
350
|
+
*
|
|
351
|
+
* @param arrays As many Uint8Arrays as you would like to concatanate.
|
|
352
|
+
*/
|
|
353
|
+
export function xConcat(...arrays: Uint8Array[]): Uint8Array {
|
|
354
|
+
const totalLength = arrays.reduce((acc, value) => acc + value.length, 0);
|
|
355
|
+
|
|
356
|
+
if (arrays.length === 0) {
|
|
357
|
+
return new Uint8Array();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const result = new Uint8Array(totalLength);
|
|
361
|
+
|
|
362
|
+
// for each array - copy it over result
|
|
363
|
+
// next array is copied right after the previous one
|
|
364
|
+
let length = 0;
|
|
365
|
+
for (const array of arrays) {
|
|
366
|
+
result.set(array, length);
|
|
367
|
+
length += array.length;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Derives a shared Secret Key from a known private key and
|
|
375
|
+
* a peer's known public key.
|
|
376
|
+
*
|
|
377
|
+
* @param myPrivateKey Your own private key
|
|
378
|
+
* @param theirPublicKey Their public key
|
|
379
|
+
* @returns The derived shared secret, SK.
|
|
380
|
+
*/
|
|
381
|
+
export function xDH(
|
|
382
|
+
myPrivateKey: Uint8Array,
|
|
383
|
+
theirPublicKey: Uint8Array,
|
|
384
|
+
): Uint8Array {
|
|
385
|
+
return nacl.box.before(theirPublicKey, myPrivateKey);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── Key pair type ───────────────────────────────────────────────────────────
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Encode an X25519 or X448 public key PK into a byte sequence.
|
|
392
|
+
* The encoding consists of 0 or 1 to represent the type of curve, followed by l
|
|
393
|
+
* ittle-endian encoding of the u-coordinate. See [rfc 7748](https://www.ietf.org/rfc/rfc7748.txt) for more
|
|
394
|
+
* details.
|
|
395
|
+
*/
|
|
396
|
+
export function xEncode(
|
|
397
|
+
curveType: "X448" | "X25519",
|
|
398
|
+
publicKey: Uint8Array,
|
|
399
|
+
): Uint8Array {
|
|
400
|
+
if (publicKey.length !== 32) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
"Invalid key length, received key of length " +
|
|
403
|
+
String(publicKey.length) +
|
|
404
|
+
" and expected length 32.",
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const bytes: number[] = [];
|
|
409
|
+
|
|
410
|
+
switch (curveType) {
|
|
411
|
+
case "X448":
|
|
412
|
+
bytes.push(1);
|
|
413
|
+
break;
|
|
414
|
+
case "X25519":
|
|
415
|
+
bytes.push(0);
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const key = BigInt("0x" + XUtils.encodeHex(publicKey));
|
|
420
|
+
|
|
421
|
+
if (isEven(key)) {
|
|
422
|
+
bytes.push(0);
|
|
423
|
+
} else {
|
|
424
|
+
bytes.push(1);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
for (const byte of publicKey) {
|
|
428
|
+
bytes.push(byte);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return Uint8Array.from(bytes);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ── Key generation ─────────────────────────────────────────────────────────
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Hashes some data.
|
|
438
|
+
*
|
|
439
|
+
* @param data the data to hash.
|
|
440
|
+
* @returns The hash of the data.
|
|
441
|
+
*/
|
|
442
|
+
export function xHash(data: Uint8Array) {
|
|
443
|
+
return XUtils.encodeHex(sha512(data));
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function xKDF(IKM: Uint8Array): Uint8Array {
|
|
447
|
+
return hkdf(
|
|
448
|
+
sha512,
|
|
449
|
+
IKM,
|
|
450
|
+
xMakeSalt(xConstants.CURVE),
|
|
451
|
+
new TextEncoder().encode(xConstants.INFO),
|
|
452
|
+
xConstants.KEY_LENGTH,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Returns a 24 byte random nonce of cryptographic quality.
|
|
458
|
+
*/
|
|
459
|
+
export function xMakeNonce(): Uint8Array {
|
|
460
|
+
return nacl.randomBytes(24);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Cryptographically secure random bytes. */
|
|
464
|
+
export function xRandomBytes(length: number): Uint8Array {
|
|
465
|
+
return nacl.randomBytes(length);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Signing ────────────────────────────────────────────────────────────────
|
|
469
|
+
|
|
470
|
+
/** Encrypt with a shared secret key. */
|
|
471
|
+
export function xSecretbox(
|
|
472
|
+
plaintext: Uint8Array,
|
|
473
|
+
nonce: Uint8Array,
|
|
474
|
+
key: Uint8Array,
|
|
475
|
+
): Uint8Array {
|
|
476
|
+
return nacl.secretbox(plaintext, nonce, key);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Decrypt with a shared secret key. Returns null if authentication fails. */
|
|
480
|
+
export function xSecretboxOpen(
|
|
481
|
+
ciphertext: Uint8Array,
|
|
482
|
+
nonce: Uint8Array,
|
|
483
|
+
key: Uint8Array,
|
|
484
|
+
): null | Uint8Array {
|
|
485
|
+
return nacl.secretbox.open(ciphertext, nonce, key);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ── Symmetric encryption (XSalsa20-Poly1305) ──────────────────────────────
|
|
489
|
+
|
|
490
|
+
/** Sign a message with an Ed25519 secret key. Returns signed message (64-byte signature prefix + message). */
|
|
491
|
+
export function xSign(message: Uint8Array, secretKey: Uint8Array): Uint8Array {
|
|
492
|
+
return nacl.sign(message, secretKey);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/** Generate a fresh Ed25519 signing key pair. */
|
|
496
|
+
export function xSignKeyPair(): KeyPair {
|
|
497
|
+
return nacl.sign.keyPair();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// ── Random ─────────────────────────────────────────────────────────────────
|
|
501
|
+
|
|
502
|
+
/** Restore an Ed25519 signing key pair from a 64-byte secret key. */
|
|
503
|
+
export function xSignKeyPairFromSecret(secretKey: Uint8Array): KeyPair {
|
|
504
|
+
return nacl.sign.keyPair.fromSecretKey(secretKey);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Verify and open a signed message. Returns the original message, or null if verification fails. */
|
|
508
|
+
export function xSignOpen(
|
|
509
|
+
signedMessage: Uint8Array,
|
|
510
|
+
publicKey: Uint8Array,
|
|
511
|
+
): null | Uint8Array {
|
|
512
|
+
return nacl.sign.open(signedMessage, publicKey);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @internal
|
|
517
|
+
*/
|
|
518
|
+
function isEven(value: bigint) {
|
|
519
|
+
if (value % BigInt(2) === BigInt(0)) {
|
|
520
|
+
return true;
|
|
521
|
+
} else {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* @internal
|
|
528
|
+
*/
|
|
529
|
+
function keyLength(curve: "X448" | "X25519"): number {
|
|
530
|
+
return curve === "X25519" ? 32 : 57;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* @internal
|
|
535
|
+
*/
|
|
536
|
+
function xMakeSalt(curve: "X448" | "X25519"): Uint8Array {
|
|
537
|
+
const salt = new Uint8Array(keyLength(curve));
|
|
538
|
+
salt.fill(0xff);
|
|
539
|
+
return salt;
|
|
540
|
+
}
|