Move SecretSessionCipher to TypeScript

This commit is contained in:
Scott Nonnenberg 2021-02-26 16:00:37 -08:00 committed by Josh Perez
parent 7e629edd21
commit c9ffb7c014
11 changed files with 569 additions and 283 deletions

View file

@ -1,16 +0,0 @@
// Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
module.exports = {
CURRENT_VERSION: 3,
// This matches Envelope.Type.CIPHERTEXT
WHISPER_TYPE: 1,
// This matches Envelope.Type.PREKEY_BUNDLE
PREKEY_TYPE: 3,
SENDERKEY_TYPE: 4,
SENDERKEY_DISTRIBUTION_TYPE: 5,
ENCRYPTED_MESSAGE_OVERHEAD: 53,
};

View file

@ -21,7 +21,6 @@ const Stickers = require('./stickers');
const Settings = require('./settings'); const Settings = require('./settings');
const RemoteConfig = require('../../ts/RemoteConfig'); const RemoteConfig = require('../../ts/RemoteConfig');
const Util = require('../../ts/util'); const Util = require('../../ts/util');
const Metadata = require('./metadata/SecretSessionCipher');
const RefreshSenderCertificate = require('./refresh_sender_certificate'); const RefreshSenderCertificate = require('./refresh_sender_certificate');
const LinkPreviews = require('./link_previews'); const LinkPreviews = require('./link_previews');
const AttachmentDownloads = require('./attachment_downloads'); const AttachmentDownloads = require('./attachment_downloads');
@ -428,7 +427,6 @@ exports.setup = (options = {}) => {
GroupChange, GroupChange,
IndexedDB, IndexedDB,
LinkPreviews, LinkPreviews,
Metadata,
Migrations, Migrations,
Notifications, Notifications,
OS, OS,

View file

@ -360,8 +360,6 @@
<script type='text/javascript' src='../js/views/banner_view.js' data-cover></script> <script type='text/javascript' src='../js/views/banner_view.js' data-cover></script>
<script type='text/javascript' src='../js/views/clear_data_view.js'></script> <script type='text/javascript' src='../js/views/clear_data_view.js'></script>
<script type="text/javascript" src="metadata/SecretSessionCipher_test.js"></script>
<script type="text/javascript" src="views/whisper_view_test.js"></script> <script type="text/javascript" src="views/whisper_view_test.js"></script>
<script type="text/javascript" src="views/list_view_test.js"></script> <script type="text/javascript" src="views/list_view_test.js"></script>

1
ts/libsignal.d.ts vendored
View file

@ -223,6 +223,7 @@ export declare class SessionCipherClass {
body: string; body: string;
}>; }>;
getRecord: () => Promise<RecordType>; getRecord: () => Promise<RecordType>;
getSessionVersion: () => Promise<number>;
getRemoteRegistrationId: () => Promise<number>; getRemoteRegistrationId: () => Promise<number>;
hasOpenSession: () => Promise<boolean>; hasOpenSession: () => Promise<boolean>;
} }

View file

@ -0,0 +1,14 @@
// Copyright 2018-2020 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
export const CURRENT_VERSION = 3;
// This matches Envelope.Type.CIPHERTEXT
export const WHISPER_TYPE = 1;
// This matches Envelope.Type.PREKEY_BUNDLE
export const PREKEY_TYPE = 3;
export const SENDERKEY_TYPE = 4;
export const SENDERKEY_DISTRIBUTION_TYPE = 5;
export const ENCRYPTED_MESSAGE_OVERHEAD = 53;

View file

