refactor: migrate WASignalGroup to TypeScript and remove deprecated files (#1320)

* refactor: migrate WASignalGroup to TypeScript and remove deprecated files

* chore: remove WASignalGroup JavaScript files from package.json

* refactor: update SenderKeyStore and SenderKeyStateStructure interfaces to export and add deserialize method. Fix types

* chore: lint issue

* refactor: improve constructor type checking and getSeed method in SenderChainKey

* refactor: update key handling to use Buffer for improved consistency

* signal: consistent naming and dir structure + add some missing types

* fix: lint issues

---------

Co-authored-by: Rajeh Taher <rajeh@reforward.dev>
This commit is contained in:
João Lucas de Oliveira Lopes
2025-06-21 12:02:15 -03:00
committed by GitHub
parent f404147736
commit 482db6edc5
33 changed files with 884 additions and 2562 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
class CiphertextMessage {
UNSUPPORTED_VERSION = 1;
CURRENT_VERSION = 3;
WHISPER_TYPE = 2;
PREKEY_TYPE = 3;
SENDERKEY_TYPE = 4;
SENDERKEY_DISTRIBUTION_TYPE = 5;
ENCRYPTED_MESSAGE_OVERHEAD = 53;
}
module.exports = CiphertextMessage;

View File

@@ -1 +0,0 @@
yarn pbjs -t static-module -w commonjs -o ./WASignalGroup/GroupProtocol.js ./WASignalGroup/group.proto

View File

@@ -1,42 +0,0 @@
package groupproto;
message SenderKeyMessage {
optional uint32 id = 1;
optional uint32 iteration = 2;
optional bytes ciphertext = 3;
}
message SenderKeyDistributionMessage {
optional uint32 id = 1;
optional uint32 iteration = 2;
optional bytes chainKey = 3;
optional bytes signingKey = 4;
}
message SenderChainKey {
optional uint32 iteration = 1;
optional bytes seed = 2;
}
message SenderMessageKey {
optional uint32 iteration = 1;
optional bytes seed = 2;
}
message SenderSigningKey {
optional bytes public = 1;
optional bytes private = 2;
}
message SenderKeyStateStructure {
optional uint32 senderKeyId = 1;
optional SenderChainKey senderChainKey = 2;
optional SenderSigningKey senderSigningKey = 3;
repeated SenderMessageKey senderMessageKeys = 4;
}
message SenderKeyRecordStructure {
repeated SenderKeyStateStructure senderKeyStates = 1;
}

View File

@@ -1,120 +0,0 @@
const queue_job = require('./queue_job');
const SenderKeyMessage = require('./sender_key_message');
const crypto = require('libsignal/src/crypto');
class GroupCipher {
constructor(senderKeyStore, senderKeyName) {
this.senderKeyStore = senderKeyStore;
this.senderKeyName = senderKeyName;
}
queueJob(awaitable) {
return queue_job(this.senderKeyName.toString(), awaitable)
}
async encrypt(paddedPlaintext) {
return await this.queueJob(async () => {
const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName);
if (!record) {
throw new Error("No SenderKeyRecord found for encryption")
}
const senderKeyState = record.getSenderKeyState();
if (!senderKeyState) {
throw new Error("No session to encrypt message");
}
const iteration = senderKeyState.getSenderChainKey().getIteration()
const senderKey = this.getSenderKey(senderKeyState, iteration === 0 ? 0 : iteration + 1)
const ciphertext = await this.getCipherText(
senderKey.getIv(),
senderKey.getCipherKey(),
paddedPlaintext
);
const senderKeyMessage = new SenderKeyMessage(
senderKeyState.getKeyId(),
senderKey.getIteration(),
ciphertext,
senderKeyState.getSigningKeyPrivate()
);
await this.senderKeyStore.storeSenderKey(this.senderKeyName, record);
return senderKeyMessage.serialize()
})
}
async decrypt(senderKeyMessageBytes) {
return await this.queueJob(async () => {
const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName);
if (!record) {
throw new Error("No SenderKeyRecord found for decryption")
}
const senderKeyMessage = new SenderKeyMessage(null, null, null, null, senderKeyMessageBytes);
const senderKeyState = record.getSenderKeyState(senderKeyMessage.getKeyId());
if (!senderKeyState) {
throw new Error("No session found to decrypt message")
}
senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic());
const senderKey = this.getSenderKey(senderKeyState, senderKeyMessage.getIteration());
// senderKeyState.senderKeyStateStructure.senderSigningKey.private =
const plaintext = await this.getPlainText(
senderKey.getIv(),
senderKey.getCipherKey(),
senderKeyMessage.getCipherText()
);
await this.senderKeyStore.storeSenderKey(this.senderKeyName, record);
return plaintext;
})
}
getSenderKey(senderKeyState, iteration) {
let senderChainKey = senderKeyState.getSenderChainKey();
if (senderChainKey.getIteration() > iteration) {
if (senderKeyState.hasSenderMessageKey(iteration)) {
return senderKeyState.removeSenderMessageKey(iteration);
}
throw new Error(
`Received message with old counter: ${senderChainKey.getIteration()}, ${iteration}`
);
}
if (iteration - senderChainKey.getIteration() > 2000) {
throw new Error('Over 2000 messages into the future!');
}
while (senderChainKey.getIteration() < iteration) {
senderKeyState.addSenderMessageKey(senderChainKey.getSenderMessageKey());
senderChainKey = senderChainKey.getNext();
}
senderKeyState.setSenderChainKey(senderChainKey.getNext());
return senderChainKey.getSenderMessageKey();
}
getPlainText(iv, key, ciphertext) {
try {
const plaintext = crypto.decrypt(key, ciphertext, iv);
return plaintext;
} catch (e) {
//console.log(e.stack);
throw new Error('InvalidMessageException');
}
}
getCipherText(iv, key, plaintext) {
try {
iv = typeof iv === 'string' ? Buffer.from(iv, 'base64') : iv;
key = typeof key === 'string' ? Buffer.from(key, 'base64') : key;
const crypted = crypto.encrypt(key, Buffer.from(plaintext), iv);
return crypted;
} catch (e) {
//console.log(e.stack);
throw new Error('InvalidMessageException');
}
}
}
module.exports = GroupCipher;

View File

@@ -1,46 +0,0 @@
//const utils = require('../../common/utils');
const SenderKeyDistributionMessage = require('./sender_key_distribution_message');
const keyhelper = require("./keyhelper");
class GroupSessionBuilder {
constructor(senderKeyStore) {
this.senderKeyStore = senderKeyStore;
}
async process(senderKeyName, senderKeyDistributionMessage) {
//console.log('GroupSessionBuilder process', senderKeyName, senderKeyDistributionMessage);
const senderKeyRecord = await this.senderKeyStore.loadSenderKey(senderKeyName);
senderKeyRecord.addSenderKeyState(
senderKeyDistributionMessage.getId(),
senderKeyDistributionMessage.getIteration(),
senderKeyDistributionMessage.getChainKey(),
senderKeyDistributionMessage.getSignatureKey()
);
await this.senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord);
}
// [{"senderKeyId":1742199468,"senderChainKey":{"iteration":0,"seed":"yxMY9VFQcXEP34olRAcGCtsgx1XoKsHfDIh+1ea4HAQ="},"senderSigningKey":{"public":""}}]
async create(senderKeyName) {
const senderKeyRecord = await this.senderKeyStore.loadSenderKey(senderKeyName);
//console.log('GroupSessionBuilder create session', senderKeyName, senderKeyRecord);
if (senderKeyRecord.isEmpty()) {
const keyId = keyhelper.generateSenderKeyId();
const senderKey = keyhelper.generateSenderKey();
const signingKey = keyhelper.generateSenderSigningKey();
senderKeyRecord.setSenderKeyState(keyId, 0, senderKey, signingKey);
await this.senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord);
}
const state = senderKeyRecord.getSenderKeyState();
return new SenderKeyDistributionMessage(
state.getKeyId(),
state.getSenderChainKey().getIteration(),
state.getSenderChainKey().getSeed(),
state.getSigningKeyPublic()
);
}
}
module.exports = GroupSessionBuilder;

View File

@@ -1,5 +0,0 @@
module.exports.GroupSessionBuilder = require('./group_session_builder')
module.exports.SenderKeyDistributionMessage = require('./sender_key_distribution_message')
module.exports.SenderKeyRecord = require('./sender_key_record')
module.exports.SenderKeyName = require('./sender_key_name')
module.exports.GroupCipher = require('./group_cipher')

View File

@@ -1,21 +0,0 @@
const curve = require('libsignal/src/curve');
const nodeCrypto = require('crypto');
exports.generateSenderKey = function() {
return nodeCrypto.randomBytes(32);
}
exports.generateSenderKeyId = function() {
return nodeCrypto.randomInt(2147483647);
}
exports.generateSenderSigningKey = function(key) {
if (!key) {
key = curve.generateKeyPair();
}
return {
public: key.pubKey,
private: key.privKey,
};
}

View File

@@ -1,3 +0,0 @@
const { groupproto } = require('./GroupProtocol')
module.exports = groupproto

View File

@@ -1,69 +0,0 @@
// vim: ts=4:sw=4:expandtab
/*
* jobQueue manages multiple queues indexed by device to serialize
* session io ops on the database.
*/
'use strict';
const _queueAsyncBuckets = new Map();
const _gcLimit = 10000;
async function _asyncQueueExecutor(queue, cleanup) {
let offt = 0;
while (true) {
let limit = Math.min(queue.length, _gcLimit); // Break up thundering hurds for GC duty.
for (let i = offt; i < limit; i++) {
const job = queue[i];
try {
job.resolve(await job.awaitable());
} catch (e) {
job.reject(e);
}
}
if (limit < queue.length) {
/* Perform lazy GC of queue for faster iteration. */
if (limit >= _gcLimit) {
queue.splice(0, limit);
offt = 0;
} else {
offt = limit;
}
} else {
break;
}
}
cleanup();
}
module.exports = function (bucket, awaitable) {
/* Run the async awaitable only when all other async calls registered
* here have completed (or thrown). The bucket argument is a hashable
* key representing the task queue to use. */
if (!awaitable.name) {
// Make debuging easier by adding a name to this function.
Object.defineProperty(awaitable, 'name', { writable: true });
if (typeof bucket === 'string') {
awaitable.name = bucket;
} else {
console.warn("Unhandled bucket type (for naming):", typeof bucket, bucket);
}
}
let inactive;
if (!_queueAsyncBuckets.has(bucket)) {
_queueAsyncBuckets.set(bucket, []);
inactive = true;
}
const queue = _queueAsyncBuckets.get(bucket);
const job = new Promise((resolve, reject) => queue.push({
awaitable,
resolve,
reject
}));
if (inactive) {
/* An executor is not currently active; Start one now. */
_asyncQueueExecutor(queue, () => _queueAsyncBuckets.delete(bucket));
}
return job;
};

View File

