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",
|
"types": "lib/index.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"lib/*",
|
"lib/*",
|
||||||
"WAProto/*.ts",
|
"WAProto/*",
|
||||||
"WAProto/*.js",
|
|
||||||
"WASignalGroup/*.js",
|
|
||||||
"engine-requirements.js"
|
"engine-requirements.js"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"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 * as libsignal from 'libsignal'
|
||||||
import {
|
|
||||||
GroupCipher,
|
|
||||||
GroupSessionBuilder,
|
|
||||||
SenderKeyDistributionMessage,
|
|
||||||
SenderKeyName,
|
|
||||||
SenderKeyRecord
|
|
||||||
} from '../../WASignalGroup'
|
|
||||||
import { SignalAuthState } from '../Types'
|
import { SignalAuthState } from '../Types'
|
||||||
import { SignalRepository } from '../Types/Signal'
|
import { SignalRepository } from '../Types/Signal'
|
||||||
import { generateSignalPubKey } from '../Utils'
|
import { generateSignalPubKey } from '../Utils'
|
||||||
import { jidDecode } from '../WABinary'
|
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 {
|
export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository {
|
||||||
const storage = signalStorage(auth)
|
const storage: SenderKeyStore = signalStorage(auth)
|
||||||
return {
|
return {
|
||||||
decryptGroupMessage({ group, authorJid, msg }) {
|
decryptGroupMessage({ group, authorJid, msg }) {
|
||||||
const senderName = jidToSignalSenderKeyName(group, authorJid)
|
const senderName = jidToSignalSenderKeyName(group, authorJid)
|
||||||
@@ -22,7 +19,11 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
},
|
},
|
||||||
async processSenderKeyDistributionMessage({ item, authorJid }) {
|
async processSenderKeyDistributionMessage({ item, authorJid }) {
|
||||||
const builder = new GroupSessionBuilder(storage)
|
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(
|
const senderMsg = new SenderKeyDistributionMessage(
|
||||||
null,
|
null,
|
||||||
@@ -31,7 +32,8 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
null,
|
null,
|
||||||
item.axolotlSenderKeyDistributionMessage
|
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) {
|
if (!senderKey) {
|
||||||
await storage.storeSenderKey(senderName, new SenderKeyRecord())
|
await storage.storeSenderKey(senderName, new SenderKeyRecord())
|
||||||
}
|
}
|
||||||
@@ -49,6 +51,8 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
case 'msg':
|
case 'msg':
|
||||||
result = await session.decryptWhisperMessage(ciphertext)
|
result = await session.decryptWhisperMessage(ciphertext)
|
||||||
break
|
break
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown message type: ${type}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
@@ -65,7 +69,8 @@ export function makeLibSignalRepository(auth: SignalAuthState): SignalRepository
|
|||||||
const senderName = jidToSignalSenderKeyName(group, meId)
|
const senderName = jidToSignalSenderKeyName(group, meId)
|
||||||
const builder = new GroupSessionBuilder(storage)
|
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) {
|
if (!senderKey) {
|
||||||
await storage.storeSenderKey(senderName, new SenderKeyRecord())
|
await storage.storeSenderKey(senderName, new SenderKeyRecord())
|
||||||
}
|
}
|
||||||
@@ -94,11 +99,11 @@ const jidToSignalProtocolAddress = (jid: string) => {
|
|||||||
return new libsignal.ProtocolAddress(user, device || 0)
|
return new libsignal.ProtocolAddress(user, device || 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const jidToSignalSenderKeyName = (group: string, user: string): string => {
|
const jidToSignalSenderKeyName = (group: string, user: string): SenderKeyName => {
|
||||||
return new SenderKeyName(group, jidToSignalProtocolAddress(user)).toString()
|
return new SenderKeyName(group, jidToSignalProtocolAddress(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
function signalStorage({ creds, keys }: SignalAuthState) {
|
function signalStorage({ creds, keys }: SignalAuthState): SenderKeyStore & Record<string, any> {
|
||||||
return {
|
return {
|
||||||
loadSession: async (id: string) => {
|
loadSession: async (id: string) => {
|
||||||
const { [id]: sess } = await keys.get('session', [id])
|
const { [id]: sess } = await keys.get('session', [id])
|
||||||
@@ -106,7 +111,7 @@ function signalStorage({ creds, keys }: SignalAuthState) {
|
|||||||
return libsignal.SessionRecord.deserialize(sess)
|
return libsignal.SessionRecord.deserialize(sess)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
storeSession: async (id, session) => {
|
storeSession: async (id: string, session: libsignal.SessionRecord) => {
|
||||||
await keys.set({ session: { [id]: session.serialize() } })
|
await keys.set({ session: { [id]: session.serialize() } })
|
||||||
},
|
},
|
||||||
isTrustedIdentity: () => {
|
isTrustedIdentity: () => {
|
||||||
@@ -130,14 +135,19 @@ function signalStorage({ creds, keys }: SignalAuthState) {
|
|||||||
pubKey: Buffer.from(key.keyPair.public)
|
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])
|
const { [keyId]: key } = await keys.get('sender-key', [keyId])
|
||||||
if (key) {
|
if (key) {
|
||||||
return new SenderKeyRecord(key)
|
return SenderKeyRecord.deserialize(key)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new SenderKeyRecord()
|
||||||
},
|
},
|
||||||
storeSenderKey: async (keyId, key) => {
|
storeSenderKey: async (senderKeyName: SenderKeyName, key: SenderKeyRecord) => {
|
||||||
await keys.set({ 'sender-key': { [keyId]: key.serialize() } })
|
const keyId = senderKeyName.toString()
|
||||||
|
const serialized = JSON.stringify(key.serialize())
|
||||||
|
await keys.set({ 'sender-key': { [keyId]: Buffer.from(serialized, 'utf-8') } })
|
||||||
},
|
},
|
||||||
getOurRegistrationId: () => creds.registrationId,
|
getOurRegistrationId: () => creds.registrationId,
|
||||||
getOurIdentity: () => {
|
getOurIdentity: () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user