Merge pull request #1794 from Bilb/fix-profile-key-config-message

Fix profile key config message
pull/1795/head
Audric Ackermann 4 years ago committed by GitHub
commit 2a371d3c57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,7 +9,6 @@ module.exports = {
concatenateBytes, concatenateBytes,
constantTimeEqual, constantTimeEqual,
decryptSymmetric, decryptSymmetric,
deriveAccessKey,
encryptAesCtr, encryptAesCtr,
encryptSymmetric, encryptSymmetric,
getRandomBytes, getRandomBytes,
@ -27,13 +26,6 @@ function bytesFromString(string) {
// High-level Operations // High-level Operations
async function deriveAccessKey(profileKey) {
const iv = getZeroes(12);
const plaintext = getZeroes(16);
const accessKey = await _encrypt_aes_gcm(profileKey, iv, plaintext);
return _getFirstBytes(accessKey, 16);
}
const IV_LENGTH = 16; const IV_LENGTH = 16;
const MAC_LENGTH = 16; const MAC_LENGTH = 16;
const NONCE_LENGTH = 16; const NONCE_LENGTH = 16;
@ -141,17 +133,6 @@ async function encryptAesCtr(key, plaintext, counter) {
return ciphertext; return ciphertext;
} }
async function _encrypt_aes_gcm(key, iv, plaintext) {
const algorithm = {
name: 'AES-GCM',
iv,
};
const extractable = false;
const cryptoKey = await crypto.subtle.importKey('raw', key, algorithm, extractable, ['encrypt']);
return crypto.subtle.encrypt(algorithm, cryptoKey, plaintext);
}
// Utility // Utility
function getRandomBytes(n) { function getRandomBytes(n) {

@ -1,6 +1,6 @@
/* global crypto */ /* global crypto */
const { isFunction, isNumber } = require('lodash'); const { isFunction } = require('lodash');
const { createLastMessageUpdate } = require('../../../ts/types/Conversation'); const { createLastMessageUpdate } = require('../../../ts/types/Conversation');
const { arrayBufferToBase64 } = require('../crypto'); const { arrayBufferToBase64 } = require('../crypto');
@ -55,60 +55,6 @@ function buildAvatarUpdater({ field }) {
} }
const maybeUpdateAvatar = buildAvatarUpdater({ field: 'avatar' }); const maybeUpdateAvatar = buildAvatarUpdater({ field: 'avatar' });
const maybeUpdateProfileAvatar = buildAvatarUpdater({
field: 'profileAvatar',
});
async function upgradeToVersion2(conversation, options) {
if (conversation.version >= 2) {
return conversation;
}
const { writeNewAttachmentData } = options;
if (!isFunction(writeNewAttachmentData)) {
throw new Error('Conversation.upgradeToVersion2: writeNewAttachmentData must be a function');
}
let { avatar, profileAvatar, profileKey } = conversation;
if (avatar && avatar.data) {
avatar = {
hash: await computeHash(avatar.data),
path: await writeNewAttachmentData(avatar.data),
};
}
if (profileAvatar && profileAvatar.data) {
profileAvatar = {
hash: await computeHash(profileAvatar.data),
path: await writeNewAttachmentData(profileAvatar.data),
};
}
if (profileKey && profileKey.byteLength) {
profileKey = arrayBufferToBase64(profileKey);
}
return {
...conversation,
version: 2,
avatar,
profileAvatar,
profileKey,
};
}
async function migrateConversation(conversation, options = {}) {
if (!conversation) {
return conversation;
}
if (!isNumber(conversation.version)) {
// eslint-disable-next-line no-param-reassign
conversation.version = 1;
}
return upgradeToVersion2(conversation, options);
}
async function deleteExternalFiles(conversation, options = {}) { async function deleteExternalFiles(conversation, options = {}) {
if (!conversation) { if (!conversation) {
@ -133,9 +79,7 @@ async function deleteExternalFiles(conversation, options = {}) {
module.exports = { module.exports = {
deleteExternalFiles, deleteExternalFiles,
migrateConversation,
maybeUpdateAvatar, maybeUpdateAvatar,
maybeUpdateProfileAvatar,
createLastMessageUpdate, createLastMessageUpdate,
arrayBufferToBase64, arrayBufferToBase64,
}; };

@ -2,7 +2,7 @@
"name": "session-desktop", "name": "session-desktop",
"productName": "Session", "productName": "Session",
"description": "Private messaging from your desktop", "description": "Private messaging from your desktop",
"version": "1.6.7", "version": "1.6.8",
"license": "GPL-3.0", "license": "GPL-3.0",
"author": { "author": {
"name": "Loki Project", "name": "Loki Project",

@ -210,7 +210,6 @@ const AvatarItem = (props: {
}; };
const ConversationListItem = (props: Props) => { const ConversationListItem = (props: Props) => {
// console.warn('ConversationListItem', props.id.substr(-1), ': ', props);
const { const {
activeAt, activeAt,
unreadCount, unreadCount,

@ -62,6 +62,7 @@ export const Image = (props: Props) => {
const role = canClick ? 'button' : undefined; const role = canClick ? 'button' : undefined;
const { loading, urlToLoad } = useEncryptedFileFetch(url, attachment.contentType); const { loading, urlToLoad } = useEncryptedFileFetch(url, attachment.contentType);
// data will be url if loading is finished and '' if not // data will be url if loading is finished and '' if not
const srcData = !loading ? urlToLoad : ''; const srcData = !loading ? urlToLoad : '';

@ -19,12 +19,13 @@ export const useEncryptedFileFetch = (url: string, contentType: string) => {
} }
useEffect(() => { useEffect(() => {
setLoading(true);
mountedRef.current = true;
void fetchUrl(); void fetchUrl();
return () => { return () => {
mountedRef.current = false; mountedRef.current = false;
}; };
}, [url]); }, [url]);
return { urlToLoad, loading }; return { urlToLoad, loading };
}; };

@ -41,7 +41,7 @@ import {
import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager'; import { getDecryptedMediaUrl } from '../session/crypto/DecryptedAttachmentsManager';
import { IMAGE_JPEG } from '../types/MIME'; import { IMAGE_JPEG } from '../types/MIME';
import { FSv2 } from '../fileserver'; import { FSv2 } from '../fileserver';
import { fromBase64ToArray, toHex } from '../session/utils/String'; import { fromBase64ToArray, fromHexToArray, toHex } from '../session/utils/String';
import { SessionButtonColor } from '../components/session/SessionButton'; import { SessionButtonColor } from '../components/session/SessionButton';
import { perfEnd, perfStart } from '../session/utils/Performance'; import { perfEnd, perfStart } from '../session/utils/Performance';
import { ReplyingToMessageProps } from '../components/session/conversation/SessionCompositionBox'; import { ReplyingToMessageProps } from '../components/session/conversation/SessionCompositionBox';
@ -350,17 +350,21 @@ export async function uploadOurAvatar(newAvatarDecrypted?: ArrayBuffer) {
return; return;
} }
let profileKey; let profileKey: Uint8Array | null;
let decryptedAvatarData; let decryptedAvatarData;
if (newAvatarDecrypted) { if (newAvatarDecrypted) {
// Encrypt with a new key every time // Encrypt with a new key every time
profileKey = window.libsignal.crypto.getRandomBytes(32); profileKey = window.libsignal.crypto.getRandomBytes(32) as Uint8Array;
decryptedAvatarData = newAvatarDecrypted; decryptedAvatarData = newAvatarDecrypted;
} else { } else {
// this is a reupload. no need to generate a new profileKey // this is a reupload. no need to generate a new profileKey
profileKey = window.textsecure.storage.get('profileKey'); const ourConvoProfileKey =
getConversationController()
.get(UserUtils.getOurPubKeyStrFromCache())
?.get('profileKey') || null;
profileKey = ourConvoProfileKey ? fromHexToArray(ourConvoProfileKey) : null;
if (!profileKey) { if (!profileKey) {
window.log.info('our profileKey not found'); window.log.info('our profileKey not found. Not reuploading our avatar');
return; return;
} }
const currentAttachmentPath = ourConvo.getAvatarPath(); const currentAttachmentPath = ourConvo.getAvatarPath();
@ -412,7 +416,6 @@ export async function uploadOurAvatar(newAvatarDecrypted?: ArrayBuffer) {
const displayName = ourConvo.get('profileName'); const displayName = ourConvo.get('profileName');
// write the profileKey even if it did not change // write the profileKey even if it did not change
window.storage.put('profileKey', profileKey);
ourConvo.set({ profileKey: toHex(profileKey) }); ourConvo.set({ profileKey: toHex(profileKey) });
// Replace our temporary image with the attachment pointer from the server: // Replace our temporary image with the attachment pointer from the server:
// this commits already // this commits already

@ -20,7 +20,7 @@ import {
saveMessages, saveMessages,
updateConversation, updateConversation,
} from '../../ts/data/data'; } from '../../ts/data/data';
import { fromArrayBufferToBase64, fromBase64ToArrayBuffer } from '../session/utils/String'; import { fromArrayBufferToBase64, fromBase64ToArrayBuffer, toHex } from '../session/utils/String';
import { import {
actions as conversationActions, actions as conversationActions,
conversationChanged, conversationChanged,
@ -91,8 +91,10 @@ export interface ConversationAttributes {
nickname?: string; nickname?: string;
profile?: any; profile?: any;
profileAvatar?: any; profileAvatar?: any;
/**
* Consider this being a hex string if it set
*/
profileKey?: string; profileKey?: string;
accessKey?: any;
triggerNotificationsFor: ConversationNotificationSettingType; triggerNotificationsFor: ConversationNotificationSettingType;
isTrustedForAttachmentDownload: boolean; isTrustedForAttachmentDownload: boolean;
isPinned: boolean; isPinned: boolean;
@ -129,8 +131,10 @@ export interface ConversationAttributesOptionals {
nickname?: string; nickname?: string;
profile?: any; profile?: any;
profileAvatar?: any; profileAvatar?: any;
/**
* Consider this being a hex string if it set
*/
profileKey?: string; profileKey?: string;
accessKey?: any;
triggerNotificationsFor?: ConversationNotificationSettingType; triggerNotificationsFor?: ConversationNotificationSettingType;
isTrustedForAttachmentDownload?: boolean; isTrustedForAttachmentDownload?: boolean;
isPinned: boolean; isPinned: boolean;
@ -1196,37 +1200,29 @@ export class ConversationModel extends Backbone.Model<ConversationAttributes> {
await this.commit(); await this.commit();
} }
} }
public async setProfileKey(profileKey: string) { /**
* profileKey MUST be a hex string
* @param profileKey MUST be a hex string
*/
public async setProfileKey(profileKey?: Uint8Array, autoCommit = true) {
const re = /[0-9A-Fa-f]*/g;
if (!profileKey) {
return;
}
const profileKeyHex = toHex(profileKey);
// profileKey is a string so we can compare it directly // profileKey is a string so we can compare it directly
if (this.get('profileKey') !== profileKey) { if (this.get('profileKey') !== profileKeyHex) {
this.set({ this.set({
profileKey, profileKey: profileKeyHex,
accessKey: null,
}); });
await this.deriveAccessKeyIfNeeded(); if (autoCommit) {
await this.commit(); await this.commit();
} }
} }
public async deriveAccessKeyIfNeeded() {
const profileKey = this.get('profileKey');
if (!profileKey) {
return;
}
if (this.get('accessKey')) {
return;
}
try {
const profileKeyBuffer = fromBase64ToArrayBuffer(profileKey);
const accessKeyBuffer = await window.Signal.Crypto.deriveAccessKey(profileKeyBuffer);
const accessKey = fromArrayBufferToBase64(accessKeyBuffer);
this.set({ accessKey });
} catch (e) {
window?.log?.warn(`Failed to derive access key for ${this.id}`);
}
} }
public async upgradeMessages(messages: any) { public async upgradeMessages(messages: any) {

@ -34,11 +34,6 @@ async function handleOurProfileUpdate(
return; return;
} }
if (profileKey?.length) {
window?.log?.info('Saving our profileKey from configuration message');
// TODO not sure why we keep our profileKey in storage AND in our conversaio
window.textsecure.storage.put('profileKey', profileKey);
}
const lokiProfile = { const lokiProfile = {
displayName, displayName,
profilePicture, profilePicture,

@ -20,11 +20,12 @@ import {
} from '../../ts/data/data'; } from '../../ts/data/data';
import { ConversationModel, ConversationTypeEnum } from '../models/conversation'; import { ConversationModel, ConversationTypeEnum } from '../models/conversation';
import { allowOnlyOneAtATime } from '../session/utils/Promise'; import { allowOnlyOneAtATime } from '../session/utils/Promise';
import { toHex } from '../session/utils/String';
export async function updateProfileOneAtATime( export async function updateProfileOneAtATime(
conversation: ConversationModel, conversation: ConversationModel,
profile: SignalService.DataMessage.ILokiProfile, profile: SignalService.DataMessage.ILokiProfile,
profileKey: any profileKey?: Uint8Array | null // was any
) { ) {
if (!conversation?.id) { if (!conversation?.id) {
window?.log?.warn('Cannot update profile with empty convoid'); window?.log?.warn('Cannot update profile with empty convoid');
@ -39,13 +40,18 @@ export async function updateProfileOneAtATime(
async function updateProfile( async function updateProfile(
conversation: ConversationModel, conversation: ConversationModel,
profile: SignalService.DataMessage.ILokiProfile, profile: SignalService.DataMessage.ILokiProfile,
profileKey: any profileKey?: Uint8Array | null // was any
) { ) {
const { dcodeIO, textsecure, Signal } = window; const { dcodeIO, textsecure, Signal } = window;
// Retain old values unless changed: // Retain old values unless changed:
const newProfile = conversation.get('profile') || {}; const newProfile = conversation.get('profile') || {};
if (!profileKey) {
window.log.warn("No need to try to update profile. We don't have a profile key");
return;
}
newProfile.displayName = profile.displayName; newProfile.displayName = profile.displayName;
if (profile.profilePicture) { if (profile.profilePicture) {
@ -79,7 +85,7 @@ async function updateProfile(
}); });
// Only update the convo if the download and decrypt is a success // Only update the convo if the download and decrypt is a success
conversation.set('avatarPointer', profile.profilePicture); conversation.set('avatarPointer', profile.profilePicture);
conversation.set('profileKey', profileKey); conversation.set('profileKey', toHex(profileKey));
({ path } = upgraded); ({ path } = upgraded);
} catch (e) { } catch (e) {
window?.log?.error(`Could not decrypt profile image: ${e}`); window?.log?.error(`Could not decrypt profile image: ${e}`);
@ -422,18 +428,15 @@ export const isDuplicate = (
async function handleProfileUpdate( async function handleProfileUpdate(
profileKeyBuffer: Uint8Array, profileKeyBuffer: Uint8Array,
convoId: string, convoId: string,
convoType: ConversationTypeEnum,
isIncoming: boolean isIncoming: boolean
) { ) {
const profileKey = StringUtils.decode(profileKeyBuffer, 'base64');
if (!isIncoming) { if (!isIncoming) {
// We update our own profileKey if it's different from what we have // We update our own profileKey if it's different from what we have
const ourNumber = UserUtils.getOurPubKeyStrFromCache(); const ourNumber = UserUtils.getOurPubKeyStrFromCache();
const me = getConversationController().getOrCreate(ourNumber, ConversationTypeEnum.PRIVATE); const me = getConversationController().getOrCreate(ourNumber, ConversationTypeEnum.PRIVATE);
// Will do the save for us if needed // Will do the save for us if needed
await me.setProfileKey(profileKey); await me.setProfileKey(profileKeyBuffer);
} else { } else {
const sender = await getConversationController().getOrCreateAndWait( const sender = await getConversationController().getOrCreateAndWait(
convoId, convoId,
@ -441,7 +444,7 @@ async function handleProfileUpdate(
); );
// Will do the save for us // Will do the save for us
await sender.setProfileKey(profileKey); await sender.setProfileKey(profileKeyBuffer);
} }
} }
@ -582,7 +585,7 @@ export async function handleMessageEvent(event: MessageEvent): Promise<void> {
return; return;
} }
if (message.profileKey?.length) { if (message.profileKey?.length) {
await handleProfileUpdate(message.profileKey, conversationId, type, isIncoming); await handleProfileUpdate(message.profileKey, conversationId, isIncoming);
} }
const msg = createMessage(data, isIncoming); const msg = createMessage(data, isIncoming);

@ -204,16 +204,14 @@ function handleLinkPreviews(messageBody: string, messagePreview: any, message: M
} }
async function processProfileKey( async function processProfileKey(
source: string,
conversation: ConversationModel, conversation: ConversationModel,
sendingDeviceConversation: ConversationModel, sendingDeviceConversation: ConversationModel,
profileKeyBuffer: Uint8Array profileKeyBuffer?: Uint8Array
) { ) {
const profileKey = StringUtils.decode(profileKeyBuffer, 'base64');
if (conversation.isPrivate()) { if (conversation.isPrivate()) {
await conversation.setProfileKey(profileKey); await conversation.setProfileKey(profileKeyBuffer);
} else { } else {
await sendingDeviceConversation.setProfileKey(profileKey); await sendingDeviceConversation.setProfileKey(profileKeyBuffer);
} }
} }
@ -360,12 +358,7 @@ async function handleRegularMessage(
} }
if (dataMessage.profileKey) { if (dataMessage.profileKey) {
await processProfileKey( await processProfileKey(conversation, sendingDeviceConversation, dataMessage.profileKey);
source,
conversation,
sendingDeviceConversation,
dataMessage.profileKey
);
} }
// we just received a message from that user so we reset the typing indicator for this convo // we just received a message from that user so we reset the typing indicator for this convo

@ -273,42 +273,6 @@ async function handleDecryptedEnvelope(envelope: EnvelopePlus, plaintext: ArrayB
} }
} }
/**
* Only used for opengroupv1 it seems.
* To be removed soon
*/
export async function handlePublicMessage(messageData: any) {
const { source } = messageData;
const { group, profile, profileKey } = messageData.message;
const isMe = UserUtils.isUsFromCache(source);
if (!isMe && profile) {
const conversation = await getConversationController().getOrCreateAndWait(
source,
ConversationTypeEnum.PRIVATE
);
await updateProfileOneAtATime(conversation, profile, profileKey);
}
const isPublicVisibleMessage = group && group.id && !!group.id.match(openGroupPrefixRegex);
if (!isPublicVisibleMessage) {
throw new Error('handlePublicMessage Should only be called with public message groups');
}
const ev = {
// Public chat messages from ourselves should be outgoing
type: isMe ? 'sent' : 'message',
data: messageData,
confirm: () => {
/* do nothing */
},
};
await handleMessageEvent(ev); // open groups v1
}
export async function handleOpenGroupV2Message( export async function handleOpenGroupV2Message(
message: OpenGroupMessageV2, message: OpenGroupMessageV2,
roomInfos: OpenGroupRequestCommonType roomInfos: OpenGroupRequestCommonType

@ -3,7 +3,7 @@ import { UserUtils } from '.';
import { getItemById } from '../../../ts/data/data'; import { getItemById } from '../../../ts/data/data';
import { KeyPair } from '../../../libtextsecure/libsignal-protocol'; import { KeyPair } from '../../../libtextsecure/libsignal-protocol';
import { PubKey } from '../types'; import { PubKey } from '../types';
import { toHex } from './String'; import { fromHexToArray, toHex } from './String';
import { getConversationController } from '../conversations'; import { getConversationController } from '../conversations';
export type HexKeyPair = { export type HexKeyPair = {
@ -97,14 +97,15 @@ export function getOurProfile(): OurLokiProfile | undefined {
// in their primary device's conversation // in their primary device's conversation
const ourNumber = window.storage.get('primaryDevicePubKey'); const ourNumber = window.storage.get('primaryDevicePubKey');
const ourConversation = getConversationController().get(ourNumber); const ourConversation = getConversationController().get(ourNumber);
const profileKey = new Uint8Array(window.storage.get('profileKey')); const ourProfileKeyHex = ourConversation.get('profileKey');
const profileKeyAsBytes = ourProfileKeyHex ? fromHexToArray(ourProfileKeyHex) : null;
const avatarPointer = ourConversation.get('avatarPointer'); const avatarPointer = ourConversation.get('avatarPointer');
const { displayName } = ourConversation.getLokiProfile(); const { displayName } = ourConversation.getLokiProfile();
return { return {
displayName, displayName,
avatarPointer, avatarPointer,
profileKey: profileKey.length ? profileKey : null, profileKey: profileKeyAsBytes?.length ? profileKeyAsBytes : null,
}; };
} catch (e) { } catch (e) {
window?.log?.error(`Failed to get our profile: ${e}`); window?.log?.error(`Failed to get our profile: ${e}`);

@ -14,7 +14,7 @@ import {
ConfigurationMessageContact, ConfigurationMessageContact,
} from '../messages/outgoing/controlMessage/ConfigurationMessage'; } from '../messages/outgoing/controlMessage/ConfigurationMessage';
import { ConversationModel } from '../../models/conversation'; import { ConversationModel } from '../../models/conversation';
import { fromBase64ToArray } from './String'; import { fromBase64ToArray, fromHexToArray, toHex } from './String';
import { SignalService } from '../../protobuf'; import { SignalService } from '../../protobuf';
import _ from 'lodash'; import _ from 'lodash';
import { import {
@ -160,9 +160,24 @@ const getValidContacts = (convos: Array<ConversationModel>) => {
const contacts = contactsModels.map(c => { const contacts = contactsModels.map(c => {
try { try {
const profileKeyForContact = c.get('profileKey') const profileKey = c.get('profileKey');
? fromBase64ToArray(c.get('profileKey') as string) let profileKeyForContact;
: undefined; if (typeof profileKey === 'string') {
// this will throw if the profileKey is not in hex.
try {
profileKeyForContact = fromHexToArray(profileKey);
} catch (e) {
profileKeyForContact = fromBase64ToArray(profileKey);
// if the line above does not fail, update the stored profileKey for this convo
void c.setProfileKey(profileKeyForContact);
}
} else if (profileKey) {
window.log.warn(
'Got a profileKey for a contact in another format than string. Contact: ',
c.id
);
return null;
}
return new ConfigurationMessageContact({ return new ConfigurationMessageContact({
publicKey: c.id, publicKey: c.id,
@ -189,8 +204,12 @@ export const getCurrentConfigurationMessage = async (convos: Array<ConversationM
if (!ourConvo) { if (!ourConvo) {
window?.log?.error('Could not find our convo while building a configuration message.'); window?.log?.error('Could not find our convo while building a configuration message.');
} }
const profileKeyFromStorage = window.storage.get('profileKey');
const profileKey = profileKeyFromStorage ? new Uint8Array(profileKeyFromStorage) : undefined; const ourProfileKeyHex =
getConversationController()
.get(UserUtils.getOurPubKeyStrFromCache())
?.get('profileKey') || null;
const profileKey = ourProfileKeyHex ? fromHexToArray(ourProfileKeyHex) : undefined;
const profilePicture = ourConvo?.get('avatarPointer') || undefined; const profilePicture = ourConvo?.get('avatarPointer') || undefined;
const displayName = ourConvo?.getLokiProfile()?.displayName || undefined; const displayName = ourConvo?.getLokiProfile()?.displayName || undefined;

Loading…
Cancel
Save