decrypt/encrypt with libsignal-client, remove libsignal-protocol-javascript
This commit is contained in:
parent
37ff4a1df4
commit
86d2a4b5dd
60 changed files with 2508 additions and 28714 deletions
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue