decrypt/encrypt with libsignal-client, remove libsignal-protocol-javascript

This commit is contained in:
Scott Nonnenberg 2021-04-16 16:13:13 -07:00
parent 37ff4a1df4
commit 86d2a4b5dd
60 changed files with 2508 additions and 28714 deletions

View file

@ -9,9 +9,21 @@
/* eslint-disable no-param-reassign */
import { reject } from 'lodash';
import * as z from 'zod';
import {
CiphertextMessageType,
PreKeyBundle,
processPreKeyBundle,
ProtocolAddress,
PublicKey,
sealedSenderEncryptMessage,
SenderCertificate,
signalEncrypt,
} from 'libsignal-client';
import { ServerKeysType, WebAPIType } from './WebAPI';
import { isEnabled as isRemoteFlagEnabled } from '../RemoteConfig';
import { SignalProtocolAddressClass } from '../libsignal.d';
import { ContentClass, DataMessageClass } from '../textsecure.d';
import {
CallbackResultType,
@ -26,12 +38,40 @@ import {
UnregisteredUserError,
} from './Errors';
import { isValidNumber } from '../types/PhoneNumber';
import { SecretSessionCipher } from '../metadata/SecretSessionCipher';
import { Sessions, IdentityKeys } from '../LibSignalStores';
export const enum SenderCertificateMode {
WithE164,
WithoutE164,
}
export const serializedCertificateSchema = z
.object({
expires: z.number().optional(),
serialized: z.instanceof(ArrayBuffer),
})
.nonstrict();
export type SerializedCertificateType = z.infer<
typeof serializedCertificateSchema
>;
type OutgoingMessageOptionsType = SendOptionsType & {
online?: boolean;
};
function ciphertextMessageTypeToEnvelopeType(type: number) {
if (type === CiphertextMessageType.PreKey) {
return window.textsecure.protobuf.Envelope.Type.PREKEY_BUNDLE;
}
if (type === CiphertextMessageType.Whisper) {
return window.textsecure.protobuf.Envelope.Type.CIPHERTEXT;
}
throw new Error(
`ciphertextMessageTypeToEnvelopeType: Unrecognized type ${type}`
);
}
export default class OutgoingMessage {
server: WebAPIType;
@ -142,7 +182,7 @@ export default class OutgoingMessage {
if (deviceIds.length === 0) {
this.registerError(
identifier,
'Got empty device list when loading device keys',
'reloadDevicesAndSend: Got empty device list when loading device keys',
undefined
);
return undefined;
@ -153,44 +193,76 @@ export default class OutgoingMessage {
async getKeysForIdentifier(
identifier: string,
updateDevices: Array<number>
updateDevices: Array<number> | undefined
): Promise<void | Array<void | null>> {
const handleResult = async (response: ServerKeysType) =>
Promise.all(
const handleResult = async (response: ServerKeysType) => {
const sessionStore = new Sessions();
const identityKeyStore = new IdentityKeys();
return Promise.all(
response.devices.map(async device => {
const { deviceId, registrationId, preKey, signedPreKey } = device;
if (
updateDevices === undefined ||
updateDevices.indexOf(device.deviceId) > -1
updateDevices.indexOf(deviceId) > -1
) {
const address = new window.libsignal.SignalProtocolAddress(
identifier,
device.deviceId
);
const builder = new window.libsignal.SessionBuilder(
window.textsecure.storage.protocol,
address
);
if (device.registrationId === 0) {
window.log.info('device registrationId 0!');
}
if (!signedPreKey) {
throw new Error(
`getKeysForIdentifier/${identifier}: Missing signed prekey for deviceId ${deviceId}`
);
}
const protocolAddress = ProtocolAddress.new(identifier, deviceId);
const preKeyId = preKey?.keyId || null;
const preKeyObject = preKey
? PublicKey.deserialize(Buffer.from(preKey.publicKey))
: null;
const signedPreKeyObject = PublicKey.deserialize(
Buffer.from(signedPreKey.publicKey)
);
const identityKey = PublicKey.deserialize(
Buffer.from(response.identityKey)
);
const deviceForProcess = {
...device,
identityKey: response.identityKey,
};
return builder.processPreKey(deviceForProcess).catch(error => {
if (error.message === 'Identity key changed') {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
error.identityKey = response.identityKey;
}
throw error;
});
const preKeyBundle = PreKeyBundle.new(
registrationId,
deviceId,
preKeyId,
preKeyObject,
signedPreKey.keyId,
signedPreKeyObject,
Buffer.from(signedPreKey.signature),
identityKey
);
const address = `${identifier}.${deviceId}`;
await window.textsecure.storage.protocol
.enqueueSessionJob(address, () =>
processPreKeyBundle(
preKeyBundle,
protocolAddress,
sessionStore,
identityKeyStore
)
)
.catch(error => {
if (
error?.message?.includes('untrusted identity for address')
) {
error.timestamp = this.timestamp;
error.originalMessage = this.message.toArrayBuffer();
error.identityKey = response.identityKey;
}
throw error;
});
}
return null;
})
);
};
const { sendMetadata } = this;
const info =
@ -329,13 +401,6 @@ export default class OutgoingMessage {
deviceIds: Array<number>,
recurse?: boolean
): Promise<void> {
const ciphers: {
[key: number]: {
closeOpenSessionForDevice: (
address: SignalProtocolAddressClass
) => Promise<void>;
};
} = {};
const plaintext = this.getPlaintext();
const { sendMetadata } = this;
@ -364,54 +429,59 @@ export default class OutgoingMessage {
);
}
const sessionStore = new Sessions();
const identityKeyStore = new IdentityKeys();
return Promise.all(
deviceIds.map(async deviceId => {
const address = new window.libsignal.SignalProtocolAddress(
deviceIds.map(async destinationDeviceId => {
const protocolAddress = ProtocolAddress.new(
identifier,
deviceId
destinationDeviceId
);
const options: any = {};
// No limit on message keys if we're communicating with our other devices
if (ourNumber === identifier || ourUuid === identifier) {
options.messageKeysLimit = false;
const activeSession = await sessionStore.getSession(protocolAddress);
if (!activeSession) {
throw new Error('OutgoingMessage.doSendMessage: No active sesssion!');
}
if (sealedSender && senderCertificate) {
const secretSessionCipher = new SecretSessionCipher(
window.textsecure.storage.protocol
);
ciphers[address.getDeviceId()] = secretSessionCipher;
const destinationRegistrationId = activeSession.remoteRegistrationId();
const ciphertext = await secretSessionCipher.encrypt(
address,
senderCertificate,
plaintext
if (sealedSender && senderCertificate) {
const certificate = SenderCertificate.deserialize(
Buffer.from(senderCertificate.serialized)
);
const buffer = await sealedSenderEncryptMessage(
Buffer.from(plaintext),
protocolAddress,
certificate,
sessionStore,
identityKeyStore
);
return {
type: window.textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER,
destinationDeviceId: address.getDeviceId(),
destinationRegistrationId: await secretSessionCipher.getRemoteRegistrationId(
address
),
content: window.Signal.Crypto.arrayBufferToBase64(ciphertext),
destinationDeviceId,
destinationRegistrationId,
content: buffer.toString('base64'),
};
}
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address,
options
);
ciphers[address.getDeviceId()] = sessionCipher;
const ciphertext = await sessionCipher.encrypt(plaintext);
const ciphertextMessage = await signalEncrypt(
Buffer.from(plaintext),
protocolAddress,
sessionStore,
identityKeyStore
);
const type = ciphertextMessageTypeToEnvelopeType(
ciphertextMessage.type()
);
return {
type: ciphertext.type,
destinationDeviceId: address.getDeviceId(),
destinationRegistrationId: ciphertext.registrationId,
content: btoa(ciphertext.body),
type,
destinationDeviceId,
destinationRegistrationId,
content: ciphertextMessage.serialize().toString('base64'),
};
})
)
@ -474,14 +544,11 @@ export default class OutgoingMessage {
);
} else {
p = Promise.all(
error.response.staleDevices.map(async (deviceId: number) =>
ciphers[deviceId].closeOpenSessionForDevice(
new window.libsignal.SignalProtocolAddress(
identifier,
deviceId
)
)
)
error.response.staleDevices.map(async (deviceId: number) => {
await window.textsecure.storage.protocol.archiveSession(
`${identifier}.${deviceId}`
);
})
);
}
@ -497,7 +564,7 @@ export default class OutgoingMessage {
);
});
}
if (error.message === 'Identity key changed') {
if (error?.message?.includes('untrusted identity for address')) {
// eslint-disable-next-line no-param-reassign
error.timestamp = this.timestamp;
// eslint-disable-next-line no-param-reassign
@ -509,34 +576,19 @@ export default class OutgoingMessage {
);
window.log.info('closing all sessions for', identifier);
const address = new window.libsignal.SignalProtocolAddress(
identifier,
1
);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
window.log.info('closing session for', address.toString());
return Promise.all([
// Primary device
sessionCipher.closeOpenSessionForDevice(),
// The rest of their devices
window.textsecure.storage.protocol.archiveSiblingSessions(
address.toString()
),
]).then(
() => {
throw error;
},
innerError => {
window.log.error(
`doSendMessage: Error closing sessions: ${innerError.stack}`
);
throw error;
}
);
window.textsecure.storage.protocol
.archiveAllSessions(identifier)
.then(
() => {
throw error;
},
innerError => {
window.log.error(
`doSendMessage: Error closing sessions: ${innerError.stack}`
);
throw error;
}
);
}
this.registerError(
@ -551,32 +603,30 @@ export default class OutgoingMessage {
async getStaleDeviceIdsForIdentifier(
identifier: string
): Promise<Array<number>> {
return window.textsecure.storage.protocol
.getDeviceIds(identifier)
.then(async deviceIds => {
if (deviceIds.length === 0) {
return [1];
): Promise<Array<number> | undefined> {
const sessionStore = new Sessions();
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
identifier
);
if (deviceIds.length === 0) {
return undefined;
}
const updateDevices: Array<number> = [];
await Promise.all(
deviceIds.map(async deviceId => {
const record = await sessionStore.getSession(
ProtocolAddress.new(identifier, deviceId)
);
if (!record || !record.hasCurrentState()) {
updateDevices.push(deviceId);
}
const updateDevices: Array<number> = [];
return Promise.all(
deviceIds.map(async deviceId => {
const address = new window.libsignal.SignalProtocolAddress(
identifier,
deviceId
);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
return sessionCipher.hasOpenSession().then(hasSession => {
if (!hasSession) {
updateDevices.push(deviceId);
}
});
})
).then(() => updateDevices);
});
})
);
return updateDevices;
}
async removeDeviceIdsForIdentifier(
@ -585,15 +635,9 @@ export default class OutgoingMessage {
): Promise<void> {
await Promise.all(
deviceIdsToRemove.map(async deviceId => {
const address = new window.libsignal.SignalProtocolAddress(
identifier,
deviceId
await window.textsecure.storage.protocol.archiveSession(
`${identifier}.${deviceId}`
);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
await sessionCipher.closeOpenSessionForDevice();
})
);
}
@ -652,14 +696,14 @@ export default class OutgoingMessage {
await this.getKeysForIdentifier(identifier, updateDevices);
await this.reloadDevicesAndSend(identifier, true)();
} catch (error) {
if (error.message === 'Identity key changed') {
if (error?.message?.includes('untrusted identity for address')) {
const newError = new OutgoingIdentityKeyError(
identifier,
error.originalMessage,
error.timestamp,
error.identityKey
);
this.registerError(identifier, 'Identity key changed', newError);
this.registerError(identifier, 'Untrusted identity', newError);
} else {
this.registerError(
identifier,