Merge pull request #661 from sachaaaaa/encrypted_avatar

Encrypted profile pic
pull/680/head
Ryan Tharp 5 years ago committed by GitHub
commit a3abf3b14a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -933,7 +933,8 @@
avatarColor: conversation.getColor(),
onOk: async (newName, avatar) => {
let newAvatarPath = '';
let url = null;
let profileKey = null;
if (avatar) {
const data = await readFile({ file: avatar });
@ -945,19 +946,30 @@
conversation.setLokiProfile({ displayName: newName });
conversation.set('avatar', tempUrl);
const avatarPointer = await textsecure.messaging.uploadAvatar(
data
// Encrypt with a new key every time
profileKey = libsignal.crypto.getRandomBytes(32);
const encryptedData = await textsecure.crypto.encryptProfile(
data.data,
profileKey
);
conversation.set('avatarPointer', avatarPointer.url);
const avatarPointer = await textsecure.messaging.uploadAvatar({
...data,
data: encryptedData,
size: encryptedData.byteLength,
});
({ url } = avatarPointer);
storage.put('profileKey', profileKey);
const downloaded = await messageReceiver.downloadAttachment({
url: avatarPointer.url,
conversation.set('avatarPointer', url);
const upgraded = await Signal.Migrations.processNewAttachment({
isRaw: true,
data: data.data,
url,
});
const upgraded = await Signal.Migrations.processNewAttachment(
downloaded
);
newAvatarPath = upgraded.path;
}
@ -973,6 +985,12 @@
// so we could disable this here
// or least it enable for the quickest response
window.lokiPublicChatAPI.setProfileName(newName);
window
.getConversations()
.filter(convo => convo.isPublic() && !convo.isRss())
.forEach(convo =>
convo.trigger('ourAvatarChanged', { url, profileKey })
);
},
});
}

@ -87,6 +87,7 @@
groupAdmins: [],
isKickedFromGroup: false,
isOnline: false,
profileSharing: false,
};
},
@ -156,6 +157,15 @@
this.on('expiration-change', this.updateAndMerge);
this.on('expired', this.onExpired);
this.on('ourAvatarChanged', avatar =>
this.updateAvatarOnPublicChat(avatar)
);
// Always share profile pics with public chats
if (this.isPublic) {
this.set('profileSharing', true);
}
const sealedSender = this.get('sealedSender');
if (sealedSender === undefined) {
this.set({ sealedSender: SEALED_SENDER.UNKNOWN });
@ -846,14 +856,18 @@
}
if (this.get('friendRequestStatus') !== newStatus) {
this.set({ friendRequestStatus: newStatus });
if (newStatus === FriendRequestStatusEnum.friends) {
if (!blockSync) {
// Sync contact
this.wrapSend(textsecure.messaging.sendContactSyncMessage(this));
}
// Only enable sending profileKey after becoming friends
this.set({ profileSharing: true });
}
await window.Signal.Data.updateConversation(this.id, this.attributes, {
Conversation: Whisper.Conversation,
});
await this.updateTextInputState();
if (!blockSync && newStatus === FriendRequestStatusEnum.friends) {
// Sync contact
this.wrapSend(textsecure.messaging.sendContactSyncMessage(this));
}
}
},
async updateGroupAdmins(groupAdmins) {
@ -1493,6 +1507,11 @@
FriendRequestStatusEnum.pendingSend
);
// Always share our profileKey in the friend request
// This will get added automatically after the FR
// is accepted, via the profileSharing flag
profileKey = storage.get('profileKey');
// Send the friend request!
messageWithSchema = await upgradeMessageSchema({
type: 'friend-request',
@ -1667,6 +1686,27 @@
);
},
async updateAvatarOnPublicChat({ url, profileKey }) {
if (!this.isPublic()) {
return;
}
if (this.isRss()) {
return;
}
if (!this.get('profileSharing')) {
return;
}
if (profileKey && typeof profileKey !== 'string') {
// eslint-disable-next-line no-param-reassign
profileKey = window.Signal.Crypto.arrayBufferToBase64(profileKey);
}
const serverAPI = await lokiPublicChatAPI.findOrCreateServer(
this.get('server')
);
await serverAPI.setAvatar(url, profileKey);
},
async handleMessageSendResult({
failoverNumbers,
unidentifiedDeliveries,
@ -2272,9 +2312,8 @@
});
}
if (newProfile.avatar) {
await this.setProfileAvatar({ path: newProfile.avatar });
}
// if set to null, it will show a jazzIcon
await this.setProfileAvatar({ path: newProfile.avatar });
await this.updateProfileName();
},