@ -1,12 +1,10 @@
// Copyright 2018-2020 Signal Messenger, LLC // Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global libsignal, textsecure */ /* eslint-disable class-methods-use-this */
/* eslint-disable no-bitwise */ import * as CiphertextMessage from './CiphertextMessage';
import {
const CiphertextMessage = require('./CiphertextMessage');
const {
bytesFromString, bytesFromString,
concatenateBytes, concatenateBytes,
constantTimeEqual, constantTimeEqual,
@ -20,44 +18,111 @@ const {
intsToByteHighAndLow, intsToByteHighAndLow,
splitBytes, splitBytes,
trimBytes, trimBytes,
} = require('../../../ts/Crypto'); } from '../Crypto';
const REVOKED_CERTIFICATES = []; import { SignalProtocolAddressClass } from '../libsignal.d';
function SecretSessionCipher(storage, options) {
this.storage = storage;
// We do this on construction because libsignal won't be available when this file loads
const { SessionCipher } = libsignal;
this.SessionCipher = SessionCipher;
this.options = options || {};
}
const REVOKED_CERTIFICATES: Array<number> = [];
const CIPHERTEXT_VERSION = 1; const CIPHERTEXT_VERSION = 1;
const UNIDENTIFIED_DELIVERY_PREFIX = 'UnidentifiedDelivery'; 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) // public CertificateValidator(ECPublicKey trustRoot)
function createCertificateValidator(trustRoot) { export function createCertificateValidator(
trustRoot: ArrayBuffer
): ValidatorType {
return { return {
// public void validate(SenderCertificate certificate, long validationTime) // public void validate(SenderCertificate certificate, long validationTime)
async validate(certificate, validationTime) { async validate(
certificate: ExplodedSenderCertificateType,
validationTime: number
): Promise<void> {
const serverCertificate = certificate.signer; const serverCertificate = certificate.signer;
await libsignal.Curve.async.verifySignature( await window.libsignal.Curve.async.verifySignature(
trustRoot, trustRoot,
serverCertificate.certificate, serverCertificate.certificate,
serverCertificate.signature serverCertificate.signature
); );
const serverCertId = serverCertificate.certificate.id; const serverCertId = serverCertificate.id;
if (REVOKED_CERTIFICATES.includes(serverCertId)) { if (REVOKED_CERTIFICATES.includes(serverCertId)) {
throw new Error( throw new Error(
`Server certificate id ${serverCertId} has been revoked` `Server certificate id ${serverCertId} has been revoked`
); );
} }
await libsignal.Curve.async.verifySignature( await window.libsignal.Curve.async.verifySignature(
serverCertificate.key, serverCertificate.key,
certificate.certificate, certificate.certificate,
certificate.signature certificate.signature
@ -70,24 +135,28 @@ function createCertificateValidator(trustRoot) {
}; };
} }
function _decodePoint(serialized, offset = 0) { function _decodePoint(serialized: ArrayBuffer, offset = 0): ArrayBuffer {
const view = const view =
offset > 0 offset > 0
? getViewOfArrayBuffer(serialized, offset, serialized.byteLength) ? getViewOfArrayBuffer(serialized, offset, serialized.byteLength)
: serialized; : serialized;
return libsignal.Curve.validatePubKeyFormat(view); return window.libsignal.Curve.validatePubKeyFormat(view);
} }
// public ServerCertificate(byte[] serialized) // public ServerCertificate(byte[] serialized)
function _createServerCertificateFromBuffer(serialized) { export function _createServerCertificateFromBuffer(
const wrapper = textsecure.protobuf.ServerCertificate.decode(serialized); serialized: ArrayBuffer
): ExplodedServerCertificateType {
const wrapper = window.textsecure.protobuf.ServerCertificate.decode(
serialized
);
if (!wrapper.certificate || !wrapper.signature) { if (!wrapper.certificate || !wrapper.signature) {
throw new Error('Missing fields'); throw new Error('Missing fields');
} }
const certificate = textsecure.protobuf.ServerCertificate.Certificate.decode( const certificate = window.textsecure.protobuf.ServerCertificate.Certificate.decode(
wrapper.certificate.toArrayBuffer() wrapper.certificate.toArrayBuffer()
); );
@ -106,54 +175,70 @@ function _createServerCertificateFromBuffer(serialized) {
} }
// public SenderCertificate(byte[] serialized) // public SenderCertificate(byte[] serialized)
function _createSenderCertificateFromBuffer(serialized) { export function _createSenderCertificateFromBuffer(
const wrapper = textsecure.protobuf.SenderCertificate.decode(serialized); serialized: ArrayBuffer
): ExplodedSenderCertificateType {
const wrapper = window.textsecure.protobuf.SenderCertificate.decode(
serialized
);
if (!wrapper.signature || !wrapper.certificate) { const { signature, certificate } = wrapper;
if (!signature || !certificate) {
throw new Error('Missing fields'); throw new Error('Missing fields');
} }
const certificate = textsecure.protobuf.SenderCertificate.Certificate.decode( const senderCertificate = window.textsecure.protobuf.SenderCertificate.Certificate.decode(
wrapper.certificate.toArrayBuffer() wrapper.certificate.toArrayBuffer()
); );
const {
signer,
identityKey,
senderDevice,
expires,
sender,
senderUuid,
} = senderCertificate;
if ( if (
!certificate.signer || !signer ||
!certificate.identityKey || !identityKey ||
!certificate.senderDevice || !senderDevice ||
!certificate.expires || !expires ||
!(certificate.sender || certificate.senderUuid) !(sender || senderUuid)
) { ) {
throw new Error('Missing fields'); throw new Error('Missing fields');
} }
return { return {
sender: certificate.sender, sender,
senderUuid: certificate.senderUuid, senderUuid,
senderDevice: certificate.senderDevice, senderDevice,
expires: certificate.expires.toNumber(), expires: expires.toNumber(),
identityKey: certificate.identityKey.toArrayBuffer(), identityKey: identityKey.toArrayBuffer(),
signer: _createServerCertificateFromBuffer( signer: _createServerCertificateFromBuffer(signer.toArrayBuffer()),
certificate.signer.toArrayBuffer()
),
certificate: wrapper.certificate.toArrayBuffer(), certificate: certificate.toArrayBuffer(),
signature: wrapper.signature.toArrayBuffer(), signature: signature.toArrayBuffer(),
serialized, serialized,
}; };
} }
// public UnidentifiedSenderMessage(byte[] serialized) // public UnidentifiedSenderMessage(byte[] serialized)
function _createUnidentifiedSenderMessageFromBuffer(serialized) { function _createUnidentifiedSenderMessageFromBuffer(
const version = highBitsToInt(serialized[0]); serialized: ArrayBuffer
): ExplodedMessageType {
const uintArray = new Uint8Array(serialized);
const version = highBitsToInt(uintArray[0]);
if (version > CIPHERTEXT_VERSION) { if (version > CIPHERTEXT_VERSION) {
throw new Error(`Unknown version: ${this.version}`); throw new Error(`Unknown version: ${version}`);
} }
const view = getViewOfArrayBuffer(serialized, 1, serialized.byteLength); const view = getViewOfArrayBuffer(serialized, 1, serialized.byteLength);
const unidentifiedSenderMessage = textsecure.protobuf.UnidentifiedSenderMessage.decode( const unidentifiedSenderMessage = window.textsecure.protobuf.UnidentifiedSenderMessage.decode(
view view
); );
@ -179,20 +264,20 @@ function _createUnidentifiedSenderMessageFromBuffer(serialized) {
// public UnidentifiedSenderMessage( // public UnidentifiedSenderMessage(
// ECPublicKey ephemeral, byte[] encryptedStatic, byte[] encryptedMessage) { // ECPublicKey ephemeral, byte[] encryptedStatic, byte[] encryptedMessage) {
function _createUnidentifiedSenderMessage( function _createUnidentifiedSenderMessage(
ephemeralPublic, ephemeralPublic: ArrayBuffer,
encryptedStatic, encryptedStatic: ArrayBuffer,
encryptedMessage encryptedMessage: ArrayBuffer
) { ): ExplodedMessageType {
const versionBytes = new Uint8Array([ const versionBytes = new Uint8Array([
intsToByteHighAndLow(CIPHERTEXT_VERSION, CIPHERTEXT_VERSION), intsToByteHighAndLow(CIPHERTEXT_VERSION, CIPHERTEXT_VERSION),
]); ]);
const unidentifiedSenderMessage = new textsecure.protobuf.UnidentifiedSenderMessage(); const unidentifiedSenderMessage = new window.textsecure.protobuf.UnidentifiedSenderMessage();
unidentifiedSenderMessage.encryptedMessage = encryptedMessage; unidentifiedSenderMessage.encryptedMessage = encryptedMessage;
unidentifiedSenderMessage.encryptedStatic = encryptedStatic; unidentifiedSenderMessage.encryptedStatic = encryptedStatic;
unidentifiedSenderMessage.ephemeralPublic = ephemeralPublic; unidentifiedSenderMessage.ephemeralPublic = ephemeralPublic;
const messageBytes = unidentifiedSenderMessage.encode().toArrayBuffer(); const messageBytes = unidentifiedSenderMessage.toArrayBuffer();
return { return {
version: CIPHERTEXT_VERSION, version: CIPHERTEXT_VERSION,
@ -206,10 +291,13 @@ function _createUnidentifiedSenderMessage(
} }
// public UnidentifiedSenderMessageContent(byte[] serialized) // public UnidentifiedSenderMessageContent(byte[] serialized)
function _createUnidentifiedSenderMessageContentFromBuffer(serialized) { function _createUnidentifiedSenderMessageContentFromBuffer(
const TypeEnum = textsecure.protobuf.UnidentifiedSenderMessage.Message.Type; serialized: ArrayBuffer
): ExplodedInnerMessageType {
const TypeEnum =
window.textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
const message = textsecure.protobuf.UnidentifiedSenderMessage.Message.decode( const message = window.textsecure.protobuf.UnidentifiedSenderMessage.Message.decode(
serialized serialized
); );
@ -241,8 +329,9 @@ function _createUnidentifiedSenderMessageContentFromBuffer(serialized) {
} }
// private int getProtoType(int type) // private int getProtoType(int type)
function _getProtoMessageType(type) { function _getProtoMessageType(type: number): number {
const TypeEnum = textsecure.protobuf.UnidentifiedSenderMessage.Message.Type; const TypeEnum =
window.textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
switch (type) { switch (type) {
case CiphertextMessage.WHISPER_TYPE: case CiphertextMessage.WHISPER_TYPE:
@ -257,39 +346,53 @@ function _getProtoMessageType(type) {
// public UnidentifiedSenderMessageContent( // public UnidentifiedSenderMessageContent(
// int type, SenderCertificate senderCertificate, byte[] content) // int type, SenderCertificate senderCertificate, byte[] content)
function _createUnidentifiedSenderMessageContent( function _createUnidentifiedSenderMessageContent(
type, type: number,
senderCertificate, senderCertificate: SerializedCertificateType,
content content: ArrayBuffer
) { ): ArrayBuffer {
const innerMessage = new textsecure.protobuf.UnidentifiedSenderMessage.Message(); const innerMessage = new window.textsecure.protobuf.UnidentifiedSenderMessage.Message();
innerMessage.type = _getProtoMessageType(type); innerMessage.type = _getProtoMessageType(type);
innerMessage.senderCertificate = textsecure.protobuf.SenderCertificate.decode( innerMessage.senderCertificate = window.textsecure.protobuf.SenderCertificate.decode(
senderCertificate.serialized senderCertificate.serialized
); );
innerMessage.content = content; innerMessage.content = content;
return { return innerMessage.toArrayBuffer();
type,
senderCertificate,
content,
serialized: innerMessage.encode().toArrayBuffer(),
};
} }
SecretSessionCipher.prototype = { 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( // public byte[] encrypt(
// SignalProtocolAddress destinationAddress, // SignalProtocolAddress destinationAddress,
// SenderCertificate senderCertificate, // SenderCertificate senderCertificate,
// byte[] paddedPlaintext // byte[] paddedPlaintext
// ) // )
async encrypt(destinationAddress, senderCertificate, paddedPlaintext) { async encrypt(
destinationAddress: SignalProtocolAddressClass,
senderCertificate: SerializedCertificateType,
paddedPlaintext: ArrayBuffer
): Promise<ArrayBuffer> {
// Capture this.xxx variables to replicate Java's implicit this syntax // Capture this.xxx variables to replicate Java's implicit this syntax
const { SessionCipher } = this; const { SessionCipher } = this;
const signalProtocolStore = this.storage; 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( const sessionCipher = new SessionCipher(
signalProtocolStore, signalProtocolStore,
@ -299,22 +402,31 @@ SecretSessionCipher.prototype = {
const message = await sessionCipher.encrypt(paddedPlaintext); const message = await sessionCipher.encrypt(paddedPlaintext);
const ourIdentity = await signalProtocolStore.getIdentityKeyPair(); const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
const theirIdentity = fromEncodedBinaryToArrayBuffer( const theirIdentityData = await signalProtocolStore.loadIdentityKey(
await signalProtocolStore.loadIdentityKey(destinationAddress.getName()) 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 libsignal.Curve.async.generateKeyPair(); const ephemeral = await window.libsignal.Curve.async.generateKeyPair();
const ephemeralSalt = concatenateBytes( const ephemeralSalt = concatenateBytes(
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX), bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
theirIdentity, theirIdentity,
ephemeral.pubKey ephemeral.pubKey
); );
const ephemeralKeys = await _calculateEphemeralKeys( const ephemeralKeys = await this._calculateEphemeralKeys(
theirIdentity, theirIdentity,
ephemeral.privKey, ephemeral.privKey,
ephemeralSalt ephemeralSalt
); );
const staticKeyCiphertext = await _encryptWithSecretKeys( const staticKeyCiphertext = await this._encryptWithSecretKeys(
ephemeralKeys.cipherKey, ephemeralKeys.cipherKey,
ephemeralKeys.macKey, ephemeralKeys.macKey,
ourIdentity.pubKey ourIdentity.pubKey
@ -324,20 +436,20 @@ SecretSessionCipher.prototype = {
ephemeralKeys.chainKey, ephemeralKeys.chainKey,
staticKeyCiphertext staticKeyCiphertext
); );
const staticKeys = await _calculateStaticKeys( const staticKeys = await this._calculateStaticKeys(
theirIdentity, theirIdentity,
ourIdentity.privKey, ourIdentity.privKey,
staticSalt staticSalt
); );
const content = _createUnidentifiedSenderMessageContent( const serializedMessage = _createUnidentifiedSenderMessageContent(
message.type, message.type,
senderCertificate, senderCertificate,
fromEncodedBinaryToArrayBuffer(message.body) fromEncodedBinaryToArrayBuffer(message.body)
); );
const messageBytes = await _encryptWithSecretKeys( const messageBytes = await this._encryptWithSecretKeys(
staticKeys.cipherKey, staticKeys.cipherKey,
staticKeys.macKey, staticKeys.macKey,
content.serialized serializedMessage
); );
const unidentifiedSenderMessage = _createUnidentifiedSenderMessage( const unidentifiedSenderMessage = _createUnidentifiedSenderMessage(
@ -347,20 +459,22 @@ SecretSessionCipher.prototype = {
); );
return unidentifiedSenderMessage.serialized; return unidentifiedSenderMessage.serialized;
}, }
// public Pair<SignalProtocolAddress, byte[]> decrypt( // public Pair<SignalProtocolAddress, byte[]> decrypt(
// CertificateValidator validator, byte[] ciphertext, long timestamp) // CertificateValidator validator, byte[] ciphertext, long timestamp)
async decrypt(validator, ciphertext, timestamp, me = {}) { async decrypt(
// Capture this.xxx variables to replicate Java's implicit this syntax validator: ValidatorType,
ciphertext: ArrayBuffer,
timestamp: number,
me?: MeType
): Promise<{
isMe?: boolean;
sender?: SignalProtocolAddressClass;
senderUuid?: SignalProtocolAddressClass;
content?: ArrayBuffer;
}> {
const signalProtocolStore = this.storage; 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 ourIdentity = await signalProtocolStore.getIdentityKeyPair();
const wrapper = _createUnidentifiedSenderMessageFromBuffer(ciphertext); const wrapper = _createUnidentifiedSenderMessageFromBuffer(ciphertext);
const ephemeralSalt = concatenateBytes( const ephemeralSalt = concatenateBytes(
@ -368,12 +482,12 @@ SecretSessionCipher.prototype = {
ourIdentity.pubKey, ourIdentity.pubKey,
wrapper.ephemeralPublic wrapper.ephemeralPublic
); );
const ephemeralKeys = await _calculateEphemeralKeys( const ephemeralKeys = await this._calculateEphemeralKeys(
wrapper.ephemeralPublic, wrapper.ephemeralPublic,
ourIdentity.privKey, ourIdentity.privKey,
ephemeralSalt ephemeralSalt
); );
const staticKeyBytes = await _decryptWithSecretKeys( const staticKeyBytes = await this._decryptWithSecretKeys(
ephemeralKeys.cipherKey, ephemeralKeys.cipherKey,
ephemeralKeys.macKey, ephemeralKeys.macKey,
wrapper.encryptedStatic wrapper.encryptedStatic
@ -384,12 +498,12 @@ SecretSessionCipher.prototype = {
ephemeralKeys.chainKey, ephemeralKeys.chainKey,
wrapper.encryptedStatic wrapper.encryptedStatic
); );
const staticKeys = await _calculateStaticKeys( const staticKeys = await this._calculateStaticKeys(
staticKey, staticKey,
ourIdentity.privKey, ourIdentity.privKey,
staticSalt staticSalt
); );
const messageBytes = await _decryptWithSecretKeys( const messageBytes = await this._decryptWithSecretKeys(
staticKeys.cipherKey, staticKeys.cipherKey,
staticKeys.macKey, staticKeys.macKey,
wrapper.encryptedMessage wrapper.encryptedMessage
@ -410,6 +524,7 @@ SecretSessionCipher.prototype = {
const { sender, senderUuid, senderDevice } = content.senderCertificate; const { sender, senderUuid, senderDevice } = content.senderCertificate;
if ( if (
me &&
((sender && me.number && sender === me.number) || ((sender && me.number && sender === me.number) ||
(senderUuid && me.uuid && senderUuid === me.uuid)) && (senderUuid && me.uuid && senderUuid === me.uuid)) &&
senderDevice === me.deviceId senderDevice === me.deviceId
@ -418,20 +533,21 @@ SecretSessionCipher.prototype = {
isMe: true, isMe: true,
}; };
} }
const addressE164 = const addressE164 = sender
sender && new libsignal.SignalProtocolAddress(sender, senderDevice); ? new window.libsignal.SignalProtocolAddress(sender, senderDevice)
const addressUuid = : undefined;
senderUuid && const addressUuid = senderUuid
new libsignal.SignalProtocolAddress( ? new window.libsignal.SignalProtocolAddress(
senderUuid.toLowerCase(), senderUuid.toLowerCase(),
senderDevice senderDevice
); )
: undefined;
try { try {
return { return {
sender: addressE164, sender: addressE164,
senderUuid: addressUuid, senderUuid: addressUuid,
content: await _decryptWithUnidentifiedSenderMessage(content), content: await this._decryptWithUnidentifiedSenderMessage(content),
}; };
} catch (error) { } catch (error) {
if (!error) { if (!error) {
@ -444,10 +560,12 @@ SecretSessionCipher.prototype = {
throw error; throw error;
} }
}, }
// public int getSessionVersion(SignalProtocolAddress remoteAddress) { // public int getSessionVersion(SignalProtocolAddress remoteAddress) {
getSessionVersion(remoteAddress) { getSessionVersion(
remoteAddress: SignalProtocolAddressClass
): Promise<number> {
const { SessionCipher } = this; const { SessionCipher } = this;
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
@ -458,10 +576,12 @@ SecretSessionCipher.prototype = {
); );
return cipher.getSessionVersion(); return cipher.getSessionVersion();
}, }
// public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) { // public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) {
getRemoteRegistrationId(remoteAddress) { getRemoteRegistrationId(
remoteAddress: SignalProtocolAddressClass
): Promise<number> {
const { SessionCipher } = this; const { SessionCipher } = this;
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
@ -472,10 +592,12 @@ SecretSessionCipher.prototype = {
); );
return cipher.getRemoteRegistrationId(); return cipher.getRemoteRegistrationId();
}, }
// Used by outgoing_message.js // Used by outgoing_message.js
closeOpenSessionForDevice(remoteAddress) { closeOpenSessionForDevice(
remoteAddress: SignalProtocolAddressClass
): Promise<void> {
const { SessionCipher } = this; const { SessionCipher } = this;
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
@ -486,19 +608,27 @@ SecretSessionCipher.prototype = {
); );
return cipher.closeOpenSessionForDevice(); return cipher.closeOpenSessionForDevice();
}, }
// private EphemeralKeys calculateEphemeralKeys( // private EphemeralKeys calculateEphemeralKeys(
// ECPublicKey ephemeralPublic, ECPrivateKey ephemeralPrivate, byte[] salt) // ECPublicKey ephemeralPublic, ECPrivateKey ephemeralPrivate, byte[] salt)
async _calculateEphemeralKeys(ephemeralPublic, ephemeralPrivate, salt) { private async _calculateEphemeralKeys(
const ephemeralSecret = await libsignal.Curve.async.calculateAgreement( ephemeralPublic: ArrayBuffer,
ephemeralPrivate: ArrayBuffer,
salt: ArrayBuffer
): Promise<{
chainKey: ArrayBuffer;
cipherKey: ArrayBuffer;
macKey: ArrayBuffer;
}> {
const ephemeralSecret = await window.libsignal.Curve.async.calculateAgreement(
ephemeralPublic, ephemeralPublic,
ephemeralPrivate ephemeralPrivate
); );
const ephemeralDerivedParts = await libsignal.HKDF.deriveSecrets( const ephemeralDerivedParts = await window.libsignal.HKDF.deriveSecrets(
ephemeralSecret, ephemeralSecret,
salt, salt,
new ArrayBuffer() new ArrayBuffer(0)
); );
// private EphemeralKeys(byte[] chainKey, byte[] cipherKey, byte[] macKey) // private EphemeralKeys(byte[] chainKey, byte[] cipherKey, byte[] macKey)
@ -507,19 +637,23 @@ SecretSessionCipher.prototype = {
cipherKey: ephemeralDerivedParts[1], cipherKey: ephemeralDerivedParts[1],
macKey: ephemeralDerivedParts[2], macKey: ephemeralDerivedParts[2],
}; };
}, }
// private StaticKeys calculateStaticKeys( // private StaticKeys calculateStaticKeys(
// ECPublicKey staticPublic, ECPrivateKey staticPrivate, byte[] salt) // ECPublicKey staticPublic, ECPrivateKey staticPrivate, byte[] salt)
async _calculateStaticKeys(staticPublic, staticPrivate, salt) { private async _calculateStaticKeys(
const staticSecret = await libsignal.Curve.async.calculateAgreement( staticPublic: ArrayBuffer,
staticPrivate: ArrayBuffer,
salt: ArrayBuffer
): Promise<{ cipherKey: ArrayBuffer; macKey: ArrayBuffer }> {
const staticSecret = await window.libsignal.Curve.async.calculateAgreement(
staticPublic, staticPublic,
staticPrivate staticPrivate
); );
const staticDerivedParts = await libsignal.HKDF.deriveSecrets( const staticDerivedParts = await window.libsignal.HKDF.deriveSecrets(
staticSecret, staticSecret,
salt, salt,
new ArrayBuffer() new ArrayBuffer(0)
); );
// private StaticKeys(byte[] cipherKey, byte[] macKey) // private StaticKeys(byte[] cipherKey, byte[] macKey)
@ -527,39 +661,59 @@ SecretSessionCipher.prototype = {
cipherKey: staticDerivedParts[1], cipherKey: staticDerivedParts[1],
macKey: staticDerivedParts[2], macKey: staticDerivedParts[2],
}; };
}, }
// private byte[] decrypt(UnidentifiedSenderMessageContent message) // private byte[] decrypt(UnidentifiedSenderMessageContent message)
_decryptWithUnidentifiedSenderMessage(message) { private _decryptWithUnidentifiedSenderMessage(
message: ExplodedInnerMessageType
): Promise<ArrayBuffer> {
const { SessionCipher } = this; const { SessionCipher } = this;
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
const sender = new libsignal.SignalProtocolAddress( if (!message.senderCertificate) {
message.senderCertificate.senderUuid || message.senderCertificate.sender, throw new Error(
message.senderCertificate.senderDevice '_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) { switch (message.type) {
case CiphertextMessage.WHISPER_TYPE: case CiphertextMessage.WHISPER_TYPE:
return new SessionCipher( return new SessionCipher(
signalProtocolStore, signalProtocolStore,
sender, address,
this.options this.options
).decryptWhisperMessage(message.content); ).decryptWhisperMessage(message.content);
case CiphertextMessage.PREKEY_TYPE: case CiphertextMessage.PREKEY_TYPE:
return new SessionCipher( return new SessionCipher(
signalProtocolStore, signalProtocolStore,
sender, address,
this.options this.options
).decryptPreKeyWhisperMessage(message.content); ).decryptPreKeyWhisperMessage(message.content);
default: default:
throw new Error(`Unknown type: ${message.type}`); throw new Error(`Unknown type: ${message.type}`);
} }
}, }
// private byte[] encrypt( // private byte[] encrypt(
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] plaintext) // SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] plaintext)
async _encryptWithSecretKeys(cipherKey, macKey, plaintext) { private async _encryptWithSecretKeys(
cipherKey: ArrayBuffer,
macKey: ArrayBuffer,
plaintext: ArrayBuffer
): Promise<ArrayBuffer> {
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding'); // Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
// cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16])); // cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
@ -574,11 +728,15 @@ SecretSessionCipher.prototype = {
const ourMac = trimBytes(ourFullMac, 10); const ourMac = trimBytes(ourFullMac, 10);
return concatenateBytes(ciphertext, ourMac); return concatenateBytes(ciphertext, ourMac);
}, }
// private byte[] decrypt( // private byte[] decrypt(
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] ciphertext) // SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] ciphertext)
async _decryptWithSecretKeys(cipherKey, macKey, ciphertext) { private async _decryptWithSecretKeys(
cipherKey: ArrayBuffer,
macKey: ArrayBuffer,
ciphertext: ArrayBuffer
): Promise<ArrayBuffer> {
if (ciphertext.byteLength < 10) { if (ciphertext.byteLength < 10) {
throw new Error('Ciphertext not long enough for MAC!'); throw new Error('Ciphertext not long enough for MAC!');
} }
@ -598,7 +756,7 @@ SecretSessionCipher.prototype = {
const theirMac = ciphertextParts[1]; const theirMac = ciphertextParts[1];
if (!constantTimeEqual(ourMac, theirMac)) { if (!constantTimeEqual(ourMac, theirMac)) {
throw new Error('Bad mac!'); throw new Error('SecretSessionCipher/_decryptWithSecretKeys: Bad MAC!');
} }
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding'); // Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
@ -606,12 +764,5 @@ SecretSessionCipher.prototype = {
// return cipher.doFinal(ciphertextParts[0]); // return cipher.doFinal(ciphertextParts[0]);
return decryptAesCtr(cipherKey, ciphertextParts[0], getZeroes(16)); return decryptAesCtr(cipherKey, ciphertextParts[0], getZeroes(16));
}, }
}; }
module.exports = {
SecretSessionCipher,
createCertificateValidator,
_createServerCertificateFromBuffer,
_createSenderCertificateFromBuffer,
};

View file

@ -1,46 +1,48 @@
// Copyright 2018-2020 Signal Messenger, LLC // Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-License-Identifier: AGPL-3.0-only
/* global libsignal, textsecure */ /* eslint-disable @typescript-eslint/no-explicit-any */
'use strict'; import { assert } from 'chai';
const { import {
ExplodedSenderCertificateType,
SecretSessionCipher, SecretSessionCipher,
createCertificateValidator, createCertificateValidator,
_createSenderCertificateFromBuffer, _createSenderCertificateFromBuffer,
_createServerCertificateFromBuffer, _createServerCertificateFromBuffer,
} = window.Signal.Metadata; } from '../../metadata/SecretSessionCipher';
const { import {
bytesFromString, bytesFromString,
stringFromBytes, stringFromBytes,
arrayBufferToBase64, arrayBufferToBase64,
} = window.Signal.Crypto; } from '../../Crypto';
import { KeyPairType } from '../../libsignal.d';
function InMemorySignalProtocolStore() { function toString(thing: string | ArrayBuffer): string {
this.store = {};
}
function toString(thing) {
if (typeof thing === 'string') { if (typeof thing === 'string') {
return thing; return thing;
} }
return arrayBufferToBase64(thing); return arrayBufferToBase64(thing);
} }
InMemorySignalProtocolStore.prototype = { class InMemorySignalProtocolStore {
Direction: { store: Record<string, any> = {};
Direction = {
SENDING: 1, SENDING: 1,
RECEIVING: 2, RECEIVING: 2,
}, };
getIdentityKeyPair() { getIdentityKeyPair(): Promise<{ privKey: ArrayBuffer; pubKey: ArrayBuffer }> {
return Promise.resolve(this.get('identityKey')); return Promise.resolve(this.get('identityKey'));
}, }
getLocalRegistrationId() {
getLocalRegistrationId(): Promise<string> {
return Promise.resolve(this.get('registrationId')); return Promise.resolve(this.get('registrationId'));
}, }
put(key, value) {
put(key: string, value: any): void {
if ( if (
key === undefined || key === undefined ||
value === undefined || value === undefined ||
@ -50,8 +52,9 @@ InMemorySignalProtocolStore.prototype = {
throw new Error('Tried to store undefined/null'); throw new Error('Tried to store undefined/null');
} }
this.store[key] = value; this.store[key] = value;
}, }
get(key, defaultValue) {
get(key: string, defaultValue?: any): any {
if (key === null || key === undefined) { if (key === null || key === undefined) {
throw new Error('Tried to get value for undefined/null key'); throw new Error('Tried to get value for undefined/null key');
} }
@ -60,15 +63,19 @@ InMemorySignalProtocolStore.prototype = {
} }
return defaultValue; return defaultValue;
}, }
remove(key) {
remove(key: string): void {
if (key === null || key === undefined) { if (key === null || key === undefined) {
throw new Error('Tried to remove value for undefined/null key'); throw new Error('Tried to remove value for undefined/null key');
} }
delete this.store[key]; delete this.store[key];
}, }
isTrustedIdentity(identifier, identityKey) { isTrustedIdentity(
identifier: string,
identityKey: ArrayBuffer
): Promise<boolean> {
if (identifier === null || identifier === undefined) { if (identifier === null || identifier === undefined) {
throw new Error('tried to check identity key for undefined/null key'); throw new Error('tried to check identity key for undefined/null key');
} }
@ -80,18 +87,22 @@ InMemorySignalProtocolStore.prototype = {
return Promise.resolve(true); return Promise.resolve(true);
} }
return Promise.resolve(toString(identityKey) === toString(trusted)); return Promise.resolve(toString(identityKey) === toString(trusted));
}, }
loadIdentityKey(identifier) {
loadIdentityKey(identifier: string): any {
if (identifier === null || identifier === undefined) { if (identifier === null || identifier === undefined) {
throw new Error('Tried to get identity key for undefined/null key'); throw new Error('Tried to get identity key for undefined/null key');
} }
return Promise.resolve(this.get(`identityKey${identifier}`)); return Promise.resolve(this.get(`identityKey${identifier}`));
}, }
saveIdentity(identifier, identityKey) {
saveIdentity(identifier: string, identityKey: ArrayBuffer): any {
if (identifier === null || identifier === undefined) { if (identifier === null || identifier === undefined) {
throw new Error('Tried to put identity key for undefined/null key'); throw new Error('Tried to put identity key for undefined/null key');
} }
const address = libsignal.SignalProtocolAddress.fromString(identifier); const address = window.libsignal.SignalProtocolAddress.fromString(
identifier
);
const existing = this.get(`identityKey${address.getName()}`); const existing = this.get(`identityKey${address.getName()}`);
this.put(`identityKey${address.getName()}`, identityKey); this.put(`identityKey${address.getName()}`, identityKey);
@ -101,48 +112,55 @@ InMemorySignalProtocolStore.prototype = {
} }
return Promise.resolve(false); return Promise.resolve(false);
}, }
/* Returns a prekeypair object or undefined */ /* Returns a prekeypair object or undefined */
loadPreKey(keyId) { loadPreKey(keyId: number): any {
let res = this.get(`25519KeypreKey${keyId}`); let res = this.get(`25519KeypreKey${keyId}`);
if (res !== undefined) { if (res !== undefined) {
res = { pubKey: res.pubKey, privKey: res.privKey }; res = { pubKey: res.pubKey, privKey: res.privKey };
} }
return Promise.resolve(res); return Promise.resolve(res);
}, }
storePreKey(keyId, keyPair) {
storePreKey(keyId: number, keyPair: any): Promise<void> {
return Promise.resolve(this.put(`25519KeypreKey${keyId}`, keyPair)); return Promise.resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
}, }
removePreKey(keyId) {
removePreKey(keyId: number): Promise<void> {
return Promise.resolve(this.remove(`25519KeypreKey${keyId}`)); return Promise.resolve(this.remove(`25519KeypreKey${keyId}`));
}, }
/* Returns a signed keypair object or undefined */ /* Returns a signed keypair object or undefined */
loadSignedPreKey(keyId) { loadSignedPreKey(keyId: number): any {
let res = this.get(`25519KeysignedKey${keyId}`); let res = this.get(`25519KeysignedKey${keyId}`);
if (res !== undefined) { if (res !== undefined) {
res = { pubKey: res.pubKey, privKey: res.privKey }; res = { pubKey: res.pubKey, privKey: res.privKey };
} }
return Promise.resolve(res); return Promise.resolve(res);
}, }
storeSignedPreKey(keyId, keyPair) {
return Promise.resolve(this.put(`25519KeysignedKey${keyId}`, keyPair));
},
removeSignedPreKey(keyId) {
return Promise.resolve(this.remove(`25519KeysignedKey${keyId}`));
},
loadSession(identifier) { storeSignedPreKey(keyId: number, keyPair: any): Promise<void> {
return Promise.resolve(this.put(`25519KeysignedKey${keyId}`, keyPair));
}
removeSignedPreKey(keyId: number): Promise<void> {
return Promise.resolve(this.remove(`25519KeysignedKey${keyId}`));
}
loadSession(identifier: string): Promise<any> {
return Promise.resolve(this.get(`session${identifier}`)); return Promise.resolve(this.get(`session${identifier}`));
}, }
storeSession(identifier, record) {
storeSession(identifier: string, record: any): Promise<void> {
return Promise.resolve(this.put(`session${identifier}`, record)); return Promise.resolve(this.put(`session${identifier}`, record));
}, }
removeSession(identifier) {
removeSession(identifier: string): Promise<void> {
return Promise.resolve(this.remove(`session${identifier}`)); return Promise.resolve(this.remove(`session${identifier}`));
}, }
removeAllSessions(identifier) {
removeAllSessions(identifier: string): Promise<void> {
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const id in this.store) { for (const id in this.store) {
if (id.startsWith(`session${identifier}`)) { if (id.startsWith(`session${identifier}`)) {
@ -150,8 +168,8 @@ InMemorySignalProtocolStore.prototype = {
} }
} }
return Promise.resolve(); return Promise.resolve();
}, }
}; }
describe('SecretSessionCipher', () => { describe('SecretSessionCipher', () => {
it('successfully roundtrips', async function thisNeeded() { it('successfully roundtrips', async function thisNeeded() {
@ -164,7 +182,7 @@ describe('SecretSessionCipher', () => {
const aliceIdentityKey = await aliceStore.getIdentityKeyPair(); const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
const trustRoot = await libsignal.Curve.async.generateKeyPair(); const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor( const senderCertificate = await _createSenderCertificateFor(
trustRoot, trustRoot,
'+14151111111', '+14151111111',
@ -172,15 +190,15 @@ describe('SecretSessionCipher', () => {
aliceIdentityKey.pubKey, aliceIdentityKey.pubKey,
31337 31337
); );
const aliceCipher = new SecretSessionCipher(aliceStore); const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt( const ciphertext = await aliceCipher.encrypt(
new libsignal.SignalProtocolAddress('+14152222222', 1), new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized }, { serialized: senderCertificate.serialized },
bytesFromString('smert za smert') bytesFromString('smert za smert')
); );
const bobCipher = new SecretSessionCipher(bobStore); const bobCipher = new SecretSessionCipher(bobStore as any);
const decryptResult = await bobCipher.decrypt( const decryptResult = await bobCipher.decrypt(
createCertificateValidator(trustRoot.pubKey), createCertificateValidator(trustRoot.pubKey),
@ -188,6 +206,13 @@ describe('SecretSessionCipher', () => {
31335 31335
); );
if (!decryptResult.content) {
throw new Error('decryptResult.content is null!');
}
if (!decryptResult.sender) {
throw new Error('decryptResult.sender is null!');
}
assert.strictEqual( assert.strictEqual(
stringFromBytes(decryptResult.content), stringFromBytes(decryptResult.content),
'smert za smert' 'smert za smert'
@ -205,8 +230,8 @@ describe('SecretSessionCipher', () => {
const aliceIdentityKey = await aliceStore.getIdentityKeyPair(); const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
const trustRoot = await libsignal.Curve.async.generateKeyPair(); const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const falseTrustRoot = await libsignal.Curve.async.generateKeyPair(); const falseTrustRoot = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor( const senderCertificate = await _createSenderCertificateFor(
falseTrustRoot, falseTrustRoot,
'+14151111111', '+14151111111',
@ -214,15 +239,15 @@ describe('SecretSessionCipher', () => {
aliceIdentityKey.pubKey, aliceIdentityKey.pubKey,
31337 31337
); );
const aliceCipher = new SecretSessionCipher(aliceStore); const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt( const ciphertext = await aliceCipher.encrypt(
new libsignal.SignalProtocolAddress('+14152222222', 1), new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized }, { serialized: senderCertificate.serialized },
bytesFromString('и вот я') bytesFromString('и вот я')
); );
const bobCipher = new SecretSessionCipher(bobStore); const bobCipher = new SecretSessionCipher(bobStore as any);
try { try {
await bobCipher.decrypt( await bobCipher.decrypt(
@ -246,7 +271,7 @@ describe('SecretSessionCipher', () => {
const aliceIdentityKey = await aliceStore.getIdentityKeyPair(); const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
const trustRoot = await libsignal.Curve.async.generateKeyPair(); const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor( const senderCertificate = await _createSenderCertificateFor(
trustRoot, trustRoot,
'+14151111111', '+14151111111',
@ -254,15 +279,15 @@ describe('SecretSessionCipher', () => {
aliceIdentityKey.pubKey, aliceIdentityKey.pubKey,
31337 31337
); );
const aliceCipher = new SecretSessionCipher(aliceStore); const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt( const ciphertext = await aliceCipher.encrypt(
new libsignal.SignalProtocolAddress('+14152222222', 1), new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized }, { serialized: senderCertificate.serialized },
bytesFromString('и вот я') bytesFromString('и вот я')
); );
const bobCipher = new SecretSessionCipher(bobStore); const bobCipher = new SecretSessionCipher(bobStore as any);
try { try {
await bobCipher.decrypt( await bobCipher.decrypt(
@ -284,8 +309,8 @@ describe('SecretSessionCipher', () => {
await _initializeSessions(aliceStore, bobStore); await _initializeSessions(aliceStore, bobStore);
const trustRoot = await libsignal.Curve.async.generateKeyPair(); const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const randomKeyPair = await libsignal.Curve.async.generateKeyPair(); const randomKeyPair = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor( const senderCertificate = await _createSenderCertificateFor(
trustRoot, trustRoot,
'+14151111111', '+14151111111',
@ -293,15 +318,15 @@ describe('SecretSessionCipher', () => {
randomKeyPair.pubKey, randomKeyPair.pubKey,
31337 31337
); );
const aliceCipher = new SecretSessionCipher(aliceStore); const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt( const ciphertext = await aliceCipher.encrypt(
new libsignal.SignalProtocolAddress('+14152222222', 1), new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized }, { serialized: senderCertificate.serialized },
bytesFromString('smert za smert') bytesFromString('smert za smert')
); );
const bobCipher = new SecretSessionCipher(bobStore); const bobCipher = new SecretSessionCipher(bobStore as any);
try { try {
await bobCipher.decrypt( await bobCipher.decrypt(
@ -326,93 +351,99 @@ describe('SecretSessionCipher', () => {
// long expires // long expires
// ) // )
async function _createSenderCertificateFor( async function _createSenderCertificateFor(
trustRoot, trustRoot: KeyPairType,
sender, sender: string,
deviceId, deviceId: number,
identityKey, identityKey: ArrayBuffer,
expires expires: number
) { ): Promise<ExplodedSenderCertificateType> {
const serverKey = await libsignal.Curve.async.generateKeyPair(); const serverKey = await window.libsignal.Curve.async.generateKeyPair();
const serverCertificateCertificateProto = new textsecure.protobuf.ServerCertificate.Certificate(); const serverCertificateCertificateProto = new window.textsecure.protobuf.ServerCertificate.Certificate();
serverCertificateCertificateProto.id = 1; serverCertificateCertificateProto.id = 1;
serverCertificateCertificateProto.key = serverKey.pubKey; serverCertificateCertificateProto.key = serverKey.pubKey;
const serverCertificateCertificateBytes = serverCertificateCertificateProto const serverCertificateCertificateBytes = serverCertificateCertificateProto.toArrayBuffer();
.encode()
.toArrayBuffer();
const serverCertificateSignature = await libsignal.Curve.async.calculateSignature( const serverCertificateSignature = await window.libsignal.Curve.async.calculateSignature(
trustRoot.privKey, trustRoot.privKey,
serverCertificateCertificateBytes serverCertificateCertificateBytes
); );
const serverCertificateProto = new textsecure.protobuf.ServerCertificate(); const serverCertificateProto = new window.textsecure.protobuf.ServerCertificate();
serverCertificateProto.certificate = serverCertificateCertificateBytes; serverCertificateProto.certificate = serverCertificateCertificateBytes;
serverCertificateProto.signature = serverCertificateSignature; serverCertificateProto.signature = serverCertificateSignature;
const serverCertificate = _createServerCertificateFromBuffer( const serverCertificate = _createServerCertificateFromBuffer(
serverCertificateProto.encode().toArrayBuffer() serverCertificateProto.toArrayBuffer()
); );
const senderCertificateCertificateProto = new textsecure.protobuf.SenderCertificate.Certificate(); const senderCertificateCertificateProto = new window.textsecure.protobuf.SenderCertificate.Certificate();
senderCertificateCertificateProto.sender = sender; senderCertificateCertificateProto.sender = sender;
senderCertificateCertificateProto.senderDevice = deviceId; senderCertificateCertificateProto.senderDevice = deviceId;
senderCertificateCertificateProto.identityKey = identityKey; senderCertificateCertificateProto.identityKey = identityKey;
senderCertificateCertificateProto.expires = expires; senderCertificateCertificateProto.expires = expires;
senderCertificateCertificateProto.signer = textsecure.protobuf.ServerCertificate.decode( senderCertificateCertificateProto.signer = window.textsecure.protobuf.ServerCertificate.decode(
serverCertificate.serialized serverCertificate.serialized
); );
const senderCertificateBytes = senderCertificateCertificateProto const senderCertificateBytes = senderCertificateCertificateProto.toArrayBuffer();
.encode()
.toArrayBuffer();
const senderCertificateSignature = await libsignal.Curve.async.calculateSignature( const senderCertificateSignature = await window.libsignal.Curve.async.calculateSignature(
serverKey.privKey, serverKey.privKey,
senderCertificateBytes senderCertificateBytes
); );
const senderCertificateProto = new textsecure.protobuf.SenderCertificate(); const senderCertificateProto = new window.textsecure.protobuf.SenderCertificate();
senderCertificateProto.certificate = senderCertificateBytes; senderCertificateProto.certificate = senderCertificateBytes;
senderCertificateProto.signature = senderCertificateSignature; senderCertificateProto.signature = senderCertificateSignature;
return _createSenderCertificateFromBuffer( return _createSenderCertificateFromBuffer(
senderCertificateProto.encode().toArrayBuffer() senderCertificateProto.toArrayBuffer()
); );
} }
// private void _initializeSessions( // private void _initializeSessions(
// SignalProtocolStore aliceStore, SignalProtocolStore bobStore) // SignalProtocolStore aliceStore, SignalProtocolStore bobStore)
async function _initializeSessions(aliceStore, bobStore) { async function _initializeSessions(
const aliceAddress = new libsignal.SignalProtocolAddress('+14152222222', 1); aliceStore: InMemorySignalProtocolStore,
bobStore: InMemorySignalProtocolStore
): Promise<void> {
const aliceAddress = new window.libsignal.SignalProtocolAddress(
'+14152222222',
1
);
await aliceStore.put( await aliceStore.put(
'identityKey', 'identityKey',
await libsignal.Curve.generateKeyPair() await window.libsignal.Curve.generateKeyPair()
);
await bobStore.put(
'identityKey',
await window.libsignal.Curve.generateKeyPair()
); );
await bobStore.put('identityKey', await libsignal.Curve.generateKeyPair());
await aliceStore.put('registrationId', 57); await aliceStore.put('registrationId', 57);
await bobStore.put('registrationId', 58); await bobStore.put('registrationId', 58);
const bobPreKey = await libsignal.Curve.async.generateKeyPair(); const bobPreKey = await window.libsignal.Curve.async.generateKeyPair();
const bobIdentityKey = await bobStore.getIdentityKeyPair(); const bobIdentityKey = await bobStore.getIdentityKeyPair();
const bobSignedPreKey = await libsignal.KeyHelper.generateSignedPreKey( const bobSignedPreKey = await window.libsignal.KeyHelper.generateSignedPreKey(
bobIdentityKey, bobIdentityKey,
2 2
); );
const bobBundle = { const bobBundle = {
deviceId: 3,
identityKey: bobIdentityKey.pubKey, identityKey: bobIdentityKey.pubKey,
registrationId: 1, registrationId: 1,
preKey: {
keyId: 1,
publicKey: bobPreKey.pubKey,
},
signedPreKey: { signedPreKey: {
keyId: 2, keyId: 2,
publicKey: bobSignedPreKey.keyPair.pubKey, publicKey: bobSignedPreKey.keyPair.pubKey,
signature: bobSignedPreKey.signature, signature: bobSignedPreKey.signature,
}, },
preKey: {
keyId: 1,
publicKey: bobPreKey.pubKey,
},
}; };
const aliceSessionBuilder = new libsignal.SessionBuilder( const aliceSessionBuilder = new window.libsignal.SessionBuilder(
aliceStore, aliceStore as any,
aliceAddress aliceAddress
); );
await aliceSessionBuilder.processPreKey(bobBundle); await aliceSessionBuilder.processPreKey(bobBundle);

96
ts/textsecure.d.ts vendored
View file

@ -216,6 +216,12 @@ type SubProtocolProtobufTypes = {
WebSocketResponseMessage: typeof WebSocketResponseMessageClass; WebSocketResponseMessage: typeof WebSocketResponseMessageClass;
}; };
type UnidentifiedDeliveryTypes = {
ServerCertificate: typeof ServerCertificateClass;
SenderCertificate: typeof SenderCertificateClass;
UnidentifiedSenderMessage: typeof UnidentifiedSenderMessageClass;
};
type ProtobufCollectionType = { type ProtobufCollectionType = {
onLoad: (callback: () => unknown) => void; onLoad: (callback: () => unknown) => void;
} & DeviceMessagesProtobufTypes & } & DeviceMessagesProtobufTypes &
@ -223,7 +229,8 @@ type ProtobufCollectionType = {
GroupsProtobufTypes & GroupsProtobufTypes &
SignalServiceProtobufTypes & SignalServiceProtobufTypes &
SignalStorageProtobufTypes & SignalStorageProtobufTypes &
SubProtocolProtobufTypes; SubProtocolProtobufTypes &
UnidentifiedDeliveryTypes;
// Note: there are a lot of places in the code that overwrite a field like this // Note: there are a lot of places in the code that overwrite a field like this
// with a type that the app can use. Being more rigorous with these // with a type that the app can use. Being more rigorous with these
@ -1354,3 +1361,90 @@ export declare class WebSocketResponseMessageClass {
} }
export { CallingMessageClass }; export { CallingMessageClass };
// UnidentifiedDelivery.proto
export declare class ServerCertificateClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => ServerCertificateClass;
toArrayBuffer: () => ArrayBuffer;
certificate?: ProtoBinaryType;
signature?: ProtoBinaryType;
}
export declare namespace ServerCertificateClass {
class Certificate {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => Certificate;
toArrayBuffer: () => ArrayBuffer;
id?: number;
key?: ProtoBinaryType;
}
}
export declare class SenderCertificateClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => SenderCertificateClass;
toArrayBuffer: () => ArrayBuffer;
certificate?: ProtoBinaryType;
signature?: ProtoBinaryType;
}
export declare namespace SenderCertificateClass {
class Certificate {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => Certificate;
toArrayBuffer: () => ArrayBuffer;
sender?: string;
senderUuid?: string;
senderDevice?: number;
expires?: ProtoBigNumberType;
identityKey?: ProtoBinaryType;
signer?: SenderCertificateClass;
}
}
export declare class UnidentifiedSenderMessageClass {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => UnidentifiedSenderMessageClass;
toArrayBuffer: () => ArrayBuffer;
ephemeralPublic?: ProtoBinaryType;
encryptedStatic?: ProtoBinaryType;
encryptedMessage?: ProtoBinaryType;
}
export declare namespace UnidentifiedSenderMessageClass {
class Message {
static decode: (
data: ArrayBuffer | ByteBufferClass,
encoding?: string
) => Message;
toArrayBuffer: () => ArrayBuffer;
type?: number;
senderCertificate?: SenderCertificateClass;
content?: ProtoBinaryType;
}
}
export declare namespace UnidentifiedSenderMessageClass.Message {
class Type {
static PREKEY_MESSAGE: number;
static MESSAGE: number;
}
}

View file

@ -27,6 +27,10 @@ import Crypto from './Crypto';
import { deriveMasterKeyFromGroupV1 } from '../Crypto'; import { deriveMasterKeyFromGroupV1 } from '../Crypto';
import { ContactBuffer, GroupBuffer } from './ContactsParser'; import { ContactBuffer, GroupBuffer } from './ContactsParser';
import { IncomingIdentityKeyError } from './Errors'; import { IncomingIdentityKeyError } from './Errors';
import {
createCertificateValidator,
SecretSessionCipher,
} from '../metadata/SecretSessionCipher';
import { import {
AttachmentPointerClass, AttachmentPointerClass,
@ -946,7 +950,7 @@ class MessageReceiverInner extends EventTarget {
address, address,
options options
); );
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher( const secretSessionCipher = new SecretSessionCipher(
window.textsecure.storage.protocol, window.textsecure.storage.protocol,
options options
); );
@ -979,7 +983,7 @@ class MessageReceiverInner extends EventTarget {
window.log.info('received unidentified sender message'); window.log.info('received unidentified sender message');
promise = secretSessionCipher promise = secretSessionCipher
.decrypt( .decrypt(
window.Signal.Metadata.createCertificateValidator(serverTrustRoot), createCertificateValidator(serverTrustRoot),
ciphertext.toArrayBuffer(), ciphertext.toArrayBuffer(),
Math.min(envelope.serverTimestamp || Date.now(), Date.now()), Math.min(envelope.serverTimestamp || Date.now(), Date.now()),
me me
@ -1028,6 +1032,12 @@ class MessageReceiverInner extends EventTarget {
originalSource || originalSourceUuid originalSource || originalSourceUuid
); );
if (!content) {
throw new Error(
'MessageReceiver.decrypt: Content returned was falsey!'
);
}
// Return just the content because that matches the signature of the other // Return just the content because that matches the signature of the other
// decrypt methods used above. // decrypt methods used above.
return this.unpad(content); return this.unpad(content);

View file

@ -26,6 +26,10 @@ import {
UnregisteredUserError, UnregisteredUserError,
} from './Errors'; } from './Errors';
import { isValidNumber } from '../types/PhoneNumber'; import { isValidNumber } from '../types/PhoneNumber';
import {
SecretSessionCipher,
SerializedCertificateType,
} from '../metadata/SecretSessionCipher';
type OutgoingMessageOptionsType = SendOptionsType & { type OutgoingMessageOptionsType = SendOptionsType & {
online?: boolean; online?: boolean;
@ -58,7 +62,7 @@ export default class OutgoingMessage {
sendMetadata?: SendMetadataType; sendMetadata?: SendMetadataType;
senderCertificate?: ArrayBuffer; senderCertificate?: SerializedCertificateType;
online?: boolean; online?: boolean;
@ -384,8 +388,8 @@ export default class OutgoingMessage {
options.messageKeysLimit = false; options.messageKeysLimit = false;
} }
if (sealedSender) { if (sealedSender && senderCertificate) {
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher( const secretSessionCipher = new SecretSessionCipher(
window.textsecure.storage.protocol window.textsecure.storage.protocol
); );
ciphers[address.getDeviceId()] = secretSessionCipher; ciphers[address.getDeviceId()] = secretSessionCipher;

View file

@ -46,6 +46,7 @@ import {
LinkPreviewImage, LinkPreviewImage,
LinkPreviewMetadata, LinkPreviewMetadata,
} from '../linkPreviews/linkPreviewFetch'; } from '../linkPreviews/linkPreviewFetch';
import { SerializedCertificateType } from '../metadata/SecretSessionCipher';
function stringToArrayBuffer(str: string): ArrayBuffer { function stringToArrayBuffer(str: string): ArrayBuffer {
if (typeof str !== 'string') { if (typeof str !== 'string') {
@ -66,7 +67,7 @@ export type SendMetadataType = {
}; };
export type SendOptionsType = { export type SendOptionsType = {
senderCertificate?: ArrayBuffer; senderCertificate?: SerializedCertificateType;
sendMetadata?: SendMetadataType; sendMetadata?: SendMetadataType;
online?: boolean; online?: boolean;
}; };