@neykoor/libsignal-node 1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +130 -0
  3. package/lib/base-key-type.d.ts +4 -0
  4. package/lib/base-key-type.js +5 -0
  5. package/lib/chain-type.d.ts +4 -0
  6. package/lib/chain-type.js +5 -0
  7. package/lib/crypto.d.ts +6 -0
  8. package/lib/crypto.js +73 -0
  9. package/lib/curve.d.ts +9 -0
  10. package/lib/curve.js +112 -0
  11. package/lib/direction.d.ts +4 -0
  12. package/lib/direction.js +5 -0
  13. package/lib/errors.d.ts +16 -0
  14. package/lib/errors.js +28 -0
  15. package/lib/index.d.ts +20 -0
  16. package/lib/index.js +16 -0
  17. package/lib/keyhelper.d.ts +18 -0
  18. package/lib/keyhelper.js +52 -0
  19. package/lib/logger.d.ts +6 -0
  20. package/lib/logger.js +11 -0
  21. package/lib/memory-storage.d.ts +25 -0
  22. package/lib/memory-storage.js +68 -0
  23. package/lib/numeric-fingerprint.d.ts +5 -0
  24. package/lib/numeric-fingerprint.js +54 -0
  25. package/lib/prekey-bundle-validator.d.ts +2 -0
  26. package/lib/prekey-bundle-validator.js +44 -0
  27. package/lib/protobufs.d.ts +5 -0
  28. package/lib/protobufs.js +3 -0
  29. package/lib/protocol-address.d.ts +8 -0
  30. package/lib/protocol-address.js +31 -0
  31. package/lib/queue-job.d.ts +3 -0
  32. package/lib/queue-job.js +49 -0
  33. package/lib/session-builder.d.ts +12 -0
  34. package/lib/session-builder.js +153 -0
  35. package/lib/session-cipher.d.ts +29 -0
  36. package/lib/session-cipher.js +326 -0
  37. package/lib/session-record.d.ts +109 -0
  38. package/lib/session-record.js +286 -0
  39. package/lib/types.d.ts +42 -0
  40. package/lib/types.js +1 -0
  41. package/lib/util.d.ts +2 -0
  42. package/lib/util.js +10 -0
  43. package/lib/whisper-text-protocol.d.ts +3 -0
  44. package/lib/whisper-text-protocol.js +483 -0
  45. package/package.json +38 -0
