diff --git a/app/containers/message/Image.tsx b/app/containers/message/Image.tsx index 695ef8c03..1c55c803c 100644 --- a/app/containers/message/Image.tsx +++ b/app/containers/message/Image.tsx @@ -165,7 +165,8 @@ const ImageContainer = ({ const imageUri = await downloadMediaFile({ downloadUrl: imgUrlToCache, type: 'image', - mimeType: imageCached.image_type + mimeType: imageCached.image_type, + encryption: file.encryption }); updateImageCached(imageUri); } catch (e) { diff --git a/app/lib/encryption/room.ts b/app/lib/encryption/room.ts index a09f8b26b..d73fa5607 100644 --- a/app/lib/encryption/room.ts +++ b/app/lib/encryption/room.ts @@ -283,6 +283,7 @@ export default class EncryptionRoom { console.log('🚀 ~ EncryptionRoom ~ encryptFile= ~ path:', path); // const vector = await SimpleCrypto.utils.randomBytes(16); const data = await SimpleCrypto.AES.encryptFile(path, this.roomKey as ArrayBuffer, iv); + console.log('🚀 ~ EncryptionRoom ~ encryptFile= ~ this.roomKey:', bufferToB64URI(this.roomKey)); console.log('🚀 ~ EncryptionRoom ~ encryptFile= ~ data:', data); // return this.keyID + bufferToB64(joinVectorData(vector, data)); @@ -336,6 +337,24 @@ export default class EncryptionRoom { return m.text; }; + // Decrypt content + decryptContent = async (msg: string | ArrayBuffer) => { + if (!msg) { + return null; + } + + msg = b64ToBuffer(msg.slice(12) as string); + const [vector, cipherText] = splitVectorData(msg); + + const decrypted = await SimpleCrypto.AES.decrypt(cipherText, this.roomKey, vector); + console.log('🚀 ~ EncryptionRoom ~ decryptContent= ~ decrypted:', decrypted); + + const m = EJSON.parse(bufferToUtf8(decrypted)); + console.log('🚀 ~ EncryptionRoom ~ decryptContent= ~ m:', m); + + return m; + }; + // Decrypt messages decrypt = async (message: IMessage) => { if (!this.ready) { @@ -356,8 +375,19 @@ export default class EncryptionRoom { tmsg = await this.decryptText(tmsg); } - if (message.attachments?.length) { - message.attachments[0].description = await this.decryptText(message.attachments[0].description as string); + // if (message.attachments?.length) { + // message.attachments[0].description = await this.decryptText(message.attachments[0].description as string); + // } + + if (message.content?.ciphertext) { + try { + const content = await this.decryptContent(message.content?.ciphertext as string); + console.log('🚀 ~ EncryptionRoom ~ decrypt= ~ content:', content); + message.attachments = content.attachments; + console.log('🚀 ~ EncryptionRoom ~ decrypt= ~ message.attachments:', message.attachments); + } catch (e) { + console.error(e); + } } const decryptedMessage: IMessage = { diff --git a/app/lib/encryption/utils.ts b/app/lib/encryption/utils.ts index 27268bb82..a9bbf1035 100644 --- a/app/lib/encryption/utils.ts +++ b/app/lib/encryption/utils.ts @@ -1,11 +1,19 @@ import ByteBuffer from 'bytebuffer'; import SimpleCrypto from 'react-native-simple-crypto'; +import EJSON from 'ejson'; +import { atob } from 'js-base64'; import { random } from '../methods/helpers'; import { fromByteArray, toByteArray } from './helpers/base64-js'; const BASE64URI = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; +// Use a lookup table to find the index. +const lookup = new Uint8Array(256); +for (let i = 0; i < BASE64URI.length; i++) { + lookup[BASE64URI.charCodeAt(i)] = i; +} + // @ts-ignore export const b64ToBuffer = (base64: string): ArrayBuffer => toByteArray(base64).buffer; export const utf8ToBuffer = SimpleCrypto.utils.convertUtf8ToArrayBuffer; @@ -32,6 +40,33 @@ export const bufferToB64URI = (buffer: ArrayBuffer): string => { return base64; }; +export const b64URIToBuffer = (base64: string): ArrayBuffer => { + console.log('🚀 ~ b64URIToBuffer ~ base64:', base64); + const bufferLength = base64.length * 0.75; + const len = base64.length; + let i; + let p = 0; + let encoded1; + let encoded2; + let encoded3; + let encoded4; + + const arraybuffer = new ArrayBuffer(bufferLength); + const bytes = new Uint8Array(arraybuffer); + + for (i = 0; i < len; i += 4) { + encoded1 = lookup[base64.charCodeAt(i)]; + encoded2 = lookup[base64.charCodeAt(i + 1)]; + encoded3 = lookup[base64.charCodeAt(i + 2)]; + encoded4 = lookup[base64.charCodeAt(i + 3)]; + + bytes[p++] = (encoded1 << 2) | (encoded2 >> 4); + bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2); + bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63); + } + + return arraybuffer; +}; // SimpleCrypto.utils.convertArrayBufferToUtf8 is not working with unicode emoji export const bufferToUtf8 = (buffer: ArrayBuffer): string => { const uintArray = new Uint8Array(buffer) as number[] & Uint8Array; @@ -58,3 +93,199 @@ export const toString = (thing: string | ByteBuffer | Buffer | ArrayBuffer | Uin return new ByteBuffer.wrap(thing).toString('binary'); }; export const randomPassword = (): string => `${random(3)}-${random(3)}-${random(3)}`.toLowerCase(); + +export const generateAESCTRKey = () => SimpleCrypto.utils.randomBytes(16); + +export const exportAESCTR = key => { + // Web Crypto format of a Secret Key + const exportedKey = { + // Type of Secret Key + kty: 'oct', + // Algorithm + alg: 'A256CTR', + // Base64URI encoded array of bytes + k: bufferToB64URI(key), + // Specific Web Crypto properties + ext: true, + key_ops: ['encrypt', 'decrypt'] + }; + + return exportedKey; + // return EJSON.stringify(exportedKey); +}; + +export const encryptAESCTR = (path: string, key: ArrayBuffer, vector: ArrayBuffer) => + SimpleCrypto.AES.encryptFile(path, key, vector); + +export const decryptAESCTR = (path: string, key: ArrayBuffer, vector: ArrayBuffer) => + SimpleCrypto.AES.decryptFile(path, key, vector); + +// Base 64 encoding + +const BASE_64_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + +const BASE_64_VALS = Object.create(null); + +const getChar = (val: number) => BASE_64_CHARS.charAt(val); +const getVal = (ch: string) => (ch === '=' ? -1 : BASE_64_VALS[ch]); + +for (let i = 0; i < BASE_64_CHARS.length; i++) { + BASE_64_VALS[getChar(i)] = i; +} + +// XXX This is a weird place for this to live, but it's used both by +// this package and 'ejson', and we can't put it in 'ejson' without +// introducing a circular dependency. It should probably be in its own +// package or as a helper in a package that both 'base64' and 'ejson' +// use. +const newBinary = (len: number) => { + if (typeof Uint8Array === 'undefined' || typeof ArrayBuffer === 'undefined') { + const ret = Object.assign( + Array.from({ length: len }, () => 0), + { + $Uint8ArrayPolyfill: true + } + ); + return ret; + } + return new Uint8Array(new ArrayBuffer(len)); +}; + +const encode = (array: ArrayLike | string) => { + if (typeof array === 'string') { + const str = array; + const binary = newBinary(str.length); + for (let i = 0; i < str.length; i++) { + const ch = str.charCodeAt(i); + if (ch > 0xff) { + throw new Error('Not ascii. Base64.encode can only take ascii strings.'); + } + + binary[i] = ch; + } + array = binary; + } + + const answer: string[] = []; + let a: number | null = null; + let b: number | null = null; + let c: number | null = null; + let d: number | null = null; + + for (let i = 0; i < array.length; i++) { + switch (i % 3) { + case 0: + a = (array[i] >> 2) & 0x3f; + b = (array[i] & 0x03) << 4; + break; + case 1: + b = (b ?? 0) | ((array[i] >> 4) & 0xf); + c = (array[i] & 0xf) << 2; + break; + case 2: + c = (c ?? 0) | ((array[i] >> 6) & 0x03); + d = array[i] & 0x3f; + answer.push(getChar(a ?? 0)); + answer.push(getChar(b ?? 0)); + answer.push(getChar(c)); + answer.push(getChar(d)); + a = null; + b = null; + c = null; + d = null; + break; + } + } + + if (a !== null) { + answer.push(getChar(a)); + answer.push(getChar(b ?? 0)); + if (c === null) { + answer.push('='); + } else { + answer.push(getChar(c)); + } + + if (d === null) { + answer.push('='); + } + } + + return answer.join(''); +}; + +const decode = (str: string) => { + let len = Math.floor((str.length * 3) / 4); + if (str.charAt(str.length - 1) === '=') { + len--; + if (str.charAt(str.length - 2) === '=') { + len--; + } + } + + const arr = newBinary(len); + + let one: number | null = null; + let two: number | null = null; + let three: number | null = null; + + let j = 0; + + for (let i = 0; i < str.length; i++) { + const c = str.charAt(i); + const v = getVal(c); + switch (i % 4) { + case 0: + if (v < 0) { + throw new Error('invalid base64 string'); + } + + one = v << 2; + break; + case 1: + if (v < 0) { + throw new Error('invalid base64 string'); + } + + one = (one ?? 0) | (v >> 4); + arr[j++] = one; + two = (v & 0x0f) << 4; + break; + case 2: + if (v >= 0) { + two = (two ?? 0) | (v >> 2); + arr[j++] = two; + three = (v & 0x03) << 6; + } + + break; + case 3: + if (v >= 0) { + arr[j++] = (three ?? 0) | v; + } + + break; + } + } + + return arr; +}; + +export function base64Decode(string) { + string = atob(string); + const { length } = string; + const buf = new ArrayBuffer(length); + const bufView = new Uint8Array(buf); + for (let i = 0; i < string.length; i++) { + bufView[i] = string.charCodeAt(i); + } + return buf; +} + +// console.log( +// atob( +// 'eyJrZXkiOnsiYWxnIjoiQTI1NkNUUiIsImV4dCI6dHJ1ZSwiayI6Ink1MDhHNTNTZHpvVnVibVM1Z01leHpmLXBkeDVDd3hZZFQwNVNBcVdURU0iLCJrZXlfb3BzIjpbImVuY3J5cHQiLCJkZWNyeXB0Il0sImt0eSI6Im9jdCJ9LCJpdiI6IkRBQnY2YnRhRTg1ZEVyTTJMdGJXakE9PSJ9' +// ) +// ); + +export const Base64 = { encode, decode, newBinary }; diff --git a/app/lib/methods/handleMediaDownload.ts b/app/lib/methods/handleMediaDownload.ts index 28abab2d6..f2c9ba1fb 100644 --- a/app/lib/methods/handleMediaDownload.ts +++ b/app/lib/methods/handleMediaDownload.ts @@ -1,10 +1,12 @@ import * as FileSystem from 'expo-file-system'; import * as mime from 'react-native-mime-types'; import { isEmpty } from 'lodash'; +// import { Base64 } from 'js-base64'; import { sanitizeLikeString } from '../database/utils'; import { store } from '../store/auxStore'; import log from './helpers/log'; +import { Base64, b64ToBuffer, b64URIToBuffer, base64Decode, decryptAESCTR } from '../encryption/utils'; export type MediaTypes = 'audio' | 'image' | 'video'; @@ -106,11 +108,13 @@ const ensureDirAsync = async (dir: string, intermediates = true): Promise export const getFilePath = ({ type, mimeType, - urlToCache + urlToCache, + encrypted = false }: { type: MediaTypes; mimeType?: string; urlToCache?: string; + encrypted?: boolean; }): string | null => { if (!urlToCache) { return null; @@ -118,7 +122,7 @@ export const getFilePath = ({ const folderPath = getFolderPath(urlToCache); const urlWithoutQueryString = urlToCache.split('?')[0]; const filename = sanitizeFileName(getFilename({ type, mimeType, url: urlWithoutQueryString })); - const filePath = `${folderPath}${filename}`; + const filePath = `${folderPath}${filename}${encrypted ? '.enc' : ''}`; return filePath; }; @@ -197,27 +201,49 @@ export async function cancelDownload(messageUrl: string): Promise { export function downloadMediaFile({ type, mimeType, - downloadUrl + downloadUrl, + encryption }: { type: MediaTypes; mimeType?: string; downloadUrl: string; + encryption: any; }): Promise { return new Promise(async (resolve, reject) => { let downloadKey = ''; try { - const path = getFilePath({ type, mimeType, urlToCache: downloadUrl }); + const path = getFilePath({ type, mimeType, urlToCache: downloadUrl, encrypted: !!encryption }); + console.log('🚀 ~ returnnewPromise ~ path:', path); if (!path) { return reject(); } downloadKey = mediaDownloadKey(downloadUrl); downloadQueue[downloadKey] = FileSystem.createDownloadResumable(downloadUrl, path); const result = await downloadQueue[downloadKey].downloadAsync(); + + console.log('🚀 ~ returnnewPromise ~ result:', result); + + // const decryptedFile = await Encryption.decryptFile(rid, result.uri.substring(7), encryption.key, encryption.iv); + // console.log('🚀 ~ downloadMediaFile ~ decryptedFile:', decryptedFile); + + console.log('🚀 ~ returnnewPromise ~ encryption:', encryption); + const exportedKeyArrayBuffer = b64URIToBuffer(encryption.key.k); + // const vector = b64URIToBuffer(encryption.iv); + // const vector = b64ToBuffer(encryption.iv); + // const vector = Base64.decode(encryption.iv); + // const vector = Base64.decode(encryption.iv); + const vector = base64Decode(encryption.iv); + console.log('🚀 ~ returnnewPromise ~ vector:', vector); + + const decryptedFile = await decryptAESCTR(result.uri.substring(7), exportedKeyArrayBuffer, vector); + console.log('🚀 ~ handleMediaDownload ~ decryptedFile:', decryptedFile); + if (result?.uri) { return resolve(result.uri); } return reject(); - } catch { + } catch (e) { + console.error(e); return reject(); } finally { delete downloadQueue[downloadKey]; diff --git a/app/views/ShareView/index.tsx b/app/views/ShareView/index.tsx index c36ed0da4..a9a966b8e 100644 --- a/app/views/ShareView/index.tsx +++ b/app/views/ShareView/index.tsx @@ -37,6 +37,7 @@ import { sendFileMessage, sendMessage } from '../../lib/methods'; import { hasPermission, isAndroid, canUploadFile, isReadOnly, isBlocked } from '../../lib/methods/helpers'; import { RoomContext } from '../RoomView/context'; import { Encryption } from '../../lib/encryption'; +import { b64URIToBuffer, decryptAESCTR, encryptAESCTR, exportAESCTR, generateAESCTRKey } from '../../lib/encryption/utils'; interface IShareViewState { selected: IShareAttachment; @@ -251,13 +252,21 @@ class ShareView extends Component { } try { - console.log(attachments[0].path); + const { path } = attachments[0]; const vector = await SimpleCrypto.utils.randomBytes(16); - const encryptedFile = await Encryption.encryptFile(room.rid, attachments[0].path, vector); - console.log('🚀 ~ ShareView ~ attachments.map ~ encryptedFile:', encryptedFile); + const key = await generateAESCTRKey(); - const decryptedFile = await Encryption.decryptFile(room.rid, encryptedFile, vector); - console.log('🚀 ~ ShareView ~ attachments.map ~ decryptedFile:', decryptedFile); + const exportedKey = exportAESCTR(key); + console.log('🚀 ~ ShareView ~ send= ~ exportedKey:', exportedKey, exportedKey.k); + + const exportedKeyArrayBuffer = b64URIToBuffer(exportedKey.k); + console.log('🚀 ~ ShareView ~ send= ~ exportedKeyArrayBuffer:', exportedKeyArrayBuffer); + + const encryptedFile = await encryptAESCTR(path, exportedKeyArrayBuffer, vector); + console.log('🚀 ~ ShareView ~ send= ~ encryptedFile:', encryptedFile); + + const decryptedFile = await decryptAESCTR(encryptedFile, exportedKeyArrayBuffer, vector); + console.log('🚀 ~ ShareView ~ send= ~ decryptedFile:', decryptedFile); } catch (e) { console.error(e); } @@ -366,7 +375,8 @@ class ShareView extends Component { selectedMessages, onSendMessage: this.send, onRemoveQuoteMessage: this.onRemoveQuoteMessage - }}> + }} + >