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)
This commit is contained in:
Audric Ackermann 2021-01-18 10:58:34 +11:00
parent 979a9058e3
commit 72c96ea998
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4
73 changed files with 35008 additions and 41289 deletions

View File

@ -179,8 +179,6 @@
<!-- CRYPTO -->
<script type='text/javascript' src='js/wall_clock_listener.js'></script>
<script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script>
<script type='text/javascript' src='js/keychange_listener.js'></script>
</head>
<body>
<div class='app-loading-screen'>

View File

@ -182,8 +182,6 @@
<!-- CRYPTO -->
<script type='text/javascript' src='js/wall_clock_listener.js'></script>
<script type='text/javascript' src='js/rotate_signed_prekey_listener.js'></script>
<script type='text/javascript' src='js/keychange_listener.js'></script>
</head>
<body>
<div class='app-loading-screen'>

View File

@ -7,7 +7,6 @@
storage,
textsecure,
Whisper,
libloki,
libsession,
libsignal,
BlockedNumberController,
@ -110,7 +109,6 @@
// start a background worker for ecc
textsecure.startWorker('js/libsignal-protocol-worker.js');
Whisper.KeyChangeListener.init(textsecure.storage.protocol);
let messageReceiver;
Whisper.events = _.clone(Backbone.Events);
Whisper.events.isListenedTo = eventName =>
@ -488,9 +486,6 @@
storage.put('link-preview-setting', false);
});
// listeners
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
connect(true);
});
@ -509,9 +504,6 @@
Whisper.Registration.isDone() &&
!Whisper.Registration.ongoingSecondaryDeviceRegistration()
) {
// listeners
Whisper.RotateSignedPreKeyListener.init(Whisper.events, newVersion);
connect();
appView.openInbox({
initialLoadComplete,
@ -1159,15 +1151,6 @@
window.textsecure.messaging = true;
libsession.Protocols.SessionProtocol.checkSessionRequestExpiry().catch(
e => {
window.log.error(
'Error occured which checking for session request expiry',
e
);
}
);
storage.onready(async () => {
idleDetector.start();
});

View File

@ -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);
});
});
},
};
})();

View File

@ -206,18 +206,10 @@
if (this.isPublic() || this.isMediumGroup()) {
return;
}
// We don't send typing messages if the setting is disabled or we do not have a session
// We don't send typing messages if the setting is disabled
// or we blocked that user
const devicePubkey = new libsession.Types.PubKey(this.id);
const hasSession = await libsession.Protocols.SessionProtocol.hasSession(
devicePubkey
);
if (
!storage.get('typing-indicators-setting') ||
!hasSession ||
this.isBlocked()
) {
if (!storage.get('typing-indicators-setting') || this.isBlocked()) {
return;
}
@ -391,13 +383,17 @@
await Promise.all(messages.map(m => m.setCalculatingPoW()));
},
async onPublicMessageSent(pubKey, timestamp, serverId, serverTimestamp) {
const messages = this.getMessagesWithTimestamp(pubKey, timestamp);
if (messages && messages.length === 1) {
await messages[0].setIsPublic(true);
await messages[0].setServerId(serverId);
await messages[0].setServerTimestamp(serverTimestamp);
async onPublicMessageSent(identifier, serverId, serverTimestamp) {
const registeredMessage = window.getMessageController().get(identifier);
if (!registeredMessage || !registeredMessage.message) {
return null;
}
const model = registeredMessage.message;
await model.setIsPublic(true);
await model.setServerId(serverId);
await model.setServerTimestamp(serverTimestamp);
return undefined;
},
async onNewMessage(message) {

View File

@ -1132,7 +1132,6 @@
e.number === number &&
(e.name === 'MessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError' ||
e.name === 'OutgoingIdentityKeyError')
);
this.set({ errors: errors[1] });
@ -1243,9 +1242,7 @@
async handleMessageSentFailure(sentMessage, error) {
if (error instanceof Error) {
this.saveErrors(error);
if (error.name === 'SignedPreKeyRotationError') {
await window.getAccountManager().rotateSignedPreKey();
} else if (error.name === 'OutgoingIdentityKeyError') {
if (error.name === 'OutgoingIdentityKeyError') {
const c = window.getConversationController().get(sentMessage.device);
await c.getProfiles();
}
@ -1503,10 +1500,7 @@
hasNetworkError() {
const error = _.find(
this.get('errors'),
e =>
e.name === 'MessageError' ||
e.name === 'SendMessageNetworkError' ||
e.name === 'SignedPreKeyRotationError'
e => e.name === 'MessageError' || e.name === 'SendMessageNetworkError'
);
return !!error;
},

View File

@ -231,14 +231,7 @@ async function _finishJob(message, id) {
});
const conversation = message.getConversation();
if (conversation) {
const fromConversation = conversation.messageCollection.get(message.id);
if (fromConversation && message !== fromConversation) {
fromConversation.set(message.attributes);
fromConversation.commit();
} else {
message.commit();
}
message.commit();
}
}

View File

@ -37,11 +37,7 @@ class LokiMessageAPI {
* Temporarily i've made it so `MessageSender` handles open group sends and calls this function for regular sends.
*/
async sendMessage(pubKey, data, messageTimeStamp, ttl, options = {}) {
const {
isPublic = false,
numConnections = DEFAULT_CONNECTIONS,
publicSendData = null,
} = options;
const { isPublic = false, numConnections = DEFAULT_CONNECTIONS } = options;
// Data required to identify a message in a conversation
const messageEventData = {
pubKey,

View File

@ -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,
};

View File

@ -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;
}>;
}

View File

@ -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,
};

View File

@ -1,9 +0,0 @@
import { SecretSessionCipherConstructor } from './metadata/SecretSessionCipher';
interface Metadata {
SecretSessionCipher: SecretSessionCipherConstructor;
}
export interface SignalInterface {
Metadata: Metadata;
}

View File

@ -10,7 +10,6 @@ const OS = require('../../ts/OS');
const Settings = require('./settings');
const Util = require('../../ts/util');
const { migrateToSQL } = require('./migrate_to_sql');
const Metadata = require('./metadata/SecretSessionCipher');
const LinkPreviews = require('./link_previews');
const AttachmentDownloads = require('./attachment_downloads');
@ -299,7 +298,6 @@ exports.setup = (options = {}) => {
Emoji,
IndexedDB,
LinkPreviews,
Metadata,
migrateToSQL,
Migrations,
Notifications,

View File

@ -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);
},
};
})();

View File

