First commit encrypting attachments locally

pull/1554/head
Audric Ackermann 4 years ago
parent a010630775
commit 5c6c5c2b8c
No known key found for this signature in database
GPG Key ID: 999F434D76324AD4

@ -6,6 +6,7 @@ const glob = require('glob');
const fse = require('fs-extra');
const toArrayBuffer = require('to-arraybuffer');
const { map, isArrayBuffer, isString } = require('lodash');
const AttachmentTS = require('../ts/types/Attachment');
const PATH = 'attachments.noindex';
@ -45,14 +46,18 @@ exports.createReader = root => {
if (!isString(relativePath)) {
throw new TypeError("'relativePath' must be a string");
}
console.time(`readFile: ${relativePath}`);
const absolutePath = path.join(root, relativePath);
const normalized = path.normalize(absolutePath);
if (!normalized.startsWith(root)) {
throw new Error('Invalid relative path');
}
const buffer = await fse.readFile(normalized);
return toArrayBuffer(buffer);
const decryptedData = await AttachmentTS.decryptAttachmentBuffer(
toArrayBuffer(buffer)
);
return decryptedData.buffer;
};
};
@ -95,7 +100,6 @@ exports.createWriterForExisting = root => {
throw new TypeError("'arrayBuffer' must be an array buffer");
}
const buffer = Buffer.from(arrayBuffer);
const absolutePath = path.join(root, relativePath);
const normalized = path.normalize(absolutePath);
if (!normalized.startsWith(root)) {
@ -103,7 +107,13 @@ exports.createWriterForExisting = root => {
}
await fse.ensureFile(normalized);
const {
encryptedBufferWithHeader,
} = await AttachmentTS.encryptAttachmentBuffer(arrayBuffer);
const buffer = Buffer.from(encryptedBufferWithHeader.buffer);
await fse.writeFile(normalized, buffer);
return relativePath;
};
};

