Move SecretSessionCipher to TypeScript
This commit is contained in:
parent
7e629edd21
commit
c9ffb7c014
11 changed files with 569 additions and 283 deletions
1
ts/libsignal.d.ts
vendored
1
ts/libsignal.d.ts
vendored
|
@ -223,6 +223,7 @@ export declare class SessionCipherClass {
|
|||
body: string;
|
||||
}>;
|
||||
getRecord: () => Promise<RecordType>;
|
||||
getSessionVersion: () => Promise<number>;
|
||||
getRemoteRegistrationId: () => Promise<number>;
|
||||
hasOpenSession: () => Promise<boolean>;
|
||||
}
|
||||
|
|
14
ts/metadata/CiphertextMessage.ts
Normal file
14
ts/metadata/CiphertextMessage.ts
Normal 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;
|
768
ts/metadata/SecretSessionCipher.ts
Normal file
768
ts/metadata/SecretSessionCipher.ts
Normal file
|
@ -0,0 +1,768 @@
|
|||
// 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));
|
||||
}
|
||||
}
|
454
ts/test-electron/metadata/SecretSessionCipher_test.ts
Normal file
454
ts/test-electron/metadata/SecretSessionCipher_test.ts
Normal file
|
@ -0,0 +1,454 @@
|
|||
// Copyright 2018-2021 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import {
|
||||
ExplodedSenderCertificateType,
|
||||
SecretSessionCipher,
|
||||
createCertificateValidator,
|
||||
_createSenderCertificateFromBuffer,
|
||||
_createServerCertificateFromBuffer,
|
||||
} from '../../metadata/SecretSessionCipher';
|
||||
import {
|
||||
bytesFromString,
|
||||
stringFromBytes,
|
||||
arrayBufferToBase64,
|
||||
} from '../../Crypto';
|
||||
import { KeyPairType } from '../../libsignal.d';
|
||||
|
||||
function toString(thing: string | ArrayBuffer): string {
|
||||
if (typeof thing === 'string') {
|
||||
return thing;
|
||||
}
|
||||
return arrayBufferToBase64(thing);
|
||||
}
|
||||
|
||||
class InMemorySignalProtocolStore {
|
||||
store: Record<string, any> = {};
|
||||
|
||||
Direction = {
|
||||
SENDING: 1,
|
||||
RECEIVING: 2,
|
||||
};
|
||||
|
||||
getIdentityKeyPair(): Promise<{ privKey: ArrayBuffer; pubKey: ArrayBuffer }> {
|
||||
return Promise.resolve(this.get('identityKey'));
|
||||
}
|
||||
|
||||
getLocalRegistrationId(): Promise<string> {
|
||||
return Promise.resolve(this.get('registrationId'));
|
||||
}
|
||||
|
||||
put(key: string, value: any): void {
|
||||
if (
|
||||
key === undefined ||
|
||||
value === undefined ||
|
||||
key === null ||
|
||||
value === null
|
||||
) {
|
||||
throw new Error('Tried to store undefined/null');
|
||||
}
|
||||
this.store[key] = value;
|
||||
}
|
||||
|
||||
get(key: string, defaultValue?: any): any {
|
||||
if (key === null || key === undefined) {
|
||||
throw new Error('Tried to get value for undefined/null key');
|
||||
}
|
||||
if (key in this.store) {
|
||||
return this.store[key];
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
remove(key: string): void {
|
||||
if (key === null || key === undefined) {
|
||||
throw new Error('Tried to remove value for undefined/null key');
|
||||
}
|
||||
delete this.store[key];
|
||||
}
|
||||
|
||||
isTrustedIdentity(
|
||||
identifier: string,
|
||||
identityKey: ArrayBuffer
|
||||
): Promise<boolean> {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('tried to check identity key for undefined/null key');
|
||||
}
|
||||
if (!(identityKey instanceof ArrayBuffer)) {
|
||||
throw new Error('Expected identityKey to be an ArrayBuffer');
|
||||
}
|
||||
const trusted = this.get(`identityKey${identifier}`);
|
||||
if (trusted === undefined) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return Promise.resolve(toString(identityKey) === toString(trusted));
|
||||
}
|
||||
|
||||
loadIdentityKey(identifier: string): any {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to get identity key for undefined/null key');
|
||||
}
|
||||
return Promise.resolve(this.get(`identityKey${identifier}`));
|
||||
}
|
||||
|
||||
saveIdentity(identifier: string, identityKey: ArrayBuffer): any {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to put identity key for undefined/null key');
|
||||
}
|
||||
const address = window.libsignal.SignalProtocolAddress.fromString(
|
||||
identifier
|
||||
);
|
||||
|
||||
const existing = this.get(`identityKey${address.getName()}`);
|
||||
this.put(`identityKey${address.getName()}`, identityKey);
|
||||
|
||||
if (existing && toString(identityKey) !== toString(existing)) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
/* Returns a prekeypair object or undefined */
|
||||
loadPreKey(keyId: number): any {
|
||||
let res = this.get(`25519KeypreKey${keyId}`);
|
||||
if (res !== undefined) {
|
||||
res = { pubKey: res.pubKey, privKey: res.privKey };
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
}
|
||||
|
||||
storePreKey(keyId: number, keyPair: any): Promise<void> {
|
||||
return Promise.resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
|
||||
}
|
||||
|
||||
removePreKey(keyId: number): Promise<void> {
|
||||
return Promise.resolve(this.remove(`25519KeypreKey${keyId}`));
|
||||
}
|
||||
|
||||
/* Returns a signed keypair object or undefined */
|
||||
loadSignedPreKey(keyId: number): any {
|
||||
let res = this.get(`25519KeysignedKey${keyId}`);
|
||||
if (res !== undefined) {
|
||||
res = { pubKey: res.pubKey, privKey: res.privKey };
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
}
|
||||
|
||||
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}`));
|
||||
}
|
||||
|
||||
storeSession(identifier: string, record: any): Promise<void> {
|
||||
return Promise.resolve(this.put(`session${identifier}`, record));
|
||||
}
|
||||
|
||||
removeSession(identifier: string): Promise<void> {
|
||||
return Promise.resolve(this.remove(`session${identifier}`));
|
||||
}
|
||||
|
||||
removeAllSessions(identifier: string): Promise<void> {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const id in this.store) {
|
||||
if (id.startsWith(`session${identifier}`)) {
|
||||
delete this.store[id];
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
describe('SecretSessionCipher', () => {
|
||||
it('successfully roundtrips', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
await _initializeSessions(aliceStore, bobStore);
|
||||
|
||||
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
|
||||
|
||||
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
trustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
aliceIdentityKey.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore as any);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
{ serialized: senderCertificate.serialized },
|
||||
bytesFromString('smert za smert')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore as any);
|
||||
|
||||
const decryptResult = await bobCipher.decrypt(
|
||||
createCertificateValidator(trustRoot.pubKey),
|
||||
ciphertext,
|
||||
31335
|
||||
);
|
||||
|
||||
if (!decryptResult.content) {
|
||||
throw new Error('decryptResult.content is null!');
|
||||
}
|
||||
if (!decryptResult.sender) {
|
||||
throw new Error('decryptResult.sender is null!');
|
||||
}
|
||||
|
||||
assert.strictEqual(
|
||||
stringFromBytes(decryptResult.content),
|
||||
'smert za smert'
|
||||
);
|
||||
assert.strictEqual(decryptResult.sender.toString(), '+14151111111.1');
|
||||
});
|
||||
|
||||
it('fails when untrusted', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
await _initializeSessions(aliceStore, bobStore);
|
||||
|
||||
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
|
||||
|
||||
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
|
||||
const falseTrustRoot = await window.libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
falseTrustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
aliceIdentityKey.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore as any);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
{ serialized: senderCertificate.serialized },
|
||||
bytesFromString('и вот я')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore as any);
|
||||
|
||||
try {
|
||||
await bobCipher.decrypt(
|
||||
createCertificateValidator(trustRoot.pubKey),
|
||||
ciphertext,
|
||||
31335
|
||||
);
|
||||
throw new Error('It did not fail!');
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, 'Invalid signature');
|
||||
}
|
||||
});
|
||||
|
||||
it('fails when expired', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
await _initializeSessions(aliceStore, bobStore);
|
||||
|
||||
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
|
||||
|
||||
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
trustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
aliceIdentityKey.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore as any);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
{ serialized: senderCertificate.serialized },
|
||||
bytesFromString('и вот я')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore as any);
|
||||
|
||||
try {
|
||||
await bobCipher.decrypt(
|
||||
createCertificateValidator(trustRoot.pubKey),
|
||||
ciphertext,
|
||||
31338
|
||||
);
|
||||
throw new Error('It did not fail!');
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, 'Certificate is expired');
|
||||
}
|
||||
});
|
||||
|
||||
it('fails when wrong identity', async function thisNeeded() {
|
||||
this.timeout(4000);
|
||||
|
||||
const aliceStore = new InMemorySignalProtocolStore();
|
||||
const bobStore = new InMemorySignalProtocolStore();
|
||||
|
||||
await _initializeSessions(aliceStore, bobStore);
|
||||
|
||||
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
|
||||
const randomKeyPair = await window.libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
trustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
randomKeyPair.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore as any);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
{ serialized: senderCertificate.serialized },
|
||||
bytesFromString('smert za smert')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore as any);
|
||||
|
||||
try {
|
||||
await bobCipher.decrypt(
|
||||
createCertificateValidator(trustRoot.pubKey),
|
||||
ciphertext,
|
||||
31335
|
||||
);
|
||||
throw new Error('It did not fail!');
|
||||
} catch (error) {
|
||||
assert.strictEqual(
|
||||
error.message,
|
||||
"Sender's certificate key does not match key used in message"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// private SenderCertificate _createCertificateFor(
|
||||
// ECKeyPair trustRoot
|
||||
// String sender
|
||||
// int deviceId
|
||||
// ECPublicKey identityKey
|
||||
// long expires
|
||||
// )
|
||||
async function _createSenderCertificateFor(
|
||||
trustRoot: KeyPairType,
|
||||
sender: string,
|
||||
deviceId: number,
|
||||
identityKey: ArrayBuffer,
|
||||
expires: number
|
||||
): Promise<ExplodedSenderCertificateType> {
|
||||
const serverKey = await window.libsignal.Curve.async.generateKeyPair();
|
||||
|
||||
const serverCertificateCertificateProto = new window.textsecure.protobuf.ServerCertificate.Certificate();
|
||||
serverCertificateCertificateProto.id = 1;
|
||||
serverCertificateCertificateProto.key = serverKey.pubKey;
|
||||
const serverCertificateCertificateBytes = serverCertificateCertificateProto.toArrayBuffer();
|
||||
|
||||
const serverCertificateSignature = await window.libsignal.Curve.async.calculateSignature(
|
||||
trustRoot.privKey,
|
||||
serverCertificateCertificateBytes
|
||||
);
|
||||
|
||||
const serverCertificateProto = new window.textsecure.protobuf.ServerCertificate();
|
||||
serverCertificateProto.certificate = serverCertificateCertificateBytes;
|
||||
serverCertificateProto.signature = serverCertificateSignature;
|
||||
const serverCertificate = _createServerCertificateFromBuffer(
|
||||
serverCertificateProto.toArrayBuffer()
|
||||
);
|
||||
|
||||
const senderCertificateCertificateProto = new window.textsecure.protobuf.SenderCertificate.Certificate();
|
||||
senderCertificateCertificateProto.sender = sender;
|
||||
senderCertificateCertificateProto.senderDevice = deviceId;
|
||||
senderCertificateCertificateProto.identityKey = identityKey;
|
||||
senderCertificateCertificateProto.expires = expires;
|
||||
senderCertificateCertificateProto.signer = window.textsecure.protobuf.ServerCertificate.decode(
|
||||
serverCertificate.serialized
|
||||
);
|
||||
const senderCertificateBytes = senderCertificateCertificateProto.toArrayBuffer();
|
||||
|
||||
const senderCertificateSignature = await window.libsignal.Curve.async.calculateSignature(
|
||||
serverKey.privKey,
|
||||
senderCertificateBytes
|
||||
);
|
||||
|
||||
const senderCertificateProto = new window.textsecure.protobuf.SenderCertificate();
|
||||
senderCertificateProto.certificate = senderCertificateBytes;
|
||||
senderCertificateProto.signature = senderCertificateSignature;
|
||||
return _createSenderCertificateFromBuffer(
|
||||
senderCertificateProto.toArrayBuffer()
|
||||
);
|
||||
}
|
||||
|
||||
// private void _initializeSessions(
|
||||
// SignalProtocolStore aliceStore, SignalProtocolStore bobStore)
|
||||
async function _initializeSessions(
|
||||
aliceStore: InMemorySignalProtocolStore,
|
||||
bobStore: InMemorySignalProtocolStore
|
||||
): Promise<void> {
|
||||
const aliceAddress = new window.libsignal.SignalProtocolAddress(
|
||||
'+14152222222',
|
||||
1
|
||||
);
|
||||
await aliceStore.put(
|
||||
'identityKey',
|
||||
await window.libsignal.Curve.generateKeyPair()
|
||||
);
|
||||
await bobStore.put(
|
||||
'identityKey',
|
||||
await window.libsignal.Curve.generateKeyPair()
|
||||
);
|
||||
|
||||
await aliceStore.put('registrationId', 57);
|
||||
await bobStore.put('registrationId', 58);
|
||||
|
||||
const bobPreKey = await window.libsignal.Curve.async.generateKeyPair();
|
||||
const bobIdentityKey = await bobStore.getIdentityKeyPair();
|
||||
const bobSignedPreKey = await window.libsignal.KeyHelper.generateSignedPreKey(
|
||||
bobIdentityKey,
|
||||
2
|
||||
);
|
||||
|
||||
const bobBundle = {
|
||||
deviceId: 3,
|
||||
identityKey: bobIdentityKey.pubKey,
|
||||
registrationId: 1,
|
||||
signedPreKey: {
|
||||
keyId: 2,
|
||||
publicKey: bobSignedPreKey.keyPair.pubKey,
|
||||
signature: bobSignedPreKey.signature,
|
||||
},
|
||||
preKey: {
|
||||
keyId: 1,
|
||||
publicKey: bobPreKey.pubKey,
|
||||
},
|
||||
};
|
||||
const aliceSessionBuilder = new window.libsignal.SessionBuilder(
|
||||
aliceStore as any,
|
||||
aliceAddress
|
||||
);
|
||||
await aliceSessionBuilder.processPreKey(bobBundle);
|
||||
|
||||
await bobStore.storeSignedPreKey(2, bobSignedPreKey.keyPair);
|
||||
await bobStore.storePreKey(1, bobPreKey);
|
||||
}
|
||||
});
|
96
ts/textsecure.d.ts
vendored
96
ts/textsecure.d.ts
vendored
|
@ -216,6 +216,12 @@ type SubProtocolProtobufTypes = {
|
|||
WebSocketResponseMessage: typeof WebSocketResponseMessageClass;
|
||||
};
|
||||
|
||||
type UnidentifiedDeliveryTypes = {
|
||||
ServerCertificate: typeof ServerCertificateClass;
|
||||
SenderCertificate: typeof SenderCertificateClass;
|
||||
UnidentifiedSenderMessage: typeof UnidentifiedSenderMessageClass;
|
||||
};
|
||||
|
||||
type ProtobufCollectionType = {
|
||||
onLoad: (callback: () => unknown) => void;
|
||||
} & DeviceMessagesProtobufTypes &
|
||||
|
@ -223,7 +229,8 @@ type ProtobufCollectionType = {
|
|||
GroupsProtobufTypes &
|
||||
SignalServiceProtobufTypes &
|
||||
SignalStorageProtobufTypes &
|
||||
SubProtocolProtobufTypes;
|
||||
SubProtocolProtobufTypes &
|
||||
UnidentifiedDeliveryTypes;
|
||||
|
||||
// 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
|
||||
|
@ -1354,3 +1361,90 @@ export declare class WebSocketResponseMessageClass {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,6 +27,10 @@ import Crypto from './Crypto';
|
|||
import { deriveMasterKeyFromGroupV1 } from '../Crypto';
|
||||
import { ContactBuffer, GroupBuffer } from './ContactsParser';
|
||||
import { IncomingIdentityKeyError } from './Errors';
|
||||
import {
|
||||
createCertificateValidator,
|
||||
SecretSessionCipher,
|
||||
} from '../metadata/SecretSessionCipher';
|
||||
|
||||
import {
|
||||
AttachmentPointerClass,
|
||||
|
@ -946,7 +950,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
address,
|
||||
options
|
||||
);
|
||||
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
||||
const secretSessionCipher = new SecretSessionCipher(
|
||||
window.textsecure.storage.protocol,
|
||||
options
|
||||
);
|
||||
|
@ -979,7 +983,7 @@ class MessageReceiverInner extends EventTarget {
|
|||
window.log.info('received unidentified sender message');
|
||||
promise = secretSessionCipher
|
||||
.decrypt(
|
||||
window.Signal.Metadata.createCertificateValidator(serverTrustRoot),
|
||||
createCertificateValidator(serverTrustRoot),
|
||||
ciphertext.toArrayBuffer(),
|
||||
Math.min(envelope.serverTimestamp || Date.now(), Date.now()),
|
||||
me
|
||||
|
@ -1028,6 +1032,12 @@ class MessageReceiverInner extends EventTarget {
|
|||
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
|
||||
// decrypt methods used above.
|
||||
return this.unpad(content);
|
||||
|
|
|
@ -26,6 +26,10 @@ import {
|
|||
UnregisteredUserError,
|
||||
} from './Errors';
|
||||
import { isValidNumber } from '../types/PhoneNumber';
|
||||
import {
|
||||
SecretSessionCipher,
|
||||
SerializedCertificateType,
|
||||
} from '../metadata/SecretSessionCipher';
|
||||
|
||||
type OutgoingMessageOptionsType = SendOptionsType & {
|
||||
online?: boolean;
|
||||
|
@ -58,7 +62,7 @@ export default class OutgoingMessage {
|
|||
|
||||
sendMetadata?: SendMetadataType;
|
||||
|
||||
senderCertificate?: ArrayBuffer;
|
||||
senderCertificate?: SerializedCertificateType;
|
||||
|
||||
online?: boolean;
|
||||
|
||||
|
@ -384,8 +388,8 @@ export default class OutgoingMessage {
|
|||
options.messageKeysLimit = false;
|
||||
}
|
||||
|
||||
if (sealedSender) {
|
||||
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
|
||||
if (sealedSender && senderCertificate) {
|
||||
const secretSessionCipher = new SecretSessionCipher(
|
||||
window.textsecure.storage.protocol
|
||||
);
|
||||
ciphers[address.getDeviceId()] = secretSessionCipher;
|
||||
|
|
|
@ -46,6 +46,7 @@ import {
|
|||
LinkPreviewImage,
|
||||
LinkPreviewMetadata,
|
||||
} from '../linkPreviews/linkPreviewFetch';
|
||||
import { SerializedCertificateType } from '../metadata/SecretSessionCipher';
|
||||
|
||||
function stringToArrayBuffer(str: string): ArrayBuffer {
|
||||
if (typeof str !== 'string') {
|
||||
|
@ -66,7 +67,7 @@ export type SendMetadataType = {
|
|||
};
|
||||
|
||||
export type SendOptionsType = {
|
||||
senderCertificate?: ArrayBuffer;
|
||||
senderCertificate?: SerializedCertificateType;
|
||||
sendMetadata?: SendMetadataType;
|
||||
online?: boolean;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue