Profiles (#1453)
* Add AES-GCM encryption for profiles With tests. * Add profileKey to DataMessage protobuf // FREEBIE * Decrypt and save profile names // FREEBIE * Save incoming profile keys * Move pad/unpad to crypto module // FREEBIE * Support fetching avatars from the cdn // FREEBIE * Translate failed authentication errors When AES-GCM authentication fails, webcrypto returns a very generic error. The same error is thrown for invalid length inputs, but our earlier checks in decryptProfile should rule out those failure modes and leave us safe to assume that we either had bad ciphertext or the wrong key. // FREEBIE * Handle profile avatars (wip) and log decrypt errors // FREEBIE * Display profile avatars Synced contact avatars will still override profile avatars. * Display profile names in convo list Only if we don't have a synced contact name. // FREEBIE * Make cdn url an environment config Use different ones for staging and production // FREEBIE * Display profile name in conversation header * Display profile name in group messages * Update conversation header if profile avatar changes // FREEBIE * Style profile names small with ~ * Save profileKeys from contact sync messages // FREEBIE * Save profile keys from provisioning messages For standalone accounts, generate a random profile key. // FREEBIE * Special case for one-time sync of our profile key Android will use a contact sync message to sync a profile key from Android clients who have just upgraded and generated their profile key. Normally we should receive this data in a provisioning message. // FREEBIE * Infer profile sharing from synced data messages * Populate profile keys on outgoing messages Requires that `profileSharing` be set on the conversation. // FREEBIE * Support for the profile key update flag When receiving a message with this flag, don't init a message record, just process the profile key and move on. // FREEBIE * Display profile names in group member list * Refresh contact's profile on profile key changes // FREEBIE * Catch errors on profile save // FREEBIE * Save our own synced contact info Don't return early if we get a contact sync for our own number // FREEBIE
This commit is contained in:
parent
02571b7ae9
commit
ae190fed44
25 changed files with 464 additions and 36 deletions
|
@ -142,6 +142,9 @@
|
|||
{{ #number }}
|
||||
<span class='conversation-number'>{{ number }}</span>
|
||||
{{ /number }}
|
||||
{{ #profileName }}
|
||||
<span class='profileName'>{{ profileName }} </span>
|
||||
{{ /profileName }}
|
||||
{{ #isVerified }}
|
||||
<span class='verified'><span class='verified-icon'></span> {{ verified }}</span>
|
||||
{{ /isVerified }}
|
||||
|
@ -267,7 +270,12 @@
|
|||
<script type='text/x-tmpl-mustache' id='message'>
|
||||
{{> avatar }}
|
||||
<div class='bubble {{ avatar.color }}'>
|
||||
<div class='sender' dir='auto'>{{ sender }}</div>
|
||||
<div class='sender' dir='auto'>
|
||||
{{ sender }}
|
||||
{{ #profileName }}
|
||||
<span class='profileName'>{{ profileName }} </span>
|
||||
{{ /profileName }}
|
||||
</div>
|
||||
<div class='attachments'></div>
|
||||
<p class='content' dir='auto'>
|
||||
{{ #message }}<span class='body'>{{ message }}</span>{{ /message }}
|
||||
|
@ -321,8 +329,13 @@
|
|||
<span>{{ name }}</span><span class='remove'>x</span>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='contact_name_and_number'>
|
||||
<h3 class='name' dir='auto'> {{ title }} </h3>
|
||||
<div class='number'>{{ #isVerified }}<span class='verified-icon'></span> {{ verified }} ·{{ /isVerified }} {{ number }}</div>
|
||||
<h3 class='name' dir='auto'>
|
||||
{{ title }}
|
||||
{{ #profileName }}
|
||||
<span class='profileName'>{{ profileName }} </span>
|
||||
{{ /profileName }}
|
||||
</h3>
|
||||
<div class='number'>{{ #isVerified }}<span class='verified-icon'></span> {{ verified }} ·{{ /isVerified }} {{ number }}</div>
|
||||
</script>
|
||||
<script type='text/x-tmpl-mustache' id='contact'>
|
||||
{{> avatar }}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"serverUrl": "https://textsecure-service-staging.whispersystems.org",
|
||||
"cdnUrl": "https://cdn-staging.signal.org",
|
||||
"disableAutoUpdate": false,
|
||||
"openDevTools": false,
|
||||
"buildExpiration": 0,
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"serverUrl": "https://textsecure-service.whispersystems.org"
|
||||
"serverUrl": "https://textsecure-service.whispersystems.org",
|
||||
"cdnUrl": "https://cdn.signal.org"
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
});
|
||||
|
||||
var SERVER_URL = window.config.serverUrl;
|
||||
var CDN_URL = window.config.cdnUrl;
|
||||
var messageReceiver;
|
||||
window.getSocketStatus = function() {
|
||||
if (messageReceiver) {
|
||||
|
@ -184,7 +185,7 @@
|
|||
messageReceiver.addEventListener('progress', onProgress);
|
||||
|
||||
window.textsecure.messaging = new textsecure.MessageSender(
|
||||
SERVER_URL, USERNAME, PASSWORD
|
||||
SERVER_URL, USERNAME, PASSWORD, CDN_URL
|
||||
);
|
||||
|
||||
// Because v0.43.2 introduced a bug that lost contact details, v0.43.4 introduces
|
||||
|
@ -242,6 +243,15 @@
|
|||
var details = ev.contactDetails;
|
||||
|
||||
var id = details.number;
|
||||
|
||||
if (id === textsecure.storage.user.getNumber()) {
|
||||
// special case for syncing details about ourselves
|
||||
if (details.profileKey) {
|
||||
console.log('Got sync message with our own profile key');
|
||||
storage.put('profileKey', details.profileKey);
|
||||
}
|
||||
}
|
||||
|
||||
var c = new Whisper.Conversation({
|
||||
id: id
|
||||
});
|
||||
|
@ -254,6 +264,9 @@
|
|||
return ConversationController.getOrCreateAndWait(id, 'private')
|
||||
.then(function(conversation) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (details.profileKey) {
|
||||
conversation.set({profileKey: details.profileKey});
|
||||
}
|
||||
conversation.save({
|
||||
name: details.name,
|
||||
avatar: details.avatar,
|
||||
|
@ -307,6 +320,12 @@
|
|||
|
||||
function onMessageReceived(ev) {
|
||||
var data = ev.data;
|
||||
if (data.message.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
|
||||
var profileKey = data.message.profileKey.toArrayBuffer();
|
||||
return ConversationController.getOrCreateAndWait(data.source, 'private').then(function(sender) {
|
||||
return sender.setProfileKey(profileKey).then(ev.confirm);
|
||||
});
|
||||
}
|
||||
var message = initIncomingMessage(data);
|
||||
|
||||
return isMessageDuplicate(message).then(function(isDuplicate) {
|
||||
|
@ -337,6 +356,13 @@
|
|||
var now = new Date().getTime();
|
||||
var data = ev.data;
|
||||
|
||||
if (data.message.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
|
||||
var id = data.message.group ? data.message.group.id : data.destination;
|
||||
return ConversationController.getOrCreateAndWait(id, 'private').then(function(convo) {
|
||||
return convo.save({profileSharing: true}).then(ev.confirm);
|
||||
});
|
||||
}
|
||||
|
||||
var message = new Whisper.Message({
|
||||
source : textsecure.storage.user.getNumber(),
|
||||
sourceDevice : data.device,
|
||||
|
|
|
@ -36716,6 +36716,11 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ
|
|||
var calculateMAC = libsignal.crypto.calculateMAC;
|
||||
var verifyMAC = libsignal.crypto.verifyMAC;
|
||||
|
||||
var PROFILE_IV_LENGTH = 12; // bytes
|
||||
var PROFILE_KEY_LENGTH = 32; // bytes
|
||||
var PROFILE_TAG_LENGTH = 128; // bits
|
||||
var PROFILE_NAME_PADDED_LENGTH = 26; // bytes
|
||||
|
||||
function verifyDigest(data, theirDigest) {
|
||||
return crypto.subtle.digest({name: 'SHA-256'}, data).then(function(ourDigest) {
|
||||
var a = new Uint8Array(ourDigest);
|
||||
|
@ -36812,6 +36817,68 @@ Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJ
|
|||
});
|
||||
});
|
||||
},
|
||||
encryptProfile: function(data, key) {
|
||||
var iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH);
|
||||
if (key.byteLength != PROFILE_KEY_LENGTH) {
|
||||
throw new Error("Got invalid length profile key");
|
||||
}
|
||||
if (iv.byteLength != PROFILE_IV_LENGTH) {
|
||||
throw new Error("Got invalid length profile iv");
|
||||
}
|
||||
return crypto.subtle.importKey('raw', key, {name: 'AES-GCM'}, false, ['encrypt']).then(function(key) {
|
||||
return crypto.subtle.encrypt({name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH}, key, data).then(function(ciphertext) {
|
||||
var ivAndCiphertext = new Uint8Array(PROFILE_IV_LENGTH + ciphertext.byteLength);
|
||||
ivAndCiphertext.set(new Uint8Array(iv));
|
||||
ivAndCiphertext.set(new Uint8Array(ciphertext), PROFILE_IV_LENGTH);
|
||||
return ivAndCiphertext.buffer;
|
||||
});
|
||||
});
|
||||
},
|
||||
decryptProfile: function(data, key) {
|
||||
if (data.byteLength < 12 + 16 + 1) {
|
||||
throw new Error("Got too short input: " + data.byteLength);
|
||||
}
|
||||
var iv = data.slice(0, PROFILE_IV_LENGTH);
|
||||
var ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength);
|
||||
if (key.byteLength != PROFILE_KEY_LENGTH) {
|
||||
throw new Error("Got invalid length profile key");
|
||||
}
|
||||
if (iv.byteLength != PROFILE_IV_LENGTH) {
|
||||
throw new Error("Got invalid length profile iv");
|
||||
}
|
||||
var error = new Error(); // save stack
|
||||
return crypto.subtle.importKey('raw', key, {name: 'AES-GCM'}, false, ['decrypt']).then(function(key) {
|
||||
return crypto.subtle.decrypt({name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH}, key, ciphertext).catch(function(e) {
|
||||
if (e.name === 'OperationError') {
|
||||
// bad mac, basically.
|
||||
error.message = 'Failed to decrypt profile data. Most likely the profile key has changed.';
|
||||
error.name = 'ProfileDecryptError';
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
encryptProfileName: function(name, key) {
|
||||
var padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH);
|
||||
padded.set(new Uint8Array(name));
|
||||
return textsecure.crypto.encryptProfile(padded.buffer, key);
|
||||
},
|
||||
decryptProfileName: function(encryptedProfileName, key) {
|
||||
var data = dcodeIO.ByteBuffer.wrap(encryptedProfileName, 'base64').toArrayBuffer();
|
||||
return textsecure.crypto.decryptProfile(data, key).then(function(decrypted) {
|
||||
// unpad
|
||||
var name = '';
|
||||
var padded = new Uint8Array(decrypted);
|
||||
for (var i = padded.length; i > 0; i--) {
|
||||
if (padded[i-1] !== 0x00) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return dcodeIO.ByteBuffer.wrap(padded).slice(0, i).toArrayBuffer();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
getRandomBytes: function(size) {
|
||||
return libsignal.crypto.getRandomBytes(size);
|
||||
|
@ -37709,11 +37776,12 @@ var TextSecureServer = (function() {
|
|||
profile : "v1/profile"
|
||||
};
|
||||
|
||||
function TextSecureServer(url, username, password) {
|
||||
function TextSecureServer(url, username, password, cdn_url) {
|
||||
if (typeof url !== 'string') {
|
||||
throw new Error('Invalid server url');
|
||||
}
|
||||
this.url = url;
|
||||
this.cdn_url = cdn_url;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
@ -37777,6 +37845,14 @@ var TextSecureServer = (function() {
|
|||
urlParameters : '/' + number,
|
||||
});
|
||||
},
|
||||
getAvatar: function(path) {
|
||||
return ajax(this.cdn_url + '/' + path, {
|
||||
type : "GET",
|
||||
responseType: "arraybuffer",
|
||||
contentType : "application/octet-stream",
|
||||
certificateAuthorities: window.config.certificateAuthorities
|
||||
});
|
||||
},
|
||||
requestVerificationSMS: function(number) {
|
||||
return this.ajax({
|
||||
call : 'accounts',
|
||||
|
@ -37994,7 +38070,8 @@ var TextSecureServer = (function() {
|
|||
var registrationDone = this.registrationDone.bind(this);
|
||||
return this.queueTask(function() {
|
||||
return libsignal.KeyHelper.generateIdentityKeyPair().then(function(identityKeyPair) {
|
||||
return createAccount(number, verificationCode, identityKeyPair).
|
||||
var profileKey = textsecure.crypto.getRandomBytes(32);
|
||||
return createAccount(number, verificationCode, identityKeyPair, profileKey).
|
||||
then(generateKeys).
|
||||
then(registerKeys).
|
||||
then(registrationDone);
|
||||
|
@ -38047,6 +38124,7 @@ var TextSecureServer = (function() {
|
|||
provisionMessage.number,
|
||||
provisionMessage.provisioningCode,
|
||||
provisionMessage.identityKeyPair,
|
||||
provisionMessage.profileKey,
|
||||
deviceName,
|
||||
provisionMessage.userAgent
|
||||
).then(generateKeys).
|
||||
|
@ -38147,7 +38225,7 @@ var TextSecureServer = (function() {
|
|||
});
|
||||
});
|
||||
},
|
||||
createAccount: function(number, verificationCode, identityKeyPair, deviceName, userAgent) {
|
||||
createAccount: function(number, verificationCode, identityKeyPair, profileKey, deviceName, userAgent) {
|
||||
var signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
|
||||
var password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
|
||||
password = password.substring(0, password.length - 2);
|
||||
|
@ -38165,6 +38243,7 @@ var TextSecureServer = (function() {
|
|||
textsecure.storage.remove('device_name');
|
||||
textsecure.storage.remove('regionCode');
|
||||
textsecure.storage.remove('userAgent');
|
||||
textsecure.storage.remove('profileKey');
|
||||
|
||||
// update our own identity key, which may have changed
|
||||
// if we're relinking after a reinstall on the master device
|
||||
|
@ -38181,6 +38260,7 @@ var TextSecureServer = (function() {
|
|||
textsecure.storage.put('signaling_key', signalingKey);
|
||||
textsecure.storage.put('password', password);
|
||||
textsecure.storage.put('registrationId', registrationId);
|
||||
textsecure.storage.put('profileKey', profileKey);
|
||||
if (userAgent) {
|
||||
textsecure.storage.put('userAgent', userAgent);
|
||||
}
|
||||
|
@ -39017,6 +39097,9 @@ MessageReceiver.prototype.extend({
|
|||
} else if (decrypted.flags & textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE ) {
|
||||
decrypted.body = null;
|
||||
decrypted.attachments = [];
|
||||
} else if (decrypted.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
|
||||
decrypted.body = null;
|
||||
decrypted.attachments = [];
|
||||
} else if (decrypted.flags != 0) {
|
||||
throw new Error("Unknown flags in message");
|
||||
}
|
||||
|
@ -39374,6 +39457,7 @@ function Message(options) {
|
|||
this.timestamp = options.timestamp;
|
||||
this.needsSync = options.needsSync;
|
||||
this.expireTimer = options.expireTimer;
|
||||
this.profileKey = options.profileKey;
|
||||
|
||||
if (!(this.recipients instanceof Array) || this.recipients.length < 1) {
|
||||
throw new Error('Invalid recipient list');
|
||||
|
@ -39447,6 +39531,10 @@ Message.prototype = {
|
|||
proto.expireTimer = this.expireTimer;
|
||||
}
|
||||
|
||||
if (this.profileKey) {
|
||||
proto.profileKey = this.profileKey;
|
||||
}
|
||||
|
||||
this.dataMessage = proto;
|
||||
return proto;
|
||||
},
|
||||
|
@ -39455,8 +39543,8 @@ Message.prototype = {
|
|||
}
|
||||
};
|
||||
|
||||
function MessageSender(url, username, password) {
|
||||
this.server = new TextSecureServer(url, username, password);
|
||||
function MessageSender(url, username, password, cdn_url) {
|
||||
this.server = new TextSecureServer(url, username, password, cdn_url);
|
||||
this.pendingMessages = {};
|
||||
}
|
||||
|
||||
|
@ -39670,6 +39758,9 @@ MessageSender.prototype = {
|
|||
getProfile: function(number) {
|
||||
return this.server.getProfile(number);
|
||||
},
|
||||
getAvatar: function(path) {
|
||||
return this.server.getAvatar(path);
|
||||
},
|
||||
|
||||
sendRequestGroupSyncMessage: function() {
|
||||
var myNumber = textsecure.storage.user.getNumber();
|
||||
|
@ -39780,14 +39871,15 @@ MessageSender.prototype = {
|
|||
}.bind(this));
|
||||
},
|
||||
|
||||
sendMessageToNumber: function(number, messageText, attachments, timestamp, expireTimer) {
|
||||
sendMessageToNumber: function(number, messageText, attachments, timestamp, expireTimer, profileKey) {
|
||||
return this.sendMessage({
|
||||
recipients : [number],
|
||||
body : messageText,
|
||||
timestamp : timestamp,
|
||||
attachments : attachments,
|
||||
needsSync : true,
|
||||
expireTimer : expireTimer
|
||||
expireTimer : expireTimer,
|
||||
profileKey : profileKey
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -39812,7 +39904,7 @@ MessageSender.prototype = {
|
|||
}.bind(this));
|
||||
},
|
||||
|
||||
sendMessageToGroup: function(groupId, messageText, attachments, timestamp, expireTimer) {
|
||||
sendMessageToGroup: function(groupId, messageText, attachments, timestamp, expireTimer, profileKey) {
|
||||
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
|
||||
if (numbers === undefined)
|
||||
return Promise.reject(new Error("Unknown Group"));
|
||||
|
@ -39830,6 +39922,7 @@ MessageSender.prototype = {
|
|||
attachments : attachments,
|
||||
needsSync : true,
|
||||
expireTimer : expireTimer,
|
||||
profileKey : profileKey,
|
||||
group: {
|
||||
id: groupId,
|
||||
type: textsecure.protobuf.GroupContext.Type.DELIVER
|
||||
|
@ -39945,7 +40038,7 @@ MessageSender.prototype = {
|
|||
}.bind(this));
|
||||
});
|
||||
},
|
||||
sendExpirationTimerUpdateToGroup: function(groupId, expireTimer, timestamp) {
|
||||
sendExpirationTimerUpdateToGroup: function(groupId, expireTimer, timestamp, profileKey) {
|
||||
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
|
||||
if (numbers === undefined)
|
||||
return Promise.reject(new Error("Unknown Group"));
|
||||
|
@ -39960,6 +40053,7 @@ MessageSender.prototype = {
|
|||
timestamp : timestamp,
|
||||
needsSync : true,
|
||||
expireTimer : expireTimer,
|
||||
profileKey : profileKey,
|
||||
flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
group: {
|
||||
id: groupId,
|
||||
|
@ -39968,13 +40062,14 @@ MessageSender.prototype = {
|
|||
});
|
||||
}.bind(this));
|
||||
},
|
||||
sendExpirationTimerUpdateToNumber: function(number, expireTimer, timestamp) {
|
||||
sendExpirationTimerUpdateToNumber: function(number, expireTimer, timestamp, profileKey) {
|
||||
var proto = new textsecure.protobuf.DataMessage();
|
||||
return this.sendMessage({
|
||||
recipients : [number],
|
||||
timestamp : timestamp,
|
||||
needsSync : true,
|
||||
expireTimer : expireTimer,
|
||||
profileKey : profileKey,
|
||||
flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE
|
||||
});
|
||||
}
|
||||
|
@ -39982,8 +40077,8 @@ MessageSender.prototype = {
|
|||
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
textsecure.MessageSender = function(url, username, password) {
|
||||
var sender = new MessageSender(url, username, password);
|
||||
textsecure.MessageSender = function(url, username, password, cdn_url) {
|
||||
var sender = new MessageSender(url, username, password, cdn_url);
|
||||
textsecure.replay.registerFunction(sender.tryMessageAgain.bind(sender), textsecure.replay.Type.ENCRYPT_MESSAGE);
|
||||
textsecure.replay.registerFunction(sender.retransmitMessage.bind(sender), textsecure.replay.Type.TRANSMIT_MESSAGE);
|
||||
textsecure.replay.registerFunction(sender.sendMessage.bind(sender), textsecure.replay.Type.REBUILD_MESSAGE);
|
||||
|
@ -40004,6 +40099,7 @@ textsecure.MessageSender = function(url, username, password) {
|
|||
this.leaveGroup = sender.leaveGroup .bind(sender);
|
||||
this.sendSyncMessage = sender.sendSyncMessage .bind(sender);
|
||||
this.getProfile = sender.getProfile .bind(sender);
|
||||
this.getAvatar = sender.getAvatar .bind(sender);
|
||||
this.syncReadMessages = sender.syncReadMessages .bind(sender);
|
||||
this.syncVerification = sender.syncVerification .bind(sender);
|
||||
};
|
||||
|
@ -40128,6 +40224,10 @@ ProtoParser.prototype = {
|
|||
this.buffer.skip(attachmentLen);
|
||||
}
|
||||
|
||||
if (proto.profileKey) {
|
||||
proto.profileKey = proto.profileKey.toArrayBuffer();
|
||||
}
|
||||
|
||||
return proto;
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
|
@ -40182,7 +40282,8 @@ ProvisioningCipher.prototype = {
|
|||
identityKeyPair : keyPair,
|
||||
number : provisionMessage.number,
|
||||
provisioningCode : provisionMessage.provisioningCode,
|
||||
userAgent : provisionMessage.userAgent
|
||||
userAgent : provisionMessage.userAgent,
|
||||
profileKey : provisionMessage.profileKey.toArrayBuffer()
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -72,6 +72,8 @@
|
|||
this.messageCollection.on('send-error', this.onMessageError, this);
|
||||
|
||||
this.on('change:avatar', this.updateAvatarUrl);
|
||||
this.on('change:profileAvatar', this.updateAvatarUrl);
|
||||
this.on('change:profileKey', this.onChangeProfileKey);
|
||||
this.on('destroy', this.revokeAvatarUrl);
|
||||
},
|
||||
|
||||
|
@ -590,7 +592,13 @@
|
|||
else {
|
||||
sendFunc = textsecure.messaging.sendMessageToGroup;
|
||||
}
|
||||
message.send(sendFunc(this.get('id'), body, attachments, now, this.get('expireTimer')));
|
||||
|
||||
var profileKey;
|
||||
if (this.get('profileSharing')) {
|
||||
profileKey = storage.get('profileKey');
|
||||
}
|
||||
|
||||
message.send(sendFunc(this.get('id'), body, attachments, now, this.get('expireTimer'), profileKey));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
@ -643,7 +651,11 @@
|
|||
else {
|
||||
sendFunc = textsecure.messaging.sendExpirationTimerUpdateToGroup;
|
||||
}
|
||||
message.send(sendFunc(this.get('id'), this.get('expireTimer'), message.get('sent_at')));
|
||||
var profileKey;
|
||||
if (this.get('profileSharing')) {
|
||||
profileKey = storage.get('profileKey');
|
||||
}
|
||||
message.send(sendFunc(this.get('id'), this.get('expireTimer'), message.get('sent_at'), profileKey));
|
||||
}
|
||||
return message;
|
||||
},
|
||||
|
@ -758,6 +770,12 @@
|
|||
}.bind(this));
|
||||
},
|
||||
|
||||
onChangeProfileKey: function() {
|
||||
if (this.isPrivate()) {
|
||||
this.getProfiles();
|
||||
}
|
||||
},
|
||||
|
||||
getProfiles: function() {
|
||||
// request all conversation members' keys
|
||||
var ids = [];
|
||||
|
@ -784,14 +802,75 @@
|
|||
var sessionCipher = new libsignal.SessionCipher(textsecure.storage.protocol, address);
|
||||
return sessionCipher.closeOpenSessionForDevice();
|
||||
}
|
||||
});
|
||||
}).catch(function(error) {
|
||||
}).then(function() {
|
||||
var c = ConversationController.get(id);
|
||||
return Promise.all([
|
||||
c.setProfileName(profile.name),
|
||||
c.setProfileAvatar(profile.avatar)
|
||||
]).then(function() {
|
||||
// success
|
||||
return new Promise(function(resolve, reject) {
|
||||
c.save().then(resolve, reject);
|
||||
});
|
||||
}, function(e) {
|
||||
// fail
|
||||
if (e.name === 'ProfileDecryptError') {
|
||||
// probably the profile key has changed.
|
||||
console.log('decryptProfile error:', e);
|
||||
}
|
||||
});
|
||||
}.bind(this));
|
||||
}.bind(this)).catch(function(error) {
|
||||
console.log(
|
||||
'getProfile error:',
|
||||
error && error.stack ? error.stack : error
|
||||
);
|
||||
});
|
||||
},
|
||||
setProfileName: function(encryptedName) {
|
||||
var key = this.get('profileKey');
|
||||
if (!key) { return; }
|
||||
|
||||
// decode
|
||||
var data = dcodeIO.ByteBuffer.wrap(encryptedName, 'base64').toArrayBuffer();
|
||||
|
||||
// decrypt
|
||||
return textsecure.crypto.decryptProfileName(data, key).then(function(decrypted) {
|
||||
|
||||
// encode
|
||||
var name = dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8');
|
||||
|
||||
// set
|
||||
this.set({profileName: name});
|
||||
}.bind(this));
|
||||
},
|
||||
setProfileAvatar: function(avatarPath) {
|
||||
if (!avatarPath) { return; }
|
||||
return textsecure.messaging.getAvatar(avatarPath).then(function(avatar) {
|
||||
var key = this.get('profileKey');
|
||||
if (!key) { return; }
|
||||
// decrypt
|
||||
return textsecure.crypto.decryptProfile(avatar, key).then(function(decrypted) {
|
||||
// set
|
||||
this.set({
|
||||
profileAvatar: {
|
||||
data: decrypted,
|
||||
contentType: 'image/jpeg',
|
||||
size: decrypted.byteLength
|
||||
}
|
||||
});
|
||||
}.bind(this));
|
||||
}.bind(this));
|
||||
},
|
||||
setProfileKey: function(key) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (!constantTimeEqualArrayBuffers(this.get('profileKey'), key)) {
|
||||
this.save({profileKey: key}).then(resolve, reject);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
fetchMessages: function() {
|
||||
if (!this.id) {
|
||||
|
@ -855,6 +934,12 @@
|
|||
}
|
||||
},
|
||||
|
||||
getProfileName: function() {
|
||||
if (this.isPrivate() && !this.get('name')) {
|
||||
return this.get('profileName');
|
||||
}
|
||||
},
|
||||
|
||||
getNumber: function() {
|
||||
if (!this.isPrivate()) {
|
||||
return '';
|
||||
|
@ -886,7 +971,7 @@
|
|||
|
||||
updateAvatarUrl: function(silent) {
|
||||
this.revokeAvatarUrl();
|
||||
var avatar = this.get('avatar');
|
||||
var avatar = this.get('avatar') || this.get('profileAvatar');
|
||||
if (avatar) {
|
||||
this.avatarUrl = URL.createObjectURL(
|
||||
new Blob([avatar.data], {type: avatar.contentType})
|
||||
|
|
|
@ -456,6 +456,19 @@
|
|||
});
|
||||
}
|
||||
|
||||
if (dataMessage.profileKey) {
|
||||
var profileKey = dataMessage.profileKey.toArrayBuffer();
|
||||
if (source == textsecure.storage.user.getNumber()) {
|
||||
conversation.set({profileSharing: true});
|
||||
} else if (conversation.isPrivate()) {
|
||||
conversation.set({profileKey: profileKey});
|
||||
} else {
|
||||
ConversationController.getOrCreateAndWait(source, 'private').then(function(sender) {
|
||||
sender.setProfileKey(profileKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var handleError = function(error) {
|
||||
error = error && error.stack ? error.stack : error;
|
||||
console.log('handleDataMessage', message.idForLogging(), 'error:', error);
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
title: this.model.getTitle(),
|
||||
number: this.model.getNumber(),
|
||||
avatar: this.model.getAvatar(),
|
||||
profileName: this.model.getProfileName(),
|
||||
isVerified: this.model.isVerified(),
|
||||
verified: i18n('verified')
|
||||
};
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
last_message_timestamp: this.model.get('timestamp'),
|
||||
number: this.model.getNumber(),
|
||||
avatar: this.model.getAvatar(),
|
||||
profileName: this.model.getProfileName(),
|
||||
unreadCount: this.model.get('unreadCount')
|
||||
}, this.render_partials())
|
||||
);
|
||||
|
|
|
@ -73,6 +73,7 @@
|
|||
verified: i18n('verified'),
|
||||
name: this.model.getName(),
|
||||
number: this.model.getNumber(),
|
||||
profileName: this.model.getProfileName()
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -105,6 +106,7 @@
|
|||
this.listenTo(this.model, 'destroy', this.stopListening);
|
||||
this.listenTo(this.model, 'change:verified', this.onVerifiedChange);
|
||||
this.listenTo(this.model, 'change:color', this.updateColor);
|
||||
this.listenTo(this.model, 'change:avatar change:profileAvatar', this.updateAvatar);
|
||||
this.listenTo(this.model, 'newmessage', this.addMessage);
|
||||
this.listenTo(this.model, 'delivered', this.updateMessage);
|
||||
this.listenTo(this.model, 'opened', this.onOpened);
|
||||
|
@ -993,6 +995,15 @@
|
|||
header.find('.avatar').replaceWith(avatarView.render().$('.avatar'));
|
||||
},
|
||||
|
||||
updateAvatar: function(model) {
|
||||
var header = this.$('.conversation-header');
|
||||
var avatarView = new (Whisper.View.extend({
|
||||
templateName: 'avatar',
|
||||
render_attributes: { avatar: this.model.getAvatar() }
|
||||
}))();
|
||||
header.find('.avatar').replaceWith(avatarView.render().$('.avatar'));
|
||||
},
|
||||
|
||||
updateMessageFieldSize: function (event) {
|
||||
var keyCode = event.which || event.keyCode;
|
||||
|
||||
|
|
|
@ -340,6 +340,7 @@
|
|||
timestamp: this.model.get('sent_at'),
|
||||
sender: (contact && contact.getTitle()) || '',
|
||||
avatar: (contact && contact.getAvatar()),
|
||||
profileName: (contact && contact.getProfileName()),
|
||||
}, this.render_partials())
|
||||
);
|
||||
this.timeStampView.setElement(this.$('.timestamp'));
|
||||
|
|
|
@ -35,7 +35,8 @@ ProvisioningCipher.prototype = {
|
|||
identityKeyPair : keyPair,
|
||||
number : provisionMessage.number,
|
||||
provisioningCode : provisionMessage.provisioningCode,
|
||||
userAgent : provisionMessage.userAgent
|
||||
userAgent : provisionMessage.userAgent,
|
||||
profileKey : provisionMessage.profileKey.toArrayBuffer()
|
||||
};
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,7 +30,8 @@
|
|||
var registrationDone = this.registrationDone.bind(this);
|
||||
return this.queueTask(function() {
|
||||
return libsignal.KeyHelper.generateIdentityKeyPair().then(function(identityKeyPair) {
|
||||
return createAccount(number, verificationCode, identityKeyPair).
|
||||
var profileKey = textsecure.crypto.getRandomBytes(32);
|
||||
return createAccount(number, verificationCode, identityKeyPair, profileKey).
|
||||
then(generateKeys).
|
||||
then(registerKeys).
|
||||
then(registrationDone);
|
||||
|
@ -83,6 +84,7 @@
|
|||
provisionMessage.number,
|
||||
provisionMessage.provisioningCode,
|
||||
provisionMessage.identityKeyPair,
|
||||
provisionMessage.profileKey,
|
||||
deviceName,
|
||||
provisionMessage.userAgent
|
||||
).then(generateKeys).
|
||||
|
@ -183,7 +185,7 @@
|
|||
});
|
||||
});
|
||||
},
|
||||
createAccount: function(number, verificationCode, identityKeyPair, deviceName, userAgent) {
|
||||
createAccount: function(number, verificationCode, identityKeyPair, profileKey, deviceName, userAgent) {
|
||||
var signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
|
||||
var password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
|
||||
password = password.substring(0, password.length - 2);
|
||||
|
@ -201,6 +203,7 @@
|
|||
textsecure.storage.remove('device_name');
|
||||
textsecure.storage.remove('regionCode');
|
||||
textsecure.storage.remove('userAgent');
|
||||
textsecure.storage.remove('profileKey');
|
||||
|
||||
// update our own identity key, which may have changed
|
||||
// if we're relinking after a reinstall on the master device
|
||||
|
@ -217,6 +220,7 @@
|
|||
textsecure.storage.put('signaling_key', signalingKey);
|
||||
textsecure.storage.put('password', password);
|
||||
textsecure.storage.put('registrationId', registrationId);
|
||||
textsecure.storage.put('profileKey', profileKey);
|
||||
if (userAgent) {
|
||||
textsecure.storage.put('userAgent', userAgent);
|
||||
}
|
||||
|
|
|
@ -135,11 +135,12 @@ var TextSecureServer = (function() {
|
|||
profile : "v1/profile"
|
||||
};
|
||||
|
||||
function TextSecureServer(url, username, password) {
|
||||
function TextSecureServer(url, username, password, cdn_url) {
|
||||
if (typeof url !== 'string') {
|
||||
throw new Error('Invalid server url');
|
||||
}
|
||||
this.url = url;
|
||||
this.cdn_url = cdn_url;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
@ -203,6 +204,14 @@ var TextSecureServer = (function() {
|
|||
urlParameters : '/' + number,
|
||||
});
|
||||
},
|
||||
getAvatar: function(path) {
|
||||
return ajax(this.cdn_url + '/' + path, {
|
||||
type : "GET",
|
||||
responseType: "arraybuffer",
|
||||
contentType : "application/octet-stream",
|
||||
certificateAuthorities: window.config.certificateAuthorities
|
||||
});
|
||||
},
|
||||
requestVerificationSMS: function(number) {
|
||||
return this.ajax({
|
||||
call : 'accounts',
|
||||
|
|
|
@ -34,6 +34,10 @@ ProtoParser.prototype = {
|
|||
this.buffer.skip(attachmentLen);
|
||||
}
|
||||
|
||||
if (proto.profileKey) {
|
||||
proto.profileKey = proto.profileKey.toArrayBuffer();
|
||||
}
|
||||
|
||||
return proto;
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
|
|
|
@ -10,6 +10,11 @@
|
|||
var calculateMAC = libsignal.crypto.calculateMAC;
|
||||
var verifyMAC = libsignal.crypto.verifyMAC;
|
||||
|
||||
var PROFILE_IV_LENGTH = 12; // bytes
|
||||
var PROFILE_KEY_LENGTH = 32; // bytes
|
||||
var PROFILE_TAG_LENGTH = 128; // bits
|
||||
var PROFILE_NAME_PADDED_LENGTH = 26; // bytes
|
||||
|
||||
function verifyDigest(data, theirDigest) {
|
||||
return crypto.subtle.digest({name: 'SHA-256'}, data).then(function(ourDigest) {
|
||||
var a = new Uint8Array(ourDigest);
|
||||
|
@ -106,6 +111,68 @@
|
|||
});
|
||||
});
|
||||
},
|
||||
encryptProfile: function(data, key) {
|
||||
var iv = libsignal.crypto.getRandomBytes(PROFILE_IV_LENGTH);
|
||||
if (key.byteLength != PROFILE_KEY_LENGTH) {
|
||||
throw new Error("Got invalid length profile key");
|
||||
}
|
||||
if (iv.byteLength != PROFILE_IV_LENGTH) {
|
||||
throw new Error("Got invalid length profile iv");
|
||||
}
|
||||
return crypto.subtle.importKey('raw', key, {name: 'AES-GCM'}, false, ['encrypt']).then(function(key) {
|
||||
return crypto.subtle.encrypt({name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH}, key, data).then(function(ciphertext) {
|
||||
var ivAndCiphertext = new Uint8Array(PROFILE_IV_LENGTH + ciphertext.byteLength);
|
||||
ivAndCiphertext.set(new Uint8Array(iv));
|
||||
ivAndCiphertext.set(new Uint8Array(ciphertext), PROFILE_IV_LENGTH);
|
||||
return ivAndCiphertext.buffer;
|
||||
});
|
||||
});
|
||||
},
|
||||
decryptProfile: function(data, key) {
|
||||
if (data.byteLength < 12 + 16 + 1) {
|
||||
throw new Error("Got too short input: " + data.byteLength);
|
||||
}
|
||||
var iv = data.slice(0, PROFILE_IV_LENGTH);
|
||||
var ciphertext = data.slice(PROFILE_IV_LENGTH, data.byteLength);
|
||||
if (key.byteLength != PROFILE_KEY_LENGTH) {
|
||||
throw new Error("Got invalid length profile key");
|
||||
}
|
||||
if (iv.byteLength != PROFILE_IV_LENGTH) {
|
||||
throw new Error("Got invalid length profile iv");
|
||||
}
|
||||
var error = new Error(); // save stack
|
||||
return crypto.subtle.importKey('raw', key, {name: 'AES-GCM'}, false, ['decrypt']).then(function(key) {
|
||||
return crypto.subtle.decrypt({name: 'AES-GCM', iv: iv, tagLength: PROFILE_TAG_LENGTH}, key, ciphertext).catch(function(e) {
|
||||
if (e.name === 'OperationError') {
|
||||
// bad mac, basically.
|
||||
error.message = 'Failed to decrypt profile data. Most likely the profile key has changed.';
|
||||
error.name = 'ProfileDecryptError';
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
encryptProfileName: function(name, key) {
|
||||
var padded = new Uint8Array(PROFILE_NAME_PADDED_LENGTH);
|
||||
padded.set(new Uint8Array(name));
|
||||
return textsecure.crypto.encryptProfile(padded.buffer, key);
|
||||
},
|
||||
decryptProfileName: function(encryptedProfileName, key) {
|
||||
var data = dcodeIO.ByteBuffer.wrap(encryptedProfileName, 'base64').toArrayBuffer();
|
||||
return textsecure.crypto.decryptProfile(data, key).then(function(decrypted) {
|
||||
// unpad
|
||||
var name = '';
|
||||
var padded = new Uint8Array(decrypted);
|
||||
for (var i = padded.length; i > 0; i--) {
|
||||
if (padded[i-1] !== 0x00) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return dcodeIO.ByteBuffer.wrap(padded).slice(0, i).toArrayBuffer();
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
getRandomBytes: function(size) {
|
||||
return libsignal.crypto.getRandomBytes(size);
|
||||
|
|
|
@ -762,6 +762,9 @@ MessageReceiver.prototype.extend({
|
|||
} else if (decrypted.flags & textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE ) {
|
||||
decrypted.body = null;
|
||||
decrypted.attachments = [];
|
||||
} else if (decrypted.flags & textsecure.protobuf.DataMessage.Flags.PROFILE_KEY_UPDATE) {
|
||||
decrypted.body = null;
|
||||
decrypted.attachments = [];
|
||||
} else if (decrypted.flags != 0) {
|
||||
throw new Error("Unknown flags in message");
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ function Message(options) {
|
|||
this.timestamp = options.timestamp;
|
||||
this.needsSync = options.needsSync;
|
||||
this.expireTimer = options.expireTimer;
|
||||
this.profileKey = options.profileKey;
|
||||
|
||||
if (!(this.recipients instanceof Array) || this.recipients.length < 1) {
|
||||
throw new Error('Invalid recipient list');
|
||||
|
@ -96,6 +97,10 @@ Message.prototype = {
|
|||
proto.expireTimer = this.expireTimer;
|
||||
}
|
||||
|
||||
if (this.profileKey) {
|
||||
proto.profileKey = this.profileKey;
|
||||
}
|
||||
|
||||
this.dataMessage = proto;
|
||||
return proto;
|
||||
},
|
||||
|
@ -104,8 +109,8 @@ Message.prototype = {
|
|||
}
|
||||
};
|
||||
|
||||
function MessageSender(url, username, password) {
|
||||
this.server = new TextSecureServer(url, username, password);
|
||||
function MessageSender(url, username, password, cdn_url) {
|
||||
this.server = new TextSecureServer(url, username, password, cdn_url);
|
||||
this.pendingMessages = {};
|
||||
}
|
||||
|
||||
|
@ -319,6 +324,9 @@ MessageSender.prototype = {
|
|||
getProfile: function(number) {
|
||||
return this.server.getProfile(number);
|
||||
},
|
||||
getAvatar: function(path) {
|
||||
return this.server.getAvatar(path);
|
||||
},
|
||||
|
||||
sendRequestGroupSyncMessage: function() {
|
||||
var myNumber = textsecure.storage.user.getNumber();
|
||||
|
@ -429,14 +437,15 @@ MessageSender.prototype = {
|
|||
}.bind(this));
|
||||
},
|
||||
|
||||
sendMessageToNumber: function(number, messageText, attachments, timestamp, expireTimer) {
|
||||
sendMessageToNumber: function(number, messageText, attachments, timestamp, expireTimer, profileKey) {
|
||||
return this.sendMessage({
|
||||
recipients : [number],
|
||||
body : messageText,
|
||||
timestamp : timestamp,
|
||||
attachments : attachments,
|
||||
needsSync : true,
|
||||
expireTimer : expireTimer
|
||||
expireTimer : expireTimer,
|
||||
profileKey : profileKey
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -461,7 +470,7 @@ MessageSender.prototype = {
|
|||
}.bind(this));
|
||||
},
|
||||
|
||||
sendMessageToGroup: function(groupId, messageText, attachments, timestamp, expireTimer) {
|
||||
sendMessageToGroup: function(groupId, messageText, attachments, timestamp, expireTimer, profileKey) {
|
||||
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
|
||||
if (numbers === undefined)
|
||||
return Promise.reject(new Error("Unknown Group"));
|
||||
|
@ -479,6 +488,7 @@ MessageSender.prototype = {
|
|||
attachments : attachments,
|
||||
needsSync : true,
|
||||
expireTimer : expireTimer,
|
||||
profileKey : profileKey,
|
||||
group: {
|
||||
id: groupId,
|
||||
type: textsecure.protobuf.GroupContext.Type.DELIVER
|
||||
|
@ -594,7 +604,7 @@ MessageSender.prototype = {
|
|||
}.bind(this));
|
||||
});
|
||||
},
|
||||
sendExpirationTimerUpdateToGroup: function(groupId, expireTimer, timestamp) {
|
||||
sendExpirationTimerUpdateToGroup: function(groupId, expireTimer, timestamp, profileKey) {
|
||||
return textsecure.storage.groups.getNumbers(groupId).then(function(numbers) {
|
||||
if (numbers === undefined)
|
||||
return Promise.reject(new Error("Unknown Group"));
|
||||
|
@ -609,6 +619,7 @@ MessageSender.prototype = {
|
|||
timestamp : timestamp,
|
||||
needsSync : true,
|
||||
expireTimer : expireTimer,
|
||||
profileKey : profileKey,
|
||||
flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE,
|
||||
group: {
|
||||
id: groupId,
|
||||
|
@ -617,13 +628,14 @@ MessageSender.prototype = {
|
|||
});
|
||||
}.bind(this));
|
||||
},
|
||||
sendExpirationTimerUpdateToNumber: function(number, expireTimer, timestamp) {
|
||||
sendExpirationTimerUpdateToNumber: function(number, expireTimer, timestamp, profileKey) {
|
||||
var proto = new textsecure.protobuf.DataMessage();
|
||||
return this.sendMessage({
|
||||
recipients : [number],
|
||||
timestamp : timestamp,
|
||||
needsSync : true,
|
||||
expireTimer : expireTimer,
|
||||
profileKey : profileKey,
|
||||
flags : textsecure.protobuf.DataMessage.Flags.EXPIRATION_TIMER_UPDATE
|
||||
});
|
||||
}
|
||||
|
@ -631,8 +643,8 @@ MessageSender.prototype = {
|
|||
|
||||
window.textsecure = window.textsecure || {};
|
||||
|
||||
textsecure.MessageSender = function(url, username, password) {
|
||||
var sender = new MessageSender(url, username, password);
|
||||
textsecure.MessageSender = function(url, username, password, cdn_url) {
|
||||
var sender = new MessageSender(url, username, password, cdn_url);
|
||||
textsecure.replay.registerFunction(sender.tryMessageAgain.bind(sender), textsecure.replay.Type.ENCRYPT_MESSAGE);
|
||||
textsecure.replay.registerFunction(sender.retransmitMessage.bind(sender), textsecure.replay.Type.TRANSMIT_MESSAGE);
|
||||
textsecure.replay.registerFunction(sender.sendMessage.bind(sender), textsecure.replay.Type.REBUILD_MESSAGE);
|
||||
|
@ -653,6 +665,7 @@ textsecure.MessageSender = function(url, username, password) {
|
|||
this.leaveGroup = sender.leaveGroup .bind(sender);
|
||||
this.sendSyncMessage = sender.sendSyncMessage .bind(sender);
|
||||
this.getProfile = sender.getProfile .bind(sender);
|
||||
this.getAvatar = sender.getAvatar .bind(sender);
|
||||
this.syncReadMessages = sender.syncReadMessages .bind(sender);
|
||||
this.syncVerification = sender.syncVerification .bind(sender);
|
||||
};
|
||||
|
|
54
libtextsecure/test/crypto_test.js
Normal file
54
libtextsecure/test/crypto_test.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
describe('encrypting and decrypting profile data', function() {
|
||||
var NAME_PADDED_LENGTH = 26;
|
||||
describe('encrypting and decrypting profile names', function() {
|
||||
it('pads, encrypts, decrypts, and unpads a short string', function() {
|
||||
var name = 'Alice';
|
||||
var buffer = dcodeIO.ByteBuffer.wrap(name).toArrayBuffer();
|
||||
var key = libsignal.crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto.encryptProfileName(buffer, key).then(function(encrypted) {
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
return textsecure.crypto.decryptProfileName(encrypted, key).then(function(decrypted) {
|
||||
assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), 'Alice');
|
||||
});
|
||||
});
|
||||
});
|
||||
it('works for empty string', function() {
|
||||
var name = dcodeIO.ByteBuffer.wrap('').toArrayBuffer();
|
||||
var key = libsignal.crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto.encryptProfileName(name.buffer, key).then(function(encrypted) {
|
||||
assert(encrypted.byteLength === NAME_PADDED_LENGTH + 16 + 12);
|
||||
return textsecure.crypto.decryptProfileName(encrypted, key).then(function(decrypted) {
|
||||
assert.strictEqual(decrypted.byteLength, 0);
|
||||
assert.strictEqual(dcodeIO.ByteBuffer.wrap(decrypted).toString('utf8'), '');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('encrypting and decrypting profile avatars', function() {
|
||||
it('encrypts and decrypts', function() {
|
||||
var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer();
|
||||
var key = libsignal.crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto.encryptProfile(buffer, key).then(function(encrypted) {
|
||||
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
|
||||
return textsecure.crypto.decryptProfile(encrypted, key).then(function(decrypted) {
|
||||
assertEqualArrayBuffers(buffer, decrypted)
|
||||
});
|
||||
});
|
||||
});
|
||||
it('throws when decrypting with the wrong key', function() {
|
||||
var buffer = dcodeIO.ByteBuffer.wrap('This is an avatar').toArrayBuffer();
|
||||
var key = libsignal.crypto.getRandomBytes(32);
|
||||
var bad_key = libsignal.crypto.getRandomBytes(32);
|
||||
|
||||
return textsecure.crypto.encryptProfile(buffer, key).then(function(encrypted) {
|
||||
assert(encrypted.byteLength === buffer.byteLength + 16 + 12);
|
||||
return textsecure.crypto.decryptProfile(encrypted, bad_key).catch(function(error) {
|
||||
assert.strictEqual(error.name, 'ProfileDecryptError');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -36,6 +36,7 @@
|
|||
<script type="text/javascript" src="fake_api.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>
|
||||
|
|
1
main.js
1
main.js
|
@ -98,6 +98,7 @@ function createWindow () {
|
|||
version: app.getVersion(),
|
||||
buildExpiration: config.get('buildExpiration'),
|
||||
serverUrl: config.get('serverUrl'),
|
||||
cdnUrl: config.get('cdnUrl'),
|
||||
certificateAuthorities: config.get('certificateAuthorities'),
|
||||
environment: config.environment,
|
||||
node_version: process.versions.node
|
||||
|
|
|
@ -15,4 +15,5 @@ message ProvisionMessage {
|
|||
optional string number = 3;
|
||||
optional string provisioningCode = 4;
|
||||
optional string userAgent = 5;
|
||||
optional bytes profileKey = 6;
|
||||
}
|
||||
|
|
|
@ -83,6 +83,7 @@ message DataMessage {
|
|||
enum Flags {
|
||||
END_SESSION = 1;
|
||||
EXPIRATION_TIMER_UPDATE = 2;
|
||||
PROFILE_KEY_UPDATE = 4;
|
||||
}
|
||||
|
||||
optional string body = 1;
|
||||
|
@ -90,6 +91,7 @@ message DataMessage {
|
|||
optional GroupContext group = 3;
|
||||
optional uint32 flags = 4;
|
||||
optional uint32 expireTimer = 5;
|
||||
optional bytes profileKey = 6;
|
||||
}
|
||||
|
||||
message SyncMessage {
|
||||
|
@ -180,6 +182,7 @@ message ContactDetails {
|
|||
optional Avatar avatar = 3;
|
||||
optional string color = 4;
|
||||
optional Verified verified = 5;
|
||||
optional bytes profileKey = 6;
|
||||
}
|
||||
|
||||
message GroupDetails {
|
||||
|
|
|
@ -287,6 +287,14 @@ $avatar-size: 44px;
|
|||
}
|
||||
}
|
||||
}
|
||||
.profileName {
|
||||
font-size: smaller;
|
||||
|
||||
&:before {
|
||||
content: '~';
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-list-item {
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
|
|
|
@ -296,6 +296,11 @@ button.hamburger {
|
|||
.new-group-update .members .contact .last-timestamp {
|
||||
display: none; }
|
||||
|
||||
.profileName {
|
||||
font-size: smaller; }
|
||||
.profileName:before {
|
||||
content: '~'; }
|
||||
|
||||
.conversation-list-item {
|
||||
cursor: pointer; }
|
||||
.conversation-list-item:hover {
|
||||
|
|
Loading…
Reference in a new issue