mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
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:
committed by
GitHub
parent
f404147736
commit
482db6edc5
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
@@ -1 +0,0 @@
|
||||
yarn pbjs -t static-module -w commonjs -o ./WASignalGroup/GroupProtocol.js ./WASignalGroup/group.proto
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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')
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
const { groupproto } = require('./GroupProtocol')
|
||||
|
||||
module.exports = groupproto
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -16,9 +16,7 @@
|
||||
"types": "lib/index.d.ts",
|
||||
"files": [
|
||||
"lib/*",
|
||||
"WAProto/*.ts",
|
||||
"WAProto/*.js",
|
||||
"WASignalGroup/*.js",
|
||||
"WAProto/*",
|
||||
"engine-requirements.js"
|
||||
],
|
||||
"scripts": {
|
||||
|
||||
9
src/Signal/Group/ciphertext-message.ts
Normal file
9
src/Signal/Group/ciphertext-message.ts
Normal 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
|
||||
}
|
||||
56
src/Signal/Group/group-session-builder.ts
Normal file
56
src/Signal/Group/group-session-builder.ts
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
132
src/Signal/Group/group_cipher.ts
Normal file
132
src/Signal/Group/group_cipher.ts
Normal 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
11
src/Signal/Group/index.ts
Normal 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'
|
||||
28
src/Signal/Group/keyhelper.ts
Normal file
28
src/Signal/Group/keyhelper.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
65
src/Signal/Group/queue-job.ts
Normal file
65
src/Signal/Group/queue-job.ts
Normal 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
|
||||
}
|
||||
38
src/Signal/Group/sender-chain-key.ts
Normal file
38
src/Signal/Group/sender-chain-key.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
95
src/Signal/Group/sender-key-distribution-message.ts
Normal file
95
src/Signal/Group/sender-key-distribution-message.ts
Normal 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
|
||||
}
|
||||
}
|
||||
96
src/Signal/Group/sender-key-message.ts
Normal file
96
src/Signal/Group/sender-key-message.ts
Normal 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
|
||||
}
|
||||
}
|
||||
66
src/Signal/Group/sender-key-name.ts
Normal file
66
src/Signal/Group/sender-key-name.ts
Normal 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())
|
||||
}
|
||||
}
|
||||
77
src/Signal/Group/sender-key-record.ts
Normal file
77
src/Signal/Group/sender-key-record.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
145
src/Signal/Group/sender-key-state.ts
Normal file
145
src/Signal/Group/sender-key-state.ts
Normal 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
|
||||
}
|
||||
}
|
||||
36
src/Signal/Group/sender-message-key.ts
Normal file
36
src/Signal/Group/sender-message-key.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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: () => {
|
||||
|
||||
Reference in New Issue
Block a user