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",
"seedNodeList": [
{
"ip": "localhost",
"port": "22129"
}
],
"openDevTools": true,
"defaultPublicChatServer": "https://team-chat.lokinet.org/"
}
"storageProfile": "swarm-testing",
"seedNodeList": [
{
"ip": "localhost",
"port": "22129"
}
],
"openDevTools": true,
"defaultPublicChatServer": "https://team-chat.lokinet.org/"
}

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

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

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

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

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

@ -45,6 +45,7 @@
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();
const myPrivateKey = myKeyPair.privKey;

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

@ -14,6 +14,23 @@
/* eslint-disable no-unreachable */
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(
server,
timestamp,
@ -289,36 +306,6 @@ OutgoingMessage.prototype = {
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(
devicesPubKeys.map(async devicePubKey => {
// 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 options = {};
const fallBackCipher = new libloki.crypto.FallBackSessionCipher(
address
);
let isMultiDeviceRequest = false;
let thisDeviceMessageType = this.messageType;
@ -387,8 +371,7 @@ OutgoingMessage.prototype = {
flags === textsecure.protobuf.DataMessage.Flags.END_SESSION;
const signalCipher = new libsignal.SessionCipher(
textsecure.storage.protocol,
address,
options
address
);
if (enableFallBackEncryption || isEndSession) {
// Encrypt them with the fallback
@ -418,7 +401,7 @@ OutgoingMessage.prototype = {
}
if (enableFallBackEncryption) {
sessionCipher = fallBackCipher;
sessionCipher = new libloki.crypto.FallBackSessionCipher(address);
} else {
sessionCipher = signalCipher;
}
@ -429,41 +412,55 @@ OutgoingMessage.prototype = {
options.messageKeysLimit = false;
}
ciphers[address.getDeviceId()] = sessionCipher;
let content;
let type;
// Encrypt our plain text
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()
if (window.lokiFeatureFlags.useSealedSender) {
const secretSessionCipher = new window.Signal.Metadata.SecretSessionCipher(
textsecure.storage.protocol
);
}
const getTTL = 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
ciphers[address.getDeviceId()] = secretSessionCipher;
const senderCert = new textsecure.protobuf.SenderCertificate();
senderCert.sender = ourKey;
senderCert.senderDevice = deviceId;
const ciphertext = await secretSessionCipher.encrypt(
address,
senderCert,
plaintext,
sessionCipher
);
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 {
type: ciphertext.type, // FallBackSessionCipher sets this to FRIEND_REQUEST
type, // FallBackSessionCipher sets this to FRIEND_REQUEST
ttl,
ourKey,
sourceDevice: 1,
destinationRegistrationId: ciphertext.registrationId,
content: ciphertext.body,
content,
pubKey: devicePubKey,
};
})

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

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

Loading…
Cancel
Save