@ -188,223 +188,21 @@
window.log.error('Could not load identityKey from SignalData');
return undefined;
},
async getLocalRegistrationId() {
const item = await window.Signal.Data.getItemById('registrationId');
if (item) {
return item.value;
}
return 1;
},
// PreKeys
async loadPreKey(keyId) {
const key = this.preKeys[keyId];
if (key) {
window.log.info('Successfully fetched prekey:', keyId);
return {
pubKey: key.publicKey,
privKey: key.privateKey,
};
}
return undefined;
},
async loadPreKeyForContact(contactPubKey) {
const key = await window.Signal.Data.getPreKeyByRecipient(contactPubKey);
if (key) {
window.log.info(
'Successfully fetched prekey for recipient:',
contactPubKey
);
return {
pubKey: key.publicKey,
privKey: key.privateKey,
keyId: key.id,
recipient: key.recipient,
};
}
return undefined;
},
async storePreKey(keyId, keyPair, contactPubKey) {
const data = {
id: keyId,
publicKey: keyPair.pubKey,
privateKey: keyPair.privKey,
recipient: contactPubKey,
};
this.preKeys[keyId] = data;
await window.Signal.Data.createOrUpdatePreKey(data);
},
async removePreKey(keyId) {
try {
this.trigger('removePreKey');
} catch (error) {
window.log.error(
'removePreKey error triggering removePreKey:',
error && error.stack ? error.stack : error
);
}
delete this.preKeys[keyId];
await window.Signal.Data.removePreKeyById(keyId);
},
async clearPreKeyStore() {
this.preKeys = Object.create(null);
await window.Signal.Data.removeAllPreKeys();
},
// Signed PreKeys
/* Returns a signed keypair object or undefined */
async loadSignedPreKey(keyId) {
const key = this.signedPreKeys[keyId];
if (key) {
window.log.info('Successfully fetched signed prekey:', key.id);
return {
pubKey: key.publicKey,
privKey: key.privateKey,
created_at: key.created_at,
keyId: key.id,
confirmed: key.confirmed,
signature: key.signature,
};
}
window.log.error('Failed to fetch signed prekey:', keyId);
return undefined;
},
async loadSignedPreKeys() {
if (arguments.length > 0) {
throw new Error('loadSignedPreKeys takes no arguments');
}
const keys = Object.values(this.signedPreKeys);
return keys.map(prekey => ({
pubKey: prekey.publicKey,
privKey: prekey.privateKey,
created_at: prekey.created_at,
keyId: prekey.id,
confirmed: prekey.confirmed,
signature: prekey.signature,
}));
},
async storeSignedPreKey(keyId, keyPair, confirmed, signature) {
const data = {
id: keyId,
publicKey: keyPair.pubKey,
privateKey: keyPair.privKey,
created_at: Date.now(),
confirmed: Boolean(confirmed),
signature,
};
this.signedPreKeys[keyId] = data;
await window.Signal.Data.createOrUpdateSignedPreKey(data);
},
async removeSignedPreKey(keyId) {
delete this.signedPreKeys[keyId];
await window.Signal.Data.removeSignedPreKeyById(keyId);
},
async clearSignedPreKeysStore() {
this.signedPreKeys = Object.create(null);
await window.Signal.Data.removeAllSignedPreKeys();
},
// Sessions
async loadSession(encodedNumber) {
if (encodedNumber === null || encodedNumber === undefined) {
throw new Error('Tried to get session for undefined/null number');
}
const session = this.sessions[encodedNumber];
if (session) {
return session.record;
}
return undefined;
},
async storeSession(encodedNumber, record) {
if (encodedNumber === null || encodedNumber === undefined) {
throw new Error('Tried to put session for undefined/null number');
}
const unencoded = textsecure.utils.unencodeNumber(encodedNumber);
const number = unencoded[0];
const deviceId = parseInt(unencoded[1], 10);
const data = {
id: encodedNumber,
number,
deviceId,
record,
};
this.sessions[encodedNumber] = data;
await window.Signal.Data.createOrUpdateSession(data);
},
async getDeviceIds(number) {
if (number === null || number === undefined) {
throw new Error('Tried to get device ids for undefined/null number');
}
const allSessions = Object.values(this.sessions);
const sessions = allSessions.filter(session => session.number === number);
return _.pluck(sessions, 'deviceId');
},
async removeAllSessions(number) {
if (number === null || number === undefined) {
throw new Error('Tried to remove sessions for undefined/null number');
}
const allSessions = Object.values(this.sessions);
for (let i = 0, max = allSessions.length; i < max; i += 1) {
const session = allSessions[i];
if (session.number === number) {
delete this.sessions[session.id];
}
}
await window.Signal.Data.removeSessionsByNumber(number);
},
async archiveSiblingSessions(identifier) {
const address = libsignal.SignalProtocolAddress.fromString(identifier);
const deviceIds = await this.getDeviceIds(address.getName());
const siblings = _.without(deviceIds, address.getDeviceId());
await Promise.all(
siblings.map(async deviceId => {
const sibling = new libsignal.SignalProtocolAddress(
address.getName(),
deviceId
);
window.log.info('closing session for', sibling.toString());
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
sibling
);
await sessionCipher.closeOpenSessionForDevice();
})
);
},
async archiveAllSessions(number) {
const deviceIds = await this.getDeviceIds(number);
await Promise.all(
deviceIds.map(async deviceId => {
const address = new libsignal.SignalProtocolAddress(number, deviceId);
window.log.info('closing session for', address.toString());
const sessionCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address
);
await sessionCipher.closeOpenSessionForDevice();
})
);
},
async clearSessionStore() {
this.sessions = Object.create(null);
window.Signal.Data.removeAllSessions();
@ -412,58 +210,6 @@
// Identity Keys
async isTrustedIdentity(identifier, publicKey, direction) {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to get identity key for undefined/null key');
}
const number = textsecure.utils.unencodeNumber(identifier)[0];
const isOurNumber = number === textsecure.storage.user.getNumber();
const identityRecord = this.identityKeys[number];
if (isOurNumber) {
const existing = identityRecord ? identityRecord.publicKey : null;
return equalArrayBuffers(existing, publicKey);
}
switch (direction) {
case Direction.SENDING:
return this.isTrustedForSending(publicKey, identityRecord);
case Direction.RECEIVING:
return true;
default:
throw new Error(`Unknown direction: ${direction}`);
}
},
isTrustedForSending(publicKey, identityRecord) {
if (!identityRecord) {
window.log.info(
'isTrustedForSending: No previous record, returning true...'
);
return true;
}
const existing = identityRecord.publicKey;
if (!existing) {
window.log.info('isTrustedForSending: Nothing here, returning true...');
return true;
}
if (!equalArrayBuffers(existing, publicKey)) {
window.log.info("isTrustedForSending: Identity keys don't match...");
return false;
}
if (identityRecord.verified === VerifiedStatus.UNVERIFIED) {
window.log.error('Needs unverified approval!');
return false;
}
if (this.isNonBlockingApprovalRequired(identityRecord)) {
window.log.error('isTrustedForSending: Needs non-blocking approval!');
return false;
}
return true;
},
async loadIdentityKey(identifier) {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to get identity key for undefined/null key');

3
libloki/crypto.d.ts vendored
View File

@ -10,9 +10,6 @@ export interface CryptoInterface {
DHEncrypt: any;
DecryptGCM: any; // AES-GCM
EncryptGCM: any; // AES-GCM
FallBackDecryptionError: any;
FallBackSessionCipher: any;
LokiSessionCipher: any;
PairingType: PairingTypeEnum;
_decodeSnodeAddressToPubKey: any;
decryptForPubkey: any;

View File

@ -15,8 +15,6 @@
(function() {
window.libloki = window.libloki || {};
class FallBackDecryptionError extends Error {}
const IV_LENGTH = 16;
const NONCE_LENGTH = 12;
@ -131,54 +129,6 @@
return libsignal.crypto.decrypt(symmetricKey, ciphertext, iv);
}
class FallBackSessionCipher {
constructor(address) {
this.identityKeyString = address.getName();
this.pubKey = StringView.hexToArrayBuffer(address.getName());
}
// Should we use ephemeral key pairs here rather than long term keys on each side?
async encrypt(plaintext) {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
if (!myKeyPair) {
throw new Error('Failed to get keypair for encryption');
}
const myPrivateKey = myKeyPair.privKey;
const symmetricKey = await libsignal.Curve.async.calculateAgreement(
this.pubKey,
myPrivateKey
);
const ivAndCiphertext = await DHEncrypt(symmetricKey, plaintext);
const binaryIvAndCiphertext = dcodeIO.ByteBuffer.wrap(
ivAndCiphertext
).toString('binary');
return {
type: textsecure.protobuf.Envelope.Type.FALLBACK_MESSAGE,
body: binaryIvAndCiphertext,
registrationId: undefined,
};
}
async decrypt(ivAndCiphertext) {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
if (!myKeyPair) {
throw new Error('Failed to get keypair for decryption');
}
const myPrivateKey = myKeyPair.privKey;
const symmetricKey = await libsignal.Curve.async.calculateAgreement(
this.pubKey,
myPrivateKey
);
try {
return await DHDecrypt(symmetricKey, ivAndCiphertext);
} catch (e) {
throw new FallBackDecryptionError(
`Could not decrypt message from ${this.identityKeyString} using FallBack encryption.`
);
}
}
}
const base32zIndex = Multibase.names.indexOf('base32z');
const base32zCode = Multibase.codes[base32zIndex];
@ -354,167 +304,17 @@
GRANT: 2,
});
/**
* A wrapper around Signal's SessionCipher.
* This handles specific session reset logic that we need.
*/
class LokiSessionCipher {
constructor(storage, protocolAddress) {
this.storage = storage;
this.protocolAddress = protocolAddress;
this.sessionCipher = new libsignal.SessionCipher(
storage,
protocolAddress
);
this.TYPE = Object.freeze({
MESSAGE: 1,
PREKEY: 2,
});
}
decryptWhisperMessage(buffer, encoding) {
return this._decryptMessage(this.TYPE.MESSAGE, buffer, encoding);
}
decryptPreKeyWhisperMessage(buffer, encoding) {
return this._decryptMessage(this.TYPE.PREKEY, buffer, encoding);
}
async _decryptMessage(type, buffer, encoding) {
// Capture active session
const activeSessionBaseKey = await this._getCurrentSessionBaseKey();
if (type === this.TYPE.PREKEY && !activeSessionBaseKey) {
const wrapped = dcodeIO.ByteBuffer.wrap(buffer);
await window.libloki.storage.verifyFriendRequestAcceptPreKey(
this.protocolAddress.getName(),
wrapped
);
}
const decryptFunction =
type === this.TYPE.PREKEY
? this.sessionCipher.decryptPreKeyWhisperMessage
: this.sessionCipher.decryptWhisperMessage;
const result = await decryptFunction(buffer, encoding);
// Handle session reset
// This needs to be done synchronously so that the next time we decrypt a message,
// we have the correct session
try {
await this._handleSessionResetIfNeeded(activeSessionBaseKey);
} catch (e) {
window.log.info('Failed to handle session reset: ', e);
}
return result;
}
async _handleSessionResetIfNeeded(previousSessionBaseKey) {
if (!previousSessionBaseKey) {
return;
}
let conversation;
try {
conversation = await window
.getConversationController()
.getOrCreateAndWait(this.protocolAddress.getName(), 'private');
} catch (e) {
window.log.info(
'Error getting conversation: ',
this.protocolAddress.getName()
);
return;
}
if (conversation.isSessionResetOngoing()) {
const currentSessionBaseKey = await this._getCurrentSessionBaseKey();
if (currentSessionBaseKey !== previousSessionBaseKey) {
if (conversation.isSessionResetReceived()) {
// The other user used an old session to contact us; wait for them to switch to a new one.
await this._restoreSession(previousSessionBaseKey);
} else {
// Our session reset was successful; we initiated one and got a new session back from the other user.
await this._deleteAllSessionExcept(currentSessionBaseKey);
await conversation.onNewSessionAdopted();
}
} else if (conversation.isSessionResetReceived()) {
// Our session reset was successful; we received a message with the same session from the other user.
await this._deleteAllSessionExcept(previousSessionBaseKey);
await conversation.onNewSessionAdopted();
}
}
}
async _getCurrentSessionBaseKey() {
const record = await this.sessionCipher.getRecord(
this.protocolAddress.toString()
);
if (!record) {
return null;
}
const openSession = record.getOpenSession();
if (!openSession) {
return null;
}
const { baseKey } = openSession.indexInfo;
return baseKey;
}
async _restoreSession(sessionBaseKey) {
const record = await this.sessionCipher.getRecord(
this.protocolAddress.toString()
);
if (!record) {
return;
}
record.archiveCurrentState();
const sessionToRestore = record.sessions[sessionBaseKey];
if (!sessionToRestore) {
throw new Error(`Cannot find session with base key ${sessionBaseKey}`);
}
record.promoteState(sessionToRestore);
record.updateSessionState(sessionToRestore);
await this.storage.storeSession(
this.protocolAddress.toString(),
record.serialize()
);
}
async _deleteAllSessionExcept(sessionBaseKey) {
const record = await this.sessionCipher.getRecord(
this.protocolAddress.toString()
);
if (!record) {
return;
}
const sessionToKeep = record.sessions[sessionBaseKey];
record.sessions = {};
record.updateSessionState(sessionToKeep);
await this.storage.storeSession(
this.protocolAddress.toString(),
record.serialize()
);
}
}
window.libloki.crypto = {
DHEncrypt,
EncryptGCM, // AES-GCM
DHDecrypt,
DecryptGCM, // AES-GCM
FallBackSessionCipher,
FallBackDecryptionError,
decryptToken,
generateSignatureForPairing,
verifyPairingSignature,
verifyAuthorisation,
validateAuthorisation,
PairingType,
LokiSessionCipher,
generateEphemeralKeyPair,
encryptForPubkey,
decryptForPubkey,

View File

@ -1,85 +1,9 @@
/* global window, libsignal, textsecure */
/* global window */
// eslint-disable-next-line func-names
(function() {
window.libloki = window.libloki || {};
async function getPreKeyBundleForContact(pubKey) {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const identityKey = myKeyPair.pubKey;
// Retrieve ids. The ids stored are always the latest generated + 1
const signedKeyId = textsecure.storage.get('signedKeyId', 2) - 1;
const [signedKey, preKey] = await Promise.all([
textsecure.storage.protocol.loadSignedPreKey(signedKeyId),
new Promise(async resolve => {
// retrieve existing prekey if we already generated one for that recipient
const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContact(
pubKey
);
if (storedPreKey) {
resolve({ pubKey: storedPreKey.pubKey, keyId: storedPreKey.keyId });
} else {
// generate and store new prekey
const preKeyId = textsecure.storage.get('maxPreKeyId', 1);
textsecure.storage.put('maxPreKeyId', preKeyId + 1);
const newPreKey = await libsignal.KeyHelper.generatePreKey(preKeyId);
await textsecure.storage.protocol.storePreKey(
newPreKey.keyId,
newPreKey.keyPair,
pubKey
);
resolve({ pubKey: newPreKey.keyPair.pubKey, keyId: preKeyId });
}
}),
]);
return {
identityKey: new Uint8Array(identityKey),
deviceId: 1, // TODO: fetch from somewhere
preKeyId: preKey.keyId,
signedKeyId,
preKey: new Uint8Array(preKey.pubKey),
signedKey: new Uint8Array(signedKey.pubKey),
signature: new Uint8Array(signedKey.signature),
};
}
async function removeContactPreKeyBundle(pubKey) {
await Promise.all([
textsecure.storage.protocol.removeContactPreKey(pubKey),
textsecure.storage.protocol.removeContactSignedPreKey(pubKey),
]);
}
async function verifyFriendRequestAcceptPreKey(pubKey, buffer) {
const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContact(
pubKey
);
if (!storedPreKey) {
throw new Error(
'Received a friend request from a pubkey for which no prekey bundle was created'
);
}
// need to pop the version
// eslint-disable-next-line no-unused-vars
const version = buffer.readUint8();
const preKeyProto = window.textsecure.protobuf.PreKeyWhisperMessage.decode(
buffer
);
if (!preKeyProto) {
throw new Error(
'Could not decode PreKeyWhisperMessage while attempting to match the preKeyId'
);
}
const { preKeyId } = preKeyProto;
if (storedPreKey.keyId !== preKeyId) {
throw new Error(
'Received a preKeyWhisperMessage (friend request accept) from an unknown source'
);
}
}
function getGuardNodes() {
return window.Signal.Data.getGuardNodes();
}
@ -89,134 +13,7 @@
}
window.libloki.storage = {
getPreKeyBundleForContact,
removeContactPreKeyBundle,
verifyFriendRequestAcceptPreKey,
getGuardNodes,
updateGuardNodes,
};
// Libloki protocol store
const store = window.SignalProtocolStore.prototype;
store.storeContactPreKey = async (pubKey, preKey) => {
const key = {
// id: (autoincrement)
identityKeyString: pubKey,
publicKey: preKey.publicKey,
keyId: preKey.keyId,
};
await window.Signal.Data.createOrUpdateContactPreKey(key);
};
store.loadContactPreKey = async pubKey => {
const preKey = await window.Signal.Data.getContactPreKeyByIdentityKey(
pubKey
);
if (preKey) {
return {
id: preKey.id,
keyId: preKey.keyId,
publicKey: preKey.publicKey,
identityKeyString: preKey.identityKeyString,
};
}
window.log.warn('Failed to fetch contact prekey:', pubKey);
return undefined;
};
store.loadContactPreKeys = async filters => {
const { keyId, identityKeyString } = filters;
const keys = await window.Signal.Data.getContactPreKeys(
keyId,
identityKeyString
);
if (keys) {
return keys.map(preKey => ({
id: preKey.id,
keyId: preKey.keyId,
publicKey: preKey.publicKey,
identityKeyString: preKey.identityKeyString,
}));
}
window.log.warn('Failed to fetch signed prekey with filters', filters);
return undefined;
};
store.removeContactPreKey = async pubKey => {
await window.Signal.Data.removeContactPreKeyByIdentityKey(pubKey);
};
store.clearContactPreKeysStore = async () => {
await window.Signal.Data.removeAllContactPreKeys();
};
store.storeContactSignedPreKey = async (pubKey, signedPreKey) => {
const key = {
// id: (autoincrement)
identityKeyString: pubKey,
keyId: signedPreKey.keyId,
publicKey: signedPreKey.publicKey,
signature: signedPreKey.signature,
created_at: Date.now(),
confirmed: false,
};
await window.Signal.Data.createOrUpdateContactSignedPreKey(key);
};
store.loadContactSignedPreKey = async pubKey => {
const preKey = await window.Signal.Data.getContactSignedPreKeyByIdentityKey(
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;
};
store.loadContactSignedPreKeys = async filters => {
const { keyId, identityKeyString } = filters;
const keys = await window.Signal.Data.getContactSignedPreKeys(
keyId,
identityKeyString
);
if (keys) {
return keys.map(preKey => ({
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 with filters',
filters
);
return undefined;
};
store.removeContactSignedPreKey = async pubKey => {
await window.Signal.Data.removeContactSignedPreKeyByIdentityKey(pubKey);
};
store.clearContactSignedPreKeysStore = async () => {
await window.Signal.Data.removeAllContactSignedPreKeys();
};
})();

View File

@ -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);
});
});
});

