remove most of the SessionProtocol unused stuff
- prekeys - SessionCipher - LokiCipher - endSession and the reset Session logic - what we called Sessionprotocol manager (to keep track of session with everyone)pull/1434/head
parent
979a9058e3
commit
72c96ea998
@ -1,32 +0,0 @@
|
||||
/* global Whisper, SignalProtocolStore, _ */
|
||||
|
||||
/* eslint-disable more/no-then */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
|
||||
Whisper.KeyChangeListener = {
|
||||
init(signalProtocolStore) {
|
||||
if (!(signalProtocolStore instanceof SignalProtocolStore)) {
|
||||
throw new Error('KeyChangeListener requires a SignalProtocolStore');
|
||||
}
|
||||
|
||||
signalProtocolStore.on('keychange', async id => {
|
||||
const conversation = await window
|
||||
.getConversationController()
|
||||
.getOrCreateAndWait(id, 'private');
|
||||
conversation.addKeyChange(id);
|
||||
|
||||
const groups = await window
|
||||
.getConversationController()
|
||||
.getAllGroupsInvolvingId(id);
|
||||
_.forEach(groups, group => {
|
||||
group.addKeyChange(id);
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
})();
|
@ -1,15 +0,0 @@
|
||||
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,
|
||||
|
||||
FALLBACK_MESSAGE: 101,
|
||||
};
|
@ -1,42 +0,0 @@
|
||||
import { SignalService } from '../../protobuf';
|
||||
import { CipherTextObject } from '../../../libtextsecure/libsignal-protocol';
|
||||
|
||||
export interface SecretSessionCipherConstructor {
|
||||
new (storage: any): SecretSessionCipherInterface;
|
||||
}
|
||||
|
||||
export interface SecretSessionCipherInterface {
|
||||
encrypt(
|
||||
destinationPubkey: string,
|
||||
senderCertificate: SignalService.SenderCertificate,
|
||||
innerEncryptedMessage: CipherTextObject
|
||||
): Promise<ArrayBuffer>;
|
||||
decrypt(
|
||||
cipherText: ArrayBuffer,
|
||||
me: { number: string; deviceId: number }
|
||||
): Promise<{
|
||||
isMe?: boolean;
|
||||
sender: string;
|
||||
content: ArrayBuffer;
|
||||
type: SignalService.Envelope.Type;
|
||||
}>;
|
||||
}
|
||||
|
||||
export declare class SecretSessionCipher
|
||||
implements SecretSessionCipherInterface {
|
||||
constructor(storage: any);
|
||||
public encrypt(
|
||||
destinationPubkey: string,
|
||||
senderCertificate: SignalService.SenderCertificate,
|
||||
innerEncryptedMessage: CipherTextObject
|
||||
): Promise<ArrayBuffer>;
|
||||
public decrypt(
|
||||
cipherText: ArrayBuffer,
|
||||
me: { number: string; deviceId: number }
|
||||
): Promise<{
|
||||
isMe?: boolean;
|
||||
sender: string;
|
||||
content: ArrayBuffer;
|
||||
type: SignalService.Envelope.Type;
|
||||
}>;
|
||||
}
|
@ -1,550 +0,0 @@
|
||||
/* global libsignal, textsecure, dcodeIO, libloki */
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
const CiphertextMessage = require('./CiphertextMessage');
|
||||
const {
|
||||
bytesFromString,
|
||||
concatenateBytes,
|
||||
constantTimeEqual,
|
||||
decryptAesCtr,
|
||||
encryptAesCtr,
|
||||
fromEncodedBinaryToArrayBuffer,
|
||||
getViewOfArrayBuffer,
|
||||
getZeroes,
|
||||
highBitsToInt,
|
||||
hmacSha256,
|
||||
intsToByteHighAndLow,
|
||||
splitBytes,
|
||||
trimBytes,
|
||||
} = require('../crypto');
|
||||
|
||||
const REVOKED_CERTIFICATES = [];
|
||||
|
||||
function SecretSessionCipher(storage) {
|
||||
this.storage = storage;
|
||||
|
||||
// We do this on construction because libsignal won't be available when this file loads
|
||||
const { SessionCipher } = libsignal;
|
||||
this.SessionCipher = SessionCipher;
|
||||
}
|
||||
|
||||
const CIPHERTEXT_VERSION = 1;
|
||||
const UNIDENTIFIED_DELIVERY_PREFIX = 'UnidentifiedDelivery';
|
||||
|
||||
// public CertificateValidator(ECPublicKey trustRoot)
|
||||
function createCertificateValidator(trustRoot) {
|
||||
return {
|
||||
// public void validate(SenderCertificate certificate, long validationTime)
|
||||
async validate(certificate, validationTime) {
|
||||
const serverCertificate = certificate.signer;
|
||||
|
||||
await libsignal.Curve.async.verifySignature(
|
||||
trustRoot,
|
||||
serverCertificate.certificate,
|
||||
serverCertificate.signature
|
||||
);
|
||||
|
||||
const serverCertId = serverCertificate.certificate.id;
|
||||
if (REVOKED_CERTIFICATES.includes(serverCertId)) {
|
||||
throw new Error(
|
||||
`Server certificate id ${serverCertId} has been revoked`
|
||||
);
|
||||
}
|
||||
|
||||
await libsignal.Curve.async.verifySignature(
|
||||
serverCertificate.key,
|
||||
certificate.certificate,
|
||||
certificate.signature
|
||||
);
|
||||
|
||||
if (validationTime > certificate.expires) {
|
||||
throw new Error('Certificate is expired');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function _decodePoint(serialized, offset = 0) {
|
||||
const view =
|
||||
offset > 0
|
||||
? getViewOfArrayBuffer(serialized, offset, serialized.byteLength)
|
||||
: serialized;
|
||||
|
||||
return libsignal.Curve.validatePubKeyFormat(view);
|
||||
}
|
||||
|
||||
// public ServerCertificate(byte[] serialized)
|
||||
function _createServerCertificateFromBuffer(serialized) {
|
||||
const wrapper = textsecure.protobuf.ServerCertificate.decode(serialized);
|
||||
|
||||
if (!wrapper.certificate || !wrapper.signature) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
const certificate = textsecure.protobuf.ServerCertificate.Certificate.decode(
|
||||
wrapper.certificate.toArrayBuffer()
|
||||
);
|
||||
|
||||
if (!certificate.id || !certificate.key) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
return {
|
||||
id: certificate.id,
|
||||
key: certificate.key.toArrayBuffer(),
|
||||
serialized,
|
||||
certificate: wrapper.certificate.toArrayBuffer(),
|
||||
|
||||
signature: wrapper.signature.toArrayBuffer(),
|
||||
};
|
||||
}
|
||||
|
||||
// public SenderCertificate(byte[] serialized)
|
||||
function _createSenderCertificateFromBuffer(serialized) {
|
||||
const cert = textsecure.protobuf.SenderCertificate.decode(serialized);
|
||||
|
||||
if (!cert.senderDevice || !cert.sender) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
return {
|
||||
sender: cert.sender,
|
||||
senderDevice: cert.senderDevice,
|
||||
|
||||
certificate: cert.toArrayBuffer(),
|
||||
|
||||
serialized,
|
||||
};
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessage(byte[] serialized)
|
||||
function _createUnidentifiedSenderMessageFromBuffer(serialized) {
|
||||
const version = highBitsToInt(serialized[0]);
|
||||
|
||||
if (version > CIPHERTEXT_VERSION) {
|
||||
throw new Error(`Unknown version: ${this.version}`);
|
||||
}
|
||||
|
||||
const view = getViewOfArrayBuffer(serialized, 1, serialized.byteLength);
|
||||
const unidentifiedSenderMessage = textsecure.protobuf.UnidentifiedSenderMessage.decode(
|
||||
view
|
||||
);
|
||||
|
||||
if (
|
||||
!unidentifiedSenderMessage.ephemeralPublic ||
|
||||
!unidentifiedSenderMessage.encryptedStatic ||
|
||||
!unidentifiedSenderMessage.encryptedMessage
|
||||
) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
return {
|
||||
version,
|
||||
|
||||
ephemeralPublic: unidentifiedSenderMessage.ephemeralPublic.toArrayBuffer(),
|
||||
encryptedStatic: unidentifiedSenderMessage.encryptedStatic.toArrayBuffer(),
|
||||
encryptedMessage: unidentifiedSenderMessage.encryptedMessage.toArrayBuffer(),
|
||||
|
||||
serialized,
|
||||
};
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessage(
|
||||
// ECPublicKey ephemeral, byte[] encryptedStatic, byte[] encryptedMessage) {
|
||||
function _createUnidentifiedSenderMessage(
|
||||
ephemeralPublic,
|
||||
encryptedStatic,
|
||||
encryptedMessage
|
||||
) {
|
||||
const versionBytes = new Uint8Array([
|
||||
intsToByteHighAndLow(CIPHERTEXT_VERSION, CIPHERTEXT_VERSION),
|
||||
]);
|
||||
const unidentifiedSenderMessage = new textsecure.protobuf.UnidentifiedSenderMessage();
|
||||
|
||||
unidentifiedSenderMessage.encryptedMessage = encryptedMessage;
|
||||
unidentifiedSenderMessage.encryptedStatic = encryptedStatic;
|
||||
unidentifiedSenderMessage.ephemeralPublic = ephemeralPublic;
|
||||
|
||||
const messageBytes = unidentifiedSenderMessage.encode().toArrayBuffer();
|
||||
|
||||
return {
|
||||
version: CIPHERTEXT_VERSION,
|
||||
|
||||
ephemeralPublic,
|
||||
encryptedStatic,
|
||||
encryptedMessage,
|
||||
|
||||
serialized: concatenateBytes(versionBytes, messageBytes),
|
||||
};
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessageContent(byte[] serialized)
|
||||
function _createUnidentifiedSenderMessageContentFromBuffer(serialized) {
|
||||
const TypeEnum = textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
|
||||
|
||||
const message = textsecure.protobuf.UnidentifiedSenderMessage.Message.decode(
|
||||
serialized
|
||||
);
|
||||
|
||||
if (!message.type || !message.senderCertificate || !message.content) {
|
||||
throw new Error('Missing fields');
|
||||
}
|
||||
|
||||
let type;
|
||||
switch (message.type) {
|
||||
case TypeEnum.MESSAGE:
|
||||
type = CiphertextMessage.WHISPER_TYPE;
|
||||
break;
|
||||
case TypeEnum.PREKEY_MESSAGE:
|
||||
type = CiphertextMessage.PREKEY_TYPE;
|
||||
break;
|
||||
case TypeEnum.FALLBACK_MESSAGE:
|
||||
type = CiphertextMessage.FALLBACK_MESSAGE;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unknown type: ${message.type}`);
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
senderCertificate: _createSenderCertificateFromBuffer(
|
||||
message.senderCertificate.toArrayBuffer()
|
||||
),
|
||||
content: message.content.toArrayBuffer(),
|
||||
|
||||
serialized,
|
||||
};
|
||||
}
|
||||
|
||||
// private int getProtoType(int type)
|
||||
function _getProtoMessageType(type) {
|
||||
const TypeEnum = textsecure.protobuf.UnidentifiedSenderMessage.Message.Type;
|
||||
|
||||
switch (type) {
|
||||
case CiphertextMessage.WHISPER_TYPE:
|
||||
return TypeEnum.MESSAGE;
|
||||
case CiphertextMessage.PREKEY_TYPE:
|
||||
return TypeEnum.PREKEY_MESSAGE;
|
||||
case CiphertextMessage.FALLBACK_MESSAGE:
|
||||
return TypeEnum.FALLBACK_MESSAGE;
|
||||
default:
|
||||
throw new Error(`_getProtoMessageType: type '${type}' does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
// public UnidentifiedSenderMessageContent(
|
||||
// int type, SenderCertificate senderCertificate, byte[] content)
|
||||
function _createUnidentifiedSenderMessageContent(
|
||||
type,
|
||||
senderCertificate,
|
||||
content
|
||||
) {
|
||||
const innerMessage = new textsecure.protobuf.UnidentifiedSenderMessage.Message();
|
||||
innerMessage.type = _getProtoMessageType(type);
|
||||
innerMessage.senderCertificate = senderCertificate;
|
||||
innerMessage.content = content;
|
||||
|
||||
return {
|
||||
type,
|
||||
senderCertificate,
|
||||
content,
|
||||
|
||||
serialized: innerMessage.encode().toArrayBuffer(),
|
||||
};
|
||||
}
|
||||
|
||||
SecretSessionCipher.prototype = {
|
||||
async encrypt(destinationPubkey, senderCertificate, innerEncryptedMessage) {
|
||||
// Capture this.xxx variables to replicate Java's implicit this syntax
|
||||
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
|
||||
const _encryptWithSecretKeys = this._encryptWithSecretKeys.bind(this);
|
||||
const _calculateStaticKeys = this._calculateStaticKeys.bind(this);
|
||||
|
||||
const ourIdentity = await this.storage.getIdentityKeyPair();
|
||||
const theirIdentity = dcodeIO.ByteBuffer.wrap(
|
||||
destinationPubkey,
|
||||
'hex'
|
||||
).toArrayBuffer();
|
||||
|
||||
const ephemeral = await libsignal.Curve.async.generateKeyPair();
|
||||
const ephemeralSalt = concatenateBytes(
|
||||
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
|
||||
theirIdentity,
|
||||
ephemeral.pubKey
|
||||
);
|
||||
const ephemeralKeys = await _calculateEphemeralKeys(
|
||||
theirIdentity,
|
||||
ephemeral.privKey,
|
||||
ephemeralSalt
|
||||
);
|
||||
const staticKeyCiphertext = await _encryptWithSecretKeys(
|
||||
ephemeralKeys.cipherKey,
|
||||
ephemeralKeys.macKey,
|
||||
ourIdentity.pubKey
|
||||
);
|
||||
|
||||
const staticSalt = concatenateBytes(
|
||||
ephemeralKeys.chainKey,
|
||||
staticKeyCiphertext
|
||||
);
|
||||
const staticKeys = await _calculateStaticKeys(
|
||||
theirIdentity,
|
||||
ourIdentity.privKey,
|
||||
staticSalt
|
||||
);
|
||||
const content = _createUnidentifiedSenderMessageContent(
|
||||
innerEncryptedMessage.type,
|
||||
senderCertificate,
|
||||
fromEncodedBinaryToArrayBuffer(innerEncryptedMessage.body)
|
||||
);
|
||||
const messageBytes = await _encryptWithSecretKeys(
|
||||
staticKeys.cipherKey,
|
||||
staticKeys.macKey,
|
||||
content.serialized
|
||||
);
|
||||
|
||||
const unidentifiedSenderMessage = _createUnidentifiedSenderMessage(
|
||||
ephemeral.pubKey,
|
||||
staticKeyCiphertext,
|
||||
messageBytes
|
||||
);
|
||||
|
||||
return unidentifiedSenderMessage.serialized;
|
||||
},
|
||||
|
||||
// public Pair<SignalProtocolAddress, byte[]> decrypt(
|
||||
// CertificateValidator validator, byte[] ciphertext, long timestamp)
|
||||
async decrypt(ciphertext, me) {
|
||||
// Capture this.xxx variables to replicate Java's implicit this syntax
|
||||
const signalProtocolStore = this.storage;
|
||||
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
|
||||
const _calculateStaticKeys = this._calculateStaticKeys.bind(this);
|
||||
const _decryptWithUnidentifiedSenderMessage = this._decryptWithUnidentifiedSenderMessage.bind(
|
||||
this
|
||||
);
|
||||
const _decryptWithSecretKeys = this._decryptWithSecretKeys.bind(this);
|
||||
|
||||
const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
|
||||
|
||||
const wrapper = _createUnidentifiedSenderMessageFromBuffer(ciphertext);
|
||||
const ephemeralSalt = concatenateBytes(
|
||||
bytesFromString(UNIDENTIFIED_DELIVERY_PREFIX),
|
||||
ourIdentity.pubKey,
|
||||
wrapper.ephemeralPublic
|
||||
);
|
||||
const ephemeralKeys = await _calculateEphemeralKeys(
|
||||
wrapper.ephemeralPublic,
|
||||
ourIdentity.privKey,
|
||||
ephemeralSalt
|
||||
);
|
||||
const staticKeyBytes = await _decryptWithSecretKeys(
|
||||
ephemeralKeys.cipherKey,
|
||||
ephemeralKeys.macKey,
|
||||
wrapper.encryptedStatic
|
||||
);
|
||||
|
||||
const staticKey = _decodePoint(staticKeyBytes, 0);
|
||||
const staticSalt = concatenateBytes(
|
||||
ephemeralKeys.chainKey,
|
||||
wrapper.encryptedStatic
|
||||
);
|
||||
const staticKeys = await _calculateStaticKeys(
|
||||
staticKey,
|
||||
ourIdentity.privKey,
|
||||
staticSalt
|
||||
);
|
||||
const messageBytes = await _decryptWithSecretKeys(
|
||||
staticKeys.cipherKey,
|
||||
staticKeys.macKey,
|
||||
wrapper.encryptedMessage
|
||||
);
|
||||
|
||||
const content = _createUnidentifiedSenderMessageContentFromBuffer(
|
||||
messageBytes
|
||||
);
|
||||
|
||||
const { sender, senderDevice } = content.senderCertificate;
|
||||
const { number, deviceId } = me || {};
|
||||
if (sender === number && senderDevice === deviceId) {
|
||||
return {
|
||||
isMe: true,
|
||||
};
|
||||
}
|
||||
const address = new libsignal.SignalProtocolAddress(sender, senderDevice);
|
||||
|
||||
try {
|
||||
return {
|
||||
sender: address,
|
||||
content: await _decryptWithUnidentifiedSenderMessage(content),
|
||||
type: content.type,
|
||||
};
|
||||
} catch (error) {
|
||||
if (!error) {
|
||||
// eslint-disable-next-line no-ex-assign
|
||||
error = new Error('Decryption error was falsey!');
|
||||
}
|
||||
|
||||
error.sender = address;
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
// public int getSessionVersion(SignalProtocolAddress remoteAddress) {
|
||||
getSessionVersion(remoteAddress) {
|
||||
const { SessionCipher } = this;
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||
|
||||
return cipher.getSessionVersion();
|
||||
},
|
||||
|
||||
// public int getRemoteRegistrationId(SignalProtocolAddress remoteAddress) {
|
||||
getRemoteRegistrationId(remoteAddress) {
|
||||
const { SessionCipher } = this;
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||
|
||||
return cipher.getRemoteRegistrationId();
|
||||
},
|
||||
|
||||
closeOpenSessionForDevice(remoteAddress) {
|
||||
const { SessionCipher } = this;
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const cipher = new SessionCipher(signalProtocolStore, remoteAddress);
|
||||
|
||||
return cipher.closeOpenSessionForDevice();
|
||||
},
|
||||
|
||||
// private EphemeralKeys calculateEphemeralKeys(
|
||||
// ECPublicKey ephemeralPublic, ECPrivateKey ephemeralPrivate, byte[] salt)
|
||||
async _calculateEphemeralKeys(ephemeralPublic, ephemeralPrivate, salt) {
|
||||
const ephemeralSecret = await libsignal.Curve.async.calculateAgreement(
|
||||
ephemeralPublic,
|
||||
ephemeralPrivate
|
||||
);
|
||||
const ephemeralDerivedParts = await libsignal.HKDF.deriveSecrets(
|
||||
ephemeralSecret,
|
||||
salt,
|
||||
new ArrayBuffer()
|
||||
);
|
||||
|
||||
// private EphemeralKeys(byte[] chainKey, byte[] cipherKey, byte[] macKey)
|
||||
return {
|
||||
chainKey: ephemeralDerivedParts[0],
|
||||
cipherKey: ephemeralDerivedParts[1],
|
||||
macKey: ephemeralDerivedParts[2],
|
||||
};
|
||||
},
|
||||
|
||||
// private StaticKeys calculateStaticKeys(
|
||||
// ECPublicKey staticPublic, ECPrivateKey staticPrivate, byte[] salt)
|
||||
async _calculateStaticKeys(staticPublic, staticPrivate, salt) {
|
||||
const staticSecret = await libsignal.Curve.async.calculateAgreement(
|
||||
staticPublic,
|
||||
staticPrivate
|
||||
);
|
||||
const staticDerivedParts = await libsignal.HKDF.deriveSecrets(
|
||||
staticSecret,
|
||||
salt,
|
||||
new ArrayBuffer()
|
||||
);
|
||||
|
||||
// private StaticKeys(byte[] cipherKey, byte[] macKey)
|
||||
return {
|
||||
cipherKey: staticDerivedParts[1],
|
||||
macKey: staticDerivedParts[2],
|
||||
};
|
||||
},
|
||||
|
||||
// private byte[] decrypt(UnidentifiedSenderMessageContent message)
|
||||
_decryptWithUnidentifiedSenderMessage(message) {
|
||||
const signalProtocolStore = this.storage;
|
||||
|
||||
const sender = new libsignal.SignalProtocolAddress(
|
||||
message.senderCertificate.sender,
|
||||
message.senderCertificate.senderDevice
|
||||
);
|
||||
|
||||
switch (message.type) {
|
||||
case CiphertextMessage.WHISPER_TYPE:
|
||||
return new libloki.crypto.LokiSessionCipher(
|
||||
signalProtocolStore,
|
||||
sender
|
||||
).decryptWhisperMessage(message.content);
|
||||
case CiphertextMessage.PREKEY_TYPE:
|
||||
return new libloki.crypto.LokiSessionCipher(
|
||||
signalProtocolStore,
|
||||
sender
|
||||
).decryptPreKeyWhisperMessage(message.content);
|
||||
case CiphertextMessage.FALLBACK_MESSAGE:
|
||||
return new libloki.crypto.FallBackSessionCipher(sender).decrypt(
|
||||
message.content
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unknown type: ${message.type}`);
|
||||
}
|
||||
},
|
||||
|
||||
// private byte[] encrypt(
|
||||
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] plaintext)
|
||||
async _encryptWithSecretKeys(cipherKey, macKey, plaintext) {
|
||||
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
|
||||
// cipher.init(Cipher.ENCRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
|
||||
|
||||
// Mac const mac = Mac.getInstance('HmacSHA256');
|
||||
// mac.init(macKey);
|
||||
|
||||
// byte[] const ciphertext = cipher.doFinal(plaintext);
|
||||
const ciphertext = await encryptAesCtr(cipherKey, plaintext, getZeroes(16));
|
||||
|
||||
// byte[] const ourFullMac = mac.doFinal(ciphertext);
|
||||
const ourFullMac = await hmacSha256(macKey, ciphertext);
|
||||
const ourMac = trimBytes(ourFullMac, 10);
|
||||
|
||||
return concatenateBytes(ciphertext, ourMac);
|
||||
},
|
||||
|
||||
// private byte[] decrypt(
|
||||
// SecretKeySpec cipherKey, SecretKeySpec macKey, byte[] ciphertext)
|
||||
async _decryptWithSecretKeys(cipherKey, macKey, ciphertext) {
|
||||
if (ciphertext.byteLength < 10) {
|
||||
throw new Error('Ciphertext not long enough for MAC!');
|
||||
}
|
||||
|
||||
const ciphertextParts = splitBytes(
|
||||
ciphertext,
|
||||
ciphertext.byteLength - 10,
|
||||
10
|
||||
);
|
||||
|
||||
// Mac const mac = Mac.getInstance('HmacSHA256');
|
||||
// mac.init(macKey);
|
||||
|
||||
// byte[] const digest = mac.doFinal(ciphertextParts[0]);
|
||||
const digest = await hmacSha256(macKey, ciphertextParts[0]);
|
||||
const ourMac = trimBytes(digest, 10);
|
||||
const theirMac = ciphertextParts[1];
|
||||
|
||||
if (!constantTimeEqual(ourMac, theirMac)) {
|
||||
throw new Error('Bad mac!');
|
||||
}
|
||||
|
||||
// Cipher const cipher = Cipher.getInstance('AES/CTR/NoPadding');
|
||||
// cipher.init(Cipher.DECRYPT_MODE, cipherKey, new IvParameterSpec(new byte[16]));
|
||||
|
||||
// return cipher.doFinal(ciphertextParts[0]);
|
||||
return decryptAesCtr(cipherKey, ciphertextParts[0], getZeroes(16));
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
SecretSessionCipher,
|
||||
createCertificateValidator,
|
||||
_createServerCertificateFromBuffer,
|
||||
_createSenderCertificateFromBuffer,
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import { SecretSessionCipherConstructor } from './metadata/SecretSessionCipher';
|
||||
|
||||
interface Metadata {
|
||||
SecretSessionCipher: SecretSessionCipherConstructor;
|
||||
}
|
||||
|
||||
export interface SignalInterface {
|
||||
Metadata: Metadata;
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
/* global Whisper, storage, getAccountManager */
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
window.Whisper = window.Whisper || {};
|
||||
const ROTATION_INTERVAL = 48 * 60 * 60 * 1000;
|
||||
let timeout;
|
||||
let scheduledTime;
|
||||
let shouldStop = false;
|
||||
|
||||
function scheduleNextRotation() {
|
||||
const now = Date.now();
|
||||
const nextTime = now + ROTATION_INTERVAL;
|
||||
storage.put('nextSignedKeyRotationTime', nextTime);
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (shouldStop) {
|
||||
return;
|
||||
}
|
||||
window.log.info('Rotating signed prekey...');
|
||||
getAccountManager()
|
||||
.rotateSignedPreKey()
|
||||
.catch(() => {
|
||||
window.log.error(
|
||||
'rotateSignedPrekey() failed. Trying again in five seconds'
|
||||
);
|
||||
setTimeout(runWhenOnline, 5000);
|
||||
});
|
||||
scheduleNextRotation();
|
||||
setTimeoutForNextRun();
|
||||
}
|
||||
|
||||
function runWhenOnline() {
|
||||
if (navigator.onLine) {
|
||||
run();
|
||||
} else {
|
||||
window.log.info(
|
||||
'We are offline; keys will be rotated when we are next online'
|
||||
);
|
||||
const listener = () => {
|
||||
window.removeEventListener('online', listener);
|
||||
run();
|
||||
};
|
||||
window.addEventListener('online', listener);
|
||||
}
|
||||
}
|
||||
|
||||
function setTimeoutForNextRun() {
|
||||
const now = Date.now();
|
||||
const time = storage.get('nextSignedKeyRotationTime', now);
|
||||
|
||||
if (scheduledTime !== time || !timeout) {
|
||||
window.log.info(
|
||||
'Next signed key rotation scheduled for',
|
||||
new Date(time).toISOString()
|
||||
);
|
||||
}
|
||||
|
||||
scheduledTime = time;
|
||||
let waitTime = time - now;
|
||||
if (waitTime < 0) {
|
||||
waitTime = 0;
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(runWhenOnline, waitTime);
|
||||
}
|
||||
function onTimeTravel() {
|
||||
if (Whisper.Registration.isDone()) {
|
||||
setTimeoutForNextRun();
|
||||
}
|
||||
}
|
||||
let initComplete;
|
||||
Whisper.RotateSignedPreKeyListener = {
|
||||
init(events, newVersion) {
|
||||
if (initComplete) {
|
||||
window.log.warn('Rotate signed prekey listener: Already initialized');
|
||||
return;
|
||||
}
|
||||
initComplete = true;
|
||||
shouldStop = false;
|
||||
|
||||
if (newVersion) {
|
||||
runWhenOnline();
|
||||
} else {
|
||||
setTimeoutForNextRun();
|
||||
}
|
||||
|
||||
events.on('timetravel', onTimeTravel);
|
||||
},
|
||||
stop(events) {
|
||||
initComplete = false;
|
||||
shouldStop = true;
|
||||
events.off('timetravel', onTimeTravel);
|
||||
clearTimeout(timeout);
|
||||
},
|
||||
};
|
||||
})();
|
@ -1,42 +0,0 @@
|
||||
/* global libsignal, libloki, textsecure, StringView, dcodeIO */
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('Crypto', () => {
|
||||
describe('FallBackSessionCipher', () => {
|
||||
let fallbackCipher;
|
||||
let identityKey;
|
||||
let address;
|
||||
const store = textsecure.storage.protocol;
|
||||
|
||||
before(async () => {
|
||||
clearDatabase();
|
||||
identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
store.put('identityKey', identityKey);
|
||||
const key = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(key);
|
||||
address = new libsignal.SignalProtocolAddress(pubKeyString, 1);
|
||||
fallbackCipher = new libloki.crypto.FallBackSessionCipher(address);
|
||||
});
|
||||
|
||||
it('should encrypt fallback cipher messages as fallback messages', async () => {
|
||||
const buffer = new ArrayBuffer(10);
|
||||
const { type } = await fallbackCipher.encrypt(buffer);
|
||||
assert.strictEqual(
|
||||
type,
|
||||
textsecure.protobuf.Envelope.Type.FALLBACK_MESSAGE
|
||||
);
|
||||
});
|
||||
|
||||
it('should encrypt and then decrypt a message with the same result', async () => {
|
||||
const arr = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const { body } = await fallbackCipher.encrypt(arr.buffer);
|
||||
const bufferBody = dcodeIO.ByteBuffer.wrap(
|
||||
body,
|
||||
'binary'
|
||||
).toArrayBuffer();
|
||||
const result = await fallbackCipher.decrypt(bufferBody);
|
||||
assert.deepEqual(result, arr.buffer);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,84 +0,0 @@
|
||||
/* global libsignal, libloki, textsecure, StringView */
|
||||
|
||||
'use strict';
|
||||
|
||||
describe('Storage', () => {
|
||||
let testKey;
|
||||
const store = textsecure.storage.protocol;
|
||||
|
||||
describe('#getPreKeyBundleForContact', () => {
|
||||
beforeEach(async () => {
|
||||
clearDatabase();
|
||||
testKey = {
|
||||
pubKey: libsignal.crypto.getRandomBytes(33),
|
||||
privKey: libsignal.crypto.getRandomBytes(32),
|
||||
};
|
||||
textsecure.storage.put('signedKeyId', 2);
|
||||
await store.storeSignedPreKey(1, testKey);
|
||||
});
|
||||
|
||||
it('should generate a new prekey bundle for a new contact', async () => {
|
||||
const pubKey = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(pubKey);
|
||||
const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1);
|
||||
const newBundle = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1);
|
||||
assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1);
|
||||
|
||||
const testKeyArray = new Uint8Array(testKey.pubKey);
|
||||
assert.isDefined(newBundle);
|
||||
assert.isDefined(newBundle.identityKey);
|
||||
assert.isDefined(newBundle.deviceId);
|
||||
assert.isDefined(newBundle.preKeyId);
|
||||
assert.isDefined(newBundle.signedKeyId);
|
||||
assert.isDefined(newBundle.preKey);
|
||||
assert.isDefined(newBundle.signedKey);
|
||||
assert.isDefined(newBundle.signature);
|
||||
assert.strictEqual(
|
||||
testKeyArray.byteLength,
|
||||
newBundle.signedKey.byteLength
|
||||
);
|
||||
for (let i = 0; i !== testKeyArray.byteLength; i += 1) {
|
||||
assert.strictEqual(testKeyArray[i], newBundle.signedKey[i]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should return the same prekey bundle after creating a contact', async () => {
|
||||
const pubKey = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(pubKey);
|
||||
const bundle1 = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
const bundle2 = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
assert.isDefined(bundle1);
|
||||
assert.isDefined(bundle2);
|
||||
assert.deepEqual(bundle1, bundle2);
|
||||
});
|
||||
|
||||
it('should save the signed keys and prekeys from a bundle', async () => {
|
||||
const pubKey = libsignal.crypto.getRandomBytes(32);
|
||||
const pubKeyString = StringView.arrayBufferToHex(pubKey);
|
||||
const preKeyIdBefore = textsecure.storage.get('maxPreKeyId', 1);
|
||||
const newBundle = await libloki.storage.getPreKeyBundleForContact(
|
||||
pubKeyString
|
||||
);
|
||||
const preKeyIdAfter = textsecure.storage.get('maxPreKeyId', 1);
|
||||
assert.strictEqual(preKeyIdAfter, preKeyIdBefore + 1);
|
||||
|
||||
const testKeyArray = new Uint8Array(testKey.pubKey);
|
||||
assert.isDefined(newBundle);
|
||||
assert.isDefined(newBundle.identityKey);
|
||||
assert.isDefined(newBundle.deviceId);
|
||||
assert.isDefined(newBundle.preKeyId);
|
||||
assert.isDefined(newBundle.signedKeyId);
|
||||
assert.isDefined(newBundle.preKey);
|
||||
assert.isDefined(newBundle.signedKey);
|
||||
assert.isDefined(newBundle.signature);
|
||||
assert.deepEqual(testKeyArray, newBundle.signedKey);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,62 +0,0 @@
|
||||
/* global window, postMessage, textsecure, close */
|
||||
|
||||
/* eslint-disable more/no-then, no-global-assign, no-restricted-globals, no-unused-vars */
|
||||
|
||||
/*
|
||||
* Load this script in a Web Worker to generate new prekeys without
|
||||
* tying up the main thread.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
|
||||
*
|
||||
* Because workers don't have access to the window or localStorage, we
|
||||
* create our own version that proxies back to the caller for actual
|
||||
* storage.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
var myWorker = new Worker('/js/key_worker.js');
|
||||
myWorker.onmessage = function(e) {
|
||||
switch(e.data.method) {
|
||||
case 'set':
|
||||
localStorage.setItem(e.data.key, e.data.value);
|
||||
break;
|
||||
case 'remove':
|
||||
localStorage.removeItem(e.data.key);
|
||||
break;
|
||||
case 'done':
|
||||
console.error(e.data.keys);
|
||||
}
|
||||
};
|
||||
*/
|
||||
let store = {};
|
||||
window.textsecure.storage.impl = {
|
||||
/** ***************************
|
||||
*** Override Storage Routines ***
|
||||
**************************** */
|
||||
put(key, value) {
|
||||
if (value === undefined) {
|
||||
throw new Error('Tried to store undefined');
|
||||
}
|
||||
store[key] = value;
|
||||
postMessage({ method: 'set', key, value });
|
||||
},
|
||||
|
||||
get(key, defaultValue) {
|
||||
if (key in store) {
|
||||
return store[key];
|
||||
}
|
||||
return defaultValue;
|
||||
},
|
||||
|
||||
remove(key) {
|
||||
delete store[key];
|
||||
postMessage({ method: 'remove', key });
|
||||
},
|
||||
};
|
||||
// eslint-disable-next-line no-undef
|
||||
onmessage = e => {
|
||||
store = e.data;
|
||||
textsecure.protocol_wrapper.generateKeys().then(keys => {
|
||||
postMessage({ method: 'done', keys });
|
||||
close();
|
||||
});
|
||||
};
|
File diff suppressed because one or more lines are too long
@ -1,169 +0,0 @@
|
||||
/* global libsignal */
|
||||
|
||||
describe('AccountManager', () => {
|
||||
let accountManager;
|
||||
|
||||
beforeEach(() => {
|
||||
accountManager = new window.textsecure.AccountManager();
|
||||
});
|
||||
|
||||
describe('#cleanSignedPreKeys', () => {
|
||||
let originalProtocolStorage;
|
||||
let signedPreKeys;
|
||||
const DAY = 1000 * 60 * 60 * 24;
|
||||
|
||||
beforeEach(async () => {
|
||||
const identityKey = await libsignal.KeyHelper.generateIdentityKeyPair();
|
||||
|
||||
originalProtocolStorage = window.textsecure.storage.protocol;
|
||||
window.textsecure.storage.protocol = {
|
||||
getIdentityKeyPair() {
|
||||
return identityKey;
|
||||
},
|
||||
loadSignedPreKeys() {
|
||||
return Promise.resolve(signedPreKeys);
|
||||
},
|
||||
};
|
||||
});
|
||||
afterEach(() => {
|
||||
window.textsecure.storage.protocol = originalProtocolStorage;
|
||||
});
|
||||
|
||||
it('keeps three confirmed keys even if over a week old', () => {
|
||||
const now = Date.now();
|
||||
signedPreKeys = [
|
||||
{
|
||||
keyId: 1,
|
||||
created_at: now - DAY * 21,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 2,
|
||||
created_at: now - DAY * 14,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 3,
|
||||
created_at: now - DAY * 18,
|
||||
confirmed: true,
|
||||
},
|
||||
];
|
||||
|
||||
// should be no calls to store.removeSignedPreKey, would cause crash
|
||||
return accountManager.cleanSignedPreKeys();
|
||||
});
|
||||
|
||||
it('eliminates confirmed keys over a week old, if more than three', async () => {
|
||||
const now = Date.now();
|
||||
signedPreKeys = [
|
||||
{
|
||||
keyId: 1,
|
||||
created_at: now - DAY * 21,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 2,
|
||||
created_at: now - DAY * 14,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 3,
|
||||
created_at: now - DAY * 4,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 4,
|
||||
created_at: now - DAY * 18,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 5,
|
||||
created_at: now - DAY,
|
||||
confirmed: true,
|
||||
},
|
||||
];
|
||||
|
||||
let count = 0;
|
||||
window.textsecure.storage.protocol.removeSignedPreKey = keyId => {
|
||||
if (keyId !== 1 && keyId !== 4) {
|
||||
throw new Error(`Wrong keys were eliminated! ${keyId}`);
|
||||
}
|
||||
|
||||
count += 1;
|
||||
};
|
||||
|
||||
await accountManager.cleanSignedPreKeys();
|
||||
assert.strictEqual(count, 2);
|
||||
});
|
||||
|
||||
it('keeps at least three unconfirmed keys if no confirmed', async () => {
|
||||
const now = Date.now();
|
||||
signedPreKeys = [
|
||||
{
|
||||
keyId: 1,
|
||||
created_at: now - DAY * 14,
|
||||
},
|
||||
{
|
||||
keyId: 2,
|
||||
created_at: now - DAY * 21,
|
||||
},
|
||||
{
|
||||
keyId: 3,
|
||||
created_at: now - DAY * 18,
|
||||
},
|
||||
{
|
||||
keyId: 4,
|
||||
created_at: now - DAY,
|
||||
},
|
||||
];
|
||||
|
||||
let count = 0;
|
||||
window.textsecure.storage.protocol.removeSignedPreKey = keyId => {
|
||||
if (keyId !== 2) {
|
||||
throw new Error(`Wrong keys were eliminated! ${keyId}`);
|
||||
}
|
||||
|
||||
count += 1;
|
||||
};
|
||||
|
||||
await accountManager.cleanSignedPreKeys();
|
||||
assert.strictEqual(count, 1);
|
||||
});
|
||||
|
||||
it('if some confirmed keys, keeps unconfirmed to addd up to three total', async () => {
|
||||
const now = Date.now();
|
||||
signedPreKeys = [
|
||||
{
|
||||
keyId: 1,
|
||||
created_at: now - DAY * 21,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 2,
|
||||
created_at: now - DAY * 14,
|
||||
confirmed: true,
|
||||
},
|
||||
{
|
||||
keyId: 3,
|
||||
created_at: now - DAY * 12,
|
||||
},
|
||||
{
|
||||
keyId: 4,
|
||||
created_at: now - DAY * 8,
|
||||
},
|
||||
];
|
||||
|
||||
let count = 0;
|
||||
window.textsecure.storage.protocol.removeSignedPreKey = keyId => {
|
||||
if (keyId !== 3) {
|
||||
throw new Error(`Wrong keys were eliminated! ${keyId}`);
|
||||
}
|
||||
|
||||
count += 1;
|
||||
};
|
||||
|
||||
await accountManager.cleanSignedPreKeys();
|
||||
assert.strictEqual(count, 1);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,156 +0,0 @@
|
||||
/* global libsignal, textsecure */
|
||||
|
||||
describe('Key generation', function thisNeeded() {
|
||||
const count = 10;
|
||||
this.timeout(count * 2000);
|
||||
|
||||
function validateStoredKeyPair(keyPair) {
|
||||
/* Ensure the keypair matches the format used internally by libsignal-protocol */
|
||||
assert.isObject(keyPair, 'Stored keyPair is not an object');
|
||||
assert.instanceOf(keyPair.pubKey, ArrayBuffer);
|
||||
assert.instanceOf(keyPair.privKey, ArrayBuffer);
|
||||
assert.strictEqual(keyPair.pubKey.byteLength, 33);
|
||||
assert.strictEqual(new Uint8Array(keyPair.pubKey)[0], 5);
|
||||
assert.strictEqual(keyPair.privKey.byteLength, 32);
|
||||
}
|
||||
function itStoresPreKey(keyId) {
|
||||
it(`prekey ${keyId} is valid`, () =>
|
||||
textsecure.storage.protocol.loadPreKey(keyId).then(keyPair => {
|
||||
validateStoredKeyPair(keyPair);
|
||||
}));
|
||||
}
|
||||
function itStoresSignedPreKey(keyId) {
|
||||
it(`signed prekey ${keyId} is valid`, () =>
|
||||
textsecure.storage.protocol.loadSignedPreKey(keyId).then(keyPair => {
|
||||
validateStoredKeyPair(keyPair);
|
||||
}));
|
||||
}
|
||||
function validateResultKey(resultKey) {
|
||||
return textsecure.storage.protocol
|
||||
.loadPreKey(resultKey.keyId)
|
||||
.then(keyPair => {
|
||||
assertEqualArrayBuffers(resultKey.publicKey, keyPair.pubKey);
|
||||
});
|
||||
}
|
||||
function validateResultSignedKey(resultSignedKey) {
|
||||
return textsecure.storage.protocol
|
||||
.loadSignedPreKey(resultSignedKey.keyId)
|
||||
.then(keyPair => {
|
||||
assertEqualArrayBuffers(resultSignedKey.publicKey, keyPair.pubKey);
|
||||
});
|
||||
}
|
||||
|
||||
before(() => {
|
||||
localStorage.clear();
|
||||
return libsignal.KeyHelper.generateIdentityKeyPair().then(keyPair =>
|
||||
textsecure.storage.protocol.put('identityKey', keyPair)
|
||||
);
|
||||
});
|
||||
|
||||
describe('the first time', () => {
|
||||
let result;
|
||||
/* result should have this format
|
||||
* {
|
||||
* preKeys: [ { keyId, publicKey }, ... ],
|
||||
* signedPreKey: { keyId, publicKey, signature },
|
||||
* identityKey: <ArrayBuffer>
|
||||
* }
|
||||
*/
|
||||
before(() => {
|
||||
const accountManager = new textsecure.AccountManager('');
|
||||
return accountManager.generateKeys(count).then(res => {
|
||||
result = res;
|
||||
});
|
||||
});
|
||||
for (let i = 1; i <= count; i += 1) {
|
||||
itStoresPreKey(i);
|
||||
}
|
||||
itStoresSignedPreKey(1);
|
||||
|
||||
it(`result contains ${count} preKeys`, () => {
|
||||
assert.isArray(result.preKeys);
|
||||
assert.lengthOf(result.preKeys, count);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
assert.isObject(result.preKeys[i]);
|
||||
}
|
||||
});
|
||||
it('result contains the correct keyIds', () => {
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
assert.strictEqual(result.preKeys[i].keyId, i + 1);
|
||||
}
|
||||
});
|
||||
it('result contains the correct public keys', () =>
|
||||
Promise.all(result.preKeys.map(validateResultKey)));
|
||||
it('returns a signed prekey', () => {
|
||||
assert.strictEqual(result.signedPreKey.keyId, 1);
|
||||
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
|
||||
return validateResultSignedKey(result.signedPreKey);
|
||||
});
|
||||
});
|
||||
describe('the second time', () => {
|
||||
let result;
|
||||
before(() => {
|
||||
const accountManager = new textsecure.AccountManager('');
|
||||
return accountManager.generateKeys(count).then(res => {
|
||||
result = res;
|
||||
});
|
||||
});
|
||||
for (let i = 1; i <= 2 * count; i += 1) {
|
||||
itStoresPreKey(i);
|
||||
}
|
||||
itStoresSignedPreKey(1);
|
||||
itStoresSignedPreKey(2);
|
||||
it(`result contains ${count} preKeys`, () => {
|
||||
assert.isArray(result.preKeys);
|
||||
assert.lengthOf(result.preKeys, count);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
assert.isObject(result.preKeys[i]);
|
||||
}
|
||||
});
|
||||
it('result contains the correct keyIds', () => {
|
||||
for (let i = 1; i <= count; i += 1) {
|
||||
assert.strictEqual(result.preKeys[i - 1].keyId, i + count);
|
||||
}
|
||||
});
|
||||
it('result contains the correct public keys', () =>
|
||||
Promise.all(result.preKeys.map(validateResultKey)));
|
||||
it('returns a signed prekey', () => {
|
||||
assert.strictEqual(result.signedPreKey.keyId, 2);
|
||||
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
|
||||
return validateResultSignedKey(result.signedPreKey);
|
||||
});
|
||||
});
|
||||
describe('the third time', () => {
|
||||
let result;
|
||||
before(() => {
|
||||
const accountManager = new textsecure.AccountManager('');
|
||||
return accountManager.generateKeys(count).then(res => {
|
||||
result = res;
|
||||
});
|
||||
});
|
||||
for (let i = 1; i <= 3 * count; i += 1) {
|
||||
itStoresPreKey(i);
|
||||
}
|
||||
itStoresSignedPreKey(2);
|
||||
itStoresSignedPreKey(3);
|
||||
it(`result contains ${count} preKeys`, () => {
|
||||
assert.isArray(result.preKeys);
|
||||
assert.lengthOf(result.preKeys, count);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
assert.isObject(result.preKeys[i]);
|
||||
}
|
||||
});
|
||||
it('result contains the correct keyIds', () => {
|
||||
for (let i = 1; i <= count; i += 1) {
|
||||
assert.strictEqual(result.preKeys[i - 1].keyId, i + 2 * count);
|
||||
}
|
||||
});
|
||||
it('result contains the correct public keys', () =>
|
||||
Promise.all(result.preKeys.map(validateResultKey)));
|
||||
it('result contains a signed prekey', () => {
|
||||
assert.strictEqual(result.signedPreKey.keyId, 3);
|
||||
assert.instanceOf(result.signedPreKey.signature, ArrayBuffer);
|
||||
return validateResultSignedKey(result.signedPreKey);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,212 +0,0 @@
|
||||
function SignalProtocolStore() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
SignalProtocolStore.prototype = {
|
||||
Direction: { SENDING: 1, RECEIVING: 2 },
|
||||
getIdentityKeyPair() {
|
||||
return Promise.resolve(this.get('identityKey'));
|
||||
},
|
||||
getLocalRegistrationId() {
|
||||
return Promise.resolve(this.get('registrationId'));
|
||||
},
|
||||
put(key, value) {
|
||||
if (
|
||||
key === undefined ||
|
||||
value === undefined ||
|
||||
key === null ||
|
||||
value === null
|
||||
) {
|
||||
throw new Error('Tried to store undefined/null');
|
||||
}
|
||||
this.store[key] = value;
|
||||
},
|
||||
get(key, defaultValue) {
|
||||
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) {
|
||||
if (key === null || key === undefined) {
|
||||
throw new Error('Tried to remove value for undefined/null key');
|
||||
}
|
||||
delete this.store[key];
|
||||
},
|
||||
|
||||
isTrustedIdentity(identifier, identityKey) {
|
||||
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(identityKey === trusted);
|
||||
},
|
||||
loadIdentityKey(identifier) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to get identity key for undefined/null key');
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
resolve(this.get(`identityKey${identifier}`));
|
||||
});
|
||||
},
|
||||
saveIdentity(identifier, identityKey) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to put identity key for undefined/null key');
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
const existing = this.get(`identityKey${identifier}`);
|
||||
this.put(`identityKey${identifier}`, identityKey);
|
||||
if (existing && existing !== identityKey) {
|
||||
resolve(true);
|
||||
} else {
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/* Returns a prekeypair object or undefined */
|
||||
loadPreKey(keyId) {
|
||||
return new Promise(resolve => {
|
||||
const res = this.get(`25519KeypreKey${keyId}`);
|
||||
resolve(res);
|
||||
});
|
||||
},
|
||||
storePreKey(keyId, keyPair, contactPubKey = null) {
|
||||
if (contactPubKey) {
|
||||
const data = {
|
||||
id: keyId,
|
||||
publicKey: keyPair.pubKey,
|
||||
privateKey: keyPair.privKey,
|
||||
recipient: contactPubKey,
|
||||
};
|
||||
return new Promise(resolve => {
|
||||
resolve(this.put(`25519KeypreKey${contactPubKey}`, data));
|
||||
});
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
|
||||
});
|
||||
},
|
||||
removePreKey(keyId) {
|
||||
return new Promise(resolve => {
|
||||
resolve(this.remove(`25519KeypreKey${keyId}`));
|
||||
});
|
||||
},
|
||||
|
||||
/* Returns a signed keypair object or undefined */
|
||||
loadSignedPreKey(keyId) {
|
||||
return new Promise(resolve => {
|
||||
const res = this.get(`25519KeysignedKey${keyId}`);
|
||||
resolve(res);
|
||||
});
|
||||
},
|
||||
loadSignedPreKeys() {
|
||||
return new Promise(resolve => {
|
||||
const res = [];
|
||||
const keys = Object.keys(this.store);
|
||||
for (let i = 0, max = keys.length; i < max; i += 1) {
|
||||
const key = keys[i];
|
||||
if (key.startsWith('25519KeysignedKey')) {
|
||||
res.push(this.store[key]);
|
||||
}
|
||||
}
|
||||
resolve(res);
|
||||
});
|
||||
},
|
||||
storeSignedPreKey(keyId, keyPair) {
|
||||
return new Promise(resolve => {
|
||||
resolve(this.put(`25519KeysignedKey${keyId}`, keyPair));
|
||||
});
|
||||
},
|
||||
removeSignedPreKey(keyId) {
|
||||
return new Promise(resolve => {
|
||||
resolve(this.remove(`25519KeysignedKey${keyId}`));
|
||||
});
|
||||
},
|
||||
|
||||
loadSession(identifier) {
|
||||
return new Promise(resolve => {
|
||||
resolve(this.get(`session${identifier}`));
|
||||
});
|
||||
},
|
||||
storeSession(identifier, record) {
|
||||
return new Promise(resolve => {
|
||||
resolve(this.put(`session${identifier}`, record));
|
||||
});
|
||||
},
|
||||
removeAllSessions(identifier) {
|
||||
return new Promise(resolve => {
|
||||
const keys = Object.keys(this.store);
|
||||
for (let i = 0, max = keys.length; i < max; i += 1) {
|
||||
const key = keys[i];
|
||||
if (key.match(RegExp(`^session${identifier.replace('+', '\\+')}.+`))) {
|
||||
delete this.store[key];
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
getDeviceIds(identifier) {
|
||||
return new Promise(resolve => {
|
||||
const deviceIds = [];
|
||||
const keys = Object.keys(this.store);
|
||||
for (let i = 0, max = keys.length; i < max; i += 1) {
|
||||
const key = keys[i];
|
||||
if (key.match(RegExp(`^session${identifier.replace('+', '\\+')}.+`))) {
|
||||
deviceIds.push(parseInt(key.split('.')[1], 10));
|
||||
}
|
||||
}
|
||||
resolve(deviceIds);
|
||||
});
|
||||
},
|
||||
async loadPreKeyForContact(contactPubKey) {
|
||||
return new Promise(resolve => {
|
||||
const key = this.get(`25519KeypreKey${contactPubKey}`);
|
||||
if (!key) {
|
||||
resolve(undefined);
|
||||
}
|
||||
resolve({
|
||||
pubKey: key.publicKey,
|
||||
privKey: key.privateKey,
|
||||
keyId: key.id,
|
||||
recipient: key.recipient,
|
||||
});
|
||||
});
|
||||
},
|
||||
async storeContactSignedPreKey(pubKey, signedPreKey) {
|
||||
const key = {
|
||||
identityKeyString: pubKey,
|
||||
keyId: signedPreKey.keyId,
|
||||
publicKey: signedPreKey.publicKey,
|
||||
signature: signedPreKey.signature,
|
||||
created_at: Date.now(),
|
||||
confirmed: false,
|
||||
};
|
||||
this.put(`contactSignedPreKey${pubKey}`, key);
|
||||
},
|
||||
async loadContactSignedPreKey(pubKey) {
|
||||
const preKey = this.get(`contactSignedPreKey${pubKey}`);
|
||||
if (preKey) {
|
||||
return {
|
||||
id: preKey.id,
|
||||
identityKeyString: preKey.identityKeyString,
|
||||
publicKey: preKey.publicKey,
|
||||
signature: preKey.signature,
|
||||
created_at: preKey.created_at,
|
||||
keyId: preKey.keyId,
|
||||
confirmed: preKey.confirmed,
|
||||
};
|
||||
}
|
||||
window.log.warn('Failed to fetch contact signed prekey:', pubKey);
|
||||
return undefined;
|
||||
},
|
||||
};
|
@ -1,35 +0,0 @@
|
||||
/* global libsignal, textsecure */
|
||||
|
||||
describe('Protocol Wrapper', function thisNeeded() {
|
||||
const store = textsecure.storage.protocol;
|
||||
const identifier = '+5558675309';
|
||||
|
||||
this.timeout(5000);
|
||||
|
||||
before(done => {
|
||||
localStorage.clear();
|
||||
libsignal.KeyHelper.generateIdentityKeyPair()
|
||||
.then(key => textsecure.storage.protocol.saveIdentity(identifier, key))
|
||||
.then(() => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('processPreKey', () => {
|
||||
it('rejects if the identity key changes', () => {
|
||||
const address = new libsignal.SignalProtocolAddress(identifier, 1);
|
||||
const builder = new libsignal.SessionBuilder(store, address);
|
||||
return builder
|
||||
.processPreKey({
|
||||
identityKey: textsecure.crypto.getRandomBytes(33),
|
||||
encodedNumber: address.toString(),
|
||||
})
|
||||
.then(() => {
|
||||
throw new Error('Allowed to overwrite identity key');
|
||||
})
|
||||
.catch(e => {
|
||||
assert.strictEqual(e.message, 'Identity key changed');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,134 +0,0 @@
|
||||
/* global libsignal, textsecure */
|
||||
|
||||
describe('SignalProtocolStore', () => {
|
||||
before(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
const store = textsecure.storage.protocol;
|
||||
const identifier = '+5558675309';
|
||||
const identityKey = {
|
||||
pubKey: libsignal.crypto.getRandomBytes(33),
|
||||
privKey: libsignal.crypto.getRandomBytes(32),
|
||||
};
|
||||
const testKey = {
|
||||
pubKey: libsignal.crypto.getRandomBytes(33),
|
||||
privKey: libsignal.crypto.getRandomBytes(32),
|
||||
};
|
||||
it('retrieves my registration id', async () => {
|
||||
store.put('registrationId', 1337);
|
||||
|
||||
const reg = await store.getLocalRegistrationId();
|
||||
assert.strictEqual(reg, 1337);
|
||||
});
|
||||
it('retrieves my identity key', async () => {
|
||||
store.put('identityKey', identityKey);
|
||||
const key = await store.getIdentityKeyPair();
|
||||
assertEqualArrayBuffers(key.pubKey, identityKey.pubKey);
|
||||
assertEqualArrayBuffers(key.privKey, identityKey.privKey);
|
||||
});
|
||||
it('stores identity keys', async () => {
|
||||
await store.saveIdentity(identifier, testKey.pubKey);
|
||||
const key = await store.loadIdentityKey(identifier);
|
||||
assertEqualArrayBuffers(key, testKey.pubKey);
|
||||
});
|
||||
it('returns whether a key is trusted', async () => {
|
||||
const newIdentity = libsignal.crypto.getRandomBytes(33);
|
||||
await store.saveIdentity(identifier, testKey.pubKey);
|
||||
|
||||
const trusted = await store.isTrustedIdentity(identifier, newIdentity);
|
||||
if (trusted) {
|
||||
throw new Error('Allowed to overwrite identity key');
|
||||
}
|
||||
});
|
||||
it('returns whether a key is untrusted', async () => {
|
||||
await store.saveIdentity(identifier, testKey.pubKey);
|
||||
const trusted = await store.isTrustedIdentity(identifier, testKey.pubKey);
|
||||
|
||||
if (!trusted) {
|
||||
throw new Error('Allowed to overwrite identity key');
|
||||
}
|
||||
});
|
||||
it('stores prekeys', async () => {
|
||||
await store.storePreKey(1, testKey);
|
||||
|
||||
const key = await store.loadPreKey(1);
|
||||
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
|
||||
assertEqualArrayBuffers(key.privKey, testKey.privKey);
|
||||
});
|
||||
it('deletes prekeys', async () => {
|
||||
await store.storePreKey(2, testKey);
|
||||
await store.removePreKey(2, testKey);
|
||||
|
||||
const key = await store.loadPreKey(2);
|
||||
assert.isUndefined(key);
|
||||
});
|
||||
it('stores signed prekeys', async () => {
|
||||
await store.storeSignedPreKey(3, testKey);
|
||||
|
||||
const key = await store.loadSignedPreKey(3);
|
||||
assertEqualArrayBuffers(key.pubKey, testKey.pubKey);
|
||||
assertEqualArrayBuffers(key.privKey, testKey.privKey);
|
||||
});
|
||||
it('deletes signed prekeys', async () => {
|
||||
await store.storeSignedPreKey(4, testKey);
|
||||
await store.removeSignedPreKey(4, testKey);
|
||||
|
||||
const key = await store.loadSignedPreKey(4);
|
||||
assert.isUndefined(key);
|
||||
});
|
||||
it('stores sessions', async () => {
|
||||
const testRecord = 'an opaque string';
|
||||
const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.'));
|
||||
|
||||
await Promise.all(
|
||||
devices.map(async encodedNumber => {
|
||||
await store.storeSession(encodedNumber, testRecord + encodedNumber);
|
||||
})
|
||||
);
|
||||
|
||||
const records = await Promise.all(
|
||||
devices.map(store.loadSession.bind(store))
|
||||
);
|
||||
|
||||
for (let i = 0, max = records.length; i < max; i += 1) {
|
||||
assert.strictEqual(records[i], testRecord + devices[i]);
|
||||
}
|
||||
});
|
||||
it('removes all sessions for a number', async () => {
|
||||
const testRecord = 'an opaque string';
|
||||
const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.'));
|
||||
|
||||
await Promise.all(
|
||||
devices.map(async encodedNumber => {
|
||||
await store.storeSession(encodedNumber, testRecord + encodedNumber);
|
||||
})
|
||||
);
|
||||
|
||||
await store.removeAllSessions(identifier);
|
||||
|
||||
const records = await Promise.all(
|
||||
devices.map(store.loadSession.bind(store))
|
||||
);
|
||||
|
||||
for (let i = 0, max = records.length; i < max; i += 1) {
|
||||
assert.isUndefined(records[i]);
|
||||
}
|
||||
});
|
||||
it('returns deviceIds for a number', async () => {
|
||||
const testRecord = 'an opaque string';
|
||||
const devices = [1, 2, 3].map(deviceId => [identifier, deviceId].join('.'));
|
||||
|
||||
await Promise.all(
|
||||
devices.map(async encodedNumber => {
|
||||
await store.storeSession(encodedNumber, testRecord + encodedNumber);
|
||||
})
|
||||
);
|
||||
|
||||
const deviceIds = await store.getDeviceIds(identifier);
|
||||
assert.sameMembers(deviceIds, [1, 2, 3]);
|
||||
});
|
||||
it('returns empty array for a number with no device ids', async () => {
|
||||
const deviceIds = await store.getDeviceIds('foo');
|
||||
assert.sameMembers(deviceIds, []);
|
||||
});
|
||||
});
|
@ -1,417 +0,0 @@
|
||||
/* global libsignal, textsecure */
|
||||
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
SecretSessionCipher,
|
||||
createCertificateValidator,
|
||||
_createSenderCertificateFromBuffer,
|
||||
_createServerCertificateFromBuffer,
|
||||
} = window.Signal.Metadata;
|
||||
const {
|
||||
bytesFromString,
|
||||
stringFromBytes,
|
||||
arrayBufferToBase64,
|
||||
} = window.Signal.Crypto;
|
||||
|
||||
function InMemorySignalProtocolStore() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
function toString(thing) {
|
||||
if (typeof thing === 'string') {
|
||||
return thing;
|
||||
}
|
||||
return arrayBufferToBase64(thing);
|
||||
}
|
||||
|
||||
InMemorySignalProtocolStore.prototype = {
|
||||
Direction: {
|
||||
SENDING: 1,
|
||||
RECEIVING: 2,
|
||||
},
|
||||
|
||||
getIdentityKeyPair() {
|
||||
return Promise.resolve(this.get('identityKey'));
|
||||
},
|
||||
getLocalRegistrationId() {
|
||||
return Promise.resolve(this.get('registrationId'));
|
||||
},
|
||||
put(key, value) {
|
||||
if (
|
||||
key === undefined ||
|
||||
value === undefined ||
|
||||
key === null ||
|
||||
value === null
|
||||
) {
|
||||
throw new Error('Tried to store undefined/null');
|
||||
}
|
||||
this.store[key] = value;
|
||||
},
|
||||
get(key, defaultValue) {
|
||||
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) {
|
||||
if (key === null || key === undefined) {
|
||||
throw new Error('Tried to remove value for undefined/null key');
|
||||
}
|
||||
delete this.store[key];
|
||||
},
|
||||
|
||||
isTrustedIdentity(identifier, identityKey) {
|
||||
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) {
|
||||
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, identityKey) {
|
||||
if (identifier === null || identifier === undefined) {
|
||||
throw new Error('Tried to put identity key for undefined/null key');
|
||||
}
|
||||
const address = 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) {
|
||||
let res = this.get(`25519KeypreKey${keyId}`);
|
||||
if (res !== undefined) {
|
||||
res = { pubKey: res.pubKey, privKey: res.privKey };
|
||||
}
|
||||
return Promise.resolve(res);
|
||||
},
|
||||
storePreKey(keyId, keyPair) {
|
||||
return Promise.resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
|
||||
},
|
||||
removePreKey(keyId) {
|
||||
return Promise.resolve(this.remove(`25519KeypreKey${keyId}`));
|
||||
},
|
||||
|
||||
/* Returns a signed keypair object or undefined */
|
||||
loadSignedPreKey(keyId) {
|
||||
let res = this.get(`25519KeysignedKey${keyId}`);
|
||||
if (res !== undefined) {
|
||||
res = { pubKey: res.pubKey, privKey: res.privKey };
|
||||
}
|
||||
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) {
|
||||
return Promise.resolve(this.get(`session${identifier}`));
|
||||
},
|
||||
storeSession(identifier, record) {
|
||||
return Promise.resolve(this.put(`session${identifier}`, record));
|
||||
},
|
||||
removeSession(identifier) {
|
||||
return Promise.resolve(this.remove(`session${identifier}`));
|
||||
},
|
||||
removeAllSessions(identifier) {
|
||||
// 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 libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
trustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
aliceIdentityKey.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
senderCertificate,
|
||||
bytesFromString('smert za smert')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore);
|
||||
|
||||
const decryptResult = await bobCipher.decrypt(
|
||||
createCertificateValidator(trustRoot.pubKey),
|
||||
ciphertext,
|
||||
31335
|
||||
);
|
||||
|
||||
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 libsignal.Curve.async.generateKeyPair();
|
||||
const falseTrustRoot = await libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
falseTrustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
aliceIdentityKey.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
senderCertificate,
|
||||
bytesFromString('и вот я')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore);
|
||||
|
||||
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 libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
trustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
aliceIdentityKey.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
senderCertificate,
|
||||
bytesFromString('и вот я')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore);
|
||||
|
||||
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 libsignal.Curve.async.generateKeyPair();
|
||||
const randomKeyPair = await libsignal.Curve.async.generateKeyPair();
|
||||
const senderCertificate = await _createSenderCertificateFor(
|
||||
trustRoot,
|
||||
'+14151111111',
|
||||
1,
|
||||
randomKeyPair.pubKey,
|
||||
31337
|
||||
);
|
||||
const aliceCipher = new SecretSessionCipher(aliceStore);
|
||||
|
||||
const ciphertext = await aliceCipher.encrypt(
|
||||
new libsignal.SignalProtocolAddress('+14152222222', 1),
|
||||
senderCertificate,
|
||||
bytesFromString('smert za smert')
|
||||
);
|
||||
|
||||
const bobCipher = new SecretSessionCipher(bobStore);
|
||||
|
||||
try {
|
||||
await bobCipher.decrypt(
|
||||
createCertificateValidator(trustRoot.puKey),
|
||||
ciphertext,
|
||||
31335
|
||||
);
|
||||
throw new Error('It did not fail!');
|
||||
} catch (error) {
|
||||
assert.strictEqual(error.message, 'Invalid public key');
|
||||
}
|
||||
});
|
||||
|
||||
// private SenderCertificate _createCertificateFor(
|
||||
// ECKeyPair trustRoot
|
||||
// String sender
|
||||
// int deviceId
|
||||
// ECPublicKey identityKey
|
||||
// long expires
|
||||
// )
|
||||
async function _createSenderCertificateFor(
|
||||
trustRoot,
|
||||
sender,
|
||||
deviceId,
|
||||
identityKey,
|
||||
expires
|
||||
) {
|
||||
const serverKey = await libsignal.Curve.async.generateKeyPair();
|
||||
|
||||
const serverCertificateCertificateProto = new textsecure.protobuf.ServerCertificate.Certificate();
|
||||
serverCertificateCertificateProto.id = 1;
|
||||
serverCertificateCertificateProto.key = serverKey.pubKey;
|
||||
const serverCertificateCertificateBytes = serverCertificateCertificateProto
|
||||
.encode()
|
||||
.toArrayBuffer();
|
||||
|
||||
const serverCertificateSignature = await libsignal.Curve.async.calculateSignature(
|
||||
trustRoot.privKey,
|
||||
serverCertificateCertificateBytes
|
||||
);
|
||||
|
||||
const serverCertificateProto = new textsecure.protobuf.ServerCertificate();
|
||||
serverCertificateProto.certificate = serverCertificateCertificateBytes;
|
||||
serverCertificateProto.signature = serverCertificateSignature;
|
||||
const serverCertificate = _createServerCertificateFromBuffer(
|
||||
serverCertificateProto.encode().toArrayBuffer()
|
||||
);
|
||||
|
||||
const senderCertificateCertificateProto = new textsecure.protobuf.SenderCertificate.Certificate();
|
||||
senderCertificateCertificateProto.sender = sender;
|
||||
senderCertificateCertificateProto.senderDevice = deviceId;
|
||||
senderCertificateCertificateProto.identityKey = identityKey;
|
||||
senderCertificateCertificateProto.expires = expires;
|
||||
senderCertificateCertificateProto.signer = textsecure.protobuf.ServerCertificate.decode(
|
||||
serverCertificate.serialized
|
||||
);
|
||||
const senderCertificateBytes = senderCertificateCertificateProto
|
||||
.encode()
|
||||
.toArrayBuffer();
|
||||
|
||||
const senderCertificateSignature = await libsignal.Curve.async.calculateSignature(
|
||||
serverKey.privKey,
|
||||
senderCertificateBytes
|
||||
);
|
||||
|
||||
const senderCertificateProto = new textsecure.protobuf.SenderCertificate();
|
||||
senderCertificateProto.certificate = senderCertificateBytes;
|
||||
senderCertificateProto.signature = senderCertificateSignature;
|
||||
return _createSenderCertificateFromBuffer(
|
||||
senderCertificateProto.encode().toArrayBuffer()
|
||||
);
|
||||
}
|
||||
|
||||
// private void _initializeSessions(
|
||||
// SignalProtocolStore aliceStore, SignalProtocolStore bobStore)
|
||||
async function _initializeSessions(aliceStore, bobStore) {
|
||||
const aliceAddress = new libsignal.SignalProtocolAddress('+14152222222', 1);
|
||||
await aliceStore.put(
|
||||
'identityKey',
|
||||
await libsignal.Curve.generateKeyPair()
|
||||
);
|
||||
await bobStore.put('identityKey', await libsignal.Curve.generateKeyPair());
|
||||
|
||||
await aliceStore.put('registrationId', 57);
|
||||
await bobStore.put('registrationId', 58);
|
||||
|
||||
const bobPreKey = await libsignal.Curve.async.generateKeyPair();
|
||||
const bobIdentityKey = await bobStore.getIdentityKeyPair();
|
||||
const bobSignedPreKey = await libsignal.KeyHelper.generateSignedPreKey(
|
||||
bobIdentityKey,
|
||||
2
|
||||
);
|
||||
|
||||
const bobBundle = {
|
||||
identityKey: bobIdentityKey.pubKey,
|
||||
registrationId: 1,
|
||||
preKey: {
|
||||
keyId: 1,
|
||||
publicKey: bobPreKey.pubKey,
|
||||
},
|
||||
signedPreKey: {
|
||||
keyId: 2,
|
||||
publicKey: bobSignedPreKey.keyPair.pubKey,
|
||||
signature: bobSignedPreKey.signature,
|
||||
},
|
||||
};
|
||||
const aliceSessionBuilder = new libsignal.SessionBuilder(
|
||||
aliceStore,
|
||||
aliceAddress
|
||||
);
|
||||
await aliceSessionBuilder.processPreKey(bobBundle);
|
||||
|
||||
await bobStore.storeSignedPreKey(2, bobSignedPreKey.keyPair);
|
||||
await bobStore.storePreKey(1, bobPreKey);
|
||||
}
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -1,86 +0,0 @@
|
||||
// tslint:disable-next-line: no-implicit-dependencies
|
||||
import { assert } from 'chai';
|
||||
import { ConversationController } from '../../ts/session/conversations';
|
||||
|
||||
const { libsignal, Whisper } = window;
|
||||
|
||||
describe('KeyChangeListener', () => {
|
||||
const phoneNumberWithKeyChange = '+13016886524'; // nsa
|
||||
const address = new libsignal.SignalProtocolAddress(
|
||||
phoneNumberWithKeyChange,
|
||||
1
|
||||
);
|
||||
const oldKey = libsignal.crypto.getRandomBytes(33);
|
||||
const newKey = libsignal.crypto.getRandomBytes(33);
|
||||
let store: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
store = new window.SignalProtocolStore();
|
||||
await store.hydrateCaches();
|
||||
Whisper.KeyChangeListener.init(store);
|
||||
return store.saveIdentity(address.toString(), oldKey);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
return store.removeIdentityKey(phoneNumberWithKeyChange);
|
||||
});
|
||||
|
||||
describe('When we have a conversation with this contact', () => {
|
||||
// this.timeout(2000);
|
||||
|
||||
let convo: any;
|
||||
before(async () => {
|
||||
convo = ConversationController.getInstance().dangerouslyCreateAndAdd({
|
||||
id: phoneNumberWithKeyChange,
|
||||
type: 'private',
|
||||
} as any);
|
||||
await window.Signal.Data.saveConversation(convo.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await convo.destroyMessages();
|
||||
await window.Signal.Data.saveConversation(convo.id);
|
||||
});
|
||||
|
||||
it('generates a key change notice in the private conversation with this contact', done => {
|
||||
convo.once('newmessage', async () => {
|
||||
await convo.fetchMessages();
|
||||
const message = convo.messageCollection.at(0);
|
||||
assert.strictEqual(message.get('type'), 'keychange');
|
||||
done();
|
||||
});
|
||||
store.saveIdentity(address.toString(), newKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When we have a group with this contact', () => {
|
||||
let convo: any;
|
||||
|
||||
before(async () => {
|
||||
convo = ConversationController.getInstance().dangerouslyCreateAndAdd({
|
||||
id: 'groupId',
|
||||
type: 'group',
|
||||
members: [phoneNumberWithKeyChange],
|
||||
} as any);
|
||||
await window.Signal.Data.saveConversation(convo.attributes, {
|
||||
Conversation: Whisper.Conversation,
|
||||
});
|
||||
});
|
||||
after(async () => {
|
||||
await convo.destroyMessages();
|
||||
await window.Signal.Data.saveConversation(convo.id);
|
||||
});
|
||||
|
||||
it('generates a key change notice in the group conversation with this contact', done => {
|
||||
convo.once('newmessage', async () => {
|
||||
await convo.fetchMessages();
|
||||
const message = convo.messageCollection.at(0);
|
||||
assert.strictEqual(message.get('type'), 'keychange');
|
||||
done();
|
||||
});
|
||||
store.saveIdentity(address.toString(), newKey);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,33 +0,0 @@
|
||||
import { ContentMessage } from './ContentMessage';
|
||||
import { SignalService } from '../../../../protobuf';
|
||||
import * as crypto from 'crypto';
|
||||
import { MessageParams } from '../Message';
|
||||
import { Constants } from '../../..';
|
||||
|
||||
export class SessionEstablishedMessage extends ContentMessage {
|
||||
public readonly padding: Buffer;
|
||||
|
||||
constructor(params: MessageParams) {
|
||||
super({ timestamp: params.timestamp, identifier: params.identifier });
|
||||
// Generate a random int from 1 and 512
|
||||
const buffer = crypto.randomBytes(1);
|
||||
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1;
|
||||
|
||||
// Generate a random padding buffer of the chosen size
|
||||
this.padding = crypto.randomBytes(paddingLength);
|
||||
}
|
||||
public ttl(): number {
|
||||
return Constants.TTL_DEFAULT.SESSION_ESTABLISHED;
|
||||
}
|
||||
|
||||
public contentProto(): SignalService.Content {
|
||||
const nullMessage = new SignalService.NullMessage({});
|
||||
|
||||
nullMessage.padding = this.padding;
|
||||
return new SignalService.Content({
|
||||
nullMessage,
|
||||
});
|
||||
}
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
import { ContentMessage } from './ContentMessage';
|
||||
import { SignalService } from '../../../../protobuf';
|
||||
import { MessageParams } from '../Message';
|
||||
import { Constants } from '../../..';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
export interface PreKeyBundleType {
|
||||
identityKey: Uint8Array;
|
||||
deviceId: number;
|
||||
preKeyId: number;
|
||||
signedKeyId: number;
|
||||
preKey: Uint8Array;
|
||||
signedKey: Uint8Array;
|
||||
signature: Uint8Array;
|
||||
}
|
||||
|
||||
interface SessionRequestParams extends MessageParams {
|
||||
preKeyBundle: PreKeyBundleType;
|
||||
}
|
||||
|
||||
export class SessionRequestMessage extends ContentMessage {
|
||||
private readonly preKeyBundle: PreKeyBundleType;
|
||||
private readonly padding: Buffer;
|
||||
|
||||
constructor(params: SessionRequestParams) {
|
||||
super({ timestamp: params.timestamp, identifier: params.identifier });
|
||||
this.preKeyBundle = params.preKeyBundle;
|
||||
// Generate a random int from 1 and 512
|
||||
const buffer = crypto.randomBytes(1);
|
||||
|
||||
// tslint:disable-next-line: no-bitwise
|
||||
const paddingLength = (new Uint8Array(buffer)[0] & 0x1ff) + 1;
|
||||
|
||||
// Generate a random padding buffer of the chosen size
|
||||
this.padding = crypto.randomBytes(paddingLength);
|
||||
}
|
||||
|
||||
public ttl(): number {
|
||||
return Constants.TTL_DEFAULT.SESSION_REQUEST;
|
||||
}
|
||||
|
||||
public contentProto(): SignalService.Content {
|
||||
const nullMessage = new SignalService.NullMessage({});
|
||||
const preKeyBundleMessage = this.getPreKeyBundleMessage();
|
||||
nullMessage.padding = this.padding;
|
||||
return new SignalService.Content({
|
||||
nullMessage,
|
||||
preKeyBundleMessage,
|
||||
});
|
||||
}
|
||||
|
||||
protected getPreKeyBundleMessage(): SignalService.PreKeyBundleMessage {
|
||||
return new SignalService.PreKeyBundleMessage(this.preKeyBundle);
|
||||
}
|
||||
}
|
@ -1,316 +0,0 @@
|
||||
import { SessionRequestMessage } from '../messages/outgoing';
|
||||
import { createOrUpdateItem, getItemById } from '../../../js/modules/data';
|
||||
import { MessageSender } from '../sending';
|
||||
import { MessageUtils } from '../utils';
|
||||
import { PubKey } from '../types';
|
||||
import { Constants } from '..';
|
||||
|
||||
interface StringToNumberMap {
|
||||
[key: string]: number;
|
||||
}
|
||||
// tslint:disable: no-unnecessary-class
|
||||
export class SessionProtocol {
|
||||
private static dbLoaded: Boolean = false;
|
||||
/**
|
||||
* This map holds the sent session timestamps, i.e. session requests message effectively sent to the recipient.
|
||||
* It is backed by a database entry so it's loaded from db on startup.
|
||||
* This map should not be used directly, but instead through
|
||||
* `updateSendSessionTimestamp()`, or `hasSendSessionRequest()`
|
||||
*/
|
||||
private static sentSessionsTimestamp: StringToNumberMap = {};
|
||||
|
||||
/**
|
||||
* This map olds the processed session timestamps, i.e. when we received a session request and handled it.
|
||||
* It is backed by a database entry so it's loaded from db on startup.
|
||||
* This map should not be used directly, but instead through
|
||||
* `updateProcessedSessionTimestamp()`, `getProcessedSessionRequest()` or `hasProcessedSessionRequest()`
|
||||
*/
|
||||
private static processedSessionsTimestamp: StringToNumberMap = {};
|
||||
|
||||
/**
|
||||
* This map olds the timestamp on which a sent session reset is triggered for a specific device.
|
||||
* Once the message is sent or failed to sent, this device is removed from here.
|
||||
* This is a memory only map. Which means that on app restart it's starts empty.
|
||||
*/
|
||||
private static readonly pendingSendSessionsTimestamp: Set<string> = new Set();
|
||||
|
||||
public static getSentSessionsTimestamp(): Readonly<StringToNumberMap> {
|
||||
return SessionProtocol.sentSessionsTimestamp;
|
||||
}
|
||||
|
||||
public static getProcessedSessionsTimestamp(): Readonly<StringToNumberMap> {
|
||||
return SessionProtocol.processedSessionsTimestamp;
|
||||
}
|
||||
|
||||
public static getPendingSendSessionTimestamp(): Readonly<Set<string>> {
|
||||
return SessionProtocol.pendingSendSessionsTimestamp;
|
||||
}
|
||||
|
||||
/** Returns true if we already have a session with that device */
|
||||
public static async hasSession(pubkey: PubKey): Promise<boolean> {
|
||||
// Session does not use the concept of a deviceId, thus it's always 1
|
||||
const address = new window.libsignal.SignalProtocolAddress(pubkey.key, 1);
|
||||
const sessionCipher = new window.libsignal.SessionCipher(
|
||||
window.textsecure.storage.protocol,
|
||||
address
|
||||
);
|
||||
|
||||
return sessionCipher.hasOpenSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we sent a session request to that device already OR
|
||||
* if a session request to that device is right now being sent.
|
||||
*/
|
||||
public static async hasSentSessionRequest(pubkey: PubKey): Promise<boolean> {
|
||||
const pendingSend = SessionProtocol.pendingSendSessionsTimestamp.has(
|
||||
pubkey.key
|
||||
);
|
||||
const hasSent = await SessionProtocol.hasAlreadySentSessionRequest(
|
||||
pubkey.key
|
||||
);
|
||||
|
||||
return pendingSend || hasSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if any outgoing session requests have expired and re-sends them again if they have.
|
||||
*/
|
||||
public static async checkSessionRequestExpiry(): Promise<any> {
|
||||
await this.fetchFromDBIfNeeded();
|
||||
|
||||
const now = Date.now();
|
||||
const sentTimestamps = Object.entries(this.sentSessionsTimestamp);
|
||||
const promises = sentTimestamps.map(async ([device, sent]) => {
|
||||
const expireTime = sent + Constants.TTL_DEFAULT.SESSION_REQUEST;
|
||||
// Check if we need to send a session request
|
||||
if (now < expireTime) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Unset the timestamp, so that if it fails to send in this function, it will be guaranteed to send later on.
|
||||
await this.updateSentSessionTimestamp(device, undefined);
|
||||
await this.sendSessionRequestIfNeeded(new PubKey(device));
|
||||
});
|
||||
|
||||
return Promise.all(promises) as Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is disabled until we remove it completely once we removed
|
||||
* Triggers a SessionRequestMessage to be sent if:
|
||||
* - we do not already have a session and
|
||||
* - we did not sent a session request already to that device and
|
||||
* - we do not have a session request currently being sent to that device
|
||||
*/
|
||||
public static async sendSessionRequestIfNeeded(
|
||||
pubkey: PubKey
|
||||
): Promise<void> {
|
||||
// if (
|
||||
// (await SessionProtocol.hasSession(pubkey)) ||
|
||||
// (await SessionProtocol.hasSentSessionRequest(pubkey))
|
||||
// ) {
|
||||
// return;
|
||||
// }
|
||||
// const preKeyBundle = await window.libloki.storage.getPreKeyBundleForContact(
|
||||
// pubkey.key
|
||||
// );
|
||||
// const sessionReset = new SessionRequestMessage({
|
||||
// preKeyBundle,
|
||||
// timestamp: Date.now(),
|
||||
// });
|
||||
// try {
|
||||
// await SessionProtocol.sendSessionRequest(sessionReset, pubkey);
|
||||
// } catch (error) {
|
||||
// window.log.warn('Failed to send session request to:', pubkey.key, error);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a session request message to that pubkey.
|
||||
* We store the sent timestamp only if the message is effectively sent.
|
||||
*/
|
||||
public static async sendSessionRequest(
|
||||
message: SessionRequestMessage,
|
||||
pubkey: PubKey
|
||||
): Promise<void> {
|
||||
// const timestamp = Date.now();
|
||||
// // mark the session as being pending send with current timestamp
|
||||
// // so we know we already triggered a new session with that device
|
||||
// // so sendSessionRequestIfNeeded does not sent another session request
|
||||
// SessionProtocol.pendingSendSessionsTimestamp.add(pubkey.key);
|
||||
// try {
|
||||
// const rawMessage = await MessageUtils.toRawMessage(pubkey, message);
|
||||
// await MessageSender.send(rawMessage);
|
||||
// await SessionProtocol.updateSentSessionTimestamp(pubkey.key, timestamp);
|
||||
// } catch (e) {
|
||||
// throw e;
|
||||
// } finally {
|
||||
// SessionProtocol.pendingSendSessionsTimestamp.delete(pubkey.key);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a session is establish so we store on database this info.
|
||||
*/
|
||||
public static async onSessionEstablished(pubkey: PubKey) {
|
||||
// remove our existing sent timestamp for that device
|
||||
return SessionProtocol.updateSentSessionTimestamp(pubkey.key, undefined);
|
||||
}
|
||||
|
||||
public static async shouldProcessSessionRequest(
|
||||
pubkey: PubKey,
|
||||
messageTimestamp: number
|
||||
): Promise<boolean> {
|
||||
const existingSentTimestamp =
|
||||
(await SessionProtocol.getSentSessionRequest(pubkey.key)) || 0;
|
||||
const existingProcessedTimestamp =
|
||||
(await SessionProtocol.getProcessedSessionRequest(pubkey.key)) || 0;
|
||||
|
||||
return (
|
||||
messageTimestamp > existingSentTimestamp &&
|
||||
messageTimestamp > existingProcessedTimestamp
|
||||
);
|
||||
}
|
||||
|
||||
public static async onSessionRequestProcessed(pubkey: PubKey) {
|
||||
return SessionProtocol.updateProcessedSessionTimestamp(
|
||||
pubkey.key,
|
||||
Date.now()
|
||||
);
|
||||
}
|
||||
|
||||
public static reset() {
|
||||
SessionProtocol.dbLoaded = false;
|
||||
SessionProtocol.sentSessionsTimestamp = {};
|
||||
SessionProtocol.processedSessionsTimestamp = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* We only need to fetch once from the database, because we are the only one writing to it
|
||||
*/
|
||||
private static async fetchFromDBIfNeeded(): Promise<void> {
|
||||
if (!SessionProtocol.dbLoaded) {
|
||||
const sentItem = await getItemById('sentSessionsTimestamp');
|
||||
if (sentItem) {
|
||||
SessionProtocol.sentSessionsTimestamp = sentItem.value;
|
||||
} else {
|
||||
SessionProtocol.sentSessionsTimestamp = {};
|
||||
}
|
||||
|
||||
const processedItem = await getItemById('processedSessionsTimestamp');
|
||||
if (processedItem) {
|
||||
SessionProtocol.processedSessionsTimestamp = processedItem.value;
|
||||
} else {
|
||||
SessionProtocol.processedSessionsTimestamp = {};
|
||||
}
|
||||
SessionProtocol.dbLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static async writeToDBSentSessions(): Promise<void> {
|
||||
const data = {
|
||||
id: 'sentSessionsTimestamp',
|
||||
value: SessionProtocol.sentSessionsTimestamp,
|
||||
};
|
||||
|
||||
await createOrUpdateItem(data);
|
||||
}
|
||||
|
||||
private static async writeToDBProcessedSessions(): Promise<void> {
|
||||
const data = {
|
||||
id: 'processedSessionsTimestamp',
|
||||
value: SessionProtocol.processedSessionsTimestamp,
|
||||
};
|
||||
|
||||
await createOrUpdateItem(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a utility function to avoid duplicated code of updateSentSessionTimestamp and updateProcessedSessionTimestamp
|
||||
*/
|
||||
private static async updateSessionTimestamp(
|
||||
device: string,
|
||||
timestamp: number | undefined,
|
||||
map: StringToNumberMap
|
||||
): Promise<boolean> {
|
||||
if (device === undefined) {
|
||||
throw new Error('Device cannot be undefined');
|
||||
}
|
||||
if (map[device] === timestamp) {
|
||||
return false;
|
||||
}
|
||||
if (!timestamp) {
|
||||
// tslint:disable-next-line: no-dynamic-delete
|
||||
delete map[device];
|
||||
} else {
|
||||
map[device] = timestamp;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param device the device id
|
||||
* @param timestamp undefined to remove the key/value pair, otherwise updates the sent timestamp and write to DB
|
||||
*/
|
||||
private static async updateSentSessionTimestamp(
|
||||
device: string,
|
||||
timestamp: number | undefined
|
||||
): Promise<void> {
|
||||
await SessionProtocol.fetchFromDBIfNeeded();
|
||||
if (
|
||||
await SessionProtocol.updateSessionTimestamp(
|
||||
device,
|
||||
timestamp,
|
||||
SessionProtocol.sentSessionsTimestamp
|
||||
)
|
||||
) {
|
||||
await SessionProtocol.writeToDBSentSessions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Timestamp undefined to remove the `key`/`value` pair, otherwise updates the processed timestamp and writes to database
|
||||
*/
|
||||
private static async updateProcessedSessionTimestamp(
|
||||
device: string,
|
||||
timestamp: number | undefined
|
||||
): Promise<void> {
|
||||
await SessionProtocol.fetchFromDBIfNeeded();
|
||||
if (
|
||||
await SessionProtocol.updateSessionTimestamp(
|
||||
device,
|
||||
timestamp,
|
||||
SessionProtocol.processedSessionsTimestamp
|
||||
)
|
||||
) {
|
||||
await SessionProtocol.writeToDBProcessedSessions();
|
||||
}
|
||||
}
|
||||
|
||||
private static async getSentSessionRequest(
|
||||
device: string
|
||||
): Promise<number | undefined> {
|
||||
await SessionProtocol.fetchFromDBIfNeeded();
|
||||
|
||||
return SessionProtocol.sentSessionsTimestamp[device];
|
||||
}
|
||||
|
||||
private static async getProcessedSessionRequest(
|
||||
device: string
|
||||
): Promise<number | undefined> {
|
||||
await SessionProtocol.fetchFromDBIfNeeded();
|
||||
|
||||
return SessionProtocol.processedSessionsTimestamp[device];
|
||||
}
|
||||
|
||||
private static async hasAlreadySentSessionRequest(
|
||||
device: string
|
||||
): Promise<boolean> {
|
||||
await SessionProtocol.fetchFromDBIfNeeded();
|
||||
|
||||
return !!SessionProtocol.sentSessionsTimestamp[device];
|
||||
}
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
/* eslint-disable func-names */
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
// tslint:disable: await-promise
|
||||
// tslint:disable: no-implicit-dependencies
|
||||
// tslint:disable: no-invalid-this
|
||||
|
||||
import { after, before, describe, it } from 'mocha';
|
||||
import { Common } from './common';
|
||||
import { Application } from 'spectron';
|
||||
|
||||
describe('Message Syncing', function() {
|
||||
this.timeout(60000);
|
||||
this.slow(20000);
|
||||
let Alice1: Application;
|
||||
let Bob1: Application;
|
||||
let Alice2: Application;
|
||||
|
||||
// this test suite builds a complex usecase over several tests,
|
||||
// so you need to run all of those tests together (running only one might fail)
|
||||
before(async () => {
|
||||
await Common.killallElectron();
|
||||
await Common.stopStubSnodeServer();
|
||||
|
||||
const alice2Props = {};
|
||||
|
||||
[Alice1, Bob1] = await Common.startAppsAsFriends(); // Alice and Bob are friends
|
||||
|
||||
await Common.addFriendToNewClosedGroup([Alice1, Bob1], false);
|
||||
await Common.joinOpenGroup(
|
||||
Alice1,
|
||||
Common.VALID_GROUP_URL,
|
||||
Common.VALID_GROUP_NAME
|
||||
);
|
||||
|
||||
Alice2 = await Common.startAndStubN(alice2Props, 4); // Alice secondary, just start the app for now. no linking
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await Common.killallElectron();
|
||||
await Common.stopStubSnodeServer();
|
||||
});
|
||||
|
||||
xit('message syncing with 1 friend, 1 closed group, 1 open group', async () => {
|
||||
// Alice1 has:
|
||||
// * no linked device
|
||||
// * Bob is a friend
|
||||
// * one open group
|
||||
// * one closed group with Bob inside
|
||||
|
||||
// Bob1 has:
|
||||
// * no linked device
|
||||
// * Alice as a friend
|
||||
// * one open group with Alice
|
||||
|
||||
// Linking Alice2 to Alice1
|
||||
// alice2 should trigger auto FR with bob1 as it's one of her friend
|
||||
// and alice2 should trigger a FALLBACK_MESSAGE with bob1 as he is in a closed group with her
|
||||
await Common.linkApp2ToApp(Alice1, Alice2, Common.TEST_PUBKEY1);
|
||||
await Common.timeout(25000);
|
||||
|
||||
// validate pubkey of app2 is the set
|
||||
const alice2Pubkey = await Alice2.webContents.executeJavaScript(
|
||||
'window.textsecure.storage.user.getNumber()'
|
||||
);
|
||||
alice2Pubkey.should.have.lengthOf(66);
|
||||
|
||||
const alice1Logs = await Alice1.client.getRenderProcessLogs();
|
||||
const bob1Logs = await Bob1.client.getRenderProcessLogs();
|
||||
const alice2Logs = await Alice2.client.getRenderProcessLogs();
|
||||
|
||||
// validate primary alice
|
||||
await Common.logsContains(
|
||||
alice1Logs,
|
||||
'Sending closed-group-sync-send:outgoing message to OUR SECONDARY PUBKEY',
|
||||
1
|
||||
);
|
||||
await Common.logsContains(
|
||||
alice1Logs,
|
||||
'Sending open-group-sync-send:outgoing message to OUR SECONDARY PUBKEY',
|
||||
1
|
||||
);
|
||||
await Common.logsContains(
|
||||
alice1Logs,
|
||||
'Sending contact-sync-send:outgoing message to OUR SECONDARY PUBKEY',
|
||||
1
|
||||
);
|
||||
|
||||
// validate secondary alice
|
||||
// what is expected is
|
||||
// alice2 receives group sync, contact sync and open group sync
|
||||
// alice2 triggers session request with closed group members and autoFR with contact sync received
|
||||
// once autoFR is auto-accepted, alice2 trigger contact sync
|
||||
await Common.logsContains(
|
||||
alice2Logs,
|
||||
'Got sync group message with group id',
|
||||
1
|
||||
);
|
||||
await Common.logsContains(
|
||||
alice2Logs,
|
||||
'Received GROUP_SYNC with open groups: [chat.getsession.org]',
|
||||
1
|
||||
);
|
||||
await Common.logsContains(
|
||||
alice2Logs,
|
||||
`Sending auto-friend-request:friend-request message to ${Common.TEST_PUBKEY2}`,
|
||||
1
|
||||
);
|
||||
await Common.logsContains(
|
||||
alice2Logs,
|
||||
`Sending session-request:friend-request message to ${Common.TEST_PUBKEY2}`,
|
||||
1
|
||||
);
|
||||
await Common.logsContains(
|
||||
alice2Logs,
|
||||
'Sending contact-sync-send:outgoing message to OUR_PRIMARY_PUBKEY',
|
||||
1
|
||||
);
|
||||
|
||||
// validate primary bob
|
||||
// what is expected is
|
||||
// bob1 receives session request from alice2
|
||||
// bob1 accept auto fr by sending a bg message
|
||||
// once autoFR is auto-accepted, alice2 trigger contact sync
|
||||
await Common.logsContains(
|
||||
bob1Logs,
|
||||
`Received FALLBACK_MESSAGE from source: ${alice2Pubkey}`,
|
||||
1
|
||||
);
|
||||
await Common.logsContains(
|
||||
bob1Logs,
|
||||
`Received AUTO_FRIEND_REQUEST from source: ${alice2Pubkey}`,
|
||||
1
|
||||
);
|
||||
await Common.logsContains(
|
||||
bob1Logs,
|
||||
`Sending auto-friend-accept:onlineBroadcast message to ${alice2Pubkey}`,
|
||||
1
|
||||
);
|
||||
// be sure only one autoFR accept was sent (even if multi device, we need to reply to that specific device only)
|
||||
await Common.logsContains(
|
||||
bob1Logs,
|
||||
'Sending auto-friend-accept:onlineBroadcast message to',
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
@ -1,79 +0,0 @@
|
||||
import { expect } from 'chai';
|
||||
import { beforeEach } from 'mocha';
|
||||
|
||||
import { EndSessionMessage } from '../../../../session/messages/outgoing';
|
||||
import { SignalService } from '../../../../protobuf';
|
||||
import { TextEncoder } from 'util';
|
||||
import { Constants } from '../../../../session';
|
||||
|
||||
describe('EndSessionMessage', () => {
|
||||
let message: EndSessionMessage;
|
||||
const preKeyBundle = {
|
||||
deviceId: 123456,
|
||||
preKeyId: 654321,
|
||||
signedKeyId: 111111,
|
||||
preKey: new TextEncoder().encode('preKey'),
|
||||
signature: new TextEncoder().encode('signature'),
|
||||
signedKey: new TextEncoder().encode('signedKey'),
|
||||
identityKey: new TextEncoder().encode('identityKey'),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const timestamp = Date.now();
|
||||
message = new EndSessionMessage({ timestamp, preKeyBundle });
|
||||
});
|
||||
|
||||
it('has a preKeyBundle', () => {
|
||||
const plainText = message.plainTextBuffer();
|
||||
const decoded = SignalService.Content.decode(plainText);
|
||||
|
||||
expect(decoded.preKeyBundleMessage).to.have.property(
|
||||
'deviceId',
|
||||
preKeyBundle.deviceId
|
||||
);
|
||||
expect(decoded.preKeyBundleMessage).to.have.property(
|
||||
'preKeyId',
|
||||
preKeyBundle.preKeyId
|
||||
);
|
||||
expect(decoded.preKeyBundleMessage).to.have.property(
|
||||
'signedKeyId',
|
||||
preKeyBundle.signedKeyId
|
||||
);
|
||||
|
||||
expect(decoded.preKeyBundleMessage).to.have.deep.property(
|
||||
'signature',
|
||||
preKeyBundle.signature
|
||||
);
|
||||
expect(decoded.preKeyBundleMessage).to.have.deep.property(
|
||||
'signedKey',
|
||||
preKeyBundle.signedKey
|
||||
);
|
||||
expect(decoded.preKeyBundleMessage).to.have.deep.property(
|
||||
'identityKey',
|
||||
preKeyBundle.identityKey
|
||||
);
|
||||
});
|
||||
|
||||
it('has a dataMessage with `END_SESSION` flag and `TERMINATE` as body', () => {
|
||||
const plainText = message.plainTextBuffer();
|
||||
const decoded = SignalService.Content.decode(plainText);
|
||||
|
||||
expect(decoded.dataMessage).to.have.property(
|
||||
'flags',
|
||||
SignalService.DataMessage.Flags.END_SESSION
|
||||
);
|
||||
expect(decoded.dataMessage).to.have.deep.property('body', 'TERMINATE');
|
||||
});
|
||||
|
||||
it('correct ttl', () => {
|
||||
expect(message.ttl()).to.equal(Constants.TTL_DEFAULT.END_SESSION_MESSAGE);
|
||||
});
|
||||
|
||||
it('has an identifier', () => {
|
||||
expect(message.identifier).to.not.equal(null, 'identifier cannot be null');
|
||||
expect(message.identifier).to.not.equal(
|
||||
undefined,
|
||||
'identifier cannot be undefined'
|
||||
);
|
||||
});
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
import { CipherTextObject } from '../../../../../libtextsecure/libsignal-protocol';
|
||||
import { SignalService } from '../../../../protobuf';
|
||||
import { StringUtils } from '../../../../session/utils';
|
||||
|
||||
export class FallBackSessionCipherStub {
|
||||
public async encrypt(buffer: ArrayBuffer): Promise<CipherTextObject> {
|
||||
return {
|
||||
type: SignalService.Envelope.Type.FALLBACK_MESSAGE,
|
||||
body: StringUtils.decode(buffer, 'binary'),
|
||||
};
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import { SignalService } from '../../../../protobuf';
|
||||
import { CipherTextObject } from '../../../../../libtextsecure/libsignal-protocol';
|
||||
import { SecretSessionCipherInterface } from '../../../../../js/modules/metadata/SecretSessionCipher';
|
||||
import { StringUtils } from '../../../../session/utils';
|
||||
|
||||
export class SecretSessionCipherStub implements SecretSessionCipherInterface {
|
||||
public async encrypt(
|
||||
_destinationPubkey: string,
|
||||
_senderCertificate: SignalService.SenderCertificate,
|
||||
innerEncryptedMessage: CipherTextObject
|
||||
): Promise<ArrayBuffer> {
|
||||
const { body } = innerEncryptedMessage;
|
||||
|
||||
return StringUtils.encode(body, 'binary');
|
||||
}
|
||||
|
||||
public async decrypt(
|
||||
_cipherText: ArrayBuffer,
|
||||
_me: { number: string; deviceId: number }
|
||||
): Promise<{
|
||||
isMe?: boolean;
|
||||
sender: string;
|
||||
content: ArrayBuffer;
|
||||
type: SignalService.Envelope.Type;
|
||||
}> {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import {
|
||||
CipherTextObject,
|
||||
SessionCipher,
|
||||
} from '../../../../../libtextsecure/libsignal-protocol';
|
||||
import { SignalService } from '../../../../protobuf';
|
||||
import { StringUtils } from '../../../../session/utils';
|
||||
|
||||
export class SessionCipherStub implements SessionCipher {
|
||||
public storage: any;
|
||||
public address: any;
|
||||
constructor(storage: any, address: any) {
|
||||
this.storage = storage;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
public async encrypt(
|
||||
buffer: ArrayBuffer | Uint8Array
|
||||
): Promise<CipherTextObject> {
|
||||
return {
|
||||
type: SignalService.Envelope.Type.CIPHERTEXT,
|
||||
body: StringUtils.decode(buffer, 'binary'),
|
||||
};
|
||||
}
|
||||
|
||||
public async decryptPreKeyWhisperMessage(
|
||||
buffer: ArrayBuffer | Uint8Array
|
||||
): Promise<ArrayBuffer> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public async decryptWhisperMessage(
|
||||
buffer: ArrayBuffer | Uint8Array
|
||||
): Promise<ArrayBuffer> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public async getRecord(encodedNumber: string): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public async getRemoteRegistrationId(): Promise<number> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public async hasOpenSession(): Promise<boolean> {
|
||||
return false;
|
||||
}
|
||||
|
||||
public async closeOpenSessionForDevice(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public async deleteAllSessionsForDevice(): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export * from './SessionCipherStub';
|
||||
export * from './SecretSessionCipherStub';
|
||||
export * from './FallBackSessionCipherStub';
|
@ -1,2 +1 @@
|
||||
export * from './ciphers';
|
||||
export * from './sending';
|
||||
|
Loading…
Reference in New Issue