@@ -1,6 +0,0 @@
# Signal-Group
This contains the code to decrypt/encrypt WA group messages.
Originally from [pokearaujo/libsignal-node](https://github.com/pokearaujo/libsignal-node)
The code has been moved outside the signal package as I felt it didn't belong in ths signal package, as it isn't inherently a part of signal but of WA.

View File

@@ -1,50 +0,0 @@
const SenderMessageKey = require('./sender_message_key');
//const HKDF = require('./hkdf');
const crypto = require('libsignal/src/crypto');
class SenderChainKey {
MESSAGE_KEY_SEED = Buffer.from([0x01]);
CHAIN_KEY_SEED = Buffer.from([0x02]);
iteration = 0;
chainKey = Buffer.alloc(0);
constructor(iteration, chainKey) {
this.iteration = iteration;
this.chainKey = chainKey;
}
getIteration() {
return this.iteration;
}
getSenderMessageKey() {
return new SenderMessageKey(
this.iteration,
this.getDerivative(this.MESSAGE_KEY_SEED, this.chainKey)
);
}
getNext() {
return new SenderChainKey(
this.iteration + 1,
this.getDerivative(this.CHAIN_KEY_SEED, this.chainKey)
);
}
getSeed() {
return typeof this.chainKey === 'string' ? Buffer.from(this.chainKey, 'base64') : this.chainKey;
}
getDerivative(seed, key) {
key = typeof key === 'string' ? Buffer.from(key, 'base64') : key;
const hash = crypto.calculateMAC(key, seed);
//const hash = new Hash().hmac_hash(key, seed, 'sha256', '');
return hash;
}
}
module.exports = SenderChainKey;

View File

@@ -1,78 +0,0 @@
const CiphertextMessage = require('./ciphertext_message');
const protobufs = require('./protobufs');
class SenderKeyDistributionMessage extends CiphertextMessage {
constructor(
id = null,
iteration = null,
chainKey = null,
signatureKey = null,
serialized = null
) {
super();
if (serialized) {
try {
const version = serialized[0];
const message = serialized.slice(1);
const distributionMessage = protobufs.SenderKeyDistributionMessage.decode(
message
).toJSON();
this.serialized = serialized;
this.id = distributionMessage.id;
this.iteration = distributionMessage.iteration;
this.chainKey = distributionMessage.chainKey;
this.signatureKey = distributionMessage.signingKey;
} catch (e) {
throw new Error(e);
}
} else {
const version = this.intsToByteHighAndLow(this.CURRENT_VERSION, this.CURRENT_VERSION);
this.id = id;
this.iteration = iteration;
this.chainKey = chainKey;
this.signatureKey = signatureKey;
const message = protobufs.SenderKeyDistributionMessage.encode(
protobufs.SenderKeyDistributionMessage.create({
id,
iteration,
chainKey,
signingKey: this.signatureKey,
})
).finish();
this.serialized = Buffer.concat([Buffer.from([version]), message]);
}
}
intsToByteHighAndLow(highValue, lowValue) {
return (((highValue << 4) | lowValue) & 0xff) % 256;
}
serialize() {
return this.serialized;
}
getType() {
return this.SENDERKEY_DISTRIBUTION_TYPE;
}
getIteration() {
return this.iteration;
}
getChainKey() {
return typeof this.chainKey === 'string' ? Buffer.from(this.chainKey, 'base64') : this.chainKey;
}
getSignatureKey() {
return typeof this.signatureKey === 'string'
? Buffer.from(this.signatureKey, 'base64')
: this.signatureKey;
}
getId() {
return this.id;
}
}
module.exports = SenderKeyDistributionMessage;

View File

@@ -1,92 +0,0 @@
const CiphertextMessage = require('./ciphertext_message');
const curve = require('libsignal/src/curve');
const protobufs = require('./protobufs');
class SenderKeyMessage extends CiphertextMessage {
SIGNATURE_LENGTH = 64;
constructor(
keyId = null,
iteration = null,
ciphertext = null,
signatureKey = null,
serialized = null
) {
super();
if (serialized) {
const version = serialized[0];
const message = serialized.slice(1, serialized.length - this.SIGNATURE_LENGTH);
const signature = serialized.slice(-1 * this.SIGNATURE_LENGTH);
const senderKeyMessage = protobufs.SenderKeyMessage.decode(message).toJSON();
senderKeyMessage.ciphertext = Buffer.from(senderKeyMessage.ciphertext, 'base64');
this.serialized = serialized;
this.messageVersion = (version & 0xff) >> 4;
this.keyId = senderKeyMessage.id;
this.iteration = senderKeyMessage.iteration;
this.ciphertext = senderKeyMessage.ciphertext;
this.signature = signature;
} else {
const version = (((this.CURRENT_VERSION << 4) | this.CURRENT_VERSION) & 0xff) % 256;
ciphertext = Buffer.from(ciphertext); // .toString('base64');
const message = protobufs.SenderKeyMessage.encode(
protobufs.SenderKeyMessage.create({
id: keyId,
iteration,
ciphertext,
})
).finish();
const signature = this.getSignature(
signatureKey,
Buffer.concat([Buffer.from([version]), message])
);
this.serialized = Buffer.concat([Buffer.from([version]), message, Buffer.from(signature)]);
this.messageVersion = this.CURRENT_VERSION;
this.keyId = keyId;
this.iteration = iteration;
this.ciphertext = ciphertext;
this.signature = signature;
}
}
getKeyId() {
return this.keyId;
}
getIteration() {
return this.iteration;
}
getCipherText() {
return this.ciphertext;
}
verifySignature(signatureKey) {
const part1 = this.serialized.slice(0, this.serialized.length - this.SIGNATURE_LENGTH);
const part2 = this.serialized.slice(-1 * this.SIGNATURE_LENGTH);
const res = curve.verifySignature(signatureKey, part1, part2);
if (!res) throw new Error('Invalid signature!');
}
getSignature(signatureKey, serialized) {
const signature = Buffer.from(
curve.calculateSignature(
signatureKey,
serialized
)
);
return signature;
}
serialize() {
return this.serialized;
}
getType() {
return 4;
}
}
module.exports = SenderKeyMessage;

View File

@@ -1,70 +0,0 @@
function isNull(str) {
return str === null || str.value === '';
}
/**
* java String hashCode 的实现
* @param strKey
* @return intValue
*/
function intValue(num) {
const MAX_VALUE = 0x7fffffff;
const MIN_VALUE = -0x80000000;
if (num > MAX_VALUE || num < MIN_VALUE) {
// eslint-disable-next-line
return (num &= 0xffffffff);
}
return num;
}
function hashCode(strKey) {
let hash = 0;
if (!isNull(strKey)) {
for (let i = 0; i < strKey.length; i++) {
hash = hash * 31 + strKey.charCodeAt(i);
hash = intValue(hash);
}
}
return hash;
}
/**
* 将js页面的number类型转换为java的int类型
* @param num
* @return intValue
*/
class SenderKeyName {
constructor(groupId, sender) {
this.groupId = groupId;
this.sender = sender;
}
getGroupId() {
return this.groupId;
}
getSender() {
return this.sender;
}
serialize() {
return `${this.groupId}::${this.sender.id}::${this.sender.deviceId}`;
}
toString() {
return this.serialize();
}
equals(other) {
if (other === null) return false;
if (!(other instanceof SenderKeyName)) return false;
return this.groupId === other.groupId && this.sender.toString() === other.sender.toString();
}
hashCode() {
return hashCode(this.groupId) ^ hashCode(this.sender.toString());
}
}
module.exports = SenderKeyName;

View File

@@ -1,56 +0,0 @@
const SenderKeyState = require('./sender_key_state');
class SenderKeyRecord {
MAX_STATES = 5;
constructor(serialized) {
this.senderKeyStates = [];
if (serialized) {
const list = serialized;
for (let i = 0; i < list.length; i++) {
const structure = list[i];
this.senderKeyStates.push(
new SenderKeyState(null, null, null, null, null, null, structure)
);
}
}
}
isEmpty() {
return this.senderKeyStates.length === 0;
}
getSenderKeyState(keyId) {
if (!keyId && this.senderKeyStates.length) return this.senderKeyStates[this.senderKeyStates.length - 1];
for (let i = 0; i < this.senderKeyStates.length; i++) {
const state = this.senderKeyStates[i];
if (state.getKeyId() === keyId) {
return state;
}
}
}
addSenderKeyState(id, iteration, chainKey, signatureKey) {
this.senderKeyStates.push(new SenderKeyState(id, iteration, chainKey, null, signatureKey));
if (this.senderKeyStates.length > 5) {
this.senderKeyStates.shift()
}
}
setSenderKeyState(id, iteration, chainKey, keyPair) {
this.senderKeyStates.length = 0;
this.senderKeyStates.push(new SenderKeyState(id, iteration, chainKey, keyPair));
}
serialize() {
const recordStructure = [];
for (let i = 0; i < this.senderKeyStates.length; i++) {
const senderKeyState = this.senderKeyStates[i];
recordStructure.push(senderKeyState.getStructure());
}
return recordStructure;
}
}
module.exports = SenderKeyRecord;

View File

@@ -1,129 +0,0 @@
const SenderChainKey = require('./sender_chain_key');
const SenderMessageKey = require('./sender_message_key');
const protobufs = require('./protobufs');
class SenderKeyState {
MAX_MESSAGE_KEYS = 2000;
constructor(
id = null,
iteration = null,
chainKey = null,
signatureKeyPair = null,
signatureKeyPublic = null,
signatureKeyPrivate = null,
senderKeyStateStructure = null
) {
if (senderKeyStateStructure) {
this.senderKeyStateStructure = senderKeyStateStructure;
} else {
if (signatureKeyPair) {
signatureKeyPublic = signatureKeyPair.public;
signatureKeyPrivate = signatureKeyPair.private;
}
chainKey = typeof chainKey === 'string' ? Buffer.from(chainKey, 'base64') : chainKey;
this.senderKeyStateStructure = protobufs.SenderKeyStateStructure.create();
const senderChainKeyStructure = protobufs.SenderChainKey.create();
senderChainKeyStructure.iteration = iteration;
senderChainKeyStructure.seed = chainKey;
this.senderKeyStateStructure.senderChainKey = senderChainKeyStructure;
const signingKeyStructure = protobufs.SenderSigningKey.create();
signingKeyStructure.public =
typeof signatureKeyPublic === 'string' ?
Buffer.from(signatureKeyPublic, 'base64') :
signatureKeyPublic;
if (signatureKeyPrivate) {
signingKeyStructure.private =
typeof signatureKeyPrivate === 'string' ?
Buffer.from(signatureKeyPrivate, 'base64') :
signatureKeyPrivate;
}
this.senderKeyStateStructure.senderKeyId = id;
this.senderChainKey = senderChainKeyStructure;
this.senderKeyStateStructure.senderSigningKey = signingKeyStructure;
}
this.senderKeyStateStructure.senderMessageKeys =
this.senderKeyStateStructure.senderMessageKeys || [];
}
SenderKeyState(senderKeyStateStructure) {
this.senderKeyStateStructure = senderKeyStateStructure;
}
getKeyId() {
return this.senderKeyStateStructure.senderKeyId;
}
getSenderChainKey() {
return new SenderChainKey(
this.senderKeyStateStructure.senderChainKey.iteration,
this.senderKeyStateStructure.senderChainKey.seed
);
}
setSenderChainKey(chainKey) {
const senderChainKeyStructure = protobufs.SenderChainKey.create({
iteration: chainKey.getIteration(),
seed: chainKey.getSeed(),
});
this.senderKeyStateStructure.senderChainKey = senderChainKeyStructure;
}
getSigningKeyPublic() {
return typeof this.senderKeyStateStructure.senderSigningKey.public === 'string' ?
Buffer.from(this.senderKeyStateStructure.senderSigningKey.public, 'base64') :
this.senderKeyStateStructure.senderSigningKey.public;
}
getSigningKeyPrivate() {
return typeof this.senderKeyStateStructure.senderSigningKey.private === 'string' ?
Buffer.from(this.senderKeyStateStructure.senderSigningKey.private, 'base64') :
this.senderKeyStateStructure.senderSigningKey.private;
}
hasSenderMessageKey(iteration) {
const list = this.senderKeyStateStructure.senderMessageKeys;
for (let o = 0; o < list.length; o++) {
const senderMessageKey = list[o];
if (senderMessageKey.iteration === iteration) return true;
}
return false;
}
addSenderMessageKey(senderMessageKey) {
const senderMessageKeyStructure = protobufs.SenderKeyStateStructure.create({
iteration: senderMessageKey.getIteration(),
seed: senderMessageKey.getSeed(),
});
this.senderKeyStateStructure.senderMessageKeys.push(senderMessageKeyStructure);
if (this.senderKeyStateStructure.senderMessageKeys.length > this.MAX_MESSAGE_KEYS) {
this.senderKeyStateStructure.senderMessageKeys.shift();
}
}
removeSenderMessageKey(iteration) {
let result = null;
this.senderKeyStateStructure.senderMessageKeys = this.senderKeyStateStructure.senderMessageKeys.filter(
senderMessageKey => {
if (senderMessageKey.iteration === iteration) result = senderMessageKey;
return senderMessageKey.iteration !== iteration;
}
);
if (result != null) {
return new SenderMessageKey(result.iteration, result.seed);
}
return null;
}
getStructure() {
return this.senderKeyStateStructure;
}
}
module.exports = SenderKeyState;

View File

@@ -1,39 +0,0 @@
const { deriveSecrets } = require('libsignal/src/crypto');
class SenderMessageKey {
iteration = 0;
iv = Buffer.alloc(0);
cipherKey = Buffer.alloc(0);
seed = Buffer.alloc(0);
constructor(iteration, seed) {
const derivative = deriveSecrets(seed, Buffer.alloc(32), Buffer.from('WhisperGroup'));
const keys = new Uint8Array(32);
keys.set(new Uint8Array(derivative[0].slice(16)));
keys.set(new Uint8Array(derivative[1].slice(0, 16)), 16);
this.iv = Buffer.from(derivative[0].slice(0, 16));
this.cipherKey = Buffer.from(keys.buffer);
this.iteration = iteration;
this.seed = seed;
}
getIteration() {
return this.iteration;
}
getIv() {
return this.iv;
}
getCipherKey() {
return this.cipherKey;
}
getSeed() {
return this.seed;
}
}
module.exports = SenderMessageKey;

View File

@@ -16,9 +16,7 @@
"types": "lib/index.d.ts",
"files": [
"lib/*",
"WAProto/*.ts",
"WAProto/*.js",
"WASignalGroup/*.js",
"WAProto/*",
"engine-requirements.js"
],
"scripts": {

View File

@@ -0,0 +1,9 @@
export class CiphertextMessage {
readonly UNSUPPORTED_VERSION: number = 1
readonly CURRENT_VERSION: number = 3
readonly WHISPER_TYPE: number = 2
readonly PREKEY_TYPE: number = 3
readonly SENDERKEY_TYPE: number = 4
readonly SENDERKEY_DISTRIBUTION_TYPE: number = 5
readonly ENCRYPTED_MESSAGE_OVERHEAD: number = 53
}

View File

@@ -0,0 +1,56 @@
import * as keyhelper from './keyhelper'
import { SenderKeyDistributionMessage } from './sender-key-distribution-message'
import { SenderKeyName } from './sender-key-name'
import { SenderKeyRecord } from './sender-key-record'
interface SenderKeyStore {
loadSenderKey(senderKeyName: SenderKeyName): Promise<SenderKeyRecord>
storeSenderKey(senderKeyName: SenderKeyName, record: SenderKeyRecord): Promise<void>
}
export class GroupSessionBuilder {
private readonly senderKeyStore: SenderKeyStore
constructor(senderKeyStore: SenderKeyStore) {
this.senderKeyStore = senderKeyStore
}
public async process(
senderKeyName: SenderKeyName,
senderKeyDistributionMessage: SenderKeyDistributionMessage
): Promise<void> {
const senderKeyRecord = await this.senderKeyStore.loadSenderKey(senderKeyName)
senderKeyRecord.addSenderKeyState(
senderKeyDistributionMessage.getId(),
senderKeyDistributionMessage.getIteration(),
senderKeyDistributionMessage.getChainKey(),
senderKeyDistributionMessage.getSignatureKey()
)
await this.senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord)
}
public async create(senderKeyName: SenderKeyName): Promise<SenderKeyDistributionMessage> {
const senderKeyRecord = await this.senderKeyStore.loadSenderKey(senderKeyName)
if (senderKeyRecord.isEmpty()) {
const keyId = keyhelper.generateSenderKeyId()
const senderKey = keyhelper.generateSenderKey()
const signingKey = keyhelper.generateSenderSigningKey()
senderKeyRecord.setSenderKeyState(keyId, 0, senderKey, signingKey)
await this.senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord)
}
const state = senderKeyRecord.getSenderKeyState()
if (!state) {
throw new Error('No session state available')
}
return new SenderKeyDistributionMessage(
state.getKeyId(),
state.getSenderChainKey().getIteration(),
state.getSenderChainKey().getSeed(),
state.getSigningKeyPublic()
)
}
}

View File

@@ -0,0 +1,132 @@
import { decrypt, encrypt } from 'libsignal/src/crypto'
import queueJob from './queue-job'
import { SenderKeyMessage } from './sender-key-message'
import { SenderKeyName } from './sender-key-name'
import { SenderKeyRecord } from './sender-key-record'
import { SenderKeyState } from './sender-key-state'
export interface SenderKeyStore {
loadSenderKey(senderKeyName: SenderKeyName): Promise<SenderKeyRecord>
storeSenderKey(senderKeyName: SenderKeyName, record: SenderKeyRecord): Promise<void>
}
export class GroupCipher {
private readonly senderKeyStore: SenderKeyStore
private readonly senderKeyName: SenderKeyName
constructor(senderKeyStore: SenderKeyStore, senderKeyName: SenderKeyName) {
this.senderKeyStore = senderKeyStore
this.senderKeyName = senderKeyName
}
private queueJob<T>(awaitable: () => Promise<T>): Promise<T> {
return queueJob(this.senderKeyName.toString(), awaitable)
}
public async encrypt(paddedPlaintext: Uint8Array | string): Promise<Uint8Array> {
return await this.queueJob(async () => {
const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName)
if (!record) {
throw new Error('No SenderKeyRecord found for encryption')
}
const senderKeyState = record.getSenderKeyState()
if (!senderKeyState) {
throw new Error('No session to encrypt message')
}
const iteration = senderKeyState.getSenderChainKey().getIteration()
const senderKey = this.getSenderKey(senderKeyState, iteration === 0 ? 0 : iteration + 1)
const ciphertext = await this.getCipherText(senderKey.getIv(), senderKey.getCipherKey(), paddedPlaintext)
const senderKeyMessage = new SenderKeyMessage(
senderKeyState.getKeyId(),
senderKey.getIteration(),
ciphertext,
senderKeyState.getSigningKeyPrivate()
)
await this.senderKeyStore.storeSenderKey(this.senderKeyName, record)
return senderKeyMessage.serialize()
})
}
public async decrypt(senderKeyMessageBytes: Uint8Array): Promise<Uint8Array> {
return await this.queueJob(async () => {
const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName)
if (!record) {
throw new Error('No SenderKeyRecord found for decryption')
}
const senderKeyMessage = new SenderKeyMessage(null, null, null, null, senderKeyMessageBytes)
const senderKeyState = record.getSenderKeyState(senderKeyMessage.getKeyId())
if (!senderKeyState) {
throw new Error('No session found to decrypt message')
}
senderKeyMessage.verifySignature(senderKeyState.getSigningKeyPublic())
const senderKey = this.getSenderKey(senderKeyState, senderKeyMessage.getIteration())
const plaintext = await this.getPlainText(
senderKey.getIv(),
senderKey.getCipherKey(),
senderKeyMessage.getCipherText()
)
await this.senderKeyStore.storeSenderKey(this.senderKeyName, record)
return plaintext
})
}
private getSenderKey(senderKeyState: SenderKeyState, iteration: number) {
let senderChainKey = senderKeyState.getSenderChainKey()
if (senderChainKey.getIteration() > iteration) {
if (senderKeyState.hasSenderMessageKey(iteration)) {
const messageKey = senderKeyState.removeSenderMessageKey(iteration)
if (!messageKey) {
throw new Error('No sender message key found for iteration')
}
return messageKey
}
throw new Error(`Received message with old counter: ${senderChainKey.getIteration()}, ${iteration}`)
}
if (iteration - senderChainKey.getIteration() > 2000) {
throw new Error('Over 2000 messages into the future!')
}
while (senderChainKey.getIteration() < iteration) {
senderKeyState.addSenderMessageKey(senderChainKey.getSenderMessageKey())
senderChainKey = senderChainKey.getNext()
}
senderKeyState.setSenderChainKey(senderChainKey.getNext())
return senderChainKey.getSenderMessageKey()
}
private async getPlainText(iv: Uint8Array, key: Uint8Array, ciphertext: Uint8Array): Promise<Uint8Array> {
try {
return decrypt(key, ciphertext, iv)
} catch (e) {
throw new Error('InvalidMessageException')
}
}
private async getCipherText(
iv: Uint8Array | string,
key: Uint8Array | string,
plaintext: Uint8Array | string
): Promise<Buffer> {
try {
const ivBuffer = typeof iv === 'string' ? Buffer.from(iv, 'base64') : iv
const keyBuffer = typeof key === 'string' ? Buffer.from(key, 'base64') : key
const plaintextBuffer = typeof plaintext === 'string' ? Buffer.from(plaintext) : plaintext
return encrypt(keyBuffer, plaintextBuffer, ivBuffer)
} catch (e) {
throw new Error('InvalidMessageException')
}
}
}

11
src/Signal/Group/index.ts Normal file
View File

@@ -0,0 +1,11 @@
export { GroupSessionBuilder } from './group-session-builder'
export { SenderKeyDistributionMessage } from './sender-key-distribution-message'
export { SenderKeyRecord } from './sender-key-record'
export { SenderKeyName } from './sender-key-name'
export { GroupCipher } from './group_cipher'
export { SenderKeyState } from './sender-key-state'
export { SenderKeyMessage } from './sender-key-message'
export { SenderMessageKey } from './sender-message-key'
export { SenderChainKey } from './sender-chain-key'
export { CiphertextMessage } from './ciphertext-message'
export * as keyhelper from './keyhelper'

View File

@@ -0,0 +1,28 @@
import * as nodeCrypto from 'crypto'
import { generateKeyPair } from 'libsignal/src/curve'
type KeyPairType = ReturnType<typeof generateKeyPair>
export function generateSenderKey(): Buffer {
return nodeCrypto.randomBytes(32)
}
export function generateSenderKeyId(): number {
return nodeCrypto.randomInt(2147483647)
}
export interface SigningKeyPair {
public: Buffer
private: Buffer
}
export function generateSenderSigningKey(key?: KeyPairType): SigningKeyPair {
if (!key) {
key = generateKeyPair()
}
return {
public: Buffer.from(key.pubKey),
private: Buffer.from(key.privKey)
}
}

View File

@@ -0,0 +1,65 @@
interface QueueJob<T> {
awaitable: () => Promise<T>
resolve: (value: T | PromiseLike<T>) => void
reject: (reason?: unknown) => void
}
const _queueAsyncBuckets = new Map<string | number, Array<QueueJob<any>>>()
const _gcLimit = 10000
async function _asyncQueueExecutor(queue: Array<QueueJob<any>>, cleanup: () => void): Promise<void> {
let offt = 0
// eslint-disable-next-line no-constant-condition
while (true) {
const limit = Math.min(queue.length, _gcLimit)
for (let i = offt; i < limit; i++) {
const job = queue[i]
try {
job.resolve(await job.awaitable())
} catch (e) {
job.reject(e)
}
}
if (limit < queue.length) {
if (limit >= _gcLimit) {
queue.splice(0, limit)
offt = 0
} else {
offt = limit
}
} else {
break
}
}
cleanup()
}
export default function queueJob<T>(bucket: string | number, awaitable: () => Promise<T>): Promise<T> {
// Skip name assignment since it's readonly in strict mode
if (typeof bucket !== 'string') {
console.warn('Unhandled bucket type (for naming):', typeof bucket, bucket)
}
let inactive = false
if (!_queueAsyncBuckets.has(bucket)) {
_queueAsyncBuckets.set(bucket, [])
inactive = true
}
const queue = _queueAsyncBuckets.get(bucket)!
const job = new Promise<T>((resolve, reject) => {
queue.push({
awaitable,
resolve: resolve as (value: any) => void,
reject
})
})
if (inactive) {
_asyncQueueExecutor(queue, () => _queueAsyncBuckets.delete(bucket))
}
return job
}

View File

@@ -0,0 +1,38 @@
import { calculateMAC } from 'libsignal/src/crypto'
import { SenderMessageKey } from './sender-message-key'
export class SenderChainKey {
private readonly MESSAGE_KEY_SEED: Uint8Array = Buffer.from([0x01])
private readonly CHAIN_KEY_SEED: Uint8Array = Buffer.from([0x02])
private readonly iteration: number
private readonly chainKey: Buffer
constructor(iteration: number, chainKey: any) {
this.iteration = iteration
if (chainKey instanceof Buffer) {
this.chainKey = chainKey
} else {
this.chainKey = Buffer.from(chainKey || [])
}
}
public getIteration(): number {
return this.iteration
}
public getSenderMessageKey(): SenderMessageKey {
return new SenderMessageKey(this.iteration, this.getDerivative(this.MESSAGE_KEY_SEED, this.chainKey))
}
public getNext(): SenderChainKey {
return new SenderChainKey(this.iteration + 1, this.getDerivative(this.CHAIN_KEY_SEED, this.chainKey))
}
public getSeed(): Uint8Array {
return this.chainKey
}
private getDerivative(seed: Uint8Array, key: Buffer): Uint8Array {
return calculateMAC(key, seed)
}
}

View File

@@ -0,0 +1,95 @@
import { proto } from '../../../WAProto'
import { CiphertextMessage } from './ciphertext-message'
interface SenderKeyDistributionMessageStructure {
id: number
iteration: number
chainKey: string | Uint8Array
signingKey: string | Uint8Array
}
export class SenderKeyDistributionMessage extends CiphertextMessage {
private readonly id: number
private readonly iteration: number
private readonly chainKey: Uint8Array
private readonly signatureKey: Uint8Array
private readonly serialized: Uint8Array
constructor(
id?: number | null,
iteration?: number | null,
chainKey?: Uint8Array | null,
signatureKey?: Uint8Array | null,
serialized?: Uint8Array | null
) {
super()
if (serialized) {
try {
const message = serialized.slice(1)
const distributionMessage = proto.SenderKeyDistributionMessage.decode(
message
).toJSON() as SenderKeyDistributionMessageStructure
this.serialized = serialized
this.id = distributionMessage.id
this.iteration = distributionMessage.iteration
this.chainKey =
typeof distributionMessage.chainKey === 'string'
? Buffer.from(distributionMessage.chainKey, 'base64')
: distributionMessage.chainKey
this.signatureKey =
typeof distributionMessage.signingKey === 'string'
? Buffer.from(distributionMessage.signingKey, 'base64')
: distributionMessage.signingKey
} catch (e) {
throw new Error(String(e))
}
} else {
const version = this.intsToByteHighAndLow(this.CURRENT_VERSION, this.CURRENT_VERSION)
this.id = id!
this.iteration = iteration!
this.chainKey = chainKey!
this.signatureKey = signatureKey!
const message = proto.SenderKeyDistributionMessage.encode(
proto.SenderKeyDistributionMessage.create({
id,
iteration,
chainKey,
signingKey: this.signatureKey
})
).finish()
this.serialized = Buffer.concat([Buffer.from([version]), message])
}
}
private intsToByteHighAndLow(highValue: number, lowValue: number): number {
return (((highValue << 4) | lowValue) & 0xff) % 256
}
public serialize(): Uint8Array {
return this.serialized
}
public getType(): number {
return this.SENDERKEY_DISTRIBUTION_TYPE
}
public getIteration(): number {
return this.iteration
}
public getChainKey(): Uint8Array {
return typeof this.chainKey === 'string' ? Buffer.from(this.chainKey, 'base64') : this.chainKey
}
public getSignatureKey(): Uint8Array {
return typeof this.signatureKey === 'string' ? Buffer.from(this.signatureKey, 'base64') : this.signatureKey
}
public getId(): number {
return this.id
}
}

View File

@@ -0,0 +1,96 @@
import { calculateSignature, verifySignature } from 'libsignal/src/curve'
import { proto } from '../../../WAProto'
import { CiphertextMessage } from './ciphertext-message'
interface SenderKeyMessageStructure {
id: number
iteration: number
ciphertext: string | Buffer
}
export class SenderKeyMessage extends CiphertextMessage {
private readonly SIGNATURE_LENGTH = 64
private readonly messageVersion: number
private readonly keyId: number
private readonly iteration: number
private readonly ciphertext: Uint8Array
private readonly signature: Uint8Array
private readonly serialized: Uint8Array
constructor(
keyId?: number | null,
iteration?: number | null,
ciphertext?: Uint8Array | null,
signatureKey?: Uint8Array | null,
serialized?: Uint8Array | null
) {
super()
if (serialized) {
const version = serialized[0]
const message = serialized.slice(1, serialized.length - this.SIGNATURE_LENGTH)
const signature = serialized.slice(-1 * this.SIGNATURE_LENGTH)
const senderKeyMessage = proto.SenderKeyMessage.decode(message).toJSON() as SenderKeyMessageStructure
this.serialized = serialized
this.messageVersion = (version & 0xff) >> 4
this.keyId = senderKeyMessage.id
this.iteration = senderKeyMessage.iteration
this.ciphertext =
typeof senderKeyMessage.ciphertext === 'string'
? Buffer.from(senderKeyMessage.ciphertext, 'base64')
: senderKeyMessage.ciphertext
this.signature = signature
} else {
const version = (((this.CURRENT_VERSION << 4) | this.CURRENT_VERSION) & 0xff) % 256
const ciphertextBuffer = Buffer.from(ciphertext!)
const message = proto.SenderKeyMessage.encode(
proto.SenderKeyMessage.create({
id: keyId!,
iteration: iteration!,
ciphertext: ciphertextBuffer
})
).finish()
const signature = this.getSignature(signatureKey!, Buffer.concat([Buffer.from([version]), message]))
this.serialized = Buffer.concat([Buffer.from([version]), message, Buffer.from(signature)])
this.messageVersion = this.CURRENT_VERSION
this.keyId = keyId!
this.iteration = iteration!
this.ciphertext = ciphertextBuffer
this.signature = signature
}
}
public getKeyId(): number {
return this.keyId
}
public getIteration(): number {
return this.iteration
}
public getCipherText(): Uint8Array {
return this.ciphertext
}
public verifySignature(signatureKey: Uint8Array): void {
const part1 = this.serialized.slice(0, this.serialized.length - this.SIGNATURE_LENGTH)
const part2 = this.serialized.slice(-1 * this.SIGNATURE_LENGTH)
const res = verifySignature(signatureKey, part1, part2)
if (!res) throw new Error('Invalid signature!')
}
private getSignature(signatureKey: Uint8Array, serialized: Uint8Array): Uint8Array {
return Buffer.from(calculateSignature(signatureKey, serialized))
}
public serialize(): Uint8Array {
return this.serialized
}
public getType(): number {
return 4
}
}

