482db6edc5
* 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>
133 lines
4.5 KiB
TypeScript
133 lines
4.5 KiB
TypeScript
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')
|
|
}
|
|
}
|
|
}
|