@hamradio/meshcore 1.2.2 → 1.3.2
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.mjs → index.cjs} +339 -323
- package/dist/{index.d.mts → index.d.cts} +118 -132
- package/dist/index.d.ts +118 -132
- package/dist/index.js +296 -364
- package/package.json +12 -8
- package/dist/crypto.d.ts +0 -42
- package/dist/crypto.js +0 -199
- package/dist/crypto.types.d.ts +0 -26
- package/dist/crypto.types.js +0 -1
- package/dist/identity.d.ts +0 -65
- package/dist/identity.js +0 -302
- package/dist/identity.types.d.ts +0 -17
- package/dist/identity.types.js +0 -1
- package/dist/packet.d.ts +0 -32
- package/dist/packet.js +0 -242
- package/dist/packet.types.d.ts +0 -161
- package/dist/packet.types.js +0 -44
- package/dist/parser.d.ts +0 -31
- package/dist/parser.js +0 -124
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hamradio/meshcore",
|
|
3
|
-
"
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "1.3.2",
|
|
4
5
|
"description": "MeshCore protocol support for Typescript",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"MeshCore",
|
|
@@ -9,12 +10,12 @@
|
|
|
9
10
|
],
|
|
10
11
|
"repository": {
|
|
11
12
|
"type": "git",
|
|
12
|
-
"url": "https://git.maze.io/ham/meshcore.
|
|
13
|
+
"url": "https://git.maze.io/ham/meshcore.ts"
|
|
13
14
|
},
|
|
14
15
|
"license": "MIT",
|
|
15
16
|
"author": "Wijnand Modderman-Lenstra",
|
|
16
17
|
"main": "dist/index.js",
|
|
17
|
-
"module": "dist/index.
|
|
18
|
+
"module": "dist/index.js",
|
|
18
19
|
"types": "dist/index.d.ts",
|
|
19
20
|
"files": [
|
|
20
21
|
"dist"
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
"exports": {
|
|
23
24
|
".": {
|
|
24
25
|
"types": "./dist/index.d.ts",
|
|
25
|
-
"import": "./dist/index.
|
|
26
|
+
"import": "./dist/index.js",
|
|
26
27
|
"require": "./dist/index.js"
|
|
27
28
|
}
|
|
28
29
|
},
|
|
@@ -37,19 +38,22 @@
|
|
|
37
38
|
"prepare": "npm run build"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
41
|
+
"@hamradio/packet": "^1.1.0",
|
|
40
42
|
"@noble/ciphers": "^2.1.1",
|
|
41
43
|
"@noble/curves": "^2.0.1",
|
|
42
|
-
"@noble/ed25519": "^3.0.
|
|
44
|
+
"@noble/ed25519": "^3.0.1",
|
|
43
45
|
"@noble/hashes": "^2.0.1"
|
|
44
46
|
},
|
|
45
47
|
"devDependencies": {
|
|
46
48
|
"@eslint/js": "^10.0.1",
|
|
47
|
-
"@vitest/coverage-v8": "^4.0
|
|
49
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
48
50
|
"eslint": "^10.0.3",
|
|
49
51
|
"globals": "^17.4.0",
|
|
52
|
+
"jiti": "^2.6.1",
|
|
53
|
+
"prettier": "3.8.1",
|
|
50
54
|
"tsup": "^8.5.1",
|
|
51
55
|
"typescript": "^5.9.3",
|
|
52
|
-
"typescript-eslint": "^8.57.
|
|
53
|
-
"vitest": "^4.0
|
|
56
|
+
"typescript-eslint": "^8.57.1",
|
|
57
|
+
"vitest": "^4.1.0"
|
|
54
58
|
}
|
|
55
59
|
}
|
package/dist/crypto.d.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { IPublicKey, ISharedSecret, IStaticSecret } from './crypto.types';
|
|
2
|
-
import { NodeHash } from './identity.types';
|
|
3
|
-
export declare class PublicKey implements IPublicKey {
|
|
4
|
-
key: Uint8Array;
|
|
5
|
-
constructor(key: Uint8Array | string);
|
|
6
|
-
toHash(): NodeHash;
|
|
7
|
-
toBytes(): Uint8Array;
|
|
8
|
-
toString(): string;
|
|
9
|
-
equals(other: PublicKey | Uint8Array | string): boolean;
|
|
10
|
-
verify(message: Uint8Array, signature: Uint8Array): boolean;
|
|
11
|
-
}
|
|
12
|
-
export declare class PrivateKey {
|
|
13
|
-
private secretKey;
|
|
14
|
-
private publicKey;
|
|
15
|
-
constructor(seed: Uint8Array | string);
|
|
16
|
-
toPublicKey(): PublicKey;
|
|
17
|
-
toBytes(): Uint8Array;
|
|
18
|
-
toString(): string;
|
|
19
|
-
sign(message: Uint8Array): Uint8Array;
|
|
20
|
-
calculateSharedSecret(other: PublicKey | Uint8Array | string): Uint8Array;
|
|
21
|
-
static generate(): PrivateKey;
|
|
22
|
-
}
|
|
23
|
-
export declare class SharedSecret implements ISharedSecret {
|
|
24
|
-
private secret;
|
|
25
|
-
constructor(secret: Uint8Array);
|
|
26
|
-
toHash(): NodeHash;
|
|
27
|
-
toBytes(): Uint8Array;
|
|
28
|
-
toString(): string;
|
|
29
|
-
decrypt(hmac: Uint8Array, ciphertext: Uint8Array): Uint8Array;
|
|
30
|
-
encrypt(data: Uint8Array): {
|
|
31
|
-
hmac: Uint8Array;
|
|
32
|
-
ciphertext: Uint8Array;
|
|
33
|
-
};
|
|
34
|
-
private calculateHmac;
|
|
35
|
-
static fromName(name: string): SharedSecret;
|
|
36
|
-
}
|
|
37
|
-
export declare class StaticSecret implements IStaticSecret {
|
|
38
|
-
private secret;
|
|
39
|
-
constructor(secret: Uint8Array | string);
|
|
40
|
-
publicKey(): IPublicKey;
|
|
41
|
-
diffieHellman(otherPublicKey: IPublicKey): SharedSecret;
|
|
42
|
-
}
|
package/dist/crypto.js
DELETED
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
import { ed25519, x25519 } from '@noble/curves/ed25519.js';
|
|
2
|
-
import { sha256 } from "@noble/hashes/sha2.js";
|
|
3
|
-
import { hmac } from '@noble/hashes/hmac.js';
|
|
4
|
-
import { ecb } from '@noble/ciphers/aes.js';
|
|
5
|
-
import { bytesToHex, equalBytes, hexToBytes } from "./parser";
|
|
6
|
-
const PUBLIC_KEY_SIZE = 32;
|
|
7
|
-
const SEED_SIZE = 32;
|
|
8
|
-
const HMAC_SIZE = 2;
|
|
9
|
-
const SHARED_SECRET_SIZE = 32;
|
|
10
|
-
const SIGNATURE_SIZE = 64;
|
|
11
|
-
const STATIC_SECRET_SIZE = 32;
|
|
12
|
-
// The "Public" group is a special group that all nodes are implicitly part of.
|
|
13
|
-
const publicSecret = hexToBytes("8b3387e9c5cdea6ac9e5edbaa115cd72", 16);
|
|
14
|
-
export class PublicKey {
|
|
15
|
-
constructor(key) {
|
|
16
|
-
if (typeof key === 'string') {
|
|
17
|
-
this.key = hexToBytes(key, PUBLIC_KEY_SIZE);
|
|
18
|
-
}
|
|
19
|
-
else if (key instanceof Uint8Array) {
|
|
20
|
-
this.key = key;
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
throw new Error('Invalid type for PublicKey constructor');
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
toHash() {
|
|
27
|
-
return sha256.create().update(this.key).digest()[0];
|
|
28
|
-
}
|
|
29
|
-
toBytes() {
|
|
30
|
-
return this.key;
|
|
31
|
-
}
|
|
32
|
-
toString() {
|
|
33
|
-
return bytesToHex(this.key);
|
|
34
|
-
}
|
|
35
|
-
equals(other) {
|
|
36
|
-
let otherKey;
|
|
37
|
-
if (other instanceof PublicKey) {
|
|
38
|
-
otherKey = other.toBytes();
|
|
39
|
-
}
|
|
40
|
-
else if (other instanceof Uint8Array) {
|
|
41
|
-
otherKey = other;
|
|
42
|
-
}
|
|
43
|
-
else if (typeof other === 'string') {
|
|
44
|
-
otherKey = hexToBytes(other, PUBLIC_KEY_SIZE);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
throw new Error('Invalid type for PublicKey comparison');
|
|
48
|
-
}
|
|
49
|
-
return equalBytes(this.key, otherKey);
|
|
50
|
-
}
|
|
51
|
-
verify(message, signature) {
|
|
52
|
-
if (signature.length !== SIGNATURE_SIZE) {
|
|
53
|
-
throw new Error(`Invalid signature length: expected ${SIGNATURE_SIZE} bytes, got ${signature.length}`);
|
|
54
|
-
}
|
|
55
|
-
return ed25519.verify(signature, message, this.key);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
export class PrivateKey {
|
|
59
|
-
constructor(seed) {
|
|
60
|
-
if (typeof seed === 'string') {
|
|
61
|
-
seed = hexToBytes(seed, SEED_SIZE);
|
|
62
|
-
}
|
|
63
|
-
if (seed.length !== SEED_SIZE) {
|
|
64
|
-
throw new Error(`Invalid seed length: expected ${SEED_SIZE} bytes, got ${seed.length}`);
|
|
65
|
-
}
|
|
66
|
-
const { secretKey, publicKey } = ed25519.keygen(seed); // Validate seed by generating keys
|
|
67
|
-
this.secretKey = secretKey;
|
|
68
|
-
this.publicKey = new PublicKey(publicKey);
|
|
69
|
-
}
|
|
70
|
-
toPublicKey() {
|
|
71
|
-
return this.publicKey;
|
|
72
|
-
}
|
|
73
|
-
toBytes() {
|
|
74
|
-
return this.secretKey;
|
|
75
|
-
}
|
|
76
|
-
toString() {
|
|
77
|
-
return bytesToHex(this.secretKey);
|
|
78
|
-
}
|
|
79
|
-
sign(message) {
|
|
80
|
-
return ed25519.sign(message, this.secretKey);
|
|
81
|
-
}
|
|
82
|
-
calculateSharedSecret(other) {
|
|
83
|
-
let otherPublicKey;
|
|
84
|
-
if (other instanceof PublicKey) {
|
|
85
|
-
otherPublicKey = other;
|
|
86
|
-
}
|
|
87
|
-
else if (other instanceof Uint8Array) {
|
|
88
|
-
otherPublicKey = new PublicKey(other);
|
|
89
|
-
}
|
|
90
|
-
else if (typeof other === 'string') {
|
|
91
|
-
otherPublicKey = new PublicKey(other);
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
throw new Error('Invalid type for calculateSharedSecret comparison');
|
|
95
|
-
}
|
|
96
|
-
return x25519.getSharedSecret(this.secretKey, otherPublicKey.toBytes());
|
|
97
|
-
}
|
|
98
|
-
static generate() {
|
|
99
|
-
const { secretKey } = ed25519.keygen(); // Ensure ed25519 is initialized
|
|
100
|
-
return new PrivateKey(secretKey);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
export class SharedSecret {
|
|
104
|
-
constructor(secret) {
|
|
105
|
-
if (secret.length === SHARED_SECRET_SIZE / 2) {
|
|
106
|
-
// Zero pad to the left if the secret is too short (e.g. from x25519)
|
|
107
|
-
const padded = new Uint8Array(SHARED_SECRET_SIZE);
|
|
108
|
-
padded.set(secret, SHARED_SECRET_SIZE - secret.length);
|
|
109
|
-
secret = padded;
|
|
110
|
-
}
|
|
111
|
-
if (secret.length !== SHARED_SECRET_SIZE) {
|
|
112
|
-
throw new Error(`Invalid shared secret length: expected ${SHARED_SECRET_SIZE} bytes, got ${secret.length}`);
|
|
113
|
-
}
|
|
114
|
-
this.secret = secret;
|
|
115
|
-
}
|
|
116
|
-
toHash() {
|
|
117
|
-
return this.secret[0];
|
|
118
|
-
}
|
|
119
|
-
toBytes() {
|
|
120
|
-
return this.secret;
|
|
121
|
-
}
|
|
122
|
-
toString() {
|
|
123
|
-
return bytesToHex(this.secret);
|
|
124
|
-
}
|
|
125
|
-
decrypt(hmac, ciphertext) {
|
|
126
|
-
if (hmac.length !== HMAC_SIZE) {
|
|
127
|
-
throw new Error(`Invalid HMAC length: expected ${HMAC_SIZE} bytes, got ${hmac.length}`);
|
|
128
|
-
}
|
|
129
|
-
const expectedHmac = this.calculateHmac(ciphertext);
|
|
130
|
-
if (!equalBytes(hmac, expectedHmac)) {
|
|
131
|
-
throw new Error(`Invalid HMAC: decryption failed: expected ${bytesToHex(expectedHmac)}, got ${bytesToHex(hmac)}`);
|
|
132
|
-
}
|
|
133
|
-
const cipher = ecb(this.secret.slice(0, 16), { disablePadding: true });
|
|
134
|
-
const plaintext = new Uint8Array(ciphertext.length);
|
|
135
|
-
for (let i = 0; i < ciphertext.length; i += 16) {
|
|
136
|
-
const block = ciphertext.slice(i, i + 16);
|
|
137
|
-
const dec = cipher.decrypt(block);
|
|
138
|
-
plaintext.set(dec, i);
|
|
139
|
-
}
|
|
140
|
-
// Remove trailing null bytes (0x00) due to padding
|
|
141
|
-
let end = plaintext.length;
|
|
142
|
-
while (end > 0 && plaintext[end - 1] === 0) {
|
|
143
|
-
end--;
|
|
144
|
-
}
|
|
145
|
-
return plaintext.slice(0, end);
|
|
146
|
-
}
|
|
147
|
-
encrypt(data) {
|
|
148
|
-
const key = this.secret.slice(0, 16);
|
|
149
|
-
const cipher = ecb(key, { disablePadding: true });
|
|
150
|
-
const fullBlocks = Math.floor(data.length / 16);
|
|
151
|
-
const remaining = data.length % 16;
|
|
152
|
-
const ciphertext = new Uint8Array((fullBlocks + (remaining > 0 ? 1 : 0)) * 16);
|
|
153
|
-
for (let i = 0; i < fullBlocks; i++) {
|
|
154
|
-
const block = data.slice(i * 16, (i + 1) * 16);
|
|
155
|
-
const enc = cipher.encrypt(block);
|
|
156
|
-
ciphertext.set(enc, i * 16);
|
|
157
|
-
}
|
|
158
|
-
if (remaining > 0) {
|
|
159
|
-
const lastBlock = new Uint8Array(16);
|
|
160
|
-
lastBlock.set(data.slice(fullBlocks * 16));
|
|
161
|
-
const enc = cipher.encrypt(lastBlock);
|
|
162
|
-
ciphertext.set(enc, fullBlocks * 16);
|
|
163
|
-
}
|
|
164
|
-
const hmac = this.calculateHmac(ciphertext);
|
|
165
|
-
return { hmac, ciphertext };
|
|
166
|
-
}
|
|
167
|
-
calculateHmac(data) {
|
|
168
|
-
return hmac(sha256, this.secret, data).slice(0, HMAC_SIZE);
|
|
169
|
-
}
|
|
170
|
-
static fromName(name) {
|
|
171
|
-
if (name === "Public") {
|
|
172
|
-
return new SharedSecret(publicSecret);
|
|
173
|
-
}
|
|
174
|
-
else if (!/^#/.test(name)) {
|
|
175
|
-
throw new Error("Only the 'Public' group or groups starting with '#' are supported");
|
|
176
|
-
}
|
|
177
|
-
const hash = sha256.create().update(new TextEncoder().encode(name)).digest();
|
|
178
|
-
return new SharedSecret(hash.slice(0, SHARED_SECRET_SIZE));
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
export class StaticSecret {
|
|
182
|
-
constructor(secret) {
|
|
183
|
-
if (typeof secret === 'string') {
|
|
184
|
-
secret = hexToBytes(secret, STATIC_SECRET_SIZE);
|
|
185
|
-
}
|
|
186
|
-
if (secret.length !== STATIC_SECRET_SIZE) {
|
|
187
|
-
throw new Error(`Invalid static secret length: expected ${STATIC_SECRET_SIZE} bytes, got ${secret.length}`);
|
|
188
|
-
}
|
|
189
|
-
this.secret = secret;
|
|
190
|
-
}
|
|
191
|
-
publicKey() {
|
|
192
|
-
const publicKey = x25519.getPublicKey(this.secret);
|
|
193
|
-
return new PublicKey(publicKey);
|
|
194
|
-
}
|
|
195
|
-
diffieHellman(otherPublicKey) {
|
|
196
|
-
const sharedSecret = x25519.getSharedSecret(this.secret, otherPublicKey.toBytes());
|
|
197
|
-
return new SharedSecret(sharedSecret);
|
|
198
|
-
}
|
|
199
|
-
}
|
package/dist/crypto.types.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { NodeHash } from "./identity.types";
|
|
2
|
-
export interface IPublicKey {
|
|
3
|
-
toHash(): NodeHash;
|
|
4
|
-
toBytes(): Uint8Array;
|
|
5
|
-
toString(): string;
|
|
6
|
-
equals(other: IPublicKey | Uint8Array | string): boolean;
|
|
7
|
-
verify(message: Uint8Array, signature: Uint8Array): boolean;
|
|
8
|
-
}
|
|
9
|
-
export interface IPrivateKey extends IPublicKey {
|
|
10
|
-
toPublicKey(): IPublicKey;
|
|
11
|
-
sign(message: Uint8Array): Uint8Array;
|
|
12
|
-
}
|
|
13
|
-
export interface ISharedSecret {
|
|
14
|
-
toHash(): NodeHash;
|
|
15
|
-
toBytes(): Uint8Array;
|
|
16
|
-
toString(): string;
|
|
17
|
-
decrypt(hmac: Uint8Array, ciphertext: Uint8Array): Uint8Array;
|
|
18
|
-
encrypt(data: Uint8Array): {
|
|
19
|
-
hmac: Uint8Array;
|
|
20
|
-
ciphertext: Uint8Array;
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
export interface IStaticSecret {
|
|
24
|
-
publicKey(): IPublicKey;
|
|
25
|
-
diffieHellman(otherPublicKey: IPublicKey): ISharedSecret;
|
|
26
|
-
}
|
package/dist/crypto.types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/identity.d.ts
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { PrivateKey, PublicKey, SharedSecret } from "./crypto";
|
|
2
|
-
import { IPublicKey } from "./crypto.types";
|
|
3
|
-
import { NodeHash, IIdentity, ILocalIdentity } from "./identity.types";
|
|
4
|
-
import { DecryptedGroupData, DecryptedGroupText } from "./packet.types";
|
|
5
|
-
export declare const parseNodeHash: (hash: NodeHash | string | Uint8Array) => NodeHash;
|
|
6
|
-
export declare class Identity implements IIdentity {
|
|
7
|
-
publicKey: PublicKey;
|
|
8
|
-
constructor(publicKey: PublicKey | Uint8Array | string);
|
|
9
|
-
hash(): NodeHash;
|
|
10
|
-
toString(): string;
|
|
11
|
-
verify(signature: Uint8Array, message: Uint8Array): boolean;
|
|
12
|
-
matches(other: Identity | PublicKey | Uint8Array | string): boolean;
|
|
13
|
-
}
|
|
14
|
-
export declare class LocalIdentity extends Identity implements ILocalIdentity {
|
|
15
|
-
private privateKey;
|
|
16
|
-
constructor(privateKey: PrivateKey | Uint8Array | string, publicKey: PublicKey | Uint8Array | string);
|
|
17
|
-
sign(message: Uint8Array): Uint8Array;
|
|
18
|
-
calculateSharedSecret(other: IIdentity | IPublicKey): SharedSecret;
|
|
19
|
-
}
|
|
20
|
-
export declare class Contact {
|
|
21
|
-
name: string;
|
|
22
|
-
identity: Identity;
|
|
23
|
-
constructor(name: string, identity: Identity | PublicKey | Uint8Array | string);
|
|
24
|
-
matches(hash: Uint8Array | PublicKey): boolean;
|
|
25
|
-
publicKey(): PublicKey;
|
|
26
|
-
calculateSharedSecret(me: LocalIdentity): SharedSecret;
|
|
27
|
-
}
|
|
28
|
-
export declare class Group {
|
|
29
|
-
name: string;
|
|
30
|
-
private secret;
|
|
31
|
-
constructor(name: string, secret?: SharedSecret);
|
|
32
|
-
hash(): NodeHash;
|
|
33
|
-
decryptText(hmac: Uint8Array, ciphertext: Uint8Array): DecryptedGroupText;
|
|
34
|
-
encryptText(plain: DecryptedGroupText): {
|
|
35
|
-
hmac: Uint8Array;
|
|
36
|
-
ciphertext: Uint8Array;
|
|
37
|
-
};
|
|
38
|
-
decryptData(hmac: Uint8Array, ciphertext: Uint8Array): DecryptedGroupData;
|
|
39
|
-
encryptData(plain: DecryptedGroupData): {
|
|
40
|
-
hmac: Uint8Array;
|
|
41
|
-
ciphertext: Uint8Array;
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
export declare class Contacts {
|
|
45
|
-
private localIdentities;
|
|
46
|
-
private contacts;
|
|
47
|
-
private groups;
|
|
48
|
-
addLocalIdentity(identity: LocalIdentity): void;
|
|
49
|
-
addContact(contact: Contact): void;
|
|
50
|
-
decrypt(src: NodeHash | PublicKey, dst: NodeHash, hmac: Uint8Array, ciphertext: Uint8Array): {
|
|
51
|
-
localIdentity: LocalIdentity;
|
|
52
|
-
contact: Contact;
|
|
53
|
-
decrypted: Uint8Array;
|
|
54
|
-
};
|
|
55
|
-
private calculateSharedSecret;
|
|
56
|
-
addGroup(group: Group): void;
|
|
57
|
-
decryptGroupText(channelHash: NodeHash, hmac: Uint8Array, ciphertext: Uint8Array): {
|
|
58
|
-
decrypted: DecryptedGroupText;
|
|
59
|
-
group: Group;
|
|
60
|
-
};
|
|
61
|
-
decryptGroupData(channelHash: NodeHash, hmac: Uint8Array, ciphertext: Uint8Array): {
|
|
62
|
-
decrypted: DecryptedGroupData;
|
|
63
|
-
group: Group;
|
|
64
|
-
};
|
|
65
|
-
}
|
package/dist/identity.js
DELETED
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import { PrivateKey, PublicKey, SharedSecret } from "./crypto";
|
|
2
|
-
import { hexToBytes, BufferReader, BufferWriter } from "./parser";
|
|
3
|
-
export const parseNodeHash = (hash) => {
|
|
4
|
-
if (hash instanceof Uint8Array) {
|
|
5
|
-
return hash[0];
|
|
6
|
-
}
|
|
7
|
-
if (typeof hash === "number") {
|
|
8
|
-
if (hash < 0 || hash > 255) {
|
|
9
|
-
throw new Error("NodeHash number must be between 0x00 and 0xFF");
|
|
10
|
-
}
|
|
11
|
-
return hash;
|
|
12
|
-
}
|
|
13
|
-
else if (typeof hash === "string") {
|
|
14
|
-
const parsed = hexToBytes(hash);
|
|
15
|
-
if (parsed.length !== 1) {
|
|
16
|
-
throw new Error("NodeHash string must represent a single byte");
|
|
17
|
-
}
|
|
18
|
-
return parsed[0];
|
|
19
|
-
}
|
|
20
|
-
throw new Error("Invalid NodeHash type");
|
|
21
|
-
};
|
|
22
|
-
const toPublicKeyBytes = (key) => {
|
|
23
|
-
if (key instanceof Identity) {
|
|
24
|
-
return key.publicKey.toBytes();
|
|
25
|
-
}
|
|
26
|
-
else if (key instanceof PublicKey) {
|
|
27
|
-
return key.toBytes();
|
|
28
|
-
}
|
|
29
|
-
else if (key instanceof Uint8Array) {
|
|
30
|
-
return key;
|
|
31
|
-
}
|
|
32
|
-
else if (typeof key === 'string') {
|
|
33
|
-
return hexToBytes(key);
|
|
34
|
-
}
|
|
35
|
-
else {
|
|
36
|
-
throw new Error('Invalid type for toPublicKeyBytes');
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
export class Identity {
|
|
40
|
-
constructor(publicKey) {
|
|
41
|
-
if (publicKey instanceof PublicKey) {
|
|
42
|
-
this.publicKey = publicKey;
|
|
43
|
-
}
|
|
44
|
-
else if (publicKey instanceof Uint8Array || typeof publicKey === 'string') {
|
|
45
|
-
this.publicKey = new PublicKey(publicKey);
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
throw new Error('Invalid type for Identity constructor');
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
hash() {
|
|
52
|
-
return this.publicKey.toHash();
|
|
53
|
-
}
|
|
54
|
-
toString() {
|
|
55
|
-
return this.publicKey.toString();
|
|
56
|
-
}
|
|
57
|
-
verify(signature, message) {
|
|
58
|
-
return this.publicKey.verify(message, signature);
|
|
59
|
-
}
|
|
60
|
-
matches(other) {
|
|
61
|
-
return this.publicKey.equals(toPublicKeyBytes(other));
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
export class LocalIdentity extends Identity {
|
|
65
|
-
constructor(privateKey, publicKey) {
|
|
66
|
-
if (publicKey instanceof PublicKey) {
|
|
67
|
-
super(publicKey.toBytes());
|
|
68
|
-
}
|
|
69
|
-
else {
|
|
70
|
-
super(publicKey);
|
|
71
|
-
}
|
|
72
|
-
if (privateKey instanceof PrivateKey) {
|
|
73
|
-
this.privateKey = privateKey;
|
|
74
|
-
}
|
|
75
|
-
else {
|
|
76
|
-
this.privateKey = new PrivateKey(privateKey);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
sign(message) {
|
|
80
|
-
return this.privateKey.sign(message);
|
|
81
|
-
}
|
|
82
|
-
calculateSharedSecret(other) {
|
|
83
|
-
let otherPublicKey;
|
|
84
|
-
if (other instanceof Identity) {
|
|
85
|
-
otherPublicKey = other.publicKey;
|
|
86
|
-
}
|
|
87
|
-
else if ('toBytes' in other) {
|
|
88
|
-
otherPublicKey = new PublicKey(other.toBytes());
|
|
89
|
-
}
|
|
90
|
-
else if ('publicKey' in other && other.publicKey instanceof Uint8Array) {
|
|
91
|
-
otherPublicKey = new PublicKey(other.publicKey);
|
|
92
|
-
}
|
|
93
|
-
else if ('publicKey' in other && other.publicKey instanceof PublicKey) {
|
|
94
|
-
otherPublicKey = other.publicKey;
|
|
95
|
-
}
|
|
96
|
-
else if ('publicKey' in other && typeof other.publicKey === 'function') {
|
|
97
|
-
otherPublicKey = new PublicKey(other.publicKey().toBytes());
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
throw new Error('Invalid type for calculateSharedSecret comparison');
|
|
101
|
-
}
|
|
102
|
-
return new SharedSecret(this.privateKey.calculateSharedSecret(otherPublicKey));
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
export class Contact {
|
|
106
|
-
constructor(name, identity) {
|
|
107
|
-
this.name = "";
|
|
108
|
-
this.name = name;
|
|
109
|
-
if (identity instanceof Identity) {
|
|
110
|
-
this.identity = identity;
|
|
111
|
-
}
|
|
112
|
-
else if (identity instanceof PublicKey) {
|
|
113
|
-
this.identity = new Identity(identity);
|
|
114
|
-
}
|
|
115
|
-
else if (identity instanceof Uint8Array || typeof identity === 'string') {
|
|
116
|
-
this.identity = new Identity(identity);
|
|
117
|
-
}
|
|
118
|
-
else {
|
|
119
|
-
throw new Error('Invalid type for Contact constructor');
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
matches(hash) {
|
|
123
|
-
return this.identity.publicKey.equals(hash);
|
|
124
|
-
}
|
|
125
|
-
publicKey() {
|
|
126
|
-
return this.identity.publicKey;
|
|
127
|
-
}
|
|
128
|
-
calculateSharedSecret(me) {
|
|
129
|
-
return me.calculateSharedSecret(this.identity);
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
export class Group {
|
|
133
|
-
constructor(name, secret) {
|
|
134
|
-
this.name = name;
|
|
135
|
-
if (secret) {
|
|
136
|
-
this.secret = secret;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
this.secret = SharedSecret.fromName(name);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
hash() {
|
|
143
|
-
return this.secret.toHash();
|
|
144
|
-
}
|
|
145
|
-
decryptText(hmac, ciphertext) {
|
|
146
|
-
const data = this.secret.decrypt(hmac, ciphertext);
|
|
147
|
-
if (data.length < 5) {
|
|
148
|
-
throw new Error("Invalid ciphertext");
|
|
149
|
-
}
|
|
150
|
-
const reader = new BufferReader(data);
|
|
151
|
-
const timestamp = reader.readTimestamp();
|
|
152
|
-
const flags = reader.readByte();
|
|
153
|
-
const textType = (flags >> 2) & 0x3F;
|
|
154
|
-
const attempt = flags & 0x03;
|
|
155
|
-
const message = new TextDecoder('utf-8').decode(reader.readBytes());
|
|
156
|
-
return {
|
|
157
|
-
timestamp,
|
|
158
|
-
textType,
|
|
159
|
-
attempt,
|
|
160
|
-
message
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
encryptText(plain) {
|
|
164
|
-
const writer = new BufferWriter();
|
|
165
|
-
writer.writeTimestamp(plain.timestamp);
|
|
166
|
-
const flags = ((plain.textType & 0x3F) << 2) | (plain.attempt & 0x03);
|
|
167
|
-
writer.writeByte(flags);
|
|
168
|
-
writer.writeBytes(new TextEncoder().encode(plain.message));
|
|
169
|
-
const data = writer.toBytes();
|
|
170
|
-
return this.secret.encrypt(data);
|
|
171
|
-
}
|
|
172
|
-
decryptData(hmac, ciphertext) {
|
|
173
|
-
const data = this.secret.decrypt(hmac, ciphertext);
|
|
174
|
-
if (data.length < 4) {
|
|
175
|
-
throw new Error("Invalid ciphertext");
|
|
176
|
-
}
|
|
177
|
-
const reader = new BufferReader(data);
|
|
178
|
-
return {
|
|
179
|
-
timestamp: reader.readTimestamp(),
|
|
180
|
-
data: reader.readBytes(reader.remainingBytes())
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
encryptData(plain) {
|
|
184
|
-
const writer = new BufferWriter();
|
|
185
|
-
writer.writeTimestamp(plain.timestamp);
|
|
186
|
-
writer.writeBytes(plain.data);
|
|
187
|
-
const data = writer.toBytes();
|
|
188
|
-
return this.secret.encrypt(data);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
export class Contacts {
|
|
192
|
-
constructor() {
|
|
193
|
-
this.localIdentities = [];
|
|
194
|
-
this.contacts = {};
|
|
195
|
-
this.groups = {};
|
|
196
|
-
}
|
|
197
|
-
addLocalIdentity(identity) {
|
|
198
|
-
this.localIdentities.push({ identity, sharedSecrets: {} });
|
|
199
|
-
}
|
|
200
|
-
addContact(contact) {
|
|
201
|
-
const hash = parseNodeHash(contact.identity.hash());
|
|
202
|
-
if (!this.contacts[hash]) {
|
|
203
|
-
this.contacts[hash] = [];
|
|
204
|
-
}
|
|
205
|
-
this.contacts[hash].push(contact);
|
|
206
|
-
}
|
|
207
|
-
decrypt(src, dst, hmac, ciphertext) {
|
|
208
|
-
// Find the public key associated with the source hash.
|
|
209
|
-
let contacts = [];
|
|
210
|
-
if (src instanceof PublicKey) {
|
|
211
|
-
// Check if we have a contact with this exact public key (for direct messages).
|
|
212
|
-
const srcHash = parseNodeHash(src.toHash());
|
|
213
|
-
for (const contact of this.contacts[srcHash] || []) {
|
|
214
|
-
if (contact.identity.matches(src)) {
|
|
215
|
-
contacts.push(contact);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
// If no contact matches the public key, add a temporary contact with the hash and no name.
|
|
219
|
-
if (contacts.length === 0) {
|
|
220
|
-
contacts.push(new Contact("", new Identity(src.toBytes())));
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
const srcHash = parseNodeHash(src);
|
|
225
|
-
contacts = this.contacts[srcHash] || [];
|
|
226
|
-
}
|
|
227
|
-
if (contacts.length === 0) {
|
|
228
|
-
throw new Error("Unknown source hash");
|
|
229
|
-
}
|
|
230
|
-
// Find the local identity associated with the destination hash.
|
|
231
|
-
const dstHash = parseNodeHash(dst);
|
|
232
|
-
const localIdentities = this.localIdentities.filter(li => li.identity.publicKey.key[0] === dstHash);
|
|
233
|
-
if (localIdentities.length === 0) {
|
|
234
|
-
throw new Error("Unknown destination hash");
|
|
235
|
-
}
|
|
236
|
-
// Try to decrypt with each combination of local identity and their public key.
|
|
237
|
-
for (const localIdentity of localIdentities) {
|
|
238
|
-
for (const contact of contacts) {
|
|
239
|
-
const sharedSecret = this.calculateSharedSecret(localIdentity, contact);
|
|
240
|
-
try {
|
|
241
|
-
const decrypted = sharedSecret.decrypt(hmac, ciphertext);
|
|
242
|
-
return { localIdentity: localIdentity.identity, contact, decrypted };
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
// Ignore decryption errors and try the next combination.
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
throw new Error("Decryption failed with all known identities and contacts");
|
|
250
|
-
}
|
|
251
|
-
// Caches the calculated shared secret for a given local identity and contact to avoid redundant calculations.
|
|
252
|
-
calculateSharedSecret(localIdentity, contact) {
|
|
253
|
-
const cacheKey = contact.identity.toString();
|
|
254
|
-
if (localIdentity.sharedSecrets[cacheKey]) {
|
|
255
|
-
return localIdentity.sharedSecrets[cacheKey];
|
|
256
|
-
}
|
|
257
|
-
const sharedSecret = localIdentity.identity.calculateSharedSecret(contact.identity);
|
|
258
|
-
localIdentity.sharedSecrets[cacheKey] = sharedSecret;
|
|
259
|
-
return sharedSecret;
|
|
260
|
-
}
|
|
261
|
-
addGroup(group) {
|
|
262
|
-
const hash = parseNodeHash(group.hash());
|
|
263
|
-
if (!this.groups[hash]) {
|
|
264
|
-
this.groups[hash] = [];
|
|
265
|
-
}
|
|
266
|
-
this.groups[hash].push(group);
|
|
267
|
-
}
|
|
268
|
-
decryptGroupText(channelHash, hmac, ciphertext) {
|
|
269
|
-
const hash = parseNodeHash(channelHash);
|
|
270
|
-
const groups = this.groups[hash] || [];
|
|
271
|
-
if (groups.length === 0) {
|
|
272
|
-
throw new Error("Unknown group hash");
|
|
273
|
-
}
|
|
274
|
-
for (const group of groups) {
|
|
275
|
-
try {
|
|
276
|
-
const decrypted = group.decryptText(hmac, ciphertext);
|
|
277
|
-
return { decrypted, group };
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
// Ignore decryption errors and try the next group.
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
throw new Error("Decryption failed with all known groups");
|
|
284
|
-
}
|
|
285
|
-
decryptGroupData(channelHash, hmac, ciphertext) {
|
|
286
|
-
const hash = parseNodeHash(channelHash);
|
|
287
|
-
const groups = this.groups[hash] || [];
|
|
288
|
-
if (groups.length === 0) {
|
|
289
|
-
throw new Error("Unknown group hash");
|
|
290
|
-
}
|
|
291
|
-
for (const group of groups) {
|
|
292
|
-
try {
|
|
293
|
-
const decrypted = group.decryptData(hmac, ciphertext);
|
|
294
|
-
return { decrypted, group };
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
// Ignore decryption errors and try the next group.
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
throw new Error("Decryption failed with all known groups");
|
|
301
|
-
}
|
|
302
|
-
}
|