768 lines
21 KiB
TypeScript
768 lines
21 KiB
TypeScript
// Copyright 2018-2021 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
/* eslint-disable class-methods-use-this */
|
|
|
|
import * as CiphertextMessage from './CiphertextMessage';
|
|
import {
|
|
bytesFromString,
|
|
concatenateBytes,
|
|
constantTimeEqual,
|
|
decryptAesCtr,
|
|
encryptAesCtr,
|
|
fromEncodedBinaryToArrayBuffer,
|
|
getViewOfArrayBuffer,
|
|
getZeroes,
|
|
highBitsToInt,
|
|
hmacSha256,
|
|
intsToByteHighAndLow,
|
|
splitBytes,
|
|
trimBytes,
|
|
} from '../Crypto';
|
|
|
|
import { SignalProtocolAddressClass } from '../libsignal.d';
|
|
|
|
const REVOKED_CERTIFICATES: Array<number> = [];
|
|
const CIPHERTEXT_VERSION = 1;
|
|
const UNIDENTIFIED_DELIVERY_PREFIX = 'UnidentifiedDelivery';
|
|
|
|
type MeType = {
|
|
number?: string;
|
|
uuid?: string;
|
|
deviceId: number;
|
|
};
|
|
|
|
type ValidatorType = {
|
|
validate(
|
|
certificate: SenderCertificateType,
|
|
validationTime: number
|
|
): Promise<void>;
|
|
};
|
|
|
|
export type SerializedCertificateType = {
|
|
serialized: ArrayBuffer;
|
|
};
|
|
|
|
type ServerCertificateType = {
|
|
id: number;
|
|
key: ArrayBuffer;
|
|
};
|
|
|
|
type ServerCertificateWrapperType = {
|
|
certificate: ArrayBuffer;
|
|
signature: ArrayBuffer;
|
|
};
|
|
|
|
type SenderCertificateType = {
|
|
sender?: string;
|
|
senderUuid?: string;
|
|
senderDevice: number;
|
|
expires: number;
|
|
identityKey: ArrayBuffer;
|
|
signer: ServerCertificateType;
|
|
};
|
|
|
|
type SenderCertificateWrapperType = {
|
|
certificate: ArrayBuffer;
|
|
signature: ArrayBuffer;
|
|
};
|
|
|
|
type MessageType = {
|
|
ephemeralPublic: ArrayBuffer;
|
|
encryptedStatic: ArrayBuffer;
|
|
encryptedMessage: ArrayBuffer;
|
|
};
|
|
|
|
type InnerMessageType = {
|
|
type: number;
|
|
senderCertificate: SenderCertificateWrapperType;
|
|
content: ArrayBuffer;
|
|
};
|
|
|
|
export type ExplodedServerCertificateType = ServerCertificateType &
|
|
ServerCertificateWrapperType &
|
|
SerializedCertificateType;
|
|
|
|
export type ExplodedSenderCertificateType = SenderCertificateType &
|
|
SenderCertificateWrapperType &
|
|
SerializedCertificateType & {
|
|
signer: ExplodedServerCertificateType;
|
|
};
|
|
|
|
type ExplodedMessageType = MessageType &
|
|
SerializedCertificateType & { version: number };
|
|
|
|
type ExplodedInnerMessageType = InnerMessageType &
|
|
SerializedCertificateType & {
|
|
senderCertificate: ExplodedSenderCertificateType;
|
|
};
|
|
|
|
// public CertificateValidator(ECPublicKey trustRoot)
|
|
export function createCertificateValidator(
|
|
trustRoot: ArrayBuffer
|
|
): ValidatorType {
|
|
return {
|
|
// public void validate(SenderCertificate certificate, long validationTime)
|
|
async validate(
|
|
certificate: ExplodedSenderCertificateType,
|
|
validationTime: number
|
|
): Promise<void> {
|
|
const serverCertificate = certificate.signer;
|
|
|
|
await window.libsignal.Curve.async.verifySignature(
|
|
trustRoot,
|
|
serverCertificate.certificate,
|
|
serverCertificate.signature
|
|
);
|
|
|
|
const serverCertId = serverCertificate.id;
|
|
if (REVOKED_CERTIFICATES.includes(serverCertId)) {
|
|
throw new Error(
|
|
`Server certificate id ${serverCertId} has been revoked`
|
|
);
|
|
}
|
|
|
|
await window.libsignal.Curve.async.verifySignature(
|
|
serverCertificate.key,
|
|
certificate.certificate,
|
|
certificate.signature
|
|
);
|
|
|
|
if (validationTime > certificate.expires) {
|
|
throw new Error('Certificate is expired');
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
function _decodePoint(serialized: ArrayBuffer, offset = 0): ArrayBuffer {
|
|
const view =
|
|
offset > 0
|
|
? getViewOfArrayBuffer(serialized, offset, serialized.byteLength)
|
|
: serialized;
|
|
|
|
return window.libsignal.Curve.validatePubKeyFormat(view);
|
|
}
|
|
|
|
// public ServerCertificate(byte[] serialized)
|
|
export function _createServerCertificateFromBuffer(
|
|
serialized: ArrayBuffer
|
|
): ExplodedServerCertificateType {
|
|
const wrapper = window.textsecure.protobuf.ServerCertificate.decode(
|
|
serialized
|
|
);
|
|
|
|
if (!wrapper.certificate || !wrapper.signature) {
|
|
throw new Error('Missing fields');
|
|
}
|
|
|
|
const certificate = window.textsecure.protobuf.ServerCertificate.Certificate.decode(
|
|
wrapper.certificate.toArrayBuffer()
|
|
);
|
|
|
|
if (!certificate.id || !certificate.key) {
|
|
throw new Error('Missing fields');
|
|
}
|
|
|
|
return {
|
|
id: certificate.id,
|
|
key: certificate.key.toArrayBuffer(),
|
|
serialized,
|
|
certificate: wrapper.certificate.toArrayBuffer(),
|
|
|
|
signature: wrapper.signature.toArrayBuffer(),
|
|
};
|
|
}
|
|
|
|
// public SenderCertificate(byte[] serialized)
|
|
export function _createSenderCertificateFromBuffer(
|
|
serialized: ArrayBuffer
|
|
): ExplodedSenderCertificateType {
|
|
const wrapper = window.textsecure.protobuf.SenderCertificate.decode(
|
|
serialized
|
|
);
|
|
|
|
const { signature, certificate } = wrapper;
|
|
|
|
if (!signature || !certificate) {
|
|
throw new Error('Missing fields');
|
|
}
|
|
|
|
const senderCertificate = window.textsecure.protobuf.SenderCertificate.Certificate.decode(
|
|
wrapper.certificate.toArrayBuffer()
|
|
);
|
|
|
|
const {
|
|
signer,
|
|
identityKey,
|
|
senderDevice,
|
|
expires,
|
|
sender,
|
|
senderUuid,
|
|
} = senderCertificate;
|
|
|
|
if (
|
|
!signer ||
|
|
!identityKey ||
|
|
!senderDevice ||
|
|
!expires ||
|
|
!(sender || senderUuid)
|
|
) {
|
|
throw new Error('Missing fields');
|
|
}
|
|
|
|
return {
|
|
sender,
|
|
senderUuid,
|
|
senderDevice,
|
|
expires: expires.toNumber(),
|
|
identityKey: identityKey.toArrayBuffer(),
|
|
signer: _createServerCertificateFromBuffer(signer.toArrayBuffer()),
|
|
|
|
certificate: certificate.toArrayBuffer(),
|
|
signature: signature.toArrayBuffer(),
|
|
|
|
serialized,
|
|
};
|
|
}
|
|
|
|
// public UnidentifiedSenderMessage(byte[] serialized)
|
|
function _createUnidentifiedSenderMessageFromBuffer(
|
|
serialized: ArrayBuffer
|
|
): ExplodedMessageType {
|
|
const uintArray = new Uint8Array(serialized);
|
|
const version = highBitsToInt(uintArray[0]);
|
|
|
|
if (version > CIPHERTEXT_VERSION) {
|
|
throw new Error(`Unknown version: ${version}`);
|
|
}
|
|
|
|
const view = getViewOfArrayBuffer(serialized, 1, serialized.byteLength);
|
|
const unidentifiedSenderMessage = window.textsecure.protobuf.UnidentifiedSenderMessage.decode(
|
|
view
|
|
);
|
|
|
|
if (
|
|
!unidentifiedSenderMessage.ephemeralPublic ||
|
|
!unidentifiedSenderMessage.encryptedStatic ||
|
|
!unidentifiedSenderMessage.encryptedMessage
|
|
) {
|
|
throw new Error('Missing fields');
|
|
}
|
|
|
|
return {
|
|
version,
|
|
|
|
ephemeralPublic: unidentifiedSenderMessage.ephemeralPublic.toArrayBuffer(),
|
|
encryptedStatic: unidentifiedSenderMessage.encryptedStatic.toArrayBuffer(),
|
|
encryptedMessage: unidentifiedSenderMessage.encryptedMessage.toArrayBuffer(),
|
|
|
|
serialized,
|
|
};
|
|
}
|
|
|
|
// public UnidentifiedSenderMessage(
|
|
// ECPublicKey ephemeral, byte[] encryptedStatic, byte[] encryptedMessage) {
|
|
function _createUnidentifiedSenderMessage(
|
|
ephemeralPublic: ArrayBuffer,
|
|
encryptedStatic: ArrayBuffer,
|
|
encryptedMessage: ArrayBuffer
|
|
): ExplodedMessageType {
|
|
const versionBytes = new Uint8Array([
|
|
intsToByteHighAndLow(CIPHERTEXT_VERSION, CIPHERTEXT_VERSION),
|
|
]);
|
|
const unidentifiedSenderMessage = new window.textsecure.protobuf.UnidentifiedSenderMessage();
|
|
|
|
unidentifiedSenderMessage.encryptedMessage = encryptedMessage;
|
|
unidentifiedSenderMessage.encryptedStatic = encryptedStatic;
|
|
unidentifiedSenderMessage.ephemeralPublic = ephemeralPublic;
|
|
|
|
const messageBytes = unidentifiedSenderMessage.toArrayBuffer();
|
|
|
|
return {
|
|
version: CIPHERTEXT_VERSION,
|
|
|
|
ephemeralPublic,
|
|
encryptedStatic,
|
|
encryptedMessage,
|
|
|
|
serialized: concatenateBytes(versionBytes, messageBytes),
|
|
};
|
|
}
|
|
|
|
// public UnidentifiedSenderMessageContent(byte[] serialized)
|
|
function _createUnidentifiedSenderMessageContentFromBuffer(
|
|
serialized: ArrayBuffer
|
|
): ExplodedInnerMessageType {
|
|
const TypeEnum =
|
|
window.textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
|
|
|
|
const message = window.textsecure.protobuf.UnidentifiedSenderMessage.Message.decode(
|
|
serialized
|
|
);
|
|
|
|
if (!message.type || !message.senderCertificate || !message.content) {
|
|
throw new Error('Missing fields');
|
|
}
|
|
|
|
let type;
|
|
switch (message.type) {
|
|
case TypeEnum.MESSAGE:
|
|
type = CiphertextMessage.WHISPER_TYPE;
|
|
break;
|
|
case TypeEnum.PREKEY_MESSAGE:
|
|
type = CiphertextMessage.PREKEY_TYPE;
|
|
break;
|
|
default:
|
|
throw new Error(`Unknown type: ${message.type}`);
|
|
}
|
|
|
|
return {
|
|
type,
|
|
senderCertificate: _createSenderCertificateFromBuffer(
|
|
message.senderCertificate.toArrayBuffer()
|
|
),
|
|
content: message.content.toArrayBuffer(),
|
|
|
|
serialized,
|
|
};
|
|
}
|
|
|
|
// private int getProtoType(int type)
|
|
function _getProtoMessageType(type: number): number {
|
|
const TypeEnum =
|
|
window.textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
|
|
|
|
switch (type) {
|
|
case CiphertextMessage.WHISPER_TYPE:
|
|
return TypeEnum.MESSAGE;
|
|
case CiphertextMessage.PREKEY_TYPE:
|
|
return TypeEnum.PREKEY_MESSAGE;
|
|
default:
|
|
throw new Error(`_getProtoMessageType: type '${type}' does not exist`);
|
|
}
|
|
}
|
|
|
|
// public UnidentifiedSenderMessageContent(
|
|
// int type, SenderCertificate senderCertificate, byte[] content)
|
|
function _createUnidentifiedSenderMessageContent(
|
|
type: number,
|
|
senderCertificate: SerializedCertificateType,
|
|
content: ArrayBuffer
|
|
): ArrayBuffer {
|
|
const innerMessage = new window.textsecure.protobuf.UnidentifiedSenderMessage.Message();
|
|
innerMessage.type = _getProtoMessageType(type);
|
|
innerMessage.senderCertificate = window.textsecure.protobuf.SenderCertificate.decode(
|
|
senderCertificate.serialized
|
|
);
|
|
innerMessage.content = content;
|
|
|
|
return innerMessage.toArrayBuffer();
|
|
}
|
|
|
|
export class SecretSessionCipher {
|
|
storage: typeof window.textsecure.storage.protocol;
|
|
|
|
options: { messageKeysLimit?: number | boolean };
|
|
|
|
SessionCipher: typeof window.libsignal.SessionCipher;
|
|
|
|
constructor(
|
|
storage: typeof window.textsecure.storage.protocol,
|
|
options?: { messageKeysLimit?: number | boolean }
|
|
) {
|
|
this.storage = storage;
|
|
|
|
// Do this on construction because libsignal won't be available when this file loads
|
|
const { SessionCipher } = window.libsignal;
|
|
this.SessionCipher = SessionCipher;
|
|
|
|
this.options = options || {};
|
|
}
|
|
|
|
// public byte[] encrypt(
|
|
// SignalProtocolAddress destinationAddress,
|
|
// SenderCertificate senderCertificate,
|
|
// byte[] paddedPlaintext
|
|
// )
|
|
async encrypt(
|
|
destinationAddress: SignalProtocolAddressClass,
|
|
senderCertificate: SerializedCertificateType,
|
|
paddedPlaintext: ArrayBuffer
|
|
): Promise<ArrayBuffer> {
|
|
// Capture this.xxx variables to replicate Java's implicit this syntax
|
|
const { SessionCipher } = this;
|
|
const signalProtocolStore = this.storage;
|
|
|
|
const sessionCipher = new SessionCipher(
|
|
signalProtocolStore,
|
|
destinationAddress,
|
|
this.options
|
|
);
|
|
|
|
const message = await sessionCipher.encrypt(paddedPlaintext);
|
|
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
|
|
const theirIdentityData = await signalProtocolStore.loadIdentityKey(
|
|
destinationAddress.getName()
|
|
);
|
|
if (!theirIdentityData) {
|
|
throw new Error(
|
|
'SecretSessionCipher.encrypt: No identity data for recipient!'
|
|
);
|
|
}
|
|
const theirIdentity =
|
|
typeof theirIdentityData === 'string'
|
|
? fromEncodedBinaryToArrayBuffer(theirIdentityData)
|
|
: theirIdentityData;
|
|
|
|
const ephemeral = await window.libsignal.Curve.async.generateKeyPair();
|
|
const ephemeralSalt = concatenateBytes(
|
|
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
|
|
theirIdentity,
|
|
ephemeral.pubKey
|
|
);
|
|
const ephemeralKeys = await this._calculateEphemeralKeys(
|
|
theirIdentity,
|
|
ephemeral.privKey,
|
|
ephemeralSalt
|
|
);
|
|
const staticKeyCiphertext = await this._encryptWithSecretKeys(
|
|
ephemeralKeys.cipherKey,
|
|
ephemeralKeys.macKey,
|
|
ourIdentity.pubKey
|
|
);
|
|
|
|
const staticSalt = concatenateBytes(
|
|
ephemeralKeys.chainKey,
|
|
staticKeyCiphertext
|
|
);
|
|
const staticKeys = await this._calculateStaticKeys(
|
|
theirIdentity,
|
|
ourIdentity.privKey,
|
|
staticSalt
|
|
);
|
|
const serializedMessage = _createUnidentifiedSenderMessageContent(
|
|
message.type,
|
|
senderCertificate,
|
|
fromEncodedBinaryToArrayBuffer(message.body)
|
|
);
|
|
const messageBytes = await this._encryptWithSecretKeys(
|
|
staticKeys.cipherKey,
|
|
staticKeys.macKey,
|
|
serializedMessage
|
|
);
|
|
|
|
const unidentifiedSenderMessage = _createUnidentifiedSenderMessage(
|
|
ephemeral.pubKey,
|
|
staticKeyCiphertext,
|
|
messageBytes
|
|
);
|
|
|
|
return unidentifiedSenderMessage.serialized;
|
|
}
|
|
|
|
// public Pair<SignalProtocolAddress, byte[]> decrypt(
|
|
// CertificateValidator validator, byte[] ciphertext, long timestamp)
|
|
async decrypt(
|
|
validator: ValidatorType,
|
|
ciphertext: ArrayBuffer,
|
|
timestamp: number,
|
|
me?: MeType
|
|
): Promise<{
|
|
isMe?: boolean;
|
|
sender?: SignalProtocolAddressClass;
|
|
senderUuid?: SignalProtocolAddressClass;
|
|
content?: ArrayBuffer;
|
|
}> {
|
|
const signalProtocolStore = this.storage;
|
|
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
|
|
const wrapper = _createUnidentifiedSenderMessageFromBuffer(ciphertext);
|
|
const ephemeralSalt = concatenateBytes(
|
|
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
|
|
ourIdentity.pubKey,
|
|
wrapper.ephemeralPublic
|
|
);
|
|
const ephemeralKeys = await this._calculateEphemeralKeys(
|
|
wrapper.ephemeralPublic,
|
|
ourIdentity.privKey,
|
|
ephemeralSalt
|
|
);
|
|
const staticKeyBytes = await this._decryptWithSecretKeys(
|
|
ephemeralKeys.cipherKey,
|
|
ephemeralKeys.macKey,
|
|
wrapper.encryptedStatic
|
|
);
|
|
|
|
const staticKey = _decodePoint(staticKeyBytes, 0);
|
|
const staticSalt = concatenateBytes(
|
|
ephemeralKeys.chainKey,
|
|
wrapper.encryptedStatic
|
|
);
|
|
const staticKeys = await this._calculateStaticKeys(
|
|
staticKey,
|
|
ourIdentity.privKey,
|
|
staticSalt
|
|
);
|
|
const messageBytes = await this._decryptWithSecretKeys(
|
|
staticKeys.cipherKey,
|
|
staticKeys.macKey,
|
|
wrapper.encryptedMessage
|
|
);
|
|
|
|
const content = _createUnidentifiedSenderMessageContentFromBuffer(
|
|
messageBytes
|
|
);
|
|
|
|
await validator.validate(content.senderCertificate, timestamp);
|
|
if (
|
|
!constantTimeEqual(content.senderCertificate.identityKey, staticKeyBytes)
|
|
) {
|
|
throw new Error(
|
|
"Sender's certificate key does not match key used in message"
|
|
);
|
|
}
|
|
|
|
const { sender, senderUuid, senderDevice } = content.senderCertificate;
|
|
if (
|
|
me &&
|
|
((sender && me.number && sender === me.number) ||
|
|
(senderUuid && me.uuid && senderUuid === me.uuid)) &&
|
|
senderDevice === me.deviceId
|
|
) {
|
|
return {
|
|
isMe: true,
|
|
};
|
|
}
|
|
const addressE164 = sender
|
|
? new window.libsignal.SignalProtocolAddress(sender, senderDevice)
|
|
: undefined;
|
|
const addressUuid = senderUuid
|
|
? new window.libsignal.SignalProtocolAddress(
|
|
senderUuid.toLowerCase(),
|
|
senderDevice
|
|
)
|
|
: undefined;
|
|
|
|
try {
|
|
return {
|
|
sender: addressE164,
|
|
senderUuid: addressUuid,
|
|
content: await this._decryptWithUnidentifiedSenderMessage(content),
|
|
};
|
|
} catch (error) {
|
|
if (!error) {
|
|
// eslint-disable-next-line no-ex-assign
|
|
error = new Error('Decryption error was falsey!');
|
|
}
|
|
|
|
error.sender = addressE164;
|
|
error.senderUuid = addressUuid;
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// public int getSessionVersion(SignalProtocolAddress remoteAddress) {
|
|
getSessionVersion(
|
|
remoteAddress: SignalProtocolAddressClass
|
|
): Promise<number> {
|
|
const { SessionCipher } = this;
|
|
const signalProtocolStore = this.storage;
|
|
|
|
const cipher = new SessionCipher(
|
|
signalProtocolStore,
|
|
remoteAddress,
|
|
this.options
|
|
);
|
|
|
|
return cipher.getSessionVersion();
|
|
}
|
|
|
|
// public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) {
|
|
getRemoteRegistrationId(
|
|
remoteAddress: SignalProtocolAddressClass
|
|
): Promise<number> {
|
|
const { SessionCipher } = this;
|
|
const signalProtocolStore = this.storage;
|
|
|
|
const cipher = new SessionCipher(
|
|
signalProtocolStore,
|
|
remoteAddress,
|
|
this.options
|
|
);
|
|
|
|
return cipher.getRemoteRegistrationId();
|
|
}
|
|
|
|
// Used by outgoing_message.js
|
|
closeOpenSessionForDevice(
|
|
remoteAddress: SignalProtocolAddressClass
|
|
): Promise<void> {
|
|
const { SessionCipher } = this;
|
|
const signalProtocolStore = this.storage;
|
|
|
|
const cipher = new SessionCipher(
|
|
signalProtocolStore,
|
|
remoteAddress,
|
|
this.options
|
|
);
|
|
|
|
return cipher.closeOpenSessionForDevice();
|
|
}
|
|
|
|
// private EphemeralKeys calculateEphemeralKeys(
|
|
// ECPublicKey ephemeralPublic, ECPrivateKey ephemeralPrivate, byte[] salt)
|
|
private async _calculateEphemeralKeys(
|
|
ephemeralPublic: ArrayBuffer,
|
|
ephemeralPrivate: ArrayBuffer,
|
|
salt: ArrayBuffer
|
|
): Promise<{
|
|
chainKey: ArrayBuffer;
|
|
cipherKey: ArrayBuffer;
|
|
macKey: ArrayBuffer;
|
|
}> {
|
|
const ephemeralSecret = await window.libsignal.Curve.async.calculateAgreement(
|
|
ephemeralPublic,
|
|
ephemeralPrivate
|
|
);
|
|
const ephemeralDerivedParts = await window.libsignal.HKDF.deriveSecrets(
|
|
ephemeralSecret,
|
|
salt,
|
|
new ArrayBuffer(0)
|
|
);
|
|
|
|
// private EphemeralKeys(byte[] chainKey, byte[] cipherKey, byte[] macKey)
|
|
return {
|
|
chainKey: ephemeralDerivedParts[0],
|
|
cipherKey: ephemeralDerivedParts[1],
|
|
macKey: ephemeralDerivedParts[2],
|
|
};
|
|
}
|
|
|
|
// private StaticKeys calculateStaticKeys(
|
|
// ECPublicKey staticPublic, ECPrivateKey staticPrivate, byte[] salt)
|
|
private async _calculateStaticKeys(
|
|
staticPublic: ArrayBuffer,
|
|
staticPrivate: ArrayBuffer,
|
|
salt: ArrayBuffer
|
|
): Promise<{ cipherKey: ArrayBuffer; macKey: ArrayBuffer }> {
|
|
const staticSecret = await window.libsignal.Curve.async.calculateAgreement(
|
|
staticPublic,
|
|
staticPrivate
|
|
);
|
|
const staticDerivedParts = await window.libsignal.HKDF.deriveSecrets(
|
|
staticSecret,
|
|
salt,
|
|
new ArrayBuffer(0)
|
|
);
|
|
|
|
// private StaticKeys(byte[] cipherKey, byte[] macKey)
|
|
return {
|
|
cipherKey: staticDerivedParts[1],
|
|
macKey: staticDerivedParts[2],
|
|
};
|
|
}
|
|
|
|
// private byte[] decrypt(UnidentifiedSenderMessageContent message)
|
|
private _decryptWithUnidentifiedSenderMessage(
|
|
message: ExplodedInnerMessageType
|
|
): Promise<ArrayBuffer> {
|
|
const { SessionCipher } = this;
|
|
const signalProtocolStore = this.storage;
|
|
|
|
if (!message.senderCertificate) {
|
|
throw new Error(
|
|
'_decryptWithUnidentifiedSenderMessage: Message had no senderCertificate'
|
|
);
|
|
}
|
|
|
|
const { senderUuid, sender, senderDevice } = message.senderCertificate;
|
|
const target = senderUuid || sender;
|
|
if (!senderDevice || !target) {
|
|
throw new Error(
|
|
'_decryptWithUnidentifiedSenderMessage: Missing sender information in senderCertificate'
|
|
);
|
|
}
|
|
|
|
const address = new window.libsignal.SignalProtocolAddress(
|
|
target,
|
|
senderDevice
|
|
);
|
|
|
|
switch (message.type) {
|
|
case CiphertextMessage.WHISPER_TYPE:
|
|
return new SessionCipher(
|
|
signalProtocolStore,
|
|
address,
|
|
this.options
|
|
).decryptWhisperMessage(message.content);
|
|
case CiphertextMessage.PREKEY_TYPE:
|
|
return new SessionCipher(
|
|
signalProtocolStore,
|
|
address,
|
|
this.options
|
|
).decryptPreKeyWhisperMessage(message.content);
|
|
default:
|
|
throw new Error(`Unknown type: ${message.type}`);
|
|
}
|
|
}
|
|
|
|
// private byte[] encrypt(
|
|
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] plaintext)
|
|
private async _encryptWithSecretKeys(
|
|
cipherKey: ArrayBuffer,
|
|
macKey: ArrayBuffer,
|
|
plaintext: ArrayBuffer
|
|
): Promise<ArrayBuffer> {
|
|
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
|
|
// cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
|
|
|
|
// Mac const mac = Mac.getInstance('HmacSHA256');
|
|
// mac.init(macKey);
|
|
|
|
// byte[] const ciphertext = cipher.doFinal(plaintext);
|
|
const ciphertext = await encryptAesCtr(cipherKey, plaintext, getZeroes(16));
|
|
|
|
// byte[] const ourFullMac = mac.doFinal(ciphertext);
|
|
const ourFullMac = await hmacSha256(macKey, ciphertext);
|
|
const ourMac = trimBytes(ourFullMac, 10);
|
|
|
|
return concatenateBytes(ciphertext, ourMac);
|
|
}
|
|
|
|
// private byte[] decrypt(
|
|
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] ciphertext)
|
|
private async _decryptWithSecretKeys(
|
|
cipherKey: ArrayBuffer,
|
|
macKey: ArrayBuffer,
|
|
ciphertext: ArrayBuffer
|
|
): Promise<ArrayBuffer> {
|
|
if (ciphertext.byteLength < 10) {
|
|
throw new Error('Ciphertext not long enough for MAC!');
|
|
}
|
|
|
|
const ciphertextParts = splitBytes(
|
|
ciphertext,
|
|
ciphertext.byteLength - 10,
|
|
10
|
|
);
|
|
|
|
// Mac const mac = Mac.getInstance('HmacSHA256');
|
|
// mac.init(macKey);
|
|
|
|
// byte[] const digest = mac.doFinal(ciphertextParts[0]);
|
|
const digest = await hmacSha256(macKey, ciphertextParts[0]);
|
|
const ourMac = trimBytes(digest, 10);
|
|
const theirMac = ciphertextParts[1];
|
|
|
|
if (!constantTimeEqual(ourMac, theirMac)) {
|
|
throw new Error('SecretSessionCipher/_decryptWithSecretKeys: Bad MAC!');
|
|
}
|
|
|
|
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
|
|
// cipher.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
|
|
|
|
// return cipher.doFinal(ciphertextParts[0]);
|
|
return decryptAesCtr(cipherKey, ciphertextParts[0], getZeroes(16));
|
|
}
|
|
}
|