Merge pull request #721 from msgmaxim/sealed-sender

Sealed sender support
pull/750/head
Maxim Shishmarev 5 years ago committed by GitHub
commit a2089919af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,12 +1,11 @@
{ {
"storageProfile": "swarm-testing", "storageProfile": "swarm-testing",
"seedNodeList": [ "seedNodeList": [
{ {
"ip": "localhost", "ip": "localhost",
"port": "22129" "port": "22129"
} }
], ],
"openDevTools": true, "openDevTools": true,
"defaultPublicChatServer": "https://team-chat.lokinet.org/" "defaultPublicChatServer": "https://team-chat.lokinet.org/"
} }

@ -1,12 +1,11 @@
{ {
"storageProfile": "swarm-testing2", "storageProfile": "swarm-testing2",
"seedNodeList": [ "seedNodeList": [
{ {
"ip": "localhost", "ip": "localhost",
"port": "22129" "port": "22129"
} }
], ],
"openDevTools": true, "openDevTools": true,
"defaultPublicChatServer": "https://team-chat.lokinet.org/" "defaultPublicChatServer": "https://team-chat.lokinet.org/"
} }

@ -10,4 +10,6 @@ module.exports = {
SENDERKEY_DISTRIBUTION_TYPE: 5, SENDERKEY_DISTRIBUTION_TYPE: 5,
ENCRYPTED_MESSAGE_OVERHEAD: 53, ENCRYPTED_MESSAGE_OVERHEAD: 53,
LOKI_FRIEND_REQUEST: 101,
}; };

@ -1,4 +1,4 @@
/* global libsignal, textsecure */ /* global libsignal, textsecure, dcodeIO, libloki */
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
@ -102,37 +102,17 @@ function _createServerCertificateFromBuffer(serialized) {
// public SenderCertificate(byte[] serialized) // public SenderCertificate(byte[] serialized)
function _createSenderCertificateFromBuffer(serialized) { function _createSenderCertificateFromBuffer(serialized) {
const wrapper = textsecure.protobuf.SenderCertificate.decode(serialized); const cert = textsecure.protobuf.SenderCertificate.decode(serialized);
if (!wrapper.signature || !wrapper.certificate) { if (!cert.senderDevice || !cert.sender) {
throw new Error('Missing fields');
}
const certificate = textsecure.protobuf.SenderCertificate.Certificate.decode(
wrapper.certificate.toArrayBuffer()
);
if (
!certificate.signer ||
!certificate.identityKey ||
!certificate.senderDevice ||
!certificate.expires ||
!certificate.sender
) {
throw new Error('Missing fields'); throw new Error('Missing fields');
} }
return { return {
sender: certificate.sender, sender: cert.sender,
senderDevice: certificate.senderDevice, senderDevice: cert.senderDevice,
expires: certificate.expires.toNumber(),
identityKey: certificate.identityKey.toArrayBuffer(),
signer: _createServerCertificateFromBuffer(
certificate.signer.toArrayBuffer()
),
certificate: wrapper.certificate.toArrayBuffer(), certificate: cert.toArrayBuffer(),
signature: wrapper.signature.toArrayBuffer(),
serialized, serialized,
}; };
@ -219,6 +199,9 @@ function _createUnidentifiedSenderMessageContentFromBuffer(serialized) {
case TypeEnum.PREKEY_MESSAGE: case TypeEnum.PREKEY_MESSAGE:
type = CiphertextMessage.PREKEY_TYPE; type = CiphertextMessage.PREKEY_TYPE;
break; break;
case TypeEnum.LOKI_FRIEND_REQUEST:
type = CiphertextMessage.LOKI_FRIEND_REQUEST;
break;
default: default:
throw new Error(`Unknown type: ${message.type}`); throw new Error(`Unknown type: ${message.type}`);
} }
@ -243,6 +226,8 @@ function _getProtoMessageType(type) {
return TypeEnum.MESSAGE; return TypeEnum.MESSAGE;
case CiphertextMessage.PREKEY_TYPE: case CiphertextMessage.PREKEY_TYPE:
return TypeEnum.PREKEY_MESSAGE; return TypeEnum.PREKEY_MESSAGE;
case CiphertextMessage.LOKI_FRIEND_REQUEST:
return TypeEnum.LOKI_FRIEND_REQUEST;
default: default:
throw new Error(`_getProtoMessageType: type '${type}' does not exist`); throw new Error(`_getProtoMessageType: type '${type}' does not exist`);
} }
@ -257,9 +242,7 @@ function _createUnidentifiedSenderMessageContent(
) { ) {
const innerMessage = new textsecure.protobuf.UnidentifiedSenderMessage.Message(); const innerMessage = new textsecure.protobuf.UnidentifiedSenderMessage.Message();
innerMessage.type = _getProtoMessageType(type); innerMessage.type = _getProtoMessageType(type);
innerMessage.senderCertificate = textsecure.protobuf.SenderCertificate.decode( innerMessage.senderCertificate = senderCertificate;
senderCertificate.serialized
);
innerMessage.content = content; innerMessage.content = content;
return { return {
@ -277,24 +260,24 @@ SecretSessionCipher.prototype = {
// SenderCertificate senderCertificate, // SenderCertificate senderCertificate,
// byte[] paddedPlaintext // byte[] paddedPlaintext
// ) // )
async encrypt(destinationAddress, senderCertificate, paddedPlaintext) { async encrypt(
destinationAddress,
senderCertificate,
paddedPlaintext,
cipher
) {
// Capture this.xxx variables to replicate Java's implicit this syntax // Capture this.xxx variables to replicate Java's implicit this syntax
const { SessionCipher } = this;
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this); const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
const _encryptWithSecretKeys = this._encryptWithSecretKeys.bind(this); const _encryptWithSecretKeys = this._encryptWithSecretKeys.bind(this);
const _calculateStaticKeys = this._calculateStaticKeys.bind(this); const _calculateStaticKeys = this._calculateStaticKeys.bind(this);
const sessionCipher = new SessionCipher( const message = await cipher.encrypt(paddedPlaintext);
signalProtocolStore,
destinationAddress
);
const message = await sessionCipher.encrypt(paddedPlaintext);
const ourIdentity = await signalProtocolStore.getIdentityKeyPair(); const ourIdentity = await signalProtocolStore.getIdentityKeyPair();
const theirIdentity = fromEncodedBinaryToArrayBuffer( const theirIdentity = dcodeIO.ByteBuffer.wrap(
await signalProtocolStore.loadIdentityKey(destinationAddress.getName()) destinationAddress.getName(),
); 'hex'
).toArrayBuffer();
const ephemeral = await libsignal.Curve.async.generateKeyPair(); const ephemeral = await libsignal.Curve.async.generateKeyPair();
const ephemeralSalt = concatenateBytes( const ephemeralSalt = concatenateBytes(
@ -344,7 +327,7 @@ SecretSessionCipher.prototype = {
// public Pair<SignalProtocolAddress, byte[]> decrypt( // public Pair<SignalProtocolAddress, byte[]> decrypt(
// CertificateValidator validator, byte[] ciphertext, long timestamp) // CertificateValidator validator, byte[] ciphertext, long timestamp)
async decrypt(validator, ciphertext, timestamp, me) { async decrypt(ciphertext, me) {
// Capture this.xxx variables to replicate Java's implicit this syntax // Capture this.xxx variables to replicate Java's implicit this syntax
const signalProtocolStore = this.storage; const signalProtocolStore = this.storage;
const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this); const _calculateEphemeralKeys = this._calculateEphemeralKeys.bind(this);
@ -392,15 +375,6 @@ SecretSessionCipher.prototype = {
messageBytes messageBytes
); );
await validator.validate(content.senderCertificate, timestamp);
if (
!constantTimeEqual(content.senderCertificate.identityKey, staticKeyBytes)
) {
throw new Error(
"Sender's certificate key does not match key used in message"
);
}
const { sender, senderDevice } = content.senderCertificate; const { sender, senderDevice } = content.senderCertificate;
const { number, deviceId } = me || {}; const { number, deviceId } = me || {};
if (sender === number && senderDevice === deviceId) { if (sender === number && senderDevice === deviceId) {
@ -414,6 +388,7 @@ SecretSessionCipher.prototype = {
return { return {
sender: address, sender: address,
content: await _decryptWithUnidentifiedSenderMessage(content), content: await _decryptWithUnidentifiedSenderMessage(content),
type: content.type,
}; };
} catch (error) { } catch (error) {
if (!error) { if (!error) {
@ -519,6 +494,10 @@ SecretSessionCipher.prototype = {
signalProtocolStore, signalProtocolStore,
sender sender
).decryptPreKeyWhisperMessage(message.content); ).decryptPreKeyWhisperMessage(message.content);
case CiphertextMessage.LOKI_FRIEND_REQUEST:
return new libloki.crypto.FallBackSessionCipher(sender).decrypt(
message.content
);
default: default:
throw new Error(`Unknown type: ${message.type}`); throw new Error(`Unknown type: ${message.type}`);
} }

@ -110,7 +110,7 @@
let existingMembers = groupConvo.get('members'); let existingMembers = groupConvo.get('members');
// Show a contact if they are our friend or if they are a member // Show a contact if they are our friend or if they are a member
let friendsAndMembers = convos.filter( const friendsAndMembers = convos.filter(
d => d =>
(d.isFriend() || existingMembers.includes(d.id)) && (d.isFriend() || existingMembers.includes(d.id)) &&
d.isPrivate() && d.isPrivate() &&

@ -226,7 +226,6 @@
// Send // Send
const options = { messageType: 'pairing-request' }; const options = { messageType: 'pairing-request' };
const p = new Promise((resolve, reject) => { const p = new Promise((resolve, reject) => {
const timestamp = Date.now(); const timestamp = Date.now();
const outgoingMessage = new textsecure.OutgoingMessage( const outgoingMessage = new textsecure.OutgoingMessage(

@ -45,6 +45,7 @@
this.pubKey = StringView.hexToArrayBuffer(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) { async encrypt(plaintext) {
const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair(); const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
const myPrivateKey = myKeyPair.privKey; const myPrivateKey = myKeyPair.privKey;

@ -714,8 +714,6 @@ MessageReceiver.prototype.extend({
return plaintext; return plaintext;
}, },
async decrypt(envelope, ciphertext) { async decrypt(envelope, ciphertext) {
const { serverTrustRoot } = this;
let promise; let promise;
const address = new libsignal.SignalProtocolAddress( const address = new libsignal.SignalProtocolAddress(
envelope.source, envelope.source,
@ -867,15 +865,10 @@ MessageReceiver.prototype.extend({
case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER: case textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER:
window.log.info('received unidentified sender message'); window.log.info('received unidentified sender message');
promise = secretSessionCipher promise = secretSessionCipher
.decrypt( .decrypt(ciphertext.toArrayBuffer(), me)
window.Signal.Metadata.createCertificateValidator(serverTrustRoot),
ciphertext.toArrayBuffer(),
Math.min(envelope.serverTimestamp || Date.now(), Date.now()),
me
)
.then( .then(
result => { result => {
const { isMe, sender, content } = result; const { isMe, sender, content, type } = result;
// We need to drop incoming messages from ourself since server can't // We need to drop incoming messages from ourself since server can't
// do it for us // do it for us
@ -883,6 +876,13 @@ MessageReceiver.prototype.extend({
return { isMe: true }; return { isMe: true };
} }
// We might have substituted the type based on decrypted content
if (type === textsecure.protobuf.Envelope.Type.FRIEND_REQUEST) {
// eslint-disable-next-line no-param-reassign
envelope.type =
textsecure.protobuf.Envelope.Type.FRIEND_REQUEST;
}
if (this.isBlocked(sender.getName())) { if (this.isBlocked(sender.getName())) {
window.log.info( window.log.info(
'Dropping blocked message after sealed sender decryption' 'Dropping blocked message after sealed sender decryption'
@ -903,7 +903,7 @@ MessageReceiver.prototype.extend({
envelope.unidentifiedDeliveryReceived = !originalSource; envelope.unidentifiedDeliveryReceived = !originalSource;
// Return just the content because that matches the signature of the other // Return just the content because that matches the signature of the other
// decrypt methods used above. // decrypt methods used above.
return this.unpad(content); return this.unpad(content);
}, },
error => { error => {
@ -933,7 +933,8 @@ MessageReceiver.prototype.extend({
throw error; throw error;
}); });
} }
); )
.then(handleSessionReset);
break; break;
default: default:
promise = Promise.reject(new Error('Unknown message type')); promise = Promise.reject(new Error('Unknown message type'));
@ -946,6 +947,9 @@ MessageReceiver.prototype.extend({
this.removeFromCache(envelope); this.removeFromCache(envelope);
return null; return null;
} }
// Type here can actually be UNIDENTIFIED_SENDER even if
// the underlying message is FRIEND_REQUEST
if ( if (
envelope.type !== textsecure.protobuf.Envelope.Type.FRIEND_REQUEST envelope.type !== textsecure.protobuf.Envelope.Type.FRIEND_REQUEST
) { ) {
@ -975,7 +979,10 @@ MessageReceiver.prototype.extend({
let errorToThrow = error; let errorToThrow = error;
const noSession = const noSession =
error && error.message.indexOf('No record for device') === 0; error &&
(error.message.indexOf('No record for device') === 0 ||
error.messaeg.indexOf('decryptWithSessionList: list is empty') ===
0);
if (error && error.message === 'Unknown identity key') { if (error && error.message === 'Unknown identity key') {
// create an error that the UI will pick up and ask the // create an error that the UI will pick up and ask the

@ -14,6 +14,23 @@
/* eslint-disable no-unreachable */ /* eslint-disable no-unreachable */
const NUM_SEND_CONNECTIONS = 3; const NUM_SEND_CONNECTIONS = 3;
const getTTLForType = type => {
switch (type) {
case 'friend-request':
return 4 * 24 * 60 * 60 * 1000; // 4 days for friend request message
case 'device-unpairing':
return 4 * 24 * 60 * 60 * 1000; // 4 days for device unpairing
case 'onlineBroadcast':
return 60 * 1000; // 1 minute for online broadcast message
case 'typing':
return 60 * 1000; // 1 minute for typing indicators
case 'pairing-request':
return 2 * 60 * 1000; // 2 minutes for pairing requests
default:
return (window.getMessageTTL() || 24) * 60 * 60 * 1000; // 1 day default for any other message
}
};
function OutgoingMessage( function OutgoingMessage(
server, server,
timestamp, timestamp,
@ -289,36 +306,6 @@ OutgoingMessage.prototype = {
this.numbers = devicesPubKeys; this.numbers = devicesPubKeys;
/* Disabled because i'm not sure how senderCertificate works :thinking:
const { numberInfo, senderCertificate } = this;
const info = numberInfo && numberInfo[number] ? numberInfo[number] : {};
const { accessKey } = info || {};
if (accessKey && !senderCertificate) {
return Promise.reject(
new Error(
'OutgoingMessage.doSendMessage: accessKey was provided, ' +
'but senderCertificate was not'
)
);
}
const sealedSender = Boolean(accessKey && senderCertificate);
// We don't send to ourselves if unless sealedSender is enabled
const ourNumber = textsecure.storage.user.getNumber();
const ourDeviceId = textsecure.storage.user.getDeviceId();
if (number === ourNumber && !sealedSender) {
// eslint-disable-next-line no-param-reassign
deviceIds = _.reject(
deviceIds,
deviceId =>
// because we store our own device ID as a string at least sometimes
deviceId === ourDeviceId || deviceId === parseInt(ourDeviceId, 10)
);
}
*/
return Promise.all( return Promise.all(
devicesPubKeys.map(async devicePubKey => { devicesPubKeys.map(async devicePubKey => {
// Session Messenger doesn't use the deviceId scheme, it's always 1. // Session Messenger doesn't use the deviceId scheme, it's always 1.
@ -339,9 +326,6 @@ OutgoingMessage.prototype = {
); );
const ourKey = textsecure.storage.user.getNumber(); const ourKey = textsecure.storage.user.getNumber();
const options = {}; const options = {};
const fallBackCipher = new libloki.crypto.FallBackSessionCipher(
address
);
let isMultiDeviceRequest = false; let isMultiDeviceRequest = false;
let thisDeviceMessageType = this.messageType; let thisDeviceMessageType = this.messageType;
@ -387,8 +371,7 @@ OutgoingMessage.prototype = {
flags === textsecure.protobuf.DataMessage.Flags.END_SESSION; flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
const signalCipher = new libsignal.SessionCipher( const signalCipher = new libsignal.SessionCipher(
textsecure.storage.protocol, textsecure.storage.protocol,
address, address
options
); );
if (enableFallBackEncryption || isEndSession) { if (enableFallBackEncryption || isEndSession) {
// Encrypt them with the fallback // Encrypt them with the fallback
@ -418,7 +401,7 @@ OutgoingMessage.prototype = {
} }
if (enableFallBackEncryption) { if (enableFallBackEncryption) {
sessionCipher = fallBackCipher; sessionCipher = new libloki.crypto.FallBackSessionCipher(address);
} else { } else {
sessionCipher = signalCipher; sessionCipher = signalCipher;
} }
@ -429,41 +412,55 @@ OutgoingMessage.prototype = {
options.messageKeysLimit = false; options.messageKeysLimit = false;
} }
ciphers[address.getDeviceId()] = sessionCipher; let content;
let type;
// Encrypt our plain text if (window.lokiFeatureFlags.useSealedSender) {
const ciphertext = await sessionCipher.encrypt(plaintext); const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
if (!enableFallBackEncryption) { textsecure.storage.protocol
// eslint-disable-next-line no-param-reassign
ciphertext.body = new Uint8Array(
dcodeIO.ByteBuffer.wrap(ciphertext.body, 'binary').toArrayBuffer()
); );
} ciphers[address.getDeviceId()] = secretSessionCipher;
const getTTL = type => {
switch (type) { const senderCert = new textsecure.protobuf.SenderCertificate();
case 'friend-request':
return 4 * 24 * 60 * 60 * 1000; // 4 days for friend request message senderCert.sender = ourKey;
case 'device-unpairing': senderCert.senderDevice = deviceId;
return 4 * 24 * 60 * 60 * 1000; // 4 days for device unpairing
case 'onlineBroadcast': const ciphertext = await secretSessionCipher.encrypt(
return 60 * 1000; // 1 minute for online broadcast message address,
case 'typing': senderCert,
return 60 * 1000; // 1 minute for typing indicators plaintext,
case 'pairing-request': sessionCipher
return 2 * 60 * 1000; // 2 minutes for pairing requests );
default:
return (window.getMessageTTL() || 24) * 60 * 60 * 1000; // 1 day default for any other message type = textsecure.protobuf.Envelope.Type.UNIDENTIFIED_SENDER;
content = window.Signal.Crypto.arrayBufferToBase64(ciphertext);
} else {
// TODO: probably remove this branch once
// mobile clients implement sealed sender
ciphers[address.getDeviceId()] = sessionCipher;
const ciphertext = await sessionCipher.encrypt(plaintext);
if (!enableFallBackEncryption) {
// eslint-disable-next-line no-param-reassign
ciphertext.body = new Uint8Array(
dcodeIO.ByteBuffer.wrap(ciphertext.body, 'binary').toArrayBuffer()
);
} }
};
const ttl = getTTL(thisDeviceMessageType); // eslint-disable-next-line prefer-destructuring
type = ciphertext.type;
content = ciphertext.body;
}
const ttl = getTTLForType(thisDeviceMessageType);
return { return {
type: ciphertext.type, // FallBackSessionCipher sets this to FRIEND_REQUEST type, // FallBackSessionCipher sets this to FRIEND_REQUEST
ttl, ttl,
ourKey, ourKey,
sourceDevice: 1, sourceDevice: 1,
destinationRegistrationId: ciphertext.registrationId, content,
content: ciphertext.body,
pubKey: devicePubKey, pubKey: devicePubKey,
}; };
}) })

@ -495,10 +495,12 @@ window.pubkeyPattern = /@[a-fA-F0-9]{64,66}\b/g;
// Limited due to the proof-of-work requirement // Limited due to the proof-of-work requirement
window.SMALL_GROUP_SIZE_LIMIT = 10; window.SMALL_GROUP_SIZE_LIMIT = 10;
// TODO: activate SealedSender once it is ready on all platforms
window.lokiFeatureFlags = { window.lokiFeatureFlags = {
multiDeviceUnpairing: true, multiDeviceUnpairing: true,
privateGroupChats: false, privateGroupChats: false,
useSnodeProxy: false, useSnodeProxy: false,
useSealedSender: false,
}; };
// eslint-disable-next-line no-extend-native,func-names // eslint-disable-next-line no-extend-native,func-names

@ -13,17 +13,10 @@ message ServerCertificate {
optional bytes signature = 2; optional bytes signature = 2;
} }
// This should perhaps be renamed to something like `SenderInfo`
message SenderCertificate { message SenderCertificate {
message Certificate { optional string sender = 1;
optional string sender = 1; optional uint32 senderDevice = 2;
optional uint32 senderDevice = 2;
optional fixed64 expires = 3;
optional bytes identityKey = 4;
optional ServerCertificate signer = 5;
}
optional bytes certificate = 1;
optional bytes signature = 2;
} }
message UnidentifiedSenderMessage { message UnidentifiedSenderMessage {
@ -32,6 +25,7 @@ message UnidentifiedSenderMessage {
enum Type { enum Type {
PREKEY_MESSAGE = 1; PREKEY_MESSAGE = 1;
MESSAGE = 2; MESSAGE = 2;
LOKI_FRIEND_REQUEST = 3;
} }
optional Type type = 1; optional Type type = 1;

Loading…
Cancel
Save