signal-desktop/js/modules/metadata/SecretSessionCipher.js

584 lines
17 KiB
JavaScript

/* global libsignal, textsecure */
/* eslint-disable no-bitwise */
const CiphertextMessage = require('./CiphertextMessage');
const {
bytesFromString,
concatenateBytes,
constantTimeEqual,
decryptAesCtr,
encryptAesCtr,
fromEncodedBinaryToArrayBuffer,
getViewOfArrayBuffer,
getZeroes,
highBitsToInt,
hmacSha256,
intsToByteHighAndLow,
splitBytes,
trimBytes,
} = require('../crypto');
const REVOKED_CERTIFICATES = [];
function SecretSessionCipher(storage) {
this.storage = storage;
// We do this on construction because libsignal won't be available when this file loads
const { SessionCipher } = libsignal;
this.SessionCipher = SessionCipher;
}
const CIPHERTEXT_VERSION = 1;
const UNIDENTIFIED_DELIVERY_PREFIX = 'UnidentifiedDelivery';
// public CertificateValidator(ECPublicKey trustRoot)
function createCertificateValidator(trustRoot) {
return {
// public void validate(SenderCertificate certificate, long validationTime)
async validate(certificate, validationTime) {
const serverCertificate = certificate.signer;
await libsignal.Curve.async.verifySignature(
trustRoot,
serverCertificate.certificate,
serverCertificate.signature
);
const serverCertId = serverCertificate.certificate.id;
if (REVOKED_CERTIFICATES.includes(serverCertId)) {
throw new Error(
`Server certificate id ${serverCertId} has been revoked`
);
}
await libsignal.Curve.async.verifySignature(
serverCertificate.key,
certificate.certificate,
certificate.signature
);
if (validationTime > certificate.expires) {
throw new Error('Certificate is expired');
}
},
};
}
function _decodePoint(serialized, offset = 0) {
const view =
offset > 0
? getViewOfArrayBuffer(serialized, offset, serialized.byteLength)
: serialized;
return libsignal.Curve.validatePubKeyFormat(view);
}
// public ServerCertificate(byte[] serialized)
function _createServerCertificateFromBuffer(serialized) {
const wrapper = textsecure.protobuf.ServerCertificate.decode(serialized);
if (!wrapper.certificate || !wrapper.signature) {
throw new Error('Missing fields');
}
const certificate = 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)
function _createSenderCertificateFromBuffer(serialized) {
const wrapper = textsecure.protobuf.SenderCertificate.decode(serialized);
if (!wrapper.signature || !wrapper.certificate) {
throw new Error('Missing fields');
}
const certificate = textsecure.protobuf.SenderCertificate.Certificate.decode(
wrapper.certificate.toArrayBuffer()
);
if (
!certificate.signer ||
!certificate.identityKey ||
!certificate.senderDevice ||
!certificate.expires ||
!certificate.sender
) {
throw new Error('Missing fields');
}
return {
sender: certificate.sender,
senderDevice: certificate.senderDevice,
expires: certificate.expires.toNumber(),
identityKey: certificate.identityKey.toArrayBuffer(),
signer: _createServerCertificateFromBuffer(
certificate.signer.toArrayBuffer()
),
certificate: wrapper.certificate.toArrayBuffer(),
signature: wrapper.signature.toArrayBuffer(),
serialized,
};
}
// public UnidentifiedSenderMessage(byte[] serialized)
function _createUnidentifiedSenderMessageFromBuffer(serialized) {
const version = highBitsToInt(serialized[0]);
if (version > CIPHERTEXT_VERSION) {
throw new Error(`Unknown version: ${this.version}`);
}
const view = getViewOfArrayBuffer(serialized, 1, serialized.byteLength);
const unidentifiedSenderMessage = 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,
encryptedStatic,
encryptedMessage
) {
const versionBytes = new Uint8Array([
intsToByteHighAndLow(CIPHERTEXT_VERSION, CIPHERTEXT_VERSION),
]);
const unidentifiedSenderMessage = new textsecure.protobuf.UnidentifiedSenderMessage();
unidentifiedSenderMessage.encryptedMessage = encryptedMessage;
unidentifiedSenderMessage.encryptedStatic = encryptedStatic;
unidentifiedSenderMessage.ephemeralPublic = ephemeralPublic;
const messageBytes = unidentifiedSenderMessage.encode().toArrayBuffer();
return {
version: CIPHERTEXT_VERSION,
ephemeralPublic,
encryptedStatic,
encryptedMessage,
serialized: concatenateBytes(versionBytes, messageBytes),
};
}
// public UnidentifiedSenderMessageContent(byte[] serialized)
function _createUnidentifiedSenderMessageContentFromBuffer(serialized) {
const TypeEnum = textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
const message = 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) {
const TypeEnum = 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,
senderCertificate,
content
) {
const innerMessage = new textsecure.protobuf.UnidentifiedSenderMessage.Message();
innerMessage.type = _getProtoMessageType(type);
innerMessage.senderCertificate = textsecure.protobuf.SenderCertificate.decode(
senderCertificate.serialized
);
innerMessage.content = content;
return {
type,
senderCertificate,
content,
serialized: innerMessage.encode().toArrayBuffer(),
};
}
SecretSessionCipher.prototype = {
// public byte[] encrypt(
// SignalProtocolAddress destinationAddress,
// SenderCertificate senderCertificate,
// byte[] paddedPlaintext
// )
async encrypt(destinationAddress, senderCertificate, paddedPlaintext) {
// Capture this.xxx variables to replicate Java's implicit this syntax
const { SessionCipher } = this;
const signalProtocolStore = this.storage;
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
const _encryptWithSecretKeys = this._encryptWithSecretKeys.bind(this);
const _calculateStaticKeys = this._calculateStaticKeys.bind(this);
const sessionCipher = new SessionCipher(
signalProtocolStore,
destinationAddress
);
const message = await sessionCipher.encrypt(paddedPlaintext);
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
const theirIdentity = fromEncodedBinaryToArrayBuffer(
await signalProtocolStore.loadIdentityKey(destinationAddress.getName())
);
const ephemeral = await libsignal.Curve.async.generateKeyPair();
const ephemeralSalt = concatenateBytes(
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
theirIdentity,
ephemeral.pubKey
);
const ephemeralKeys = await _calculateEphemeralKeys(
theirIdentity,
ephemeral.privKey,
ephemeralSalt
);
const staticKeyCiphertext = await _encryptWithSecretKeys(
ephemeralKeys.cipherKey,
ephemeralKeys.macKey,
ourIdentity.pubKey
);
const staticSalt = concatenateBytes(
ephemeralKeys.chainKey,
staticKeyCiphertext
);
const staticKeys = await _calculateStaticKeys(
theirIdentity,
ourIdentity.privKey,
staticSalt
);
const content = _createUnidentifiedSenderMessageContent(
message.type,
senderCertificate,
fromEncodedBinaryToArrayBuffer(message.body)
);
const messageBytes = await _encryptWithSecretKeys(
staticKeys.cipherKey,
staticKeys.macKey,
content.serialized
);
const unidentifiedSenderMessage = _createUnidentifiedSenderMessage(
ephemeral.pubKey,
staticKeyCiphertext,
messageBytes
);
return unidentifiedSenderMessage.serialized;
},
// public Pair<SignalProtocolAddress, byte[]> decrypt(
// CertificateValidator validator, byte[] ciphertext, long timestamp)
async decrypt(validator, ciphertext, timestamp, me) {
// Capture this.xxx variables to replicate Java's implicit this syntax
const signalProtocolStore = this.storage;
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
const _calculateStaticKeys = this._calculateStaticKeys.bind(this);
const _decryptWithUnidentifiedSenderMessage = this._decryptWithUnidentifiedSenderMessage.bind(
this
);
const _decryptWithSecretKeys = this._decryptWithSecretKeys.bind(this);
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
const wrapper = _createUnidentifiedSenderMessageFromBuffer(ciphertext);
const ephemeralSalt = concatenateBytes(
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
ourIdentity.pubKey,
wrapper.ephemeralPublic
);
const ephemeralKeys = await _calculateEphemeralKeys(
wrapper.ephemeralPublic,
ourIdentity.privKey,
ephemeralSalt
);
const staticKeyBytes = await _decryptWithSecretKeys(
ephemeralKeys.cipherKey,
ephemeralKeys.macKey,
wrapper.encryptedStatic
);
const staticKey = _decodePoint(staticKeyBytes, 0);
const staticSalt = concatenateBytes(
ephemeralKeys.chainKey,
wrapper.encryptedStatic
);
const staticKeys = await _calculateStaticKeys(
staticKey,
ourIdentity.privKey,
staticSalt
);
const messageBytes = await _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, senderDevice } = content.senderCertificate;
const { number, deviceId } = me || {};
if (sender === number && senderDevice === deviceId) {
return {
isMe: true,
};
}
const address = new libsignal.SignalProtocolAddress(sender, senderDevice);
try {
return {
sender: address,
content: await _decryptWithUnidentifiedSenderMessage(content),
};
} catch (error) {
if (!error) {
// eslint-disable-next-line no-ex-assign
error = new Error('Decryption error was falsey!');
}
error.sender = address;
throw error;
}
},
// public int getSessionVersion(SignalProtocolAddress remoteAddress) {
getSessionVersion(remoteAddress) {
const { SessionCipher } = this;
const signalProtocolStore = this.storage;
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
return cipher.getSessionVersion();
},
// public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) {
getRemoteRegistrationId(remoteAddress) {
const { SessionCipher } = this;
const signalProtocolStore = this.storage;
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
return cipher.getRemoteRegistrationId();
},
// Used by outgoing_message.js
closeOpenSessionForDevice(remoteAddress) {
const { SessionCipher } = this;
const signalProtocolStore = this.storage;
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
return cipher.closeOpenSessionForDevice();
},
// private EphemeralKeys calculateEphemeralKeys(
// ECPublicKey ephemeralPublic, ECPrivateKey ephemeralPrivate, byte[] salt)
async _calculateEphemeralKeys(ephemeralPublic, ephemeralPrivate, salt) {
const ephemeralSecret = await libsignal.Curve.async.calculateAgreement(
ephemeralPublic,
ephemeralPrivate
);
const ephemeralDerivedParts = await libsignal.HKDF.deriveSecrets(
ephemeralSecret,
salt,
new ArrayBuffer()
);
// 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)
async _calculateStaticKeys(staticPublic, staticPrivate, salt) {
const staticSecret = await libsignal.Curve.async.calculateAgreement(
staticPublic,
staticPrivate
);
const staticDerivedParts = await libsignal.HKDF.deriveSecrets(
staticSecret,
salt,
new ArrayBuffer()
);
// private StaticKeys(byte[] cipherKey, byte[] macKey)
return {
cipherKey: staticDerivedParts[1],
macKey: staticDerivedParts[2],
};
},
// private byte[] decrypt(UnidentifiedSenderMessageContent message)
_decryptWithUnidentifiedSenderMessage(message) {
const { SessionCipher } = this;
const signalProtocolStore = this.storage;
const sender = new libsignal.SignalProtocolAddress(
message.senderCertificate.sender,
message.senderCertificate.senderDevice
);
switch (message.type) {
case CiphertextMessage.WHISPER_TYPE:
return new SessionCipher(
signalProtocolStore,
sender
).decryptWhisperMessage(message.content);
case CiphertextMessage.PREKEY_TYPE:
return new SessionCipher(
signalProtocolStore,
sender
).decryptPreKeyWhisperMessage(message.content);
default:
throw new Error(`Unknown type: ${message.type}`);
}
},
// private byte[] encrypt(
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] plaintext)
async _encryptWithSecretKeys(cipherKey, macKey, plaintext) {
// 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)
async _decryptWithSecretKeys(cipherKey, macKey, ciphertext) {
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('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));
},
};
module.exports = {
SecretSessionCipher,
createCertificateValidator,
_createServerCertificateFromBuffer,
_createSenderCertificateFromBuffer,
};