@@ -0,0 +1,54 @@
1
+ import * as crypto from './crypto.js';
2
+ const VERSION = 0;
3
+ function iterateHash(data, key, count) {
4
+ let result = Buffer.from(data);
5
+ for (let i = 0; i < count; i++) {
6
+ const combined = Buffer.concat([result, Buffer.from(key)]);
7
+ result = crypto.hash(combined);
8
+ }
9
+ return new Uint8Array(result).buffer;
10
+ }
11
+ function shortToArrayBuffer(number) {
12
+ return new Uint16Array([number]).buffer;
13
+ }
14
+ function getEncodedChunk(hash, offset) {
15
+ const chunk = (hash[offset] * Math.pow(2, 32) +
16
+ hash[offset + 1] * Math.pow(2, 24) +
17
+ hash[offset + 2] * Math.pow(2, 16) +
18
+ hash[offset + 3] * Math.pow(2, 8) +
19
+ hash[offset + 4]) %
20
+ 100000;
21
+ let s = chunk.toString();
22
+ while (s.length < 5) {
23
+ s = '0' + s;
24
+ }
25
+ return s;
26
+ }
27
+ async function getDisplayStringFor(identifier, key, iterations) {
28
+ const bytes = Buffer.concat([Buffer.from(shortToArrayBuffer(VERSION)), Buffer.from(key), Buffer.from(identifier)]);
29
+ const arraybuf = new Uint8Array(bytes).buffer;
30
+ const output = new Uint8Array(iterateHash(arraybuf, key, iterations));
31
+ return (getEncodedChunk(output, 0) +
32
+ getEncodedChunk(output, 5) +
33
+ getEncodedChunk(output, 10) +
34
+ getEncodedChunk(output, 15) +
35
+ getEncodedChunk(output, 20) +
36
+ getEncodedChunk(output, 25));
37
+ }
38
+ export class FingerprintGenerator {
39
+ constructor(iterations) {
40
+ this.iterations = iterations;
41
+ }
42
+ createFor(localIdentifier, localIdentityKey, remoteIdentifier, remoteIdentityKey) {
43
+ if (typeof localIdentifier !== 'string' ||
44
+ typeof remoteIdentifier !== 'string' ||
45
+ !(localIdentityKey instanceof ArrayBuffer) ||
46
+ !(remoteIdentityKey instanceof ArrayBuffer)) {
47
+ throw new Error('Invalid arguments');
48
+ }
49
+ return Promise.all([
50
+ getDisplayStringFor(localIdentifier, localIdentityKey, this.iterations),
51
+ getDisplayStringFor(remoteIdentifier, remoteIdentityKey, this.iterations)
52
+ ]).then(fingerprints => fingerprints.sort().join(''));
53
+ }
54
+ }
@@ -0,0 +1,2 @@
1
+ import type { DeviceKeyBundle } from './types.js';
2
+ export declare function assertValidDeviceKeyBundle(bundle: DeviceKeyBundle): void;
@@ -0,0 +1,44 @@
1
+ import { PreKeyError } from './errors.js';
2
+ const MIN_REGISTRATION_ID = 1;
3
+ const MAX_REGISTRATION_ID = 0x3fff;
4
+ const IDENTITY_KEY_LENGTH = 33;
5
+ const PUBLIC_KEY_LENGTH = 33;
6
+ const SIGNATURE_LENGTH = 64;
7
+ const MAX_KEY_ID = 0xffffff;
8
+ function isNonNegativeInteger(n) {
9
+ return typeof n === 'number' && Number.isInteger(n) && n >= 0;
10
+ }
11
+ function assertBufferLength(value, length, field) {
12
+ if (!(value instanceof Uint8Array)) {
13
+ throw new PreKeyError(`${field} must be a Uint8Array`);
14
+ }
15
+ if (value.byteLength !== length) {
16
+ throw new PreKeyError(`${field} has invalid length: expected ${length}, got ${value.byteLength}`);
17
+ }
18
+ }
19
+ function assertKeyId(value, field) {
20
+ if (!isNonNegativeInteger(value) || value > MAX_KEY_ID) {
21
+ throw new PreKeyError(`${field} is out of range: ${String(value)}`);
22
+ }
23
+ }
24
+ export function assertValidDeviceKeyBundle(bundle) {
25
+ if (!bundle || typeof bundle !== 'object') {
26
+ throw new PreKeyError('Device key bundle must be an object');
27
+ }
28
+ if (!isNonNegativeInteger(bundle.registrationId) ||
29
+ bundle.registrationId < MIN_REGISTRATION_ID ||
30
+ bundle.registrationId > MAX_REGISTRATION_ID) {
31
+ throw new PreKeyError(`Invalid registrationId: ${String(bundle.registrationId)}`);
32
+ }
33
+ assertBufferLength(bundle.identityKey, IDENTITY_KEY_LENGTH, 'identityKey');
34
+ if (!bundle.signedPreKey || typeof bundle.signedPreKey !== 'object') {
35
+ throw new PreKeyError('Missing signedPreKey');
36
+ }
37
+ assertKeyId(bundle.signedPreKey.keyId, 'signedPreKey.keyId');
38
+ assertBufferLength(bundle.signedPreKey.publicKey, PUBLIC_KEY_LENGTH, 'signedPreKey.publicKey');
39
+ assertBufferLength(bundle.signedPreKey.signature, SIGNATURE_LENGTH, 'signedPreKey.signature');
40
+ if (bundle.preKey) {
41
+ assertKeyId(bundle.preKey.keyId, 'preKey.keyId');
42
+ assertBufferLength(bundle.preKey.publicKey, PUBLIC_KEY_LENGTH, 'preKey.publicKey');
43
+ }
44
+ }
@@ -0,0 +1,5 @@
1
+ import { textsecure } from './whisper-text-protocol.js';
2
+ export declare const WhisperMessage: typeof textsecure.WhisperMessage;
3
+ export declare const PreKeyWhisperMessage: typeof textsecure.PreKeyWhisperMessage;
4
+ export type WhisperMessage = textsecure.WhisperMessage;
5
+ export type PreKeyWhisperMessage = textsecure.PreKeyWhisperMessage;
@@ -0,0 +1,3 @@
1
+ import root, { textsecure } from './whisper-text-protocol.js';
2
+ export const WhisperMessage = root.textsecure.WhisperMessage;
3
+ export const PreKeyWhisperMessage = root.textsecure.PreKeyWhisperMessage;
@@ -0,0 +1,8 @@
1
+ export declare class ProtocolAddress {
2
+ id: string;
3
+ deviceId: number;
4
+ static from(encodedAddress: string): ProtocolAddress;
5
+ constructor(id: string, deviceId: number);
6
+ toString(): string;
7
+ is(other: unknown): boolean;
8
+ }
@@ -0,0 +1,31 @@
1
+ export class ProtocolAddress {
2
+ static from(encodedAddress) {
3
+ if (typeof encodedAddress !== 'string' || !encodedAddress.match(/.*\.\d+/)) {
4
+ throw new Error('Invalid address encoding');
5
+ }
6
+ const parts = encodedAddress.split('.');
7
+ return new ProtocolAddress(parts[0], parseInt(parts[1], 10));
8
+ }
9
+ constructor(id, deviceId) {
10
+ if (typeof id !== 'string') {
11
+ throw new TypeError('id required for addr');
12
+ }
13
+ if (id.indexOf('.') !== -1) {
14
+ throw new TypeError('encoded addr detected');
15
+ }
16
+ this.id = id;
17
+ if (typeof deviceId !== 'number') {
18
+ throw new TypeError('number required for deviceId');
19
+ }
20
+ this.deviceId = deviceId;
21
+ }
22
+ toString() {
23
+ return `${this.id}.${this.deviceId}`;
24
+ }
25
+ is(other) {
26
+ if (!(other instanceof ProtocolAddress)) {
27
+ return false;
28
+ }
29
+ return other.id === this.id && other.deviceId === this.deviceId;
30
+ }
31
+ }
@@ -0,0 +1,3 @@
1
+ type Awaitable<T> = () => Promise<T>;
2
+ export declare function queueJob<T>(bucket: unknown, awaitable: Awaitable<T>): Promise<T>;
3
+ export {};
@@ -0,0 +1,49 @@
1
+ const _queueAsyncBuckets = new Map();
2
+ const _gcLimit = 500;
3
+ async function _asyncQueueExecutor(queue, cleanup) {
4
+ let offt = 0;
5
+ while (true) {
6
+ const limit = Math.min(queue.length, _gcLimit);
7
+ for (let i = offt; i < limit; i++) {
8
+ const job = queue[i];
9
+ try {
10
+ job.resolve(await job.awaitable());
11
+ }
12
+ catch (e) {
13
+ job.reject(e);
14
+ }
15
+ }
16
+ if (limit < queue.length) {
17
+ queue.splice(0, limit);
18
+ offt = 0;
19
+ }
20
+ else {
21
+ break;
22
+ }
23
+ }
24
+ cleanup();
25
+ }
26
+ export function queueJob(bucket, awaitable) {
27
+ const namedAwaitable = awaitable;
28
+ if (!namedAwaitable.name) {
29
+ Object.defineProperty(namedAwaitable, 'name', { writable: true });
30
+ if (typeof bucket === 'string') {
31
+ namedAwaitable.name = bucket;
32
+ }
33
+ }
34
+ let inactive = false;
35
+ if (!_queueAsyncBuckets.has(bucket)) {
36
+ _queueAsyncBuckets.set(bucket, []);
37
+ inactive = true;
38
+ }
39
+ const queue = _queueAsyncBuckets.get(bucket);
40
+ const job = new Promise((resolve, reject) => queue.push({
41
+ awaitable: awaitable,
42
+ resolve: resolve,
43
+ reject
44
+ }));
45
+ if (inactive) {
46
+ _asyncQueueExecutor(queue, () => _queueAsyncBuckets.delete(bucket));
47
+ }
48
+ return job;
49
+ }
@@ -0,0 +1,12 @@
1
+ import type { ProtocolAddress } from './protocol-address.js';
2
+ import { SessionRecord } from './session-record.js';
3
+ import type { DeviceKeyBundle, IncomingPreKeyMessage, SignalStorage } from './types.js';
4
+ export declare class SessionBuilder {
5
+ private addr;
6
+ private storage;
7
+ constructor(storage: SignalStorage, protocolAddress: ProtocolAddress);
8
+ initOutgoing(device: DeviceKeyBundle): Promise<void>;
9
+ initIncoming(record: SessionRecord, message: IncomingPreKeyMessage): Promise<number | undefined>;
10
+ private initSession;
11
+ private calculateSendingRatchet;
12
+ }
@@ -0,0 +1,153 @@
1
+ import { BaseKeyType } from './base-key-type.js';
2
+ import { ChainType } from './chain-type.js';
3
+ import * as crypto from './crypto.js';
4
+ import * as curve from './curve.js';
5
+ import { Direction } from './direction.js';
6
+ import * as errors from './errors.js';
7
+ import { queueJob } from './queue-job.js';
8
+ import { assertValidDeviceKeyBundle } from './prekey-bundle-validator.js';
9
+ import { SessionRecord } from './session-record.js';
10
+ export class SessionBuilder {
11
+ constructor(storage, protocolAddress) {
12
+ this.addr = protocolAddress;
13
+ this.storage = storage;
14
+ }
15
+ async initOutgoing(device) {
16
+ assertValidDeviceKeyBundle(device);
17
+ const fqAddr = this.addr.toString();
18
+ return await queueJob(fqAddr, async () => {
19
+ if (!(await this.storage.isTrustedIdentity(this.addr.id, device.identityKey, Direction.SENDING))) {
20
+ throw new errors.UntrustedIdentityKeyError(this.addr.id, device.identityKey);
21
+ }
22
+ if (!curve.verifySignature(device.identityKey, device.signedPreKey.publicKey, device.signedPreKey.signature)) {
23
+ throw new Error('Signature validation failed');
24
+ }
25
+ const baseKey = curve.generateKeyPair();
26
+ const devicePreKey = device.preKey && device.preKey.publicKey;
27
+ const session = await this.initSession(true, baseKey, undefined, device.identityKey, devicePreKey, device.signedPreKey.publicKey, device.registrationId);
28
+ session.pendingPreKey = {
29
+ signedKeyId: device.signedPreKey.keyId,
30
+ baseKey: baseKey.pubKey
31
+ };
32
+ if (device.preKey) {
33
+ session.pendingPreKey.preKeyId = device.preKey.keyId;
34
+ }
35
+ let record = await this.storage.loadSession(fqAddr);
36
+ if (!record) {
37
+ record = new SessionRecord();
38
+ }
39
+ else {
40
+ const openSession = record.getOpenSession();
41
+ if (openSession) {
42
+ record.closeSession(openSession);
43
+ }
44
+ }
45
+ record.setSession(session);
46
+ record.removeOldSessions();
47
+ await this.storage.storeSession(fqAddr, record);
48
+ });
49
+ }
50
+ async initIncoming(record, message) {
51
+ if (!(await this.storage.isTrustedIdentity(this.addr.id, message.identityKey, Direction.RECEIVING))) {
52
+ throw new errors.UntrustedIdentityKeyError(this.addr.id, message.identityKey);
53
+ }
54
+ if (record.getSession(message.baseKey)) {
55
+ return undefined;
56
+ }
57
+ let preKeyPair;
58
+ if (message.preKeyId !== undefined) {
59
+ preKeyPair = await this.storage.loadPreKey(message.preKeyId);
60
+ if (!preKeyPair) {
61
+ throw new errors.PreKeyError('Invalid PreKey ID');
62
+ }
63
+ }
64
+ const signedPreKeyPair = await this.storage.loadSignedPreKey(message.signedPreKeyId);
65
+ if (!signedPreKeyPair) {
66
+ throw new errors.PreKeyError('Missing SignedPreKey');
67
+ }
68
+ const existingOpenSession = record.getOpenSession();
69
+ if (existingOpenSession) {
70
+ record.closeSession(existingOpenSession);
71
+ }
72
+ record.setSession(await this.initSession(false, preKeyPair, signedPreKeyPair, message.identityKey, message.baseKey, undefined, message.registrationId));
73
+ return message.preKeyId;
74
+ }
75
+ async initSession(isInitiator, ourEphemeralKey, ourSignedKeyInput, theirIdentityPubKey, theirEphemeralPubKey, theirSignedPubKeyInput, registrationId) {
76
+ let ourSignedKey = ourSignedKeyInput;
77
+ let theirSignedPubKey = theirSignedPubKeyInput;
78
+ if (isInitiator) {
79
+ if (ourSignedKey) {
80
+ throw new Error('Invalid call to initSession');
81
+ }
82
+ ourSignedKey = ourEphemeralKey;
83
+ }
84
+ else {
85
+ if (theirSignedPubKey) {
86
+ throw new Error('Invalid call to initSession');
87
+ }
88
+ theirSignedPubKey = theirEphemeralPubKey;
89
+ }
90
+ let sharedSecret;
91
+ if (!ourEphemeralKey || !theirEphemeralPubKey) {
92
+ sharedSecret = new Uint8Array(32 * 4);
93
+ }
94
+ else {
95
+ sharedSecret = new Uint8Array(32 * 5);
96
+ }
97
+ for (let i = 0; i < 32; i++) {
98
+ sharedSecret[i] = 0xff;
99
+ }
100
+ const ourIdentityKey = await this.storage.getOurIdentity();
101
+ const a1 = curve.calculateAgreement(theirSignedPubKey, ourIdentityKey.privKey);
102
+ const a2 = curve.calculateAgreement(theirIdentityPubKey, ourSignedKey.privKey);
103
+ const a3 = curve.calculateAgreement(theirSignedPubKey, ourSignedKey.privKey);
104
+ if (isInitiator) {
105
+ sharedSecret.set(new Uint8Array(a1), 32);
106
+ sharedSecret.set(new Uint8Array(a2), 32 * 2);
107
+ }
108
+ else {
109
+ sharedSecret.set(new Uint8Array(a1), 32 * 2);
110
+ sharedSecret.set(new Uint8Array(a2), 32);
111
+ }
112
+ sharedSecret.set(new Uint8Array(a3), 32 * 3);
113
+ if (ourEphemeralKey && theirEphemeralPubKey) {
114
+ const a4 = curve.calculateAgreement(theirEphemeralPubKey, ourEphemeralKey.privKey);
115
+ sharedSecret.set(new Uint8Array(a4), 32 * 4);
116
+ }
117
+ const masterKey = crypto.deriveSecrets(Buffer.from(sharedSecret), Buffer.alloc(32), Buffer.from('WhisperText'));
118
+ const session = SessionRecord.createEntry();
119
+ session.registrationId = registrationId;
120
+ session.currentRatchet = {
121
+ rootKey: masterKey[0],
122
+ ephemeralKeyPair: isInitiator ? curve.generateKeyPair() : ourSignedKey,
123
+ lastRemoteEphemeralKey: theirSignedPubKey,
124
+ previousCounter: 0
125
+ };
126
+ session.indexInfo = {
127
+ created: Date.now(),
128
+ used: Date.now(),
129
+ remoteIdentityKey: theirIdentityPubKey,
130
+ baseKey: isInitiator ? ourEphemeralKey.pubKey : theirEphemeralPubKey,
131
+ baseKeyType: isInitiator ? BaseKeyType.OURS : BaseKeyType.THEIRS,
132
+ closed: -1
133
+ };
134
+ if (isInitiator) {
135
+ this.calculateSendingRatchet(session, theirSignedPubKey);
136
+ }
137
+ return session;
138
+ }
139
+ calculateSendingRatchet(session, remoteKey) {
140
+ const ratchet = session.currentRatchet;
141
+ const sharedSecret = curve.calculateAgreement(remoteKey, ratchet.ephemeralKeyPair.privKey);
142
+ const masterKey = crypto.deriveSecrets(sharedSecret, ratchet.rootKey, Buffer.from('WhisperRatchet'));
143
+ session.addChain(ratchet.ephemeralKeyPair.pubKey, {
144
+ messageKeys: {},
145
+ chainKey: {
146
+ counter: -1,
147
+ key: masterKey[1]
148
+ },
149
+ chainType: ChainType.SENDING
150
+ });
151
+ ratchet.rootKey = masterKey[0];
152
+ }
153
+ }
@@ -0,0 +1,29 @@
1
+ import { ProtocolAddress } from './protocol-address.js';
2
+ import type { Chain, SessionEntry } from './session-record.js';
3
+ import { SessionRecord } from './session-record.js';
4
+ import type { EncryptedMessage, SignalStorage } from './types.js';
5
+ export declare class SessionCipher {
6
+ private addr;
7
+ private storage;
8
+ private staleSessionMaxAgeMs?;
9
+ constructor(storage: SignalStorage, protocolAddress: ProtocolAddress, staleSessionMaxAgeMs?: number | undefined);
10
+ private _encodeTupleByte;
11
+ private _decodeTupleByte;
12
+ toString(): string;
13
+ getRecord(): Promise<SessionRecord | undefined>;
14
+ storeRecord(record: SessionRecord): Promise<void>;
15
+ queueJob<T>(awaitable: () => Promise<T>): Promise<T>;
16
+ encrypt(data: Uint8Array): Promise<EncryptedMessage>;
17
+ decryptWithSessions(data: Uint8Array, sessions: SessionEntry[]): Promise<{
18
+ session: SessionEntry;
19
+ plaintext: Buffer;
20
+ }>;
21
+ decryptWhisperMessage(data: Uint8Array): Promise<Buffer>;
22
+ decryptPreKeyWhisperMessage(data: Uint8Array): Promise<Buffer>;
23
+ doDecryptWhisperMessage(messageBuffer: Uint8Array, session: SessionEntry): Promise<Buffer>;
24
+ fillMessageKeys(chain: Chain, counter: number): void;
25
+ maybeStepRatchet(session: SessionEntry, remoteKey: Buffer, previousCounter: number): void;
26
+ calculateRatchet(session: SessionEntry, remoteKey: Buffer, sending: boolean): void;
27
+ hasOpenSession(): Promise<boolean>;
28
+ closeOpenSession(): Promise<void>;
29
+ }