View File

@ -28,10 +28,8 @@
<script type="text/javascript" src="../service_nodes.js" data-cover></script>
<script type="text/javascript" src="../storage.js" data-cover></script>
<script type="text/javascript" src="crypto_test.js"></script>
<script type="text/javascript" src="proof-of-work_test.js"></script>
<script type="text/javascript" src="service_nodes_test.js"></script>
<script type="text/javascript" src="storage_test.js"></script>
<script type="text/javascript" src="messages.js"></script>
<!-- Comment out to turn off code coverage. Useful for getting real callstacks. -->

View File

@ -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);
});
});
});

View File

@ -49,8 +49,6 @@
registerSingleDevice(mnemonic, mnemonicLanguage, profileName) {
const createAccount = this.createAccount.bind(this);
const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
const generateKeys = this.generateKeys.bind(this, 0);
const confirmKeys = this.confirmKeys.bind(this);
const registrationDone = this.registrationDone.bind(this);
let generateKeypair;
if (mnemonic) {
@ -73,8 +71,6 @@
createAccount(identityKeyPair)
.then(() => this.saveRecoveryPhrase(mnemonic))
.then(clearSessionsAndPreKeys)
.then(generateKeys)
.then(confirmKeys)
.then(() => {
const pubKeyString = StringView.arrayBufferToHex(
identityKeyPair.pubKey
@ -84,163 +80,16 @@
)
);
},
rotateSignedPreKey() {
return this.queueTask(() => {
const signedKeyId = textsecure.storage.get('signedKeyId', 1);
if (typeof signedKeyId !== 'number') {
throw new Error('Invalid signedKeyId');
}
const store = textsecure.storage.protocol;
const { cleanSignedPreKeys } = this;
return store
.getIdentityKeyPair()
.then(
identityKey =>
libsignal.KeyHelper.generateSignedPreKey(
identityKey,
signedKeyId
),
() => {
// We swallow any error here, because we don't want to get into
// a loop of repeated retries.
window.log.error(
'Failed to get identity key. Canceling key rotation.'
);
}
)
.then(res => {
if (!res) {
return null;
}
window.log.info('Saving new signed prekey', res.keyId);
return Promise.all([
textsecure.storage.put('signedKeyId', signedKeyId + 1),
store.storeSignedPreKey(
res.keyId,
res.keyPair,
undefined,
res.signature
),
])
.then(() => {
const confirmed = true;
window.log.info('Confirming new signed prekey', res.keyId);
return Promise.all([
textsecure.storage.remove('signedKeyRotationRejected'),
store.storeSignedPreKey(
res.keyId,
res.keyPair,
confirmed,
res.signature
),
]);
})
.then(() => cleanSignedPreKeys());
})
.catch(e => {
window.log.error(
'rotateSignedPrekey error:',
e && e.stack ? e.stack : e
);
if (
e instanceof Error &&
e.name === 'HTTPError' &&
e.code >= 400 &&
e.code <= 599
) {
const rejections =
1 + textsecure.storage.get('signedKeyRotationRejected', 0);
textsecure.storage.put('signedKeyRotationRejected', rejections);
window.log.error(
'Signed key rotation rejected count:',
rejections
);
} else {
throw e;
}
});
});
},
queueTask(task) {
const taskWithTimeout = textsecure.createTaskWithTimeout(task);
this.pending = this.pending.then(taskWithTimeout, taskWithTimeout);
return this.pending;
},
cleanSignedPreKeys() {
const MINIMUM_KEYS = 3;
const store = textsecure.storage.protocol;
return store.loadSignedPreKeys().then(allKeys => {
allKeys.sort((a, b) => (a.created_at || 0) - (b.created_at || 0));
allKeys.reverse(); // we want the most recent first
let confirmed = allKeys.filter(key => key.confirmed);
const unconfirmed = allKeys.filter(key => !key.confirmed);
const recent = allKeys[0] ? allKeys[0].keyId : 'none';
const recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none';
window.log.info(`Most recent signed key: ${recent}`);
window.log.info(`Most recent confirmed signed key: ${recentConfirmed}`);
window.log.info(
'Total signed key count:',
allKeys.length,
'-',
confirmed.length,
'confirmed'
);
let confirmedCount = confirmed.length;
// Keep MINIMUM_KEYS confirmed keys, then drop if older than a week
confirmed = confirmed.forEach((key, index) => {
if (index < MINIMUM_KEYS) {
return;
}
const createdAt = key.created_at || 0;
const age = Date.now() - createdAt;
if (age > ARCHIVE_AGE) {
window.log.info(
'Removing confirmed signed prekey:',
key.keyId,
'with timestamp:',
createdAt
);
store.removeSignedPreKey(key.keyId);
confirmedCount -= 1;
}
});
const stillNeeded = MINIMUM_KEYS - confirmedCount;
// If we still don't have enough total keys, we keep as many unconfirmed
// keys as necessary. If not necessary, and over a week old, we drop.
unconfirmed.forEach((key, index) => {
if (index < stillNeeded) {
return;
}
const createdAt = key.created_at || 0;
const age = Date.now() - createdAt;
if (age > ARCHIVE_AGE) {
window.log.info(
'Removing unconfirmed signed prekey:',
key.keyId,
'with timestamp:',
createdAt
);
store.removeSignedPreKey(key.keyId);
}
});
});
},
async createAccount(identityKeyPair, userAgent, readReceipts) {
const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
let password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
password = password.substring(0, password.length - 2);
const registrationId = libsignal.KeyHelper.generateRegistrationId();
await Promise.all([
textsecure.storage.remove('identityKey'),
@ -272,7 +121,6 @@
await textsecure.storage.put('identityKey', identityKeyPair);
await textsecure.storage.put('signaling_key', signalingKey);
await textsecure.storage.put('password', password);
await textsecure.storage.put('registrationId', registrationId);
if (userAgent) {
await textsecure.storage.put('userAgent', userAgent);
}
@ -291,12 +139,8 @@
async clearSessionsAndPreKeys() {
const store = textsecure.storage.protocol;
window.log.info('clearing all sessions, prekeys, and signed prekeys');
await Promise.all([
store.clearContactPreKeysStore(),
store.clearContactSignedPreKeysStore(),
store.clearSessionStore(),
]);
window.log.info('clearing all sessions');
await Promise.all([store.clearSessionStore()]);
// During secondary device registration we need to keep our prekeys sent
// to other pubkeys
if (textsecure.storage.get('secondaryDeviceStatus') !== 'ongoing') {
@ -306,83 +150,6 @@
]);
}
},
// Takes the same object returned by generateKeys
async confirmKeys(keys) {
const store = textsecure.storage.protocol;
const key = keys.signedPreKey;
const confirmed = true;
window.log.info('confirmKeys: confirming key', key.keyId);
await store.storeSignedPreKey(
key.keyId,
key.keyPair,
confirmed,
key.signature
);
},
generateKeys(count, providedProgressCallback) {
const progressCallback =
typeof providedProgressCallback === 'function'
? providedProgressCallback
: null;
const startId = textsecure.storage.get('maxPreKeyId', 1);
const signedKeyId = textsecure.storage.get('signedKeyId', 1);
if (typeof startId !== 'number') {
throw new Error('Invalid maxPreKeyId');
}
if (typeof signedKeyId !== 'number') {
throw new Error('Invalid signedKeyId');
}
const store = textsecure.storage.protocol;
return store.getIdentityKeyPair().then(identityKey => {
const result = { preKeys: [], identityKey: identityKey.pubKey };
const promises = [];
for (let keyId = startId; keyId < startId + count; keyId += 1) {
promises.push(
libsignal.KeyHelper.generatePreKey(keyId).then(res => {
store.storePreKey(res.keyId, res.keyPair);
result.preKeys.push({
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
});
if (progressCallback) {
progressCallback();
}
})
);
}
promises.push(
libsignal.KeyHelper.generateSignedPreKey(
identityKey,
signedKeyId
).then(res => {
store.storeSignedPreKey(
res.keyId,
res.keyPair,
undefined,
res.signature
);
result.signedPreKey = {
keyId: res.keyId,
publicKey: res.keyPair.pubKey,
signature: res.signature,
keyPair: res.keyPair,
};
})
);
textsecure.storage.put('maxPreKeyId', startId + count);
textsecure.storage.put('signedKeyId', signedKeyId + 1);
return Promise.all(promises).then(() =>
// This is primarily for the signed prekey summary it logs out
this.cleanSignedPreKeys().then(() => result)
);
});
},
async generateMnemonic(language = 'english') {
// Note: 4 bytes are converted into 3 seed words, so length 12 seed words
// (13 - 1 checksum) are generated using 12 * 4 / 3 = 16 bytes.

View File

@ -72,14 +72,6 @@
}
inherit(ReplayableError, SendMessageNetworkError);
function SignedPreKeyRotationError() {
ReplayableError.call(this, {
name: 'SignedPreKeyRotationError',
message: 'Too many signed prekey rotation failures',
});
}
inherit(ReplayableError, SignedPreKeyRotationError);
function MessageError(message, httpError) {
this.code = httpError.code;
@ -218,7 +210,6 @@
window.textsecure.OutgoingIdentityKeyError = OutgoingIdentityKeyError;
window.textsecure.ReplayableError = ReplayableError;
window.textsecure.MessageError = MessageError;
window.textsecure.SignedPreKeyRotationError = SignedPreKeyRotationError;
window.textsecure.EmptySwarmError = EmptySwarmError;
window.textsecure.SeedNodeError = SeedNodeError;
window.textsecure.DNSResolutionError = DNSResolutionError;

View File

@ -9,7 +9,6 @@ export interface LibTextsecure {
OutgoingIdentityKeyError: any;
ReplayableError: any;
MessageError: any;
SignedPreKeyRotationError: any;
EmptySwarmError: any;
SeedNodeError: any;
DNSResolutionError: any;

View File

@ -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();
});
};

View File

@ -5,7 +5,6 @@ export type BinaryString = string;
export type CipherTextObject = {
type: SignalService.Envelope.Type;
body: BinaryString;
registrationId?: number;
};
export interface SignalProtocolAddressConstructor {
new (hexEncodedPublicKey: string, deviceId: number): SignalProtocolAddress;
@ -83,15 +82,6 @@ export interface CryptoInterface {
export interface KeyHelperInterface {
generateIdentityKeyPair(): Promise<KeyPair>;
generateRegistrationId(): number;
generateSignedPreKey(
identityKeyPair: KeyPair,
signedKeyId: number
): Promise<{
keyId: number;
keyPair: KeyPair;
signature: ArrayBuffer;
}>;
generatePreKey(
keyId: number
): Promise<{
@ -100,30 +90,9 @@ export interface KeyHelperInterface {
}>;
}
export type SessionCipherConstructor = new (
storage: any,
remoteAddress: SignalProtocolAddress
) => SessionCipher;
export interface SessionCipher {
/**
* @returns The envelope type, registration id and binary encoded encrypted body.
*/
encrypt(buffer: ArrayBuffer | Uint8Array): Promise<CipherTextObject>;
decryptPreKeyWhisperMessage(
buffer: ArrayBuffer | Uint8Array
): Promise<ArrayBuffer>;
decryptWhisperMessage(buffer: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>;
getRecord(encodedNumber: string): Promise<any | undefined>;
getRemoteRegistrationId(): Promise<number>;
hasOpenSession(): Promise<boolean>;
closeOpenSessionForDevice(): Promise<void>;
deleteAllSessionsForDevice(): Promise<void>;
}
export interface LibsignalProtocol {
SignalProtocolAddress: SignalProtocolAddressConstructor;
Curve: CurveInterface;
crypto: CryptoInterface;
KeyHelper: KeyHelperInterface;
SessionCipher: SessionCipherConstructor;
}

File diff suppressed because one or more lines are too long

View File

@ -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);
});
});
});

View File

@ -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);
});
});
});

View File

@ -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;
},
};

View File

@ -34,11 +34,7 @@
<script type="text/javascript" src="errors_test.js"></script>
<script type="text/javascript" src="helpers_test.js"></script>
<script type="text/javascript" src="storage_test.js"></script>
<script type="text/javascript" src="crypto_test.js"></script>
<script type="text/javascript" src="protocol_wrapper_test.js"></script>
<script type="text/javascript" src="contacts_parser_test.js"></script>
<script type="text/javascript" src="generate_keys_test.js"></script>
<script type="text/javascript" src="websocket-resources_test.js"></script>
<script type="text/javascript" src="task_with_timeout_test.js"></script>
<script type="text/javascript" src="account_manager_test.js"></script>

View File

@ -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');
});
});
});
});

View File

@ -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, []);
});
});

View File

@ -635,33 +635,6 @@
text-align: center;
}
// Module: Safety Number Notification
.module-safety-number-notification {
margin-top: 14px;
text-align: center;
}
.module-safety-number-notification__icon {
height: 24px;
width: 24px;
margin-inline-start: auto;
margin-inline-end: auto;
margin-bottom: 7px;
@include color-svg('../images/shield.svg', $color-gray-60);
}
.module-safety-number-notification__text {
font-size: 14px;
line-height: 20px;
letter-spacing: 0.3px;
color: $color-gray-60;
}
.module-safety-number-notification__contact {
font-weight: 300;
}
.module-verification-notification__button {
margin-top: 5px;
display: inline-block;

View File

@ -1356,70 +1356,3 @@ input {
padding: $session-margin-sm $session-margin-lg;
border-radius: 4px;
}
/* ************************************* */
/* KEY VERIFICATION VIEW (SAFETY NUMBER) */
/* ************************************* */
.key-verification {
display: flex;
flex-direction: column;
align-items: center;
padding-top: $session-margin-lg;
text-align: center;
@include themify($themes) {
background: themed('inboxBackground');
color: themed('textColor');
}
&__header {
word-break: break-all;
h2 {
margin-bottom: 0px;
}
small {
margin-top: -25px;
opacity: 0.6;
}
}
&__key {
font-family: $session-font-mono;
margin: 30px 0;
width: 250px;
color: white;
}
&__is-verified {
display: flex;
flex-direction: column;
align-items: center;
font-size: $session-font-md;
margin: 30px 0;
& > span {
svg {
min-width: 30px;
margin-right: 10px;
}
height: 50px;
display: inline-flex;
align-items: center;
}
.session-button {
margin: 20px 0;
width: 100%;
}
}
.session-loader {
margin-top: $session-margin-md;
}
}
/* ************************************* */
/* ************************************* */
/* ************************************* */

View File

@ -183,7 +183,6 @@
<script type="text/javascript" src="../js/models/messages.js" data-cover></script>
<script type="text/javascript" src="../js/models/conversations.js" data-cover></script>
<script type="text/javascript" src="../js/keychange_listener.js" data-cover></script>
<script type="text/javascript" src="../js/expiring_messages.js" data-cover></script>
<script type="text/javascript" src="../js/notifications.js" data-cover></script>
<script type="text/javascript" src="../js/focus_listener.js"></script>
@ -217,19 +216,14 @@
<script type="text/javascript" src="../js/views/password_dialog_view.js"></script>
<script type="text/javascript" src="../js/views/seed_dialog_view.js"></script>
<!-- <script type="text/javascript" src="metadata/SecretSessionCipher_test.js"></script> -->
<script type="text/javascript" src="views/whisper_view_test.js"></script>
<script type="text/javascript" src="views/timestamp_view_test.js"></script>
<script type="text/javascript" src="models/conversations_test.js"></script>
<script type="text/javascript" src="models/messages_test.js"></script>
<script type="text/javascript" src="storage_test.js"></script>
<script type="text/javascript" src="keychange_listener_test.js"></script>
<script type="text/javascript" src="reliable_trigger_test.js"></script>
<script type="text/javascript" src="backup_test.js"></script>
<script type="text/javascript" src="crypto_test.js"></script>
<script type="text/javascript" src="database_test.js"></script>
<script type="text/javascript" src="i18n_test.js"></script>

View File

@ -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);
}
});