View File

@@ -0,0 +1,66 @@
interface Sender {
id: string
deviceId: number
toString(): string
}
function isNull(str: string | null): boolean {
return str === null || str === ''
}
function intValue(num: number): number {
const MAX_VALUE = 0x7fffffff
const MIN_VALUE = -0x80000000
if (num > MAX_VALUE || num < MIN_VALUE) {
return num & 0xffffffff
}
return num
}
function hashCode(strKey: string): number {
let hash = 0
if (!isNull(strKey)) {
for (let i = 0; i < strKey.length; i++) {
hash = hash * 31 + strKey.charCodeAt(i)
hash = intValue(hash)
}
}
return hash
}
export class SenderKeyName {
private readonly groupId: string
private readonly sender: Sender
constructor(groupId: string, sender: Sender) {
this.groupId = groupId
this.sender = sender
}
public getGroupId(): string {
return this.groupId
}
public getSender(): Sender {
return this.sender
}
public serialize(): string {
return `${this.groupId}::${this.sender.id}::${this.sender.deviceId}`
}
public toString(): string {
return this.serialize()
}
public equals(other: SenderKeyName | null): boolean {
if (other === null) return false
return this.groupId === other.groupId && this.sender.toString() === other.sender.toString()
}
public hashCode(): number {
return hashCode(this.groupId) ^ hashCode(this.sender.toString())
}
}

View File

@@ -0,0 +1,77 @@
import { BufferJSON } from '../../Utils/generics'
import { SenderKeyState } from './sender-key-state'
export interface SenderKeyStateStructure {
senderKeyId: number
senderChainKey: {
iteration: number
seed: Uint8Array
}
senderSigningKey: {
public: Uint8Array
private?: Uint8Array
}
senderMessageKeys: Array<{
iteration: number
seed: Uint8Array
}>
}
export class SenderKeyRecord {
private readonly MAX_STATES = 5
private readonly senderKeyStates: SenderKeyState[] = []
constructor(serialized?: SenderKeyStateStructure[]) {
if (serialized) {
for (const structure of serialized) {
this.senderKeyStates.push(new SenderKeyState(null, null, null, null, null, null, structure))
}
}
}
public isEmpty(): boolean {
return this.senderKeyStates.length === 0
}
public getSenderKeyState(keyId?: number): SenderKeyState | undefined {
if (keyId === undefined && this.senderKeyStates.length) {
return this.senderKeyStates[this.senderKeyStates.length - 1]
}
return this.senderKeyStates.find(state => state.getKeyId() === keyId)
}
public addSenderKeyState(id: number, iteration: number, chainKey: Uint8Array, signatureKey: Uint8Array): void {
this.senderKeyStates.push(new SenderKeyState(id, iteration, chainKey, null, signatureKey))
if (this.senderKeyStates.length > this.MAX_STATES) {
this.senderKeyStates.shift()
}
}
public setSenderKeyState(
id: number,
iteration: number,
chainKey: Uint8Array,
keyPair: { public: Uint8Array; private: Uint8Array }
): void {
this.senderKeyStates.length = 0
this.senderKeyStates.push(new SenderKeyState(id, iteration, chainKey, keyPair))
}
public serialize(): SenderKeyStateStructure[] {
return this.senderKeyStates.map(state => state.getStructure())
}
static deserialize(data: Uint8Array | string | SenderKeyStateStructure[]): SenderKeyRecord {
let parsed: SenderKeyStateStructure[]
if (typeof data === 'string') {
parsed = JSON.parse(data, BufferJSON.reviver)
} else if (data instanceof Uint8Array) {
const str = Buffer.from(data).toString('utf-8')
parsed = JSON.parse(str, BufferJSON.reviver)
} else {
parsed = data
}
return new SenderKeyRecord(parsed)
}
}

View File

@@ -0,0 +1,145 @@
import { SenderChainKey } from './sender-chain-key'
import { SenderMessageKey } from './sender-message-key'
interface SenderChainKeyStructure {
iteration: number
seed: Uint8Array
}
interface SenderSigningKeyStructure {
public: Uint8Array
private?: Uint8Array
}
interface SenderMessageKeyStructure {
iteration: number
seed: Uint8Array
}
interface SenderKeyStateStructure {
senderKeyId: number
senderChainKey: SenderChainKeyStructure
senderSigningKey: SenderSigningKeyStructure
senderMessageKeys: SenderMessageKeyStructure[]
}
export class SenderKeyState {
private readonly MAX_MESSAGE_KEYS = 2000
private readonly senderKeyStateStructure: SenderKeyStateStructure
constructor(
id?: number | null,
iteration?: number | null,
chainKey?: Uint8Array | null,
signatureKeyPair?: { public: Uint8Array; private: Uint8Array } | null,
signatureKeyPublic?: Uint8Array | null,
signatureKeyPrivate?: Uint8Array | null,
senderKeyStateStructure?: SenderKeyStateStructure | null
) {
if (senderKeyStateStructure) {
this.senderKeyStateStructure = senderKeyStateStructure
} else {
if (signatureKeyPair) {
signatureKeyPublic = signatureKeyPair.public
signatureKeyPrivate = signatureKeyPair.private
}
chainKey = typeof chainKey === 'string' ? Buffer.from(chainKey, 'base64') : chainKey
const senderChainKeyStructure: SenderChainKeyStructure = {
iteration: iteration || 0,
seed: chainKey || Buffer.alloc(0)
}
const signingKeyStructure: SenderSigningKeyStructure = {
public:
typeof signatureKeyPublic === 'string'
? Buffer.from(signatureKeyPublic, 'base64')
: signatureKeyPublic || Buffer.alloc(0)
}
if (signatureKeyPrivate) {
signingKeyStructure.private =
typeof signatureKeyPrivate === 'string' ? Buffer.from(signatureKeyPrivate, 'base64') : signatureKeyPrivate
}
this.senderKeyStateStructure = {
senderKeyId: id || 0,
senderChainKey: senderChainKeyStructure,
senderSigningKey: signingKeyStructure,
senderMessageKeys: []
}
}
}
public getKeyId(): number {
return this.senderKeyStateStructure.senderKeyId
}
public getSenderChainKey(): SenderChainKey {
return new SenderChainKey(
this.senderKeyStateStructure.senderChainKey.iteration,
this.senderKeyStateStructure.senderChainKey.seed
)
}
public setSenderChainKey(chainKey: SenderChainKey): void {
this.senderKeyStateStructure.senderChainKey = {
iteration: chainKey.getIteration(),
seed: chainKey.getSeed()
}
}
public getSigningKeyPublic(): Buffer {
const publicKey = this.senderKeyStateStructure.senderSigningKey.public
if (publicKey instanceof Buffer) {
return publicKey
}
return Buffer.from(publicKey || [])
}
public getSigningKeyPrivate(): Buffer | undefined {
const privateKey = this.senderKeyStateStructure.senderSigningKey.private
if (!privateKey) {
return undefined
}
if (privateKey instanceof Buffer) {
return privateKey
}
return Buffer.from(privateKey)
}
public hasSenderMessageKey(iteration: number): boolean {
return this.senderKeyStateStructure.senderMessageKeys.some(key => key.iteration === iteration)
}
public addSenderMessageKey(senderMessageKey: SenderMessageKey): void {
this.senderKeyStateStructure.senderMessageKeys.push({
iteration: senderMessageKey.getIteration(),
seed: senderMessageKey.getSeed()
})
if (this.senderKeyStateStructure.senderMessageKeys.length > this.MAX_MESSAGE_KEYS) {
this.senderKeyStateStructure.senderMessageKeys.shift()
}
}
public removeSenderMessageKey(iteration: number): SenderMessageKey | null {
const index = this.senderKeyStateStructure.senderMessageKeys.findIndex(key => key.iteration === iteration)
if (index !== -1) {
const messageKey = this.senderKeyStateStructure.senderMessageKeys[index]
this.senderKeyStateStructure.senderMessageKeys.splice(index, 1)
return new SenderMessageKey(messageKey.iteration, messageKey.seed)
}
return null
}
public getStructure(): SenderKeyStateStructure {
return this.senderKeyStateStructure
}
}

