finalize multi-device

This commit is contained in:
Adhiraj Singh
2021-09-15 13:40:02 +05:30
parent 9cba28e891
commit f267f27ada
82 changed files with 35228 additions and 10644 deletions

View File

@@ -0,0 +1,16 @@
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;

41
WASignalGroup/group.proto Normal file
View File

@@ -0,0 +1,41 @@
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 SenderKeyStateStructure {
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;
}
optional uint32 senderKeyId = 1;
optional SenderChainKey senderChainKey = 2;
optional SenderSigningKey senderSigningKey = 3;
repeated SenderMessageKey senderMessageKeys = 4;
}
message SenderKeyRecordStructure {
repeated SenderKeyStateStructure senderKeyStates = 1;
}

View File

@@ -0,0 +1,106 @@
const SenderKeyMessage = require('./sender_key_message');
const crypto = require('libsignal/src/crypto');
class GroupCipher {
constructor(senderKeyStore, senderKeyName) {
this.senderKeyStore = senderKeyStore;
this.senderKeyName = senderKeyName;
}
async encrypt(paddedPlaintext) {
try {
const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName);
const senderKeyState = record.getSenderKeyState();
const senderKey = senderKeyState.getSenderChainKey().getSenderMessageKey();
const ciphertext = await this.getCipherText(
senderKey.getIv(),
senderKey.getCipherKey(),
paddedPlaintext
);
const senderKeyMessage = new SenderKeyMessage(
senderKeyState.getKeyId(),
senderKey.getIteration(),
ciphertext,
senderKeyState.getSigningKeyPrivate()
);
senderKeyState.setSenderChainKey(senderKeyState.getSenderChainKey().getNext());
await this.senderKeyStore.storeSenderKey(this.senderKeyName, record);
return senderKeyMessage.serialize();
} catch (e) {
//console.log(e.stack);
throw new Error('NoSessionException');
}
}
async decrypt(senderKeyMessageBytes) {
const record = await this.senderKeyStore.loadSenderKey(this.senderKeyName);
if (!record) throw new Error(`No sender key for: ${this.senderKeyName}`);
const senderKeyMessage = new SenderKeyMessage(null, null, null, null, senderKeyMessageBytes);
const senderKeyState = record.getSenderKeyState(senderKeyMessage.getKeyId());
//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 (senderChainKey.getIteration() - iteration > 2000) {
throw new Error('Over 2000 messages into the future!');
}
while (senderChainKey.getIteration() < iteration) {
senderKeyState.addSenderMessageKey(senderChainKey.getSenderMessageKey());
senderChainKey = senderChainKey.getNext();
}
senderKeyState.setSenderChainKey(senderChainKey.getNext());
return senderChainKey.getSenderMessageKey();
}
getPlainText(iv, key, ciphertext) {
try {
const plaintext = crypto.decrypt(key, ciphertext, iv);
return plaintext;
} catch (e) {
//console.log(e.stack);
throw new Error('InvalidMessageException');
}
}
getCipherText(iv, key, plaintext) {
try {
iv = typeof iv === 'string' ? Buffer.from(iv, 'base64') : iv;
key = typeof key === 'string' ? Buffer.from(key, 'base64') : key;
const crypted = crypto.encrypt(key, Buffer.from(plaintext), iv);
return crypted;
} catch (e) {
//console.log(e.stack);
throw new Error('InvalidMessageException');
}
}
}
module.exports = GroupCipher;

View File

@@ -0,0 +1,51 @@
//const utils = require('../../common/utils');
const SenderKeyDistributionMessage = require('./sender_key_distribution_message');
const keyhelper = require("libsignal/src/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) {
try {
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()
);
} catch (e) {
//console.log(e.stack);
throw new Error(e);
}
}
}
module.exports = GroupSessionBuilder;

5
WASignalGroup/index.js Normal file
View File

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

View File

@@ -0,0 +1,13 @@
const path = require('path');
const protobuf = require('protobufjs');
const protodir = path.resolve(__dirname);
const group = protobuf.loadSync(path.join(protodir, 'group.proto')).lookup('groupproto');
module.exports = {
SenderKeyDistributionMessage: group.lookup('SenderKeyDistributionMessage'),
SenderKeyMessage: group.lookup('SenderKeyMessage'),
SenderKeyStateStructure: group.lookup('SenderKeyStateStructure'),
SenderChainKey: group.lookup('SenderChainKey'),
SenderSigningKey: group.lookup('SenderSigningKey'),
};

6
WASignalGroup/readme.md Normal file
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,92 @@
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 + 1);
const part2 = this.serialized.slice(-1 * this.SIGNATURE_LENGTH);
const res = curve.verifySignature(signatureKey, part1, part2);
if (!res) throw new Error('Invalid signature!');
}
getSignature(signatureKey, serialized) {
const signature = Buffer.from(
curve.calculateSignature(
signatureKey,
serialized
)
);
return signature;
}
serialize() {
return this.serialized;
}
getType() {
return 4;
}
}
module.exports = SenderKeyMessage;

View File

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

View File

@@ -0,0 +1,54 @@
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[0];
for (let i = 0; i < this.senderKeyStates.length; i++) {
const state = this.senderKeyStates[i];
if (state.getKeyId() === keyId) {
return state;
}
}
throw new Error(`No keys for: ${keyId}`);
}
addSenderKeyState(id, iteration, chainKey, signatureKey) {
this.senderKeyStates.push(new SenderKeyState(id, iteration, chainKey, null, signatureKey));
}
setSenderKeyState(id, iteration, chainKey, keyPair) {
this.senderKeyStates.length = 0;
this.senderKeyStates.push(new SenderKeyState(id, iteration, chainKey, keyPair));
}
serialize() {
const recordStructure = [];
for (let i = 0; i < this.senderKeyStates.length; i++) {
const senderKeyState = this.senderKeyStates[i];
recordStructure.push(senderKeyState.getStructure());
}
return recordStructure;
}
}
module.exports = SenderKeyRecord;

View File

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

View File

@@ -0,0 +1,39 @@
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;