Skip to content

Commit a86f529

Browse files
authored
Merge pull request #1632 from session-foundation/feat/attachments-changes-release-1
feat: allow serverPubkey to be provided for incoming attachments
2 parents 985d9fe + a5c9738 commit a86f529

File tree

57 files changed

+1139
-909
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1139
-909
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
"fs-extra": "11.3.0",
7777
"glob": "10.4.5",
7878
"image-type": "^4.1.0",
79-
"libsession_util_nodejs": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.5.8/libsession_util_nodejs-v0.5.8.tar.gz",
79+
"libsession_util_nodejs": "https://github.com/session-foundation/libsession-util-nodejs/releases/download/v0.5.9/libsession_util_nodejs-v0.5.9.tar.gz",
8080
"libsodium-wrappers-sumo": "^0.7.15",
8181
"linkify-it": "^5.0.0",
8282
"lodash": "^4.17.21",

preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ window.sessionFeatureFlags = {
5757
replaceLocalizedStringsWithKeys: false,
5858
// Hooks
5959
useClosedGroupV2QAButtons: false, // TODO DO NOT MERGE
60+
useDeterministicEncryption: !isEmpty(process.env.SESSION_ATTACH_DETERMINISTIC_ENCRYPTION),
6061
useOnionRequests: true,
6162
useTestNet: isTestNet() || isTestIntegration(),
6263
useLocalDevNet: !isEmpty(process.env.LOCAL_DEVNET_SEED_URL)

protos/SignalService.proto

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -178,17 +178,11 @@ message DataMessage {
178178
}
179179

180180
message Quote {
181-
182-
message QuotedAttachment {
183-
optional string contentType = 1;
184-
optional string fileName = 2;
185-
optional AttachmentPointer thumbnail = 3;
186-
}
181+
reserved 3, 4;
182+
reserved "text", "attachments";
187183

188184
required uint64 id = 1;
189185
required string author = 2;
190-
optional string text = 3;
191-
repeated QuotedAttachment attachments = 4;
192186
}
193187

194188
message Preview {
@@ -268,16 +262,22 @@ message AttachmentPointer {
268262

269263
// @required
270264
required fixed64 deprecated_id = 1;
271-
optional string contentType = 2;
272-
optional bytes key = 3;
273-
optional uint32 size = 4;
274-
optional bytes digest = 6;
275-
optional string fileName = 7;
276-
optional uint32 flags = 8;
277-
optional uint32 width = 9;
278-
optional uint32 height = 10;
279-
optional string caption = 11;
280-
optional string url = 101;
265+
optional string contentType = 2;
266+
optional bytes key = 3;
267+
optional uint32 size = 4;
268+
optional bytes digest = 6;
269+
optional string fileName = 7;
270+
optional uint32 flags = 8;
271+
optional uint32 width = 9;
272+
optional uint32 height = 10;
273+
optional string caption = 11;
274+
/**
275+
* This field can be just an url to the file, or have a fragment appended to it that can contain:
276+
* - `p=<server_pubkey_hex>` // hex encoded pubkey of the file server
277+
* - `d=` // if the file is deterministically encrypted, this field is present, otherwise it is not
278+
* If needed, those fields are a &, and can be parsed/built with the usual URLSearchParams logic
279+
*/
280+
optional string url = 101;
281281
}
282282

283283

ts/components/conversation/composition/CompositionBox.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,10 @@ export interface StagedLinkPreviewData {
8383
scaledDown: ProcessedLinkPreviewThumbnailType | null;
8484
}
8585

86-
export interface StagedAttachmentType extends AttachmentType {
86+
export type StagedAttachmentType = AttachmentType & {
8787
file: File;
8888
path?: string; // a bit hacky, but this is the only way to make our sending audio message be playable, this must be used only for those message
89-
}
89+
};
9090

9191
export type SendMessageType = {
9292
conversationId: string;

ts/components/conversation/right-panel/overlay/message-info/components/AttachmentInfo.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ function formatAttachmentUrl(attachment: PropsForAttachment) {
2828
return tr('attachmentsNa');
2929
}
3030

31-
const fileId = attachment.url.split('/').pop() || '';
31+
const fileUrl = URL.canParse(attachment.url) && new URL(attachment.url);
32+
const fileId = fileUrl ? fileUrl?.pathname.split('/').pop() || '' : '';
3233

3334
if (!fileId) {
3435
return tr('attachmentsNa');

ts/components/leftpane/ActionsPanel.tsx

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import { DecryptedAttachmentsManager } from '../../session/crypto/DecryptedAttac
2222

2323
import { DURATION } from '../../session/constants';
2424

25-
import { reuploadCurrentAvatarUs } from '../../interactions/avatar-interactions/nts-avatar-interactions';
2625
import {
2726
onionPathModal,
2827
updateDebugMenuModal,
@@ -52,14 +51,13 @@ import { useDebugMode } from '../../state/selectors/debug';
5251
import { networkDataActions } from '../../state/ducks/networkData';
5352
import { LUCIDE_ICONS_UNICODE } from '../icon/lucide';
5453
import { AvatarMigrate } from '../../session/utils/job_runners/jobs/AvatarMigrateJob';
55-
import { NetworkTime } from '../../util/NetworkTime';
5654
import { Storage } from '../../util/storage';
57-
import { getFileInfoFromFileServer } from '../../session/apis/file_server_api/FileServerApi';
5855
import { themesArray } from '../../themes/constants/colors';
5956
import { isDebugMode, isDevProd } from '../../shared/env_vars';
6057
import { GearAvatarButton } from '../buttons/avatar/GearAvatarButton';
6158
import { useZoomShortcuts } from '../../hooks/useZoomingShortcut';
6259
import { OnionStatusLight } from '../dialog/OnionStatusPathDialog';
60+
import { AvatarReupload } from '../../session/utils/job_runners/jobs/AvatarReuploadJob';
6361

6462
const StyledContainerAvatar = styled.div`
6563
padding: var(--margins-lg);
@@ -98,17 +96,6 @@ const triggerSyncIfNeeded = async () => {
9896
}
9997
};
10098

101-
const triggerAvatarReUploadIfNeeded = async () => {
102-
const lastAvatarUploadExpiryMs =
103-
(await Data.getItemById(SettingsKey.ntsAvatarExpiryMs))?.value || Number.MAX_SAFE_INTEGER;
104-
105-
if (NetworkTime.now() > lastAvatarUploadExpiryMs) {
106-
window.log.info('Reuploading avatar...');
107-
// reupload the avatar
108-
await reuploadCurrentAvatarUs();
109-
}
110-
};
111-
11299
/**
113100
* This function is called only once: on app startup with a logged in user
114101
*/
@@ -127,9 +114,8 @@ const doAppStartUp = async () => {
127114
}); // refresh our swarm on start to speed up the first message fetching event
128115
void Data.cleanupOrphanedAttachments();
129116

130-
// TODOLATER make this a job of the JobRunner
131117
// Note: do not make this a debounce call (as for some reason it doesn't work with promises)
132-
void triggerAvatarReUploadIfNeeded();
118+
await AvatarReupload.addAvatarReuploadJob();
133119

134120
/* Postpone a little bit of the polling of sogs messages to let the swarm messages come in first. */
135121
global.setTimeout(() => {
@@ -147,17 +133,6 @@ const doAppStartUp = async () => {
147133
// Schedule a confSyncJob in some time to let anything incoming from the network be applied and see if there is a push needed
148134
// Note: this also starts periodic jobs, so we don't need to keep doing it
149135
await UserSync.queueNewJobIfNeeded();
150-
151-
// on app startup, check that the avatar expiry on the file server
152-
const avatarPointer = ConvoHub.use()
153-
.get(UserUtils.getOurPubKeyStrFromCache())
154-
.getAvatarPointer();
155-
if (avatarPointer) {
156-
const details = await getFileInfoFromFileServer(avatarPointer);
157-
if (details?.expiryMs) {
158-
await Storage.put(SettingsKey.ntsAvatarExpiryMs, details.expiryMs);
159-
}
160-
}
161136
}, 20000);
162137

163138
global.setTimeout(() => {
@@ -283,8 +258,7 @@ export const ActionsPanel = () => {
283258
if (!ourPrimaryConversation) {
284259
return;
285260
}
286-
// this won't be run every days, but if the app stays open for more than 10 days
287-
void triggerAvatarReUploadIfNeeded();
261+
void AvatarReupload.addAvatarReuploadJob();
288262
},
289263
window.sessionFeatureFlags.fsTTL30s ? DURATION.SECONDS * 1 : DURATION.DAYS * 1
290264
);

ts/data/settings-key.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ const settingsOpengroupPruning = 'prune-setting';
1111
const settingsNotification = 'notification-setting';
1212
const settingsAudioNotification = 'audio-notification-setting';
1313
const hasSyncedInitialConfigurationItem = 'hasSyncedInitialConfigurationItem';
14-
const ntsAvatarExpiryMs = 'ntsAvatarExpiryMs';
1514
const hasLinkPreviewPopupBeenDisplayed = 'hasLinkPreviewPopupBeenDisplayed';
1615
const hasFollowSystemThemeEnabled = 'hasFollowSystemThemeEnabled';
1716
const hideRecoveryPassword = 'hideRecoveryPassword';
@@ -44,7 +43,6 @@ export const SettingsKey = {
4443
settingsNotification,
4544
settingsAudioNotification,
4645
hasSyncedInitialConfigurationItem,
47-
ntsAvatarExpiryMs,
4846
hasLinkPreviewPopupBeenDisplayed,
4947
latestUserProfileEnvelopeTimestamp,
5048
latestUserGroupEnvelopeTimestamp,

ts/interactions/avatar-interactions/nts-avatar-interactions.ts

Lines changed: 42 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,68 @@
1-
import { isEmpty } from 'lodash';
2-
import { SettingsKey } from '../../data/settings-key';
1+
import { randombytes_buf } from 'libsodium-wrappers-sumo';
2+
33
import { uploadFileToFsWithOnionV4 } from '../../session/apis/file_server_api/FileServerApi';
4-
import { ConvoHub } from '../../session/conversations';
5-
import { DecryptedAttachmentsManager } from '../../session/crypto/DecryptedAttachmentsManager';
6-
import { UserUtils } from '../../session/utils';
7-
import { fromHexToArray } from '../../session/utils/String';
8-
import { urlToBlob } from '../../types/attachments/VisualAttachment';
94
import { processNewAttachment } from '../../types/MessageAttachment';
10-
import { IMAGE_JPEG } from '../../types/MIME';
115
import { encryptProfile } from '../../util/crypto/profileEncrypter';
12-
import { Storage } from '../../util/storage';
136
import type { ConversationModel } from '../../models/conversation';
147
import { processAvatarData } from '../../util/avatar/processAvatarData';
15-
import { UserConfigWrapperActions } from '../../webworker/workers/browser/libsession_worker_interface';
16-
17-
/**
18-
* This function can be used for reupload our avatar to the file server.
19-
* It will reuse the same profileKey and avatarContent if we have some, or do nothing if one of those is missing.
20-
*/
21-
export async function reuploadCurrentAvatarUs() {
22-
const ourConvo = ConvoHub.use().get(UserUtils.getOurPubKeyStrFromCache());
23-
if (!ourConvo) {
24-
window.log.warn('ourConvo not found... This is not a valid case');
25-
return null;
26-
}
27-
28-
// this is a reupload. no need to generate a new profileKey
29-
const ourConvoProfileKey =
30-
ConvoHub.use().get(UserUtils.getOurPubKeyStrFromCache())?.getProfileKey() || null;
31-
32-
const profileKey = ourConvoProfileKey ? fromHexToArray(ourConvoProfileKey) : null;
33-
if (!profileKey || isEmpty(profileKey)) {
34-
window.log.info('reuploadCurrentAvatarUs: our profileKey empty');
35-
36-
return null;
37-
}
38-
// Note: we do want to grab the current non-static avatar path here
39-
// to reupload it, no matter if we are a pro user or not.
40-
const currentNonStaticAvatarPath = ourConvo.getAvatarInProfilePath();
41-
42-
if (!currentNonStaticAvatarPath) {
43-
window.log.info('No attachment currently set for our convo.. Nothing to do.');
44-
return null;
45-
}
46-
47-
const decryptedAvatarUrl = await DecryptedAttachmentsManager.getDecryptedMediaUrl(
48-
currentNonStaticAvatarPath,
49-
IMAGE_JPEG,
50-
true
51-
);
52-
53-
if (!decryptedAvatarUrl) {
54-
window.log.warn('Could not decrypt avatar stored locally..');
55-
return null;
56-
}
57-
const blob = await urlToBlob(decryptedAvatarUrl);
58-
59-
const decryptedAvatarData = await blob.arrayBuffer();
60-
61-
return uploadAndSetOurAvatarShared({
62-
decryptedAvatarData,
63-
ourConvo,
64-
profileKey,
65-
context: 'reuploadAvatar',
66-
});
67-
}
8+
import {
9+
MultiEncryptWrapperActions,
10+
UserConfigWrapperActions,
11+
} from '../../webworker/workers/browser/libsession_worker_interface';
12+
import { UserUtils } from '../../session/utils';
13+
import { fromBase64ToArray } from '../../session/utils/String';
6814

6915
export async function uploadAndSetOurAvatarShared({
7016
decryptedAvatarData,
7117
ourConvo,
72-
profileKey,
7318
context,
7419
}: {
7520
ourConvo: ConversationModel;
7621
decryptedAvatarData: ArrayBuffer;
77-
profileKey: Uint8Array;
7822
context: 'uploadNewAvatar' | 'reuploadAvatar';
7923
}) {
8024
if (!decryptedAvatarData?.byteLength) {
8125
window.log.warn('uploadAndSetOurAvatarShared: avatar content is empty');
8226
return null;
8327
}
28+
// Note: we want to encrypt & upload the **processed** avatar
29+
// below (resized & converted), not the original one.
30+
const { avatarFallback, mainAvatarDetails } = await processAvatarData(decryptedAvatarData, true);
31+
32+
let encryptedData: ArrayBuffer;
33+
let encryptionKey: Uint8Array;
34+
const deterministicEncryption = window.sessionFeatureFlags?.useDeterministicEncryption;
35+
if (deterministicEncryption) {
36+
const encryptedContent = await MultiEncryptWrapperActions.attachmentEncrypt({
37+
allowLarge: false,
38+
seed: await UserUtils.getUserEd25519Seed(),
39+
data: new Uint8Array(mainAvatarDetails.outputBuffer),
40+
domain: 'profilePic',
41+
});
42+
encryptedData = encryptedContent.encryptedData;
43+
encryptionKey = encryptedContent.encryptionKey;
44+
} else {
45+
// if this is a reupload, reuse the current profile key. Otherwise generate a new one
46+
const existingProfileKey = ourConvo.getProfileKey();
47+
const profileKey =
48+
context === 'reuploadAvatar' && existingProfileKey
49+
? fromBase64ToArray(existingProfileKey)
50+
: randombytes_buf(32);
51+
encryptedData = await encryptProfile(mainAvatarDetails.outputBuffer, profileKey);
52+
encryptionKey = profileKey;
53+
}
8454

85-
const encryptedData = await encryptProfile(decryptedAvatarData, profileKey);
86-
87-
const avatarPointer = await uploadFileToFsWithOnionV4(encryptedData);
55+
const avatarPointer = await uploadFileToFsWithOnionV4(encryptedData, deterministicEncryption);
8856
if (!avatarPointer) {
8957
window.log.warn('failed to upload avatar to file server');
9058
return null;
9159
}
92-
const { fileUrl, expiresMs } = avatarPointer;
60+
// Note: we don't care about the expiry of the file anymore.
61+
// This is because we renew the expiry of the file itself, and only when that fails we reupload the avatar content.
62+
const { fileUrl } = avatarPointer;
9363

9464
// Note: processing the avatar here doesn't change the buffer (unless the first one was uploaded as an image too big for an avatar.)
9565
// so, once we have deterministic encryption of avatars, the uploaded should always have the same hash
96-
const { avatarFallback, mainAvatarDetails } = await processAvatarData(decryptedAvatarData);
9766

9867
// this encrypts and save the new avatar and returns a new attachment path
9968
const savedMainAvatar = await processNewAttachment({
@@ -106,7 +75,7 @@ export async function uploadAndSetOurAvatarShared({
10675
? await processNewAttachment({
10776
isRaw: true,
10877
data: avatarFallback.outputBuffer,
109-
contentType: avatarFallback.contentType, // contentType is mostly used to generate previews and screenshot. We do not care for those in this case.
78+
contentType: avatarFallback.contentType,
11079
})
11180
: null;
11281

@@ -118,17 +87,16 @@ export async function uploadAndSetOurAvatarShared({
11887
displayName: null,
11988
avatarPointer: fileUrl,
12089
type: 'setAvatarDownloadedPrivate',
121-
profileKey,
90+
profileKey: encryptionKey,
12291
});
123-
await Storage.put(SettingsKey.ntsAvatarExpiryMs, expiresMs);
12492
if (context === 'uploadNewAvatar') {
12593
await UserConfigWrapperActions.setNewProfilePic({
126-
key: profileKey,
94+
key: encryptionKey,
12795
url: fileUrl,
12896
});
12997
} else if (context === 'reuploadAvatar') {
13098
await UserConfigWrapperActions.setReuploadProfilePic({
131-
key: profileKey,
99+
key: encryptionKey,
132100
url: fileUrl,
133101
});
134102
}

ts/models/conversation.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1772,9 +1772,8 @@ export class ConversationModel extends Model<ConversationAttributes> {
17721772
const updatedAtSeconds = this.getProfileUpdatedSeconds();
17731773

17741774
return new OutgoingUserProfile({
1775-
avatarPointer,
1775+
profilePic: { url: avatarPointer, key: profileKey ? from_hex(profileKey) : null },
17761776
displayName,
1777-
profileKey,
17781777
updatedAtSeconds,
17791778
});
17801779
}

0 commit comments

Comments
 (0)