View File

@@ -0,0 +1,36 @@
import { deriveSecrets } from 'libsignal/src/crypto'
export class SenderMessageKey {
private readonly iteration: number
private readonly iv: Uint8Array
private readonly cipherKey: Uint8Array
private readonly seed: Uint8Array
constructor(iteration: number, seed: Uint8Array) {
const derivative = deriveSecrets(seed, Buffer.alloc(32), Buffer.from('WhisperGroup'))
const keys = new Uint8Array(32)
keys.set(new Uint8Array(derivative[0].slice(16)))
keys.set(new Uint8Array(derivative[1].slice(0, 16)), 16)
this.iv = Buffer.from(derivative[0].slice(0, 16))
this.cipherKey = Buffer.from(keys.buffer)
this.iteration = iteration
this.seed = seed
}
public getIteration(): number {
return this.iteration
}
public getIv(): Uint8Array {
return this.iv
}
public getCipherKey(): Uint8Array {
return this.cipherKey
}
public getSeed(): Uint8Array {
return this.seed
}
}

View File

@@ -1,18 +1,15 @@
import * as libsignal from 'libsignal'
import {
GroupCipher,
GroupSessionBuilder,
SenderKeyDistributionMessage,
SenderKeyName,
SenderKeyRecord
} from '../../WASignalGroup'
import { SignalAuthState } from '../Types'
import { SignalRepository } from '../Types/Signal'
import { generateSignalPubKey } from '../Utils'
import { jidDecode } from '../WABinary'
import type { SenderKeyStore } from './Group/group_cipher'
import { SenderKeyName } from './Group/sender-key-name'
import { SenderKeyRecord } from './Group/sender-key-record'
import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage } from './Group'
export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository {
const storage = signalStorage(auth)
const storage: SenderKeyStore = signalStorage(auth)
return {
decryptGroupMessage({ group, authorJid, msg }) {
const senderName = jidToSignalSenderKeyName(group, authorJid)
@@ -22,7 +19,11 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
},
async processSenderKeyDistributionMessage({ item, authorJid }) {
const builder = new GroupSessionBuilder(storage)
const senderName = jidToSignalSenderKeyName(item.groupId!, authorJid)
if (!item.groupId) {
throw new Error('Group ID is required for sender key distribution message')
}
const senderName = jidToSignalSenderKeyName(item.groupId, authorJid)
const senderMsg = new SenderKeyDistributionMessage(
null,
@@ -31,7 +32,8 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
null,
item.axolotlSenderKeyDistributionMessage
)
const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName])
const senderNameStr = senderName.toString()
const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr])
if (!senderKey) {
await storage.storeSenderKey(senderName, new SenderKeyRecord())
}
@@ -49,6 +51,8 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
case 'msg':
result = await session.decryptWhisperMessage(ciphertext)
break
default:
throw new Error(`Unknown message type: ${type}`)
}
return result
@@ -65,7 +69,8 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
const senderName = jidToSignalSenderKeyName(group, meId)
const builder = new GroupSessionBuilder(storage)
const { [senderName]: senderKey } = await auth.keys.get('sender-key', [senderName])
const senderNameStr = senderName.toString()
const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr])
if (!senderKey) {
await storage.storeSenderKey(senderName, new SenderKeyRecord())
}
@@ -94,11 +99,11 @@ const jidToSignalProtocolAddress = (jid: string) => {
return new libsignal.ProtocolAddress(user, device || 0)
}
const jidToSignalSenderKeyName = (group: string, user: string): string => {
return new SenderKeyName(group, jidToSignalProtocolAddress(user)).toString()
const jidToSignalSenderKeyName = (group: string, user: string): SenderKeyName => {
return new SenderKeyName(group, jidToSignalProtocolAddress(user))
}
function signalStorage({ creds, keys }: SignalAuthState) {
function signalStorage({ creds, keys }: SignalAuthState): SenderKeyStore & Record<string, any> {
return {
loadSession: async (id: string) => {
const { [id]: sess } = await keys.get('session', [id])
@@ -106,7 +111,7 @@ function signalStorage({ creds, keys }: SignalAuthState) {
return libsignal.SessionRecord.deserialize(sess)
}
},
storeSession: async (id, session) => {
storeSession: async (id: string, session: libsignal.SessionRecord) => {
await keys.set({ session: { [id]: session.serialize() } })
},
isTrustedIdentity: () => {
@@ -130,14 +135,19 @@ function signalStorage({ creds, keys }: SignalAuthState) {
pubKey: Buffer.from(key.keyPair.public)
}
},
loadSenderKey: async (keyId: string) => {
loadSenderKey: async (senderKeyName: SenderKeyName) => {
const keyId = senderKeyName.toString()
const { [keyId]: key } = await keys.get('sender-key', [keyId])
if (key) {
return new SenderKeyRecord(key)
return SenderKeyRecord.deserialize(key)
}
return new SenderKeyRecord()
},
storeSenderKey: async (keyId, key) => {
await keys.set({ 'sender-key': { [keyId]: key.serialize() } })
storeSenderKey: async (senderKeyName: SenderKeyName, key: SenderKeyRecord) => {
const keyId = senderKeyName.toString()
const serialized = JSON.stringify(key.serialize())
await keys.set({ 'sender-key': { [keyId]: Buffer.from(serialized, 'utf-8') } })
},
getOurRegistrationId: () => creds.registrationId,
getOurIdentity: () => {