@ -254,18 +254,6 @@ async function _addAttachmentToMessage(message, attachment, { type, index }) {
const logPrefix = `${message.idForLogging()} (type: ${type}, index: ${index})`;
if (type === 'long-message') {
try {
const { data } = await Signal.Migrations.loadAttachmentData(attachment);
message.set({
body: attachment.isError ? message.get('body') : stringFromBytes(data),
});
} finally {
Signal.Migrations.deleteAttachmentData(attachment.path);
}
return;
}
if (type === 'attachment') {
const attachments = message.get('attachments');
if (!attachments || attachments.length <= index) {

@ -1,12 +1,18 @@
/* eslint-disable more/no-then */
/* global document, URL, Blob */
const loadImage = require('blueimp-load-image');
const { toLogFormat } = require('./errors');
const toArrayBuffer = require('to-arraybuffer');
const dataURLToBlobSync = require('blueimp-canvas-to-blob');
const fse = require('fs-extra');
const { blobToArrayBuffer } = require('blob-util');
const {
arrayBufferToObjectURL,
} = require('../../../ts/util/arrayBufferToObjectURL');
const AttachmentTS = require('../../../ts/types/Attachment');
exports.blobToArrayBuffer = blobToArrayBuffer;
@ -24,8 +30,17 @@ exports.getImageDimensions = ({ objectUrl, logger }) =>
logger.error('getImageDimensions error', toLogFormat(error));
reject(error);
});
image.src = objectUrl;
fse.readFile(objectUrl).then(buffer => {
AttachmentTS.decryptAttachmentBuffer(toArrayBuffer(buffer)).then(
decryptedData => {
//FIXME image/jpeg is hard coded
const srcData = `data:image/jpg;base64,${window.libsession.Utils.StringUtils.fromArrayBufferToBase64(
toArrayBuffer(decryptedData)
)}`;
image.src = srcData;
}
);
});
});
exports.makeImageThumbnail = ({
@ -70,7 +85,17 @@ exports.makeImageThumbnail = ({
reject(error);
});
image.src = objectUrl;
fse.readFile(objectUrl).then(buffer => {
AttachmentTS.decryptAttachmentBuffer(toArrayBuffer(buffer)).then(
decryptedData => {
//FIXME image/jpeg is hard coded
const srcData = `data:image/jpg;base64,${window.libsession.Utils.StringUtils.fromArrayBufferToBase64(
toArrayBuffer(decryptedData)
)}`;
image.src = srcData;
}
);
});
});
exports.makeVideoScreenshot = ({

@ -4,8 +4,11 @@ import classNames from 'classnames';
import { Spinner } from '../Spinner';
import { LocalizerType } from '../../types/Util';
import { AttachmentType } from '../../types/Attachment';
import { useEncryptedFileFetch } from '../../hooks/useEncryptedFileFetch';
import { fromArrayBufferToBase64 } from '../../session/utils/String';
import toArrayBuffer from 'to-arraybuffer';
interface Props {
type Props = {
alt: string;
attachment: AttachmentType;
url: string;
@ -32,135 +35,140 @@ interface Props {
onClick?: (attachment: AttachmentType) => void;
onClickClose?: (attachment: AttachmentType) => void;
onError?: () => void;
}
};
export class Image extends React.Component<Props> {
export const Image = (props: Props) => {
// tslint:disable-next-line max-func-body-length cyclomatic-complexity
public render() {
const {
alt,
attachment,
bottomOverlay,
closeButton,
curveBottomLeft,
curveBottomRight,
curveTopLeft,
curveTopRight,
darkOverlay,
height,
i18n,
onClick,
onClickClose,
onError,
overlayText,
playIconOverlay,
smallCurveTopLeft,
softCorners,
url,
width,
} = this.props;
const {
alt,
attachment,
bottomOverlay,
closeButton,
curveBottomLeft,
curveBottomRight,
curveTopLeft,
curveTopRight,
darkOverlay,
height,
i18n,
onClick,
onClickClose,
onError,
overlayText,
playIconOverlay,
smallCurveTopLeft,
softCorners,
url,
width,
} = props;
const { caption, pending } = attachment || { caption: null, pending: true };
const canClick = onClick && !pending;
const role = canClick ? 'button' : undefined;
const { caption, pending } = attachment || { caption: null, pending: true };
const canClick = onClick && !pending;
const role = canClick ? 'button' : undefined;
return (
const { loading, data } = useEncryptedFileFetch(url);
//FIXME jpg is hardcoded
const srcData = !loading
? data?.length
? `data:image/jpg;base64,${fromArrayBufferToBase64(toArrayBuffer(data))}`
: url
: '';
return (
<div
role={role}
onClick={(e: any) => {
if (canClick && onClick) {
e.stopPropagation();
onClick(attachment);
}
}}
className={classNames(
'module-image',
canClick ? 'module-image__with-click-handler' : null,
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
curveBottomRight ? 'module-image--curved-bottom-right' : null,
curveTopLeft ? 'module-image--curved-top-left' : null,
curveTopRight ? 'module-image--curved-top-right' : null,
smallCurveTopLeft ? 'module-image--small-curved-top-left' : null,
softCorners ? 'module-image--soft-corners' : null
)}
>
{pending || loading ? (
<div
className="module-image__loading-placeholder"
style={{
height: `${height}px`,
width: `${width}px`,
lineHeight: `${height}px`,
textAlign: 'center',
}}
>
<Spinner size="normal" />
</div>
) : (
<img
onError={onError}
className="module-image__image"
alt={alt}
height={height}
width={width}
src={srcData}
/>
)}
{caption ? (
<img
className="module-image__caption-icon"
src="images/caption-shadow.svg"
alt={i18n('imageCaptionIconAlt')}
/>
) : null}
<div
role={role}
onClick={(e: any) => {
if (canClick && onClick) {
e.stopPropagation();
onClick(attachment);
}
}}
className={classNames(
'module-image',
canClick ? 'module-image__with-click-handler' : null,
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
curveBottomRight ? 'module-image--curved-bottom-right' : null,
'module-image__border-overlay',
curveTopLeft ? 'module-image--curved-top-left' : null,
curveTopRight ? 'module-image--curved-top-right' : null,
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
curveBottomRight ? 'module-image--curved-bottom-right' : null,
smallCurveTopLeft ? 'module-image--small-curved-top-left' : null,
softCorners ? 'module-image--soft-corners' : null
softCorners ? 'module-image--soft-corners' : null,
darkOverlay ? 'module-image__border-overlay--dark' : null
)}
>
{pending ? (
<div
className="module-image__loading-placeholder"
style={{
height: `${height}px`,
width: `${width}px`,
lineHeight: `${height}px`,
textAlign: 'center',
}}
// alt={i18n('loading')}
>
<Spinner size="normal" />
</div>
) : (
<img
onError={onError}
className="module-image__image"
alt={alt}
height={height}
width={width}
src={url}
/>
)}
{caption ? (
<img
className="module-image__caption-icon"
src="images/caption-shadow.svg"
alt={i18n('imageCaptionIconAlt')}
/>
) : null}
/>
{closeButton ? (
<div
role="button"
onClick={(e: any) => {
e.stopPropagation();
if (onClickClose) {
onClickClose(attachment);
}
}}
className="module-image__close-button"
/>
) : null}
{bottomOverlay ? (
<div
className={classNames(
'module-image__border-overlay',
curveTopLeft ? 'module-image--curved-top-left' : null,
curveTopRight ? 'module-image--curved-top-right' : null,
'module-image__bottom-overlay',
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
curveBottomRight ? 'module-image--curved-bottom-right' : null,
smallCurveTopLeft ? 'module-image--small-curved-top-left' : null,
softCorners ? 'module-image--soft-corners' : null,
darkOverlay ? 'module-image__border-overlay--dark' : null
curveBottomRight ? 'module-image--curved-bottom-right' : null
)}
/>
{closeButton ? (
<div
role="button"
onClick={(e: any) => {
e.stopPropagation();
if (onClickClose) {
onClickClose(attachment);
}
}}
className="module-image__close-button"
/>
) : null}
{bottomOverlay ? (
<div
className={classNames(
'module-image__bottom-overlay',
curveBottomLeft ? 'module-image--curved-bottom-left' : null,
curveBottomRight ? 'module-image--curved-bottom-right' : null
)}
/>
) : null}
{!pending && playIconOverlay ? (
<div className="module-image__play-overlay__circle">
<div className="module-image__play-overlay__icon" />
</div>
) : null}
{overlayText ? (
<div
className="module-image__text-container"
style={{ lineHeight: `${height}px` }}
>
{overlayText}
</div>
) : null}
</div>
);
}
}
) : null}
{!(pending || loading) && playIconOverlay ? (
<div className="module-image__play-overlay__circle">
<div className="module-image__play-overlay__icon" />
</div>
) : null}
{overlayText ? (
<div
className="module-image__text-container"
style={{ lineHeight: `${height}px` }}
>
{overlayText}
</div>
) : null}
</div>
);
};

@ -0,0 +1 @@
declare module 'to-arraybuffer';

@ -0,0 +1,29 @@
import { useEffect, useState } from 'react';
import toArrayBuffer from 'to-arraybuffer';
import * as fse from 'fs-extra';
import { decryptAttachmentBuffer } from '../types/Attachment';
export const useEncryptedFileFetch = (url: string) => {
// tslint:disable-next-line: no-bitwise
const [data, setData] = useState(new Uint8Array());
const [loading, setLoading] = useState(true);
async function fetchUrl() {
// this is a file encoded by session
//FIXME find another way to know if the file in encrypted or not
// maybe rely on
if (url.includes('/attachments.noindex/')) {
const encryptedFileContent = await fse.readFile(url);
const decryptedContent = await decryptAttachmentBuffer(
toArrayBuffer(encryptedFileContent)
);
setData(decryptedContent);
}
setLoading(false);
}
useEffect(() => {
void fetchUrl();
}, [url]);
return { data, loading };
};

@ -1,9 +1,8 @@
import is from '@sindresorhus/is';
import moment from 'moment';
import { padStart } from 'lodash';
import { isArrayBuffer, padStart } from 'lodash';
import * as MIME from './MIME';
import { arrayBufferToObjectURL } from '../util/arrayBufferToObjectURL';
import { saveURLAsFile } from '../util/saveURLAsFile';
import { SignalService } from '../protobuf';
import {
@ -11,6 +10,9 @@ import {
isVideoTypeSupported,
} from '../util/GoogleChrome';
import { LocalizerType } from './Util';
import { fromHexToArray, toHex } from '../session/utils/String';
import { getSodium } from '../session/crypto';
import { fromHex } from 'bytebuffer';
const MAX_WIDTH = 300;
const MAX_HEIGHT = MAX_WIDTH * 1.5;
@ -416,3 +418,85 @@ export const getFileExtension = (
return attachment.contentType.split('/')[1];
}
};
let indexEncrypt = 0;
export const encryptAttachmentBuffer = async (bufferIn: ArrayBuffer) => {
if (!isArrayBuffer(bufferIn)) {
throw new TypeError("'bufferIn' must be an array buffer");
}
const ourIndex = indexEncrypt;
indexEncrypt++;
console.time(`timer #*. encryptAttachmentBuffer ${ourIndex}`);
const uintArrayIn = new Uint8Array(bufferIn);
const sodium = await getSodium();
/* Shared secret key required to encrypt/decrypt the stream */
// const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const key = fromHexToArray(
'0c5f7147b6d3239cbb5a418814cee1bfca2df5c94bffddf22ee37eea3ede972b'
);
console.warn('key', toHex(key));
/* Set up a new stream: initialize the state and create the header */
const {
state,
header,
} = sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
console.warn('header', toHex(header));
/* Now, encrypt the buffer. */
const bufferOut = sodium.crypto_secretstream_xchacha20poly1305_push(
state,
uintArrayIn,
null,
sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
);
const encryptedBufferWithHeader = new Uint8Array(
bufferOut.length + header.length
);
encryptedBufferWithHeader.set(header);
encryptedBufferWithHeader.set(bufferOut, header.length);
console.warn('bufferOut', toHex(encryptedBufferWithHeader));
console.timeEnd(`timer #*. encryptAttachmentBuffer ${ourIndex}`);
return { encryptedBufferWithHeader, header, key };
};
let indexDecrypt = 0;
export const decryptAttachmentBuffer = async (
bufferIn: ArrayBuffer,
key: string = '0c5f7147b6d3239cbb5a418814cee1bfca2df5c94bffddf22ee37eea3ede972b'
) => {
if (!isArrayBuffer(bufferIn)) {
throw new TypeError("'bufferIn' must be an array buffer");
}
const ourIndex = indexDecrypt;
indexDecrypt++;
console.time(`timer .*# decryptAttachmentBuffer ${ourIndex}`);
const sodium = await getSodium();
const header = new Uint8Array(
bufferIn.slice(0, sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES)
);
const encryptedBuffer = new Uint8Array(
bufferIn.slice(sodium.crypto_secretstream_xchacha20poly1305_HEADERBYTES)
);
/* Decrypt the stream: initializes the state, using the key and a header */
const state = sodium.crypto_secretstream_xchacha20poly1305_init_pull(
header,
fromHexToArray(key)
);
// what if ^ this call fail (? try to load as a unencrypted attachment?)
const messageTag = sodium.crypto_secretstream_xchacha20poly1305_pull(
state,
encryptedBuffer
);
console.timeEnd(`timer .*# decryptAttachmentBuffer ${ourIndex}`);
return messageTag.message;
};

Loading…
Cancel
Save