View File

@ -1,4 +1,4 @@
/* global i18n, Whisper */
/* global Whisper */
'use strict';

File diff suppressed because it is too large Load Diff

View File

@ -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);
});
});
});

View File

@ -33,15 +33,7 @@ export class ContactListItem extends React.Component<Props> {
}
public render() {
const {
i18n,
name,
onClick,
isMe,
phoneNumber,
profileName,
verified,
} = this.props;
const { i18n, name, onClick, isMe, phoneNumber, profileName } = this.props;
const title = name ? name : phoneNumber;
const displayName = isMe ? i18n('me') : title;

View File

@ -791,7 +791,6 @@ export class RegistrationTabs extends React.Component<any, State> {
await window.storage.fetch();
ConversationController.getInstance().reset();
await ConversationController.getInstance().load();
window.Whisper.RotateSignedPreKeyListener.stop(window.Whisper.events);
this.setState({
loading: false,

View File

@ -5,16 +5,13 @@ import { getEnvelopeId } from './common';
import { removeFromCache, updateCache } from './cache';
import { SignalService } from '../protobuf';
import * as Lodash from 'lodash';
import * as libsession from '../session';
import { handlePairingAuthorisationMessage } from './multidevice';
import { MultiDeviceProtocol, SessionProtocol } from '../session/protocols';
import { MultiDeviceProtocol } from '../session/protocols';
import { PubKey } from '../session/types';
import { handleSyncMessage } from './syncMessages';
import { onError } from './errors';
import ByteBuffer from 'bytebuffer';
import { BlockedNumberController } from '../util/blockedNumberController';
import { GroupUtils, StringUtils } from '../session/utils';
import { GroupUtils } from '../session/utils';
import { UserUtil } from '../util';
import { fromHexToArray, toHex } from '../session/utils/String';
import { concatUInt8Array, getSodium } from '../session/crypto';
@ -221,30 +218,6 @@ export async function isBlocked(number: string) {
return BlockedNumberController.isBlockedAsync(number);
}
async function decryptPreKeyWhisperMessage(
ciphertext: any,
sessionCipher: any,
address: any
): Promise<ArrayBuffer> {
const padded = await sessionCipher.decryptPreKeyWhisperMessage(ciphertext);
try {
return unpad(padded);
} catch (e) {
if (e.message === 'Unknown identity key') {
// create an error that the UI will pick up and ask the
// user if they want to re-negotiate
const buffer = ByteBuffer.wrap(ciphertext);
throw new window.textsecure.IncomingIdentityKeyError(
address.toString(),
buffer.toArrayBuffer(),
e.identityKey
);
}
throw e;
}
}
async function decryptUnidentifiedSender(
envelope: EnvelopePlus,
ciphertext: ArrayBuffer
@ -277,46 +250,36 @@ async function decryptUnidentifiedSender(
async function doDecrypt(
envelope: EnvelopePlus,
ciphertext: ArrayBuffer,
address: any
ciphertext: ArrayBuffer
): Promise<ArrayBuffer | null> {
const { textsecure, libloki } = window;
const lokiSessionCipher = new libloki.crypto.LokiSessionCipher(
textsecure.storage.protocol,
address
);
if (ciphertext.byteLength === 0) {
throw new Error('Received an empty envelope.'); // Error.noData
}
switch (envelope.type) {
case SignalService.Envelope.Type.CIPHERTEXT:
window.log.info('message from', getEnvelopeId(envelope));
return lokiSessionCipher.decryptWhisperMessage(ciphertext).then(unpad);
// Only UNIDENTIFIED_SENDER and CLOSED_GROUP_CIPHERTEXT are supported
case SignalService.Envelope.Type.CLOSED_GROUP_CIPHERTEXT:
return decryptForClosedGroupV2(envelope, ciphertext);
case SignalService.Envelope.Type.FALLBACK_MESSAGE: {
window.log.info('Fallback message from ', envelope.source);
const fallBackSessionCipher = new libloki.crypto.FallBackSessionCipher(
address
);
return fallBackSessionCipher.decrypt(ciphertext).then(unpad);
}
case SignalService.Envelope.Type.PREKEY_BUNDLE:
window.log.info('prekey message from', getEnvelopeId(envelope));
return decryptPreKeyWhisperMessage(
ciphertext,
lokiSessionCipher,
address
);
case SignalService.Envelope.Type.UNIDENTIFIED_SENDER: {
return decryptUnidentifiedSender(envelope, ciphertext);
}
case SignalService.Envelope.Type.PREKEY_BUNDLE: {
window.log.info('prekey message from', getEnvelopeId(envelope));
throw new Error('Envelope.Type.PREKEY_BUNDLE cannot happen anymore');
}
case SignalService.Envelope.Type.CIPHERTEXT: {
window.log.info('CIPHERTEXT envelope from', getEnvelopeId(envelope));
throw new Error('Envelope.Type.CIPHERTEXT cannot happen anymore');
}
case SignalService.Envelope.Type.FALLBACK_MESSAGE: {
window.log.info(
'FALLBACK_MESSAGE envelope from',
getEnvelopeId(envelope)
);
throw new Error('Envelope.Type.FALLBACK_MESSAGE cannot happen anymore');
}
default:
throw new Error('Unknown message type');
throw new Error(`Unknown message type:${envelope.type}`);
}
}
@ -325,17 +288,10 @@ async function decrypt(
envelope: EnvelopePlus,
ciphertext: ArrayBuffer
): Promise<any> {
const { textsecure, libsignal, log } = window;
// Envelope.source will be null on UNIDENTIFIED_SENDER
// Don't use it there!
const address = new libsignal.SignalProtocolAddress(
envelope.source,
envelope.sourceDevice
);
const { textsecure } = window;
try {
const plaintext = await doDecrypt(envelope, ciphertext, address);
const plaintext = await doDecrypt(envelope, ciphertext);
if (!plaintext) {
await removeFromCache(envelope);
@ -351,34 +307,7 @@ async function decrypt(
return plaintext;
} catch (error) {
let errorToThrow = error;
const noSession =
error &&
(error.message.indexOf('No record for device') === 0 ||
error.message.indexOf('decryptWithSessionList: list is empty') === 0);
if (error && error.message === 'Unknown identity key') {
// create an error that the UI will pick up and ask the
// user if they want to re-negotiate
const buffer = ByteBuffer.wrap(ciphertext);
errorToThrow = new textsecure.IncomingIdentityKeyError(
address.toString(),
buffer.toArrayBuffer(),
error.identityKey
);
} else if (!noSession) {
// We want to handle "no-session" error, not re-throw it
throw error;
}
const ev: any = new Event('error');
ev.error = errorToThrow;
ev.proto = envelope;
ev.confirm = removeFromCache.bind(null, envelope);
const returnError = async () => Promise.reject(errorToThrow);
onError(ev).then(returnError, returnError);
throw error;
}
}
@ -445,20 +374,12 @@ export async function innerHandleContentMessage(
);
}
}
const { FALLBACK_MESSAGE } = SignalService.Envelope.Type;
await ConversationController.getInstance().getOrCreateAndWait(
envelope.source,
'private'
);
if (envelope.type !== FALLBACK_MESSAGE) {
const device = new PubKey(envelope.source);
await SessionProtocol.onSessionEstablished(device);
await libsession.getMessageQueue().processPending(device);
}
if (content.pairingAuthorisation) {
await handlePairingAuthorisationMessage(
envelope,

View File

@ -174,7 +174,6 @@ export async function processDecrypted(envelope: EnvelopePlus, decrypted: any) {
if (decrypted.expireTimer == null) {
decrypted.expireTimer = 0;
}
if (decrypted.flags & FLAGS.END_SESSION) {
decrypted.body = '';
decrypted.attachments = [];
@ -287,13 +286,11 @@ export async function handleDataMessage(
await handleClosedGroupV2(envelope, dataMessage.closedGroupUpdateV2);
return;
}
// tslint:disable no-bitwise
if (
dataMessage.flags &&
dataMessage.flags & SignalService.DataMessage.Flags.END_SESSION
) {
await handleEndSession(envelope.source);
return removeFromCache(envelope);
}
// tslint:enable no-bitwise
@ -605,10 +602,7 @@ export async function handleMessageEvent(event: MessageEvent): Promise<void> {
? ConversationType.GROUP
: ConversationType.PRIVATE;
const {
PROFILE_KEY_UPDATE,
SESSION_RESTORE,
} = SignalService.DataMessage.Flags;
const { PROFILE_KEY_UPDATE } = SignalService.DataMessage.Flags;
// tslint:disable-next-line: no-bitwise
const isProfileUpdate = Boolean(message.flags & PROFILE_KEY_UPDATE);

View File

@ -1,44 +1,8 @@
import { initIncomingMessage } from './dataMessage';
import { toNumber } from 'lodash';
import { SessionProtocol } from '../session/protocols';
import { PubKey } from '../session/types';
import { ConversationController } from '../session/conversations';
async function onNoSession(ev: any) {
const pubkey = ev.proto.source;
const convo = await ConversationController.getInstance().getOrCreateAndWait(
pubkey,
'private'
);
if (!convo.get('sessionRestoreSeen')) {
convo.set({ sessionRestoreSeen: true });
await convo.commit();
await SessionProtocol.sendSessionRequestIfNeeded(new PubKey(pubkey));
} else {
window.log.debug(`Already seen session restore for pubkey: ${pubkey}`);
if (ev.confirm) {
ev.confirm();
}
}
}
export async function onError(ev: any) {
const noSession =
ev.error &&
ev.error.message &&
ev.error.message.indexOf('No record for device') === 0;
if (noSession) {
await onNoSession(ev);
// We don't want to display any failed messages in the conversation:
return;
}
const { error } = ev;
window.log.error(
'background onError:',

View File

@ -5,11 +5,9 @@ import * as Data from '../../js/modules/data';
import { SignalService } from '../protobuf';
import { updateProfile } from './dataMessage';
import { onVerified } from './syncMessages';
import { StringUtils } from '../session/utils';
import { MultiDeviceProtocol, SessionProtocol } from '../session/protocols';
import { PubKey } from '../session/types';
import { MultiDeviceProtocol } from '../session/protocols';
import ByteBuffer from 'bytebuffer';
import { BlockedNumberController } from '../util';
@ -336,35 +334,6 @@ async function onContactReceived(details: any) {
conversation.set('active_at', activeAt);
}
const primaryDevice = await MultiDeviceProtocol.getPrimaryDevice(id);
const secondaryDevices = await MultiDeviceProtocol.getSecondaryDevices(id);
const primaryConversation = await ConversationController.getInstance().getOrCreateAndWait(
primaryDevice.key,
'private'
);
const secondaryConversations = await Promise.all(
secondaryDevices.map(async d => {
const secondaryConv = await ConversationController.getInstance().getOrCreateAndWait(
d.key,
'private'
);
await secondaryConv.setSecondaryStatus(true, primaryDevice.key);
return conversation;
})
);
const deviceConversations = [
primaryConversation,
...secondaryConversations,
];
// triger session request with every devices of that user
// when we do not have a session with it already
deviceConversations.forEach(device => {
// tslint:disable-next-line: no-floating-promises
SessionProtocol.sendSessionRequestIfNeeded(new PubKey(device.id));
});
if (details.profileKey) {
const profileKey = StringUtils.decode(details.profileKey, 'base64');
void conversation.setProfileKey(profileKey);

View File

@ -210,8 +210,6 @@ async function queueCached(item: any) {
envelope.sourceDevice = 1;
envelope.senderIdentity = envelope.senderIdentity || item.senderIdentity;
envelope.serverTimestamp = envelope.serverTimestamp || item.serverTimestamp;
envelope.preKeyBundleMessage =
envelope.preKeyBundleMessage || item.preKeyBundleMessage;
const { decrypted } = item;

View File

@ -223,23 +223,6 @@ export class ConversationController {
} else {
channelAPI.serverAPI.partChannel(channelAPI.channelId);
}
} else if (conversation.isPrivate()) {
const deviceIds = await window.textsecure.storage.protocol.getDeviceIds(
id
);
await Promise.all(
deviceIds.map((deviceId: string) => {
const address = new window.libsignal.SignalProtocolAddress(
id,
deviceId
);
const sessionCipher = new window.libsignal.SessionCipher(
window.textsecure.storage.protocol,
address
);
return sessionCipher.deleteAllSessionsForDevice();
})
);
}
await conversation.destroyMessages();

View File

@ -58,10 +58,7 @@ export class MessageController {
const { message, timestamp } = messageEntry;
const conversation = message.getConversation();
if (
now - timestamp > 5 * 60 * 1000 &&
(!conversation || !conversation.messageCollection.length)
) {
if (now - timestamp > 5 * 60 * 1000 && !conversation) {
this.unregister(message.id);
}
});

View File

@ -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,
});
}
}

View File

@ -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);
}
}

View File

@ -1,7 +1,4 @@
export * from './ContentMessage';
export * from './EndSessionMessage';
export * from './SessionEstablishedMessage';
export * from './SessionRequestMessage';
export * from './TypingMessage';
export * from './data';
export * from './link';

View File

@ -1,7 +1,7 @@
import { SyncMessage } from './SyncMessage';
import { SignalService } from '../../../../../protobuf';
import { MessageParams } from '../../Message';
import { StringUtils, SyncMessageUtils } from '../../../../utils';
import { SyncMessageUtils } from '../../../../utils';
interface RawContact {
name: string;

View File

@ -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];
}
}

View File

@ -1,4 +1 @@
import { SessionProtocol } from './SessionProtocol';
export * from './MultiDeviceProtocol';
export { SessionProtocol };

View File

@ -9,17 +9,15 @@ import {
ContentMessage,
ExpirationTimerUpdateMessage,
OpenGroupMessage,
SessionRequestMessage,
SyncMessage,
TypingMessage,
} from '../messages/outgoing';
import { PendingMessageCache } from './PendingMessageCache';
import { GroupUtils, JobQueue, TypedEventEmitter } from '../utils';
import { JobQueue, TypedEventEmitter } from '../utils';
import { PubKey, RawMessage } from '../types';
import { MessageSender } from '.';
import { MultiDeviceProtocol, SessionProtocol } from '../protocols';
import { MultiDeviceProtocol } from '../protocols';
import { UserUtil } from '../../util';
import { ClosedGroupV2ChatMessage } from '../messages/outgoing/content/data/groupv2/ClosedGroupV2ChatMessage';
export class MessageQueue implements MessageQueueInterface {
public readonly events: TypedEventEmitter<MessageQueueInterfaceEvents>;
@ -77,6 +75,7 @@ export class MessageQueue implements MessageQueueInterface {
this.events.emit('sendFail', message, error);
} else {
const messageEventData = {
identifier: message.identifier,
pubKey: message.group.groupId,
timestamp: message.timestamp,
serverId: result.serverId,
@ -128,14 +127,6 @@ export class MessageQueue implements MessageQueueInterface {
public async processPending(device: PubKey) {
const messages = await this.pendingMessageCache.getForDevice(device);
const isMediumGroup = GroupUtils.isMediumGroup(device);
const hasSession = await SessionProtocol.hasSession(device);
// If we don't have a session then try and establish one and then continue sending messages
if (!isMediumGroup && !hasSession) {
await SessionProtocol.sendSessionRequestIfNeeded(device);
}
const jobQueue = this.getJobQueue(device);
messages.forEach(async message => {
const messageId = String(message.timestamp);
@ -196,10 +187,6 @@ export class MessageQueue implements MessageQueueInterface {
return;
}
if (message instanceof SessionRequestMessage) {
return SessionProtocol.sendSessionRequest(message, device);
}
await this.pendingMessageCache.add(device, message, sentCb);
void this.processPending(device);
}

View File

@ -98,8 +98,8 @@ export function serialiseByteBuffers(buffers: Array<Uint8Array>): ByteBuffer {
}
export async function sendContactSyncMessage(convos: Array<ConversationModel>) {
throw new Error('Still in use?');
window.log.warn('sendContactSyncMessage TODO');
return;
// let convosToSync: Array<ConversationModel>;
// if (!convos?.length) {
// convosToSync = await getSyncContacts();
@ -180,8 +180,8 @@ async function createContactSyncMessage(sessionContacts: Array<any>) {
export async function sendGroupSyncMessage(
conversations: Array<ConversationModel>
) {
throw new Error('Still in use?');
window.log.warn('sendGroupSyncMessage TODO');
return;
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
// primaryDevicePubKey is set to our own number if we are the master device
// const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
@ -216,7 +216,8 @@ export async function sendGroupSyncMessage(
export async function sendOpenGroupsSyncMessage(
convos: Array<ConversationModel>
) {
throw new Error('Still in use?');
window.log.warn('sendOpenGroupsSyncMessage TODO');
return;
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
// primaryDevicePubKey is set to our own number if we are the master device
@ -248,8 +249,8 @@ export async function sendOpenGroupsSyncMessage(
}
export async function sendBlockedListSyncMessage() {
throw new Error('Still in use?');
window.log.warn('sendBlockedListSyncMessage TODO');
return;
// If we havn't got a primaryDeviceKey then we are in the middle of pairing
// primaryDevicePubKey is set to our own number if we are the master device
// const primaryDeviceKey = window.storage.get('primaryDevicePubKey');
@ -269,6 +270,7 @@ export async function sendBlockedListSyncMessage() {
}
export async function syncReadMessages() {
window.log.warn('syncReadMessages TODO');
return;
// FIXME currently not in used
// const syncReadMessages = new SyncReadMessage(

View File

@ -4,7 +4,6 @@ import { Constants } from '../../session';
import { createAsyncThunk } from '@reduxjs/toolkit';
import { MessageModel } from '../../../js/models/messages';
import { ConversationController } from '../../session/conversations';
import { StateType } from '../reducer';
// State

View File

@ -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
);
});
});

View File

@ -104,7 +104,6 @@ describe('MessageEncrypter', () => {
beforeEach(() => {
TestUtils.stubWindow('libsignal', {
SignalProtocolAddress: sandbox.stub(),
SessionCipher: Stubs.SessionCipherStub,
} as any);
TestUtils.stubWindow('textsecure', {
@ -113,15 +112,8 @@ describe('MessageEncrypter', () => {
},
});
TestUtils.stubWindow('Signal', {
Metadata: {
SecretSessionCipher: Stubs.SecretSessionCipherStub,
},
});
TestUtils.stubWindow('libloki', {
crypto: {
FallBackSessionCipher: Stubs.FallBackSessionCipherStub,
encryptForPubkey: sinon.fake.returns(''),
} as any,
});

View File

@ -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'
);
});
});

View File

@ -13,10 +13,7 @@ import {
import { PrimaryPubKey, PubKey, RawMessage } from '../../../../session/types';
import { UserUtil } from '../../../../util';
import { MessageSender } from '../../../../session/sending';
import {
MultiDeviceProtocol,
SessionProtocol,
} from '../../../../session/protocols';
import { MultiDeviceProtocol } from '../../../../session/protocols';
import { PendingMessageCacheStub } from '../../../test-utils/stubs';
import { TestSyncMessage } from '../../../test-utils/stubs/messages/TestSyncMessage';
import { ClosedGroupV2Message } from '../../../../session/messages/outgoing/content/data/groupv2';
@ -42,9 +39,6 @@ describe('MessageQueue', () => {
let sendStub: sinon.SinonStub<[RawMessage, (number | undefined)?]>;
// Utils Stubs
let isMediumGroupStub: sinon.SinonStub<[PubKey], boolean>;
// Session Protocol Stubs
let hasSessionStub: sinon.SinonStub<[PubKey]>;
let sendSessionRequestIfNeededStub: sinon.SinonStub<[PubKey], Promise<void>>;
beforeEach(async () => {
// Utils Stubs
@ -52,7 +46,6 @@ describe('MessageQueue', () => {
TestUtils.stubWindow('libsignal', {
SignalProtocolAddress: sandbox.stub(),
SessionCipher: Stubs.SessionCipherStub,
} as any);
// Message Sender Stubs
@ -63,13 +56,6 @@ describe('MessageQueue', () => {
.stub(GroupUtils, 'isMediumGroup')
.returns(false);
// Session Protocol Stubs
sandbox.stub(SessionProtocol, 'sendSessionRequest').resolves();
hasSessionStub = sandbox.stub(SessionProtocol, 'hasSession').resolves(true);
sendSessionRequestIfNeededStub = sandbox
.stub(SessionProtocol, 'sendSessionRequestIfNeeded')
.resolves();
// Init Queue
pendingMessageCache = new PendingMessageCacheStub();
messageQueueStub = new MessageQueue(pendingMessageCache);
@ -81,48 +67,18 @@ describe('MessageQueue', () => {
});
describe('processPending', () => {
it('will send session request if no session and not sending to medium group', async () => {
hasSessionStub.resolves(false);
isMediumGroupStub.returns(false);
const device = TestUtils.generateFakePubKey();
await messageQueueStub.processPending(device);
const stubCallPromise = PromiseUtils.waitUntil(
() => sendSessionRequestIfNeededStub.callCount === 1
);
await expect(stubCallPromise).to.be.fulfilled;
});
it('will not send session request if sending to medium group', async () => {
hasSessionStub.resolves(false);
isMediumGroupStub.returns(true);
const device = TestUtils.generateFakePubKey();
await messageQueueStub.processPending(device);
expect(sendSessionRequestIfNeededStub.callCount).to.equal(0);
});
it('will send messages', async () => {
for (const hasSession of [true, false]) {
hasSessionStub.resolves(hasSession);
const device = TestUtils.generateFakePubKey();
await pendingMessageCache.add(device, TestUtils.generateChatMessage());
const device = TestUtils.generateFakePubKey();
await pendingMessageCache.add(device, TestUtils.generateChatMessage());
const successPromise = PromiseUtils.waitForTask(done => {
messageQueueStub.events.once('sendSuccess', done);
});
await messageQueueStub.processPending(device);
await expect(successPromise).to.be.fulfilled;
}
const successPromise = PromiseUtils.waitForTask(done => {
messageQueueStub.events.once('sendSuccess', done);
});
await messageQueueStub.processPending(device);
await expect(successPromise).to.be.fulfilled;
});
it('should remove message from cache', async () => {
hasSessionStub.resolves(true);
const events = ['sendSuccess', 'sendFail'];
for (const event of events) {
if (event === 'sendSuccess') {
@ -148,8 +104,6 @@ describe('MessageQueue', () => {
describe('events', () => {
it('should send a success event if message was sent', async () => {
hasSessionStub.resolves(true);
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
await pendingMessageCache.add(device, message);
@ -168,7 +122,6 @@ describe('MessageQueue', () => {
});
it('should send a fail event if something went wrong while sending', async () => {
hasSessionStub.resolves(true);
sendStub.throws(new Error('failure'));
const spy = sandbox.spy();
@ -227,8 +180,6 @@ describe('MessageQueue', () => {
describe('sendMessageToDevices', () => {
it('can send to many devices', async () => {
hasSessionStub.resolves(false);
const devices = TestUtils.generateFakePubKeys(5);
const message = TestUtils.generateChatMessage();
@ -239,8 +190,6 @@ describe('MessageQueue', () => {
describe('sendSyncMessage', () => {
it('should send a message to all our devices', async () => {
hasSessionStub.resolves(false);
const ourOtherDevices = TestUtils.generateFakePubKeys(2);
const ourDevices = [ourDevice, ...ourOtherDevices];
sandbox.stub(MultiDeviceProtocol, 'getAllDevices').resolves(ourDevices);

View File

@ -4,7 +4,6 @@ import * as _ from 'lodash';
import { MessageUtils } from '../../../../session/utils';
import { TestUtils } from '../../../../test/test-utils';
import { PendingMessageCache } from '../../../../session/sending/PendingMessageCache';
import { SessionProtocol } from '../../../../session/protocols';
// Equivalent to Data.StorageItem
interface StorageItem {
@ -39,8 +38,6 @@ describe('PendingMessageCache', () => {
});
pendingMessageCacheStub = new PendingMessageCache();
sandbox.stub(SessionProtocol, 'hasSession').resolves(true);
});
afterEach(() => {

View File

@ -3,7 +3,6 @@ import * as sinon from 'sinon';
import { TestUtils } from '../../../test-utils';
import { MessageUtils } from '../../../../session/utils';
import { EncryptionType, PubKey } from '../../../../session/types';
import { SessionProtocol } from '../../../../session/protocols';
import { ClosedGroupV2ChatMessage } from '../../../../session/messages/outgoing/content/data/groupv2/ClosedGroupV2ChatMessage';
import {
ClosedGroupV2EncryptionPairMessage,
@ -25,14 +24,6 @@ describe('Message Utils', () => {
});
describe('toRawMessage', () => {
let hasSessionStub: sinon.SinonStub<[PubKey], Promise<boolean>>;
beforeEach(() => {
hasSessionStub = sandbox
.stub(SessionProtocol, 'hasSession')
.resolves(true);
});
it('can convert to raw message', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
@ -90,9 +81,7 @@ describe('Message Utils', () => {
);
});
it('should set encryption to MediumGroup if a MediumGroupMessage is passed in', async () => {
hasSessionStub.resolves(true);
it('should set encryption to ClosedGroup if a ClosedGroupV2ChatMessage is passed in', async () => {
const device = TestUtils.generateFakePubKey();
const groupId = TestUtils.generateFakePubKey();
const chatMessage = TestUtils.generateChatMessage();
@ -102,19 +91,7 @@ describe('Message Utils', () => {
expect(rawMessage.encryption).to.equal(EncryptionType.ClosedGroup);
});
it('should set encryption to Fallback on other messages if we do not have a session', async () => {
hasSessionStub.resolves(false);
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = await MessageUtils.toRawMessage(device, message);
expect(rawMessage.encryption).to.equal(EncryptionType.Fallback);
});
it('should set encryption to Fallback on other messages even if we have a session', async () => {
hasSessionStub.resolves(true);
it('should set encryption to Fallback on other messages', async () => {
const device = TestUtils.generateFakePubKey();
const message = TestUtils.generateChatMessage();
const rawMessage = await MessageUtils.toRawMessage(device, message);

View File

@ -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'),
};
}
}

View File

@ -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');
}
}

View File

@ -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.');
}
}

View File

@ -1,3 +0,0 @@
export * from './SessionCipherStub';
export * from './SecretSessionCipherStub';
export * from './FallBackSessionCipherStub';

View File

@ -1,2 +1 @@
export * from './ciphers';
export * from './sending';

View File

@ -1,7 +1,7 @@
import { assert } from 'chai';
import * as Conversation from '../../types/Conversation';
import { IncomingMessage, VerifiedChangeMessage } from '../../types/Message';
import { IncomingMessage } from '../../types/Message';
describe('Conversation', () => {
describe('createLastMessageUpdate', () => {

View File

@ -25,26 +25,18 @@ export const createLastMessageUpdate = ({
};
}
const { type, expirationTimerUpdate } = lastMessage;
const isVerifiedChangeMessage = type === 'verified-change';
const { expirationTimerUpdate } = lastMessage;
const isExpireTimerUpdateFromSync = Boolean(
expirationTimerUpdate && expirationTimerUpdate.fromSync
);
const shouldUpdateTimestamp = Boolean(
!isVerifiedChangeMessage && !isExpireTimerUpdateFromSync
);
const shouldUpdateTimestamp = Boolean(!isExpireTimerUpdateFromSync);
const newTimestamp = shouldUpdateTimestamp
? lastMessage.sent_at
: currentTimestamp;
const shouldUpdateLastMessageText = !isVerifiedChangeMessage;
const newLastMessageText = shouldUpdateLastMessageText
? lastMessageNotificationText
: '';
return {
lastMessage: newLastMessageText || '',
lastMessage: lastMessageNotificationText || '',
lastMessageStatus: lastMessageStatus || null,
timestamp: newTimestamp || null,
};

View File

@ -2,7 +2,7 @@ import { Attachment } from './Attachment';
import { Contact } from './Contact';
import { IndexableBoolean, IndexablePresence } from './IndexedDB';
export type Message = UserMessage | VerifiedChangeMessage;
export type Message = UserMessage;
export type UserMessage = IncomingMessage;
export type IncomingMessage = Readonly<

1
ts/window.d.ts vendored
View File

@ -28,7 +28,6 @@ If you import anything in global.d.ts, the type system won't work correctly.
declare global {
interface Window {
CONSTANTS: any;
SignalProtocolStore: any;
Events: any;
Lodash: any;
LokiAppDotNetServerAPI: any;