@ -14,6 +14,7 @@ const PUBLICCHAT_MOD_POLL_EVERY = 30 * 1000; // 30s
const PUBLICCHAT_MIN_TIME_BETWEEN_DUPLICATE_MESSAGES = 10 * 1000; // 10s
const HOMESERVER_USER_ANNOTATION_TYPE = 'network.loki.messenger.homeserver';
const AVATAR_USER_ANNOTATION_TYPE = 'network.loki.messenger.avatar';
const MESSAGE_ATTACHMENT_TYPE = 'net.app.core.oembed';
const LOKI_ATTACHMENT_TYPE = 'attachment';
const LOKI_PREVIEW_TYPE = 'preview';
@ -133,6 +134,17 @@ class LokiAppDotNetAPI extends EventEmitter {
})
);
}
async setAvatar(url, profileKey) {
await Promise.all(
this.servers.map(async server => {
// this may fail
// but we can't create a sql table to remember to retry forever
// I think we just silently fail for now
await server.setAvatar(url, profileKey);
})
);
}
}
class LokiAppDotNetServerAPI {
@ -254,6 +266,14 @@ class LokiAppDotNetServerAPI {
return res.response.data.annotations || [];
}
async setAvatar(url, profileKey) {
let value = null;
if (url && profileKey) {
value = { url, profileKey };
}
return this.setSelfAnnotation(AVATAR_USER_ANNOTATION_TYPE, value);
}
// get active token for this server
async getOrRefreshServerToken(forceRefresh = false) {
let token;
@ -956,7 +976,16 @@ class LokiPublicChannelAPI {
}
// timestamp is the only required field we've had since the first deployed version
const { timestamp, quote, avatar } = noteValue;
const { timestamp, quote } = noteValue;
let profileKey = null;
let avatar = null;
const avatarNote = adnMessage.user.annotations.find(
note => note.type === AVATAR_USER_ANNOTATION_TYPE
);
if (avatarNote) {
({ profileKey, url: avatar } = avatarNote.value);
}
if (quote) {
// TODO: Enable quote attachments again using proper ADN style
@ -1020,6 +1049,7 @@ class LokiPublicChannelAPI {
preview,
quote,
avatar,
profileKey,
};
}
@ -1088,12 +1118,21 @@ class LokiPublicChannelAPI {
return false; // Invalid or delete message
}
const pubKey = adnMessage.user.username;
const messengerData = await this.getMessengerData(adnMessage);
if (messengerData === false) {
return false;
}
const { timestamp, quote, attachments, preview } = messengerData;
const {
timestamp,
quote,
attachments,
preview,
avatar,
profileKey,
} = messengerData;
if (!timestamp) {
return false; // Invalid message
}
@ -1101,7 +1140,7 @@ class LokiPublicChannelAPI {
// Duplicate check
const isDuplicate = message => {
// The username in this case is the users pubKey
const sameUsername = message.username === adnMessage.user.username;
const sameUsername = message.username === pubKey;
const sameText = message.text === adnMessage.text;
// Don't filter out messages that are too far apart from each other
const timestampsSimilar =
@ -1121,7 +1160,7 @@ class LokiPublicChannelAPI {
this.lastMessagesCache = [
...this.lastMessagesCache,
{
username: adnMessage.user.username,
username: pubKey,
text: adnMessage.text,
timestamp,
},
@ -1130,7 +1169,7 @@ class LokiPublicChannelAPI {
const from = adnMessage.user.name || 'Anonymous'; // profileName
// if us
if (adnMessage.user.username === ourNumber) {
if (pubKey === ourNumber) {
// update the last name we saw from ourself
lastProfileName = from;
}
@ -1151,12 +1190,8 @@ class LokiPublicChannelAPI {
if (homeServerPubKeys[homeServer] === undefined) {
homeServerPubKeys[homeServer] = [];
}
if (
homeServerPubKeys[homeServer].indexOf(
`@${adnMessage.user.username}`
) === -1
) {
homeServerPubKeys[homeServer].push(`@${adnMessage.user.username}`);
if (homeServerPubKeys[homeServer].indexOf(`@${pubKey}`) === -1) {
homeServerPubKeys[homeServer].push(`@${pubKey}`);
}
// generate signal message object
@ -1164,7 +1199,7 @@ class LokiPublicChannelAPI {
serverId: adnMessage.id,
clientVerified: true,
friendRequest: false,
source: adnMessage.user.username,
source: pubKey,
sourceDevice: 1,
timestamp,
@ -1181,7 +1216,7 @@ class LokiPublicChannelAPI {
},
flags: 0,
expireTimer: 0,
profileKey: null,
profileKey,
timestamp,
received_at: receivedAt,
sent_at: timestamp,
@ -1190,6 +1225,7 @@ class LokiPublicChannelAPI {
preview,
profile: {
displayName: from,
avatar,
},
},
};
@ -1209,8 +1245,6 @@ class LokiPublicChannelAPI {
// slave to primary map for this group of messages
let slavePrimaryMap = {};
// pubKey to avatar
let avatarMap = {};
// reduce list of servers into verified maps and keys
const verifiedPrimaryPKs = await Object.keys(homeServerPubKeys).reduce(
@ -1229,53 +1263,20 @@ class LokiPublicChannelAPI {
// should not be any collisions, since each pubKey can only have one home server
slavePrimaryMap = { ...slavePrimaryMap, ...result.slaveMap };
// merge this servers avatarMap into our map
// again shouldn't be any collisions
avatarMap = { ...avatarMap, ...serverAPI.avatarMap };
// copy verified pub keys into result
return curVal.concat(result.verifiedPrimaryPKs);
},
[]
);
// sort pending messages by if slave device or not
/* eslint-disable no-param-reassign */
const slaveMessages = pendingMessages
.filter(messageData => !!messageData) // filter out false messages
.reduce((retval, messageData) => {
// if a known slave
if (slavePrimaryMap[messageData.source]) {
// pop primary device avatars in
if (avatarMap[slavePrimaryMap[messageData.source]]) {
// modify messageData for user's avatar
messageData.message.profile.avatar =
avatarMap[slavePrimaryMap[messageData.source]];
}
// delay sending the message
if (retval[messageData.source] === undefined) {
retval[messageData.source] = [messageData];
} else {
retval[messageData.source].push(messageData);
}
} else {
// not from a paired/slave/unregistered device
// pop current device avatars in
if (avatarMap[messageData.source]) {
// modify messageData for user's avatar
messageData.message.profile.avatar = avatarMap[messageData.source];
}
// send event now
this.serverAPI.chatAPI.emit('publicMessage', {
message: messageData,
});
}
return retval;
}, {});
/* eslint-enable no-param-reassign */
// filter out invalid messages
pendingMessages = pendingMessages.filter(messageData => !!messageData);
// separate messages coming from primary and secondary devices
const [primaryMessages, slaveMessages] = _.partition(pendingMessages, message => !(message.source in slavePrimaryMap));
// process primary devices' message directly
primaryMessages.forEach(message => this.serverAPI.chatAPI.emit('publicMessage', {
message,
}));
pendingMessages = []; // allow memory to be freed
@ -1288,7 +1289,19 @@ class LokiPublicChannelAPI {
/* eslint-disable no-param-reassign */
this.primaryUserProfileName = verifiedDeviceResults.reduce(
(mapOut, user) => {
mapOut[user.username] = user.name;
let avatar = null;
let profileKey = null;
const avatarNote = user.annotations.find(
note => note.type === AVATAR_USER_ANNOTATION_TYPE
);
if (avatarNote) {
({ profileKey, url: avatar } = avatarNote.value);
}
mapOut[user.username] = {
name: user.name,
avatar,
profileKey,
};
return mapOut;
},
{}
@ -1312,9 +1325,12 @@ class LokiPublicChannelAPI {
if (slavePrimaryMap[messageData.source]) {
// rewrite source, profile
messageData.source = primaryPubKey;
messageData.message.profile.displayName = this.primaryUserProfileName[
const { name, avatar, profileKey } = this.primaryUserProfileName[
primaryPubKey
];
messageData.message.profile.displayName = name;
messageData.message.profile.avatar = avatar;
messageData.message.profileKey = profileKey;
}
this.serverAPI.chatAPI.emit('publicMessage', {
message: messageData,
@ -1426,8 +1442,6 @@ class LokiPublicChannelAPI {
LokiPublicChannelAPI.getAnnotationFromPreview
);
const avatarAnnotation = data.profile.avatar || null;
const payload = {
text,
annotations: [
@ -1435,8 +1449,6 @@ class LokiPublicChannelAPI {
type: 'network.loki.messenger.publicChat',
value: {
timestamp: messageTimeStamp,
// can remove after this release
avatar: avatarAnnotation,
},
},
...attachmentAnnotations,

@ -17,6 +17,10 @@ class LokiFileServerInstance {
this._adnApi = new LokiAppDotNetAPI(ourKey);
this.avatarMap = {};
}
// FIXME: this is not file-server specific
// and is currently called by LokiAppDotNetAPI.
// LokiAppDotNetAPI (base) should not know about LokiFileServer.
async establishConnection(serverUrl) {
// FIXME: we don't always need a token...
this._server = await this._adnApi.findOrCreateServer(serverUrl);

@ -202,11 +202,17 @@
if (isGrant) {
// Send profile name to secondary device
const lokiProfile = ourConversation.getLokiProfile();
// profile.avatar is the path to the local image
// replace with the avatar URL
const avatarPointer = ourConversation.get('avatarPointer');
lokiProfile.avatar = avatarPointer;
const profile = new textsecure.protobuf.DataMessage.LokiProfile(
lokiProfile
);
const profileKey = window.storage.get('profileKey');
const dataMessage = new textsecure.protobuf.DataMessage({
profile,
profileKey,
});
// Attach contact list
const conversations = await window.Signal.Data.getConversationsWithFriendStatus(

@ -172,7 +172,11 @@ MessageReceiver.prototype.extend({
message.source,
'private'
);
await this.updateProfile(conversation, message.message.profile);
await this.updateProfile(
conversation,
message.message.profile,
message.message.profileKey
);
}
const ev = new Event('message');
@ -1012,6 +1016,8 @@ MessageReceiver.prototype.extend({
throw e;
}
},
// handle a SYNC message for a message
// sent by another device
handleSentMessage(envelope, sentContainer, msg) {
const {
destination,
@ -1029,9 +1035,10 @@ MessageReceiver.prototype.extend({
this.processDecrypted(envelope, msg).then(message => {
const groupId = message.group && message.group.id;
const isBlocked = this.isGroupBlocked(groupId);
const primaryDevicePubKey = window.storage.get('primaryDevicePubKey');
const isMe =
envelope.source === textsecure.storage.user.getNumber() ||
envelope.source === window.storage.get('primaryDevicePubKey');
envelope.source === primaryDevicePubKey;
const isLeavingGroup = Boolean(
message.group &&
message.group.type === textsecure.protobuf.GroupContext.Type.QUIT
@ -1046,6 +1053,16 @@ MessageReceiver.prototype.extend({
return this.removeFromCache(envelope);
}
// handle profileKey and avatar updates
if (envelope.source === primaryDevicePubKey) {
const { profileKey, profile } = message;
const primaryConversation = ConversationController.get(
primaryDevicePubKey
);
//
this.updateProfile(primaryConversation, profile, profileKey);
}
const ev = new Event('sent');
ev.confirm = this.removeFromCache.bind(this, envelope);
ev.data = {
@ -1127,12 +1144,13 @@ MessageReceiver.prototype.extend({
);
primaryConversation.trigger('change');
Whisper.events.trigger('secondaryDeviceRegistration');
// Update profile name
if (dataMessage && dataMessage.profile) {
// Update profile
if (dataMessage) {
const { profile, profileKey } = dataMessage;
const ourNumber = window.storage.get('primaryDevicePubKey');
const me = window.ConversationController.get(ourNumber);
if (me) {
me.setLokiProfile(dataMessage.profile);
this.updateProfile(me, profile, profileKey);
}
}
// Update contact list
@ -1238,7 +1256,7 @@ MessageReceiver.prototype.extend({
return true;
},
async updateProfile(conversation, profile) {
async updateProfile(conversation, profile, profileKey) {
// Retain old values unless changed:
const newProfile = conversation.get('profile') || {};
@ -1252,17 +1270,40 @@ MessageReceiver.prototype.extend({
if (needsUpdate) {
conversation.set('avatarPointer', profile.avatar);
conversation.set('profileKey', profileKey);
const downloaded = await this.downloadAttachment({
url: profile.avatar,
isRaw: true,
});
const upgraded = await Signal.Migrations.processNewAttachment(
downloaded
);
newProfile.avatar = upgraded.path;
// null => use jazzicon
let path = null;
if (profileKey) {
// Convert profileKey to ArrayBuffer, if needed
const encoding = typeof profileKey === 'string' ? 'base64' : null;
try {
const profileKeyArrayBuffer = dcodeIO.ByteBuffer.wrap(
profileKey,
encoding
).toArrayBuffer();
const decryptedData = await textsecure.crypto.decryptProfile(
downloaded.data,
profileKeyArrayBuffer
);
const upgraded = await Signal.Migrations.processNewAttachment({
...downloaded,
data: decryptedData,
});
({ path } = upgraded);
} catch (e) {
window.log.error(`Could not decrypt profile image: ${e}`);
}
}
newProfile.avatar = path;
}
} else {
newProfile.avatar = null;
}
await conversation.setLokiProfile(newProfile);
@ -1361,7 +1402,11 @@ MessageReceiver.prototype.extend({
// Check if we need to update any profile names
if (!isMe && conversation) {
if (message.profile) {
await this.updateProfile(conversation, message.profile);
await this.updateProfile(
conversation,
message.profile,
message.profileKey
);
}
}

@ -191,8 +191,7 @@ MessageSender.prototype = {
async makeAttachmentPointer(
attachment,
publicServer = null,
isRaw = false,
isAvatar = false
{ isRaw = false, isAvatar = false }
) {
if (typeof attachment !== 'object' || attachment == null) {
return Promise.resolve(undefined);
@ -211,13 +210,7 @@ MessageSender.prototype = {
const proto = new textsecure.protobuf.AttachmentPointer();
let attachmentData;
let server;
if (publicServer) {
server = publicServer;
} else {
({ server } = this);
}
const server = publicServer || this.server;
if (publicServer || isRaw) {
attachmentData = attachment.data;
@ -572,7 +565,12 @@ MessageSender.prototype = {
},
uploadAvatar(attachment) {
return this.makeAttachmentPointer(attachment, null, true, true);
// isRaw is true since the data is already encrypted
// and doesn't need to be encrypted again
return this.makeAttachmentPointer(attachment, null, {
isRaw: true,
isAvatar: true,
});
},
sendRequestConfigurationSyncMessage(options) {
@ -926,7 +924,9 @@ MessageSender.prototype = {
getOurProfile() {
try {
const ourNumber = textsecure.storage.user.getNumber();
// Secondary devices have their profile stored
// in their primary device's conversation
const ourNumber = window.storage.get('primaryDevicePubKey');
const conversation = window.ConversationController.get(ourNumber);
return conversation.getLokiProfile();
} catch (e) {

@ -333,13 +333,6 @@ export class MainHeader extends React.Component<Props, any> {
name: i18n('copyPublicKey'),
onClick: onCopyPublicKey,
},
{
id: 'editProfile',
name: i18n('editProfile'),
onClick: () => {
trigger('onEditProfile');
},
},
{
id: 'showSeed',
name: i18n('showSeed'),
@ -396,6 +389,14 @@ export class MainHeader extends React.Component<Props, any> {
}
if (!isSecondaryDevice) {
// insert as second element
menuItems.splice(1, 0, {
id: 'editProfile',
name: i18n('editProfile'),
onClick: () => {
trigger('onEditProfile');
},
});
menuItems.push({
id: 'pairNewDevice',
name: 'Device Pairing',

Loading…
Cancel
Save