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

* refactor: migrate WASignalGroup to TypeScript and remove deprecated files

* chore: remove WASignalGroup JavaScript files from package.json

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

* chore: lint issue

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

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

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

* fix: lint issues

---------

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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