Chore: Migrate lib/encryption folder to TypeScript (#3639)

* Initial commit

* add types/bytebuffer, add type definitions to params and update interfaces

* add more types and type assertions

* update types

* change bang operator by type assertion and update class variables definitions

* add types for deferred class

* minor tweaks on types definitions

* add ts-ignore

* Update encryption.ts

* update deferred and encryption

* update encryption.ts

* Update room.ts

* update toDecrypt type

* initialize sessionKeyExportedString

* remove return types
This commit is contained in:
Gerzon Z 2022-02-16 17:14:28 -04:00 committed by GitHub
parent 97f8271127
commit 352a718631
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 178 additions and 94 deletions

View File

@ -3,7 +3,8 @@ import { MarkdownAST } from '@rocket.chat/message-parser';
import { IAttachment } from './IAttachment'; import { IAttachment } from './IAttachment';
import { IReaction } from './IReaction'; import { IReaction } from './IReaction';
import { SubscriptionType } from './ISubscription';
export type MessageType = 'jitsi_call_started' | 'discussion-created' | 'e2e' | 'load_more' | 'rm' | 'uj';
export interface IUserMessage { export interface IUserMessage {
_id: string; _id: string;
@ -34,12 +35,16 @@ export interface ITranslations {
value: string; value: string;
} }
export type E2EType = 'pending' | 'done';
export interface ILastMessage { export interface ILastMessage {
_id: string; _id: string;
rid: string; rid: string;
tshow: boolean; tshow: boolean;
t: MessageType;
tmid: string; tmid: string;
msg: string; msg: string;
e2e: E2EType;
ts: Date; ts: Date;
u: IUserMessage; u: IUserMessage;
_updatedAt: Date; _updatedAt: Date;
@ -55,8 +60,9 @@ export interface ILastMessage {
export interface IMessage { export interface IMessage {
_id: string; _id: string;
rid: string;
msg?: string; msg?: string;
t?: SubscriptionType; t?: MessageType;
ts: Date; ts: Date;
u: IUserMessage; u: IUserMessage;
alias: string; alias: string;

View File

@ -12,6 +12,7 @@ export enum SubscriptionType {
DIRECT = 'd', DIRECT = 'd',
CHANNEL = 'c', CHANNEL = 'c',
OMNICHANNEL = 'l', OMNICHANNEL = 'l',
E2E = 'e2e',
THREAD = 'thread' // FIXME: this is not a type of subscription THREAD = 'thread' // FIXME: this is not a type of subscription
} }

View File

@ -2,9 +2,8 @@ import Model from '@nozbe/watermelondb/Model';
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
import { IAttachment } from './IAttachment'; import { IAttachment } from './IAttachment';
import { IEditedBy, IUserChannel, IUserMention, IUserMessage } from './IMessage'; import { IEditedBy, IUserChannel, IUserMention, IUserMessage, MessageType } from './IMessage';
import { IReaction } from './IReaction'; import { IReaction } from './IReaction';
import { SubscriptionType } from './ISubscription';
import { IUrl } from './IUrl'; import { IUrl } from './IUrl';
interface IFileThread { interface IFileThread {
@ -35,8 +34,9 @@ export interface IThreadResult {
export interface IThread { export interface IThread {
id: string; id: string;
tmsg?: string;
msg?: string; msg?: string;
t?: SubscriptionType; t?: MessageType;
rid: string; rid: string;
_updatedAt?: Date; _updatedAt?: Date;
ts?: Date; ts?: Date;

View File

@ -1,13 +1,13 @@
import Model from '@nozbe/watermelondb/Model'; import Model from '@nozbe/watermelondb/Model';
import { IAttachment } from './IAttachment'; import { IAttachment } from './IAttachment';
import { IEditedBy, ITranslations, IUserChannel, IUserMention, IUserMessage } from './IMessage'; import { IEditedBy, ITranslations, IUserChannel, IUserMention, IUserMessage, MessageType } from './IMessage';
import { IReaction } from './IReaction'; import { IReaction } from './IReaction';
import { SubscriptionType } from './ISubscription';
export interface IThreadMessage { export interface IThreadMessage {
tmsg?: string;
msg?: string; msg?: string;
t?: SubscriptionType; t?: MessageType;
rid: string; rid: string;
ts: Date; ts: Date;
u: IUserMessage; u: IUserMessage;

View File

@ -99,8 +99,8 @@ export interface IUser extends IRocketChatRecord {
roles: string[]; roles: string[];
type: string; type: string;
active: boolean; active: boolean;
username?: string;
name?: string; name?: string;
username: string;
services?: IUserServices; services?: IUserServices;
emails?: IUserEmail[]; emails?: IUserEmail[];
status?: UserStatus; status?: UserStatus;

View File

@ -1,7 +1,7 @@
import EJSON from 'ejson'; import EJSON from 'ejson';
import SimpleCrypto from 'react-native-simple-crypto'; import SimpleCrypto from 'react-native-simple-crypto';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import { Q } from '@nozbe/watermelondb'; import { Q, Model } from '@nozbe/watermelondb';
import RocketChat from '../rocketchat'; import RocketChat from '../rocketchat';
import UserPreferences from '../userPreferences'; import UserPreferences from '../userPreferences';
@ -20,9 +20,25 @@ import {
} from './constants'; } from './constants';
import { joinVectorData, randomPassword, splitVectorData, toString, utf8ToBuffer } from './utils'; import { joinVectorData, randomPassword, splitVectorData, toString, utf8ToBuffer } from './utils';
import { EncryptionRoom } from './index'; import { EncryptionRoom } from './index';
import { IMessage, ISubscription, TMessageModel, TSubscriptionModel, TThreadMessageModel, TThreadModel } from '../../definitions';
class Encryption { class Encryption {
ready: boolean;
privateKey: string | null;
readyPromise: Deferred;
userId: string | null;
roomInstances: {
[rid: string]: {
ready: boolean;
provideKeyToUser: Function;
handshake: Function;
decrypt: Function;
encrypt: Function;
};
};
constructor() { constructor() {
this.userId = '';
this.ready = false; this.ready = false;
this.privateKey = null; this.privateKey = null;
this.roomInstances = {}; this.roomInstances = {};
@ -37,7 +53,7 @@ class Encryption {
} }
// Initialize Encryption client // Initialize Encryption client
initialize = userId => { initialize = (userId: string) => {
this.userId = userId; this.userId = userId;
this.roomInstances = {}; this.roomInstances = {};
@ -82,7 +98,7 @@ class Encryption {
}; };
// When a new participant join and request a new room encryption key // When a new participant join and request a new room encryption key
provideRoomKeyToUser = async (keyId, rid) => { provideRoomKeyToUser = async (keyId: string, rid: string) => {
// If the client is not ready // If the client is not ready
if (!this.ready) { if (!this.ready) {
try { try {
@ -100,14 +116,14 @@ class Encryption {
}; };
// Persist keys on UserPreferences // Persist keys on UserPreferences
persistKeys = async (server, publicKey, privateKey) => { persistKeys = async (server: string, publicKey: string, privateKey: string) => {
this.privateKey = await SimpleCrypto.RSA.importKey(EJSON.parse(privateKey)); this.privateKey = await SimpleCrypto.RSA.importKey(EJSON.parse(privateKey));
await UserPreferences.setStringAsync(`${server}-${E2E_PUBLIC_KEY}`, EJSON.stringify(publicKey)); await UserPreferences.setStringAsync(`${server}-${E2E_PUBLIC_KEY}`, EJSON.stringify(publicKey));
await UserPreferences.setStringAsync(`${server}-${E2E_PRIVATE_KEY}`, privateKey); await UserPreferences.setStringAsync(`${server}-${E2E_PRIVATE_KEY}`, privateKey);
}; };
// Could not obtain public-private keypair from server. // Could not obtain public-private keypair from server.
createKeys = async (userId, server) => { createKeys = async (userId: string, server: string) => {
// Generate new keys // Generate new keys
const key = await SimpleCrypto.RSA.generateKeys(2048); const key = await SimpleCrypto.RSA.generateKeys(2048);
@ -132,7 +148,7 @@ class Encryption {
}; };
// Encode a private key before send it to the server // Encode a private key before send it to the server
encodePrivateKey = async (privateKey, password, userId) => { encodePrivateKey = async (privateKey: string, password: string, userId: string) => {
const masterKey = await this.generateMasterKey(password, userId); const masterKey = await this.generateMasterKey(password, userId);
const vector = await SimpleCrypto.utils.randomBytes(16); const vector = await SimpleCrypto.utils.randomBytes(16);
@ -142,7 +158,7 @@ class Encryption {
}; };
// Decode a private key fetched from server // Decode a private key fetched from server
decodePrivateKey = async (privateKey, password, userId) => { decodePrivateKey = async (privateKey: string, password: string, userId: string) => {
const masterKey = await this.generateMasterKey(password, userId); const masterKey = await this.generateMasterKey(password, userId);
const [vector, cipherText] = splitVectorData(EJSON.parse(privateKey)); const [vector, cipherText] = splitVectorData(EJSON.parse(privateKey));
@ -152,7 +168,7 @@ class Encryption {
}; };
// Generate a user master key, this is based on userId and a password // Generate a user master key, this is based on userId and a password
generateMasterKey = async (password, userId) => { generateMasterKey = async (password: string, userId: string) => {
const iterations = 1000; const iterations = 1000;
const hash = 'SHA256'; const hash = 'SHA256';
const keyLen = 32; const keyLen = 32;
@ -166,18 +182,18 @@ class Encryption {
}; };
// Create a random password to local created keys // Create a random password to local created keys
createRandomPassword = async server => { createRandomPassword = async (server: string) => {
const password = randomPassword(); const password = randomPassword();
await UserPreferences.setStringAsync(`${server}-${E2E_RANDOM_PASSWORD_KEY}`, password); await UserPreferences.setStringAsync(`${server}-${E2E_RANDOM_PASSWORD_KEY}`, password);
return password; return password;
}; };
changePassword = async (server, password) => { changePassword = async (server: string, password: string) => {
// Cast key to the format server is expecting // Cast key to the format server is expecting
const privateKey = await SimpleCrypto.RSA.exportKey(this.privateKey); const privateKey = await SimpleCrypto.RSA.exportKey(this.privateKey as string);
// Encode the private key // Encode the private key
const encodedPrivateKey = await this.encodePrivateKey(EJSON.stringify(privateKey), password, this.userId); const encodedPrivateKey = await this.encodePrivateKey(EJSON.stringify(privateKey), password, this.userId as string);
const publicKey = await UserPreferences.getStringAsync(`${server}-${E2E_PUBLIC_KEY}`); const publicKey = await UserPreferences.getStringAsync(`${server}-${E2E_PUBLIC_KEY}`);
// Send the new keys to the server // Send the new keys to the server
@ -185,7 +201,7 @@ class Encryption {
}; };
// get a encryption room instance // get a encryption room instance
getRoomInstance = async rid => { getRoomInstance = async (rid: string) => {
// Prevent handshake again // Prevent handshake again
if (this.roomInstances[rid]?.ready) { if (this.roomInstances[rid]?.ready) {
return this.roomInstances[rid]; return this.roomInstances[rid];
@ -193,7 +209,7 @@ class Encryption {
// If doesn't have a instance of this room // If doesn't have a instance of this room
if (!this.roomInstances[rid]) { if (!this.roomInstances[rid]) {
this.roomInstances[rid] = new EncryptionRoom(rid, this.userId); this.roomInstances[rid] = new EncryptionRoom(rid, this.userId as string);
} }
const roomE2E = this.roomInstances[rid]; const roomE2E = this.roomInstances[rid];
@ -206,7 +222,7 @@ class Encryption {
// Logic to decrypt all pending messages/threads/threadMessages // Logic to decrypt all pending messages/threads/threadMessages
// after initialize the encryption client // after initialize the encryption client
decryptPendingMessages = async roomId => { decryptPendingMessages = async (roomId?: string) => {
const db = database.active; const db = database.active;
const messagesCollection = db.get('messages'); const messagesCollection = db.get('messages');
@ -228,8 +244,12 @@ class Encryption {
const threadMessagesToDecrypt = await threadMessagesCollection.query(...whereClause).fetch(); const threadMessagesToDecrypt = await threadMessagesCollection.query(...whereClause).fetch();
// Concat messages/threads/threadMessages // Concat messages/threads/threadMessages
let toDecrypt = [...messagesToDecrypt, ...threadsToDecrypt, ...threadMessagesToDecrypt]; let toDecrypt: (TThreadModel | TThreadMessageModel)[] = [
toDecrypt = await Promise.all( ...messagesToDecrypt,
...threadsToDecrypt,
...threadMessagesToDecrypt
];
toDecrypt = (await Promise.all(
toDecrypt.map(async message => { toDecrypt.map(async message => {
const { t, msg, tmsg } = message; const { t, msg, tmsg } = message;
const { id: rid } = message.subscription; const { id: rid } = message.subscription;
@ -240,9 +260,10 @@ class Encryption {
msg, msg,
tmsg tmsg
}); });
try { try {
return message.prepareUpdate( return message.prepareUpdate(
protectedFunction(m => { protectedFunction((m: TMessageModel) => {
Object.assign(m, newMessage); Object.assign(m, newMessage);
}) })
); );
@ -250,9 +271,9 @@ class Encryption {
return null; return null;
} }
}) })
); )) as (TThreadModel | TThreadMessageModel)[];
await db.action(async () => { await db.write(async () => {
await db.batch(...toDecrypt); await db.batch(...toDecrypt);
}); });
} catch (e) { } catch (e) {
@ -271,19 +292,19 @@ class Encryption {
const subsEncrypted = await subCollection.query(Q.where('e2e_key_id', Q.notEq(null))).fetch(); const subsEncrypted = await subCollection.query(Q.where('e2e_key_id', Q.notEq(null))).fetch();
// We can't do this on database level since lastMessage is not a database object // We can't do this on database level since lastMessage is not a database object
const subsToDecrypt = subsEncrypted.filter( const subsToDecrypt = subsEncrypted.filter(
sub => (sub: ISubscription) =>
// Encrypted message // Encrypted message
sub?.lastMessage?.t === E2E_MESSAGE_TYPE && sub?.lastMessage?.t === E2E_MESSAGE_TYPE &&
// Message pending decrypt // Message pending decrypt
sub?.lastMessage?.e2e === E2E_STATUS.PENDING sub?.lastMessage?.e2e === E2E_STATUS.PENDING
); );
await Promise.all( await Promise.all(
subsToDecrypt.map(async sub => { subsToDecrypt.map(async (sub: TSubscriptionModel) => {
const { rid, lastMessage } = sub; const { rid, lastMessage } = sub;
const newSub = await this.decryptSubscription({ rid, lastMessage }); const newSub = await this.decryptSubscription({ rid, lastMessage });
try { try {
return sub.prepareUpdate( return sub.prepareUpdate(
protectedFunction(m => { protectedFunction((m: TSubscriptionModel) => {
Object.assign(m, newSub); Object.assign(m, newSub);
}) })
); );
@ -293,7 +314,7 @@ class Encryption {
}) })
); );
await db.action(async () => { await db.write(async () => {
await db.batch(...subsToDecrypt); await db.batch(...subsToDecrypt);
}); });
} catch (e) { } catch (e) {
@ -302,7 +323,7 @@ class Encryption {
}; };
// Decrypt a subscription lastMessage // Decrypt a subscription lastMessage
decryptSubscription = async subscription => { decryptSubscription = async (subscription: Partial<ISubscription>) => {
// If the subscription doesn't have a lastMessage just return // If the subscription doesn't have a lastMessage just return
if (!subscription?.lastMessage) { if (!subscription?.lastMessage) {
return subscription; return subscription;
@ -334,18 +355,18 @@ class Encryption {
let subRecord; let subRecord;
try { try {
subRecord = await subCollection.find(rid); subRecord = await subCollection.find(rid as string);
} catch { } catch {
// Do nothing // Do nothing
} }
try { try {
const batch = []; const batch: (Model | null | void | false | Promise<void>)[] = [];
// If the subscription doesn't exists yet // If the subscription doesn't exists yet
if (!subRecord) { if (!subRecord) {
// Let's create the subscription with the data received // Let's create the subscription with the data received
batch.push( batch.push(
subCollection.prepareCreate(s => { subCollection.prepareCreate((s: TSubscriptionModel) => {
s._raw = sanitizedRaw({ id: rid }, subCollection.schema); s._raw = sanitizedRaw({ id: rid }, subCollection.schema);
Object.assign(s, subscription); Object.assign(s, subscription);
}) })
@ -355,7 +376,7 @@ class Encryption {
try { try {
// Let's update the subscription with the received E2EKey // Let's update the subscription with the received E2EKey
batch.push( batch.push(
subRecord.prepareUpdate(s => { subRecord.prepareUpdate((s: TSubscriptionModel) => {
s.E2EKey = subscription.E2EKey; s.E2EKey = subscription.E2EKey;
}) })
); );
@ -366,7 +387,7 @@ class Encryption {
// If batch has some operation // If batch has some operation
if (batch.length) { if (batch.length) {
await db.action(async () => { await db.write(async () => {
await db.batch(...batch); await db.batch(...batch);
}); });
} }
@ -377,7 +398,7 @@ class Encryption {
} }
// Get a instance using the subscription // Get a instance using the subscription
const roomE2E = await this.getRoomInstance(rid); const roomE2E = await this.getRoomInstance(rid as string);
const decryptedMessage = await roomE2E.decrypt(lastMessage); const decryptedMessage = await roomE2E.decrypt(lastMessage);
return { return {
...subscription, ...subscription,
@ -386,7 +407,7 @@ class Encryption {
}; };
// Encrypt a message // Encrypt a message
encryptMessage = async message => { encryptMessage = async (message: IMessage) => {
const { rid } = message; const { rid } = message;
const db = database.active; const db = database.active;
const subCollection = db.get('subscriptions'); const subCollection = db.get('subscriptions');
@ -419,7 +440,7 @@ class Encryption {
}; };
// Decrypt a message // Decrypt a message
decryptMessage = async message => { decryptMessage = async (message: Partial<IMessage>) => {
const { t, e2e } = message; const { t, e2e } = message;
// Prevent create a new instance if this room was encrypted sometime ago // Prevent create a new instance if this room was encrypted sometime ago
@ -440,15 +461,15 @@ class Encryption {
} }
const { rid } = message; const { rid } = message;
const roomE2E = await this.getRoomInstance(rid); const roomE2E = await this.getRoomInstance(rid as string);
return roomE2E.decrypt(message); return roomE2E.decrypt(message);
}; };
// Decrypt multiple messages // Decrypt multiple messages
decryptMessages = messages => Promise.all(messages.map(m => this.decryptMessage(m))); decryptMessages = (messages: IMessage[]) => Promise.all(messages.map((m: IMessage) => this.decryptMessage(m)));
// Decrypt multiple subscriptions // Decrypt multiple subscriptions
decryptSubscriptions = subscriptions => Promise.all(subscriptions.map(s => this.decryptSubscription(s))); decryptSubscriptions = (subscriptions: ISubscription[]) => Promise.all(subscriptions.map(s => this.decryptSubscription(s)));
} }
const encryption = new Encryption(); const encryption = new Encryption();

View File

@ -1,7 +1,9 @@
import EJSON from 'ejson'; import EJSON from 'ejson';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
import SimpleCrypto from 'react-native-simple-crypto'; import SimpleCrypto from 'react-native-simple-crypto';
import ByteBuffer from 'bytebuffer';
import { IMessage } from '../../definitions';
import RocketChat from '../rocketchat'; import RocketChat from '../rocketchat';
import Deferred from '../../utils/deferred'; import Deferred from '../../utils/deferred';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
@ -19,13 +21,26 @@ import {
utf8ToBuffer utf8ToBuffer
} from './utils'; } from './utils';
import { Encryption } from './index'; import { Encryption } from './index';
import { IUser } from '../../definitions/IUser';
export default class EncryptionRoom { export default class EncryptionRoom {
constructor(roomId, userId) { ready: boolean;
roomId: string;
userId: string;
establishing: boolean;
readyPromise: Deferred;
sessionKeyExportedString: string | ByteBuffer;
keyID: string;
roomKey: ArrayBuffer;
constructor(roomId: string, userId: string) {
this.ready = false; this.ready = false;
this.roomId = roomId; this.roomId = roomId;
this.userId = userId; this.userId = userId;
this.establishing = false; this.establishing = false;
this.keyID = '';
this.sessionKeyExportedString = '';
this.roomKey = new ArrayBuffer(0);
this.readyPromise = new Deferred(); this.readyPromise = new Deferred();
this.readyPromise.then(() => { this.readyPromise.then(() => {
// Mark as ready // Mark as ready
@ -57,7 +72,7 @@ export default class EncryptionRoom {
const { E2EKey, e2eKeyId } = subscription; const { E2EKey, e2eKeyId } = subscription;
// If this room has a E2EKey, we import it // If this room has a E2EKey, we import it
if (E2EKey) { if (E2EKey && Encryption.privateKey) {
// We're establishing a new room encryption client // We're establishing a new room encryption client
this.establishing = true; this.establishing = true;
await this.importRoomKey(E2EKey, Encryption.privateKey); await this.importRoomKey(E2EKey, Encryption.privateKey);
@ -82,25 +97,25 @@ export default class EncryptionRoom {
}; };
// Import roomKey as an AES Decrypt key // Import roomKey as an AES Decrypt key
importRoomKey = async (E2EKey, privateKey) => { importRoomKey = async (E2EKey: string, privateKey: string) => {
const roomE2EKey = E2EKey.slice(12); const roomE2EKey = E2EKey.slice(12);
const decryptedKey = await SimpleCrypto.RSA.decrypt(roomE2EKey, privateKey); const decryptedKey = await SimpleCrypto.RSA.decrypt(roomE2EKey, privateKey);
this.sessionKeyExportedString = toString(decryptedKey); this.sessionKeyExportedString = toString(decryptedKey);
this.keyID = Base64.encode(this.sessionKeyExportedString).slice(0, 12); this.keyID = Base64.encode(this.sessionKeyExportedString as string).slice(0, 12);
// Extract K from Web Crypto Secret Key // Extract K from Web Crypto Secret Key
// K is a base64URL encoded array of bytes // K is a base64URL encoded array of bytes
// Web Crypto API uses this as a private key to decrypt/encrypt things // Web Crypto API uses this as a private key to decrypt/encrypt things
// Reference: https://www.javadoc.io/doc/com.nimbusds/nimbus-jose-jwt/5.1/com/nimbusds/jose/jwk/OctetSequenceKey.html // Reference: https://www.javadoc.io/doc/com.nimbusds/nimbus-jose-jwt/5.1/com/nimbusds/jose/jwk/OctetSequenceKey.html
const { k } = EJSON.parse(this.sessionKeyExportedString); const { k } = EJSON.parse(this.sessionKeyExportedString as string);
this.roomKey = b64ToBuffer(k); this.roomKey = b64ToBuffer(k);
}; };
// Create a key to a room // Create a key to a room
createRoomKey = async () => { createRoomKey = async () => {
const key = await SimpleCrypto.utils.randomBytes(16); const key = (await SimpleCrypto.utils.randomBytes(16)) as Uint8Array;
this.roomKey = key; this.roomKey = key;
// Web Crypto format of a Secret Key // Web Crypto format of a Secret Key
@ -131,7 +146,7 @@ export default class EncryptionRoom {
// Each time you see a encrypted message of a room that you don't have a key // Each time you see a encrypted message of a room that you don't have a key
// this will be called again and run once in 5 seconds // this will be called again and run once in 5 seconds
requestRoomKey = debounce( requestRoomKey = debounce(
async e2eKeyId => { async (e2eKeyId: string) => {
await RocketChat.e2eRequestRoomKey(this.roomId, e2eKeyId); await RocketChat.e2eRequestRoomKey(this.roomId, e2eKeyId);
}, },
5000, 5000,
@ -143,22 +158,22 @@ export default class EncryptionRoom {
const result = await RocketChat.e2eGetUsersOfRoomWithoutKey(this.roomId); const result = await RocketChat.e2eGetUsersOfRoomWithoutKey(this.roomId);
if (result.success) { if (result.success) {
const { users } = result; const { users } = result;
await Promise.all(users.map(user => this.encryptRoomKeyForUser(user))); await Promise.all(users.map((user: IUser) => this.encryptRoomKeyForUser(user)));
} }
}; };
// Encrypt the room key to each user in // Encrypt the room key to each user in
encryptRoomKeyForUser = async user => { encryptRoomKeyForUser = async (user: IUser) => {
if (user?.e2e?.public_key) { if (user?.e2e?.public_key) {
const { public_key: publicKey } = user.e2e; const { public_key: publicKey } = user.e2e;
const userKey = await SimpleCrypto.RSA.importKey(EJSON.parse(publicKey)); const userKey = await SimpleCrypto.RSA.importKey(EJSON.parse(publicKey));
const encryptedUserKey = await SimpleCrypto.RSA.encrypt(this.sessionKeyExportedString, userKey); const encryptedUserKey = await SimpleCrypto.RSA.encrypt(this.sessionKeyExportedString as string, userKey);
await RocketChat.e2eUpdateGroupKey(user?._id, this.roomId, this.keyID + encryptedUserKey); await RocketChat.e2eUpdateGroupKey(user?._id, this.roomId, this.keyID + encryptedUserKey);
} }
}; };
// Provide this room key to a user // Provide this room key to a user
provideKeyToUser = async keyId => { provideKeyToUser = async (keyId: string) => {
// Don't provide a key if the keyId received // Don't provide a key if the keyId received
// is different than the current one // is different than the current one
if (this.keyID !== keyId) { if (this.keyID !== keyId) {
@ -169,16 +184,16 @@ export default class EncryptionRoom {
}; };
// Encrypt text // Encrypt text
encryptText = async text => { encryptText = async (text: string | ArrayBuffer) => {
text = utf8ToBuffer(text); text = utf8ToBuffer(text as string);
const vector = await SimpleCrypto.utils.randomBytes(16); const vector = await SimpleCrypto.utils.randomBytes(16);
const data = await SimpleCrypto.AES.encrypt(text, this.roomKey, vector); const data = await SimpleCrypto.AES.encrypt(text, this.roomKey as ArrayBuffer, vector);
return this.keyID + bufferToB64(joinVectorData(vector, data)); return this.keyID + bufferToB64(joinVectorData(vector, data));
}; };
// Encrypt messages // Encrypt messages
encrypt = async message => { encrypt = async (message: IMessage) => {
if (!this.ready) { if (!this.ready) {
return message; return message;
} }
@ -207,8 +222,8 @@ export default class EncryptionRoom {
}; };
// Decrypt text // Decrypt text
decryptText = async msg => { decryptText = async (msg: string | ArrayBuffer) => {
msg = b64ToBuffer(msg.slice(12)); msg = b64ToBuffer(msg.slice(12) as string);
const [vector, cipherText] = splitVectorData(msg); const [vector, cipherText] = splitVectorData(msg);
const decrypted = await SimpleCrypto.AES.decrypt(cipherText, this.roomKey, vector); const decrypted = await SimpleCrypto.AES.decrypt(cipherText, this.roomKey, vector);
@ -219,7 +234,7 @@ export default class EncryptionRoom {
}; };
// Decrypt messages // Decrypt messages
decrypt = async message => { decrypt = async (message: IMessage) => {
if (!this.ready) { if (!this.ready) {
return message; return message;
} }
@ -231,7 +246,7 @@ export default class EncryptionRoom {
if (t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE) { if (t === E2E_MESSAGE_TYPE && e2e !== E2E_STATUS.DONE) {
let { msg, tmsg } = message; let { msg, tmsg } = message;
// Decrypt msg // Decrypt msg
msg = await this.decryptText(msg); msg = await this.decryptText(msg as string);
// Decrypt tmsg // Decrypt tmsg
if (tmsg) { if (tmsg) {

View File

@ -1,4 +1,3 @@
/* eslint-disable no-bitwise */
import ByteBuffer from 'bytebuffer'; import ByteBuffer from 'bytebuffer';
import SimpleCrypto from 'react-native-simple-crypto'; import SimpleCrypto from 'react-native-simple-crypto';
@ -7,12 +6,13 @@ import { fromByteArray, toByteArray } from '../../utils/base64-js';
const BASE64URI = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; const BASE64URI = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
export const b64ToBuffer = base64 => toByteArray(base64).buffer; // @ts-ignore
export const b64ToBuffer = (base64: string): ArrayBuffer => toByteArray(base64).buffer;
export const utf8ToBuffer = SimpleCrypto.utils.convertUtf8ToArrayBuffer; export const utf8ToBuffer = SimpleCrypto.utils.convertUtf8ToArrayBuffer;
export const bufferToB64 = arrayBuffer => fromByteArray(new Uint8Array(arrayBuffer)); export const bufferToB64 = (arrayBuffer: ArrayBuffer): string => fromByteArray(new Uint8Array(arrayBuffer));
// ArrayBuffer -> Base64 URI Safe // ArrayBuffer -> Base64 URI Safe
// https://github.com/herrjemand/Base64URL-ArrayBuffer/blob/master/lib/base64url-arraybuffer.js // https://github.com/herrjemand/Base64URL-ArrayBuffer/blob/master/lib/base64url-arraybuffer.js
export const bufferToB64URI = buffer => { export const bufferToB64URI = (buffer: ArrayBuffer): string => {
const uintArray = new Uint8Array(buffer); const uintArray = new Uint8Array(buffer);
const len = uintArray.length; const len = uintArray.length;
let base64 = ''; let base64 = '';
@ -33,28 +33,28 @@ export const bufferToB64URI = buffer => {
return base64; return base64;
}; };
// SimpleCrypto.utils.convertArrayBufferToUtf8 is not working with unicode emoji // SimpleCrypto.utils.convertArrayBufferToUtf8 is not working with unicode emoji
export const bufferToUtf8 = buffer => { export const bufferToUtf8 = (buffer: ArrayBuffer): string => {
const uintArray = new Uint8Array(buffer); const uintArray = new Uint8Array(buffer) as number[] & Uint8Array;
const encodedString = String.fromCharCode.apply(null, uintArray); const encodedString = String.fromCharCode.apply(null, uintArray);
const decodedString = decodeURIComponent(escape(encodedString)); return decodeURIComponent(escape(encodedString));
return decodedString;
}; };
export const splitVectorData = text => { export const splitVectorData = (text: ArrayBuffer): ArrayBuffer[] => {
const vector = text.slice(0, 16); const vector = text.slice(0, 16);
const data = text.slice(16); const data = text.slice(16);
return [vector, data]; return [vector, data];
}; };
export const joinVectorData = (vector, data) => {
export const joinVectorData = (vector: ArrayBuffer, data: ArrayBuffer): ArrayBufferLike => {
const output = new Uint8Array(vector.byteLength + data.byteLength); const output = new Uint8Array(vector.byteLength + data.byteLength);
output.set(new Uint8Array(vector), 0); output.set(new Uint8Array(vector), 0);
output.set(new Uint8Array(data), vector.byteLength); output.set(new Uint8Array(data), vector.byteLength);
return output.buffer; return output.buffer;
}; };
export const toString = thing => { export const toString = (thing: string | ByteBuffer | Buffer | ArrayBuffer | Uint8Array): string | ByteBuffer => {
if (typeof thing === 'string') { if (typeof thing === 'string') {
return thing; return thing;
} }
// eslint-disable-next-line new-cap // @ts-ignore
return new ByteBuffer.wrap(thing).toString('binary'); return new ByteBuffer.wrap(thing).toString('binary');
}; };
export const randomPassword = () => `${random(3)}-${random(3)}-${random(3)}`.toLowerCase(); export const randomPassword = (): string => `${random(3)}-${random(3)}-${random(3)}`.toLowerCase();

View File

@ -37,7 +37,7 @@ const getLens = (b64: string) => {
}; };
// base64 is 4/3 + up to two characters of the original data // base64 is 4/3 + up to two characters of the original data
export const byteLength = (b64: string) => { export const byteLength = (b64: string): number => {
const lens = getLens(b64); const lens = getLens(b64);
const validLen = lens[0]; const validLen = lens[0];
const placeHoldersLen = lens[1]; const placeHoldersLen = lens[1];
@ -47,7 +47,7 @@ export const byteLength = (b64: string) => {
const _byteLength = (b64: string, validLen: number, placeHoldersLen: number) => const _byteLength = (b64: string, validLen: number, placeHoldersLen: number) =>
((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen;
export const toByteArray = (b64: string) => { export const toByteArray = (b64: string): any[] | Uint8Array => {
let tmp; let tmp;
const lens = getLens(b64); const lens = getLens(b64);
const validLen = lens[0]; const validLen = lens[0];
@ -106,7 +106,7 @@ const encodeChunk = (uint8: number[] | Uint8Array, start: number, end: number) =
return output.join(''); return output.join('');
}; };
export const fromByteArray = (uint8: number[] | Uint8Array) => { export const fromByteArray = (uint8: number[] | Uint8Array): string => {
let tmp; let tmp;
const len = uint8.length; const len = uint8.length;
const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes

View File

@ -1,14 +0,0 @@
// https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred
export default class Deferred {
constructor() {
const promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
promise.resolve = this.resolve;
promise.reject = this.reject;
return promise;
}
}

41
app/utils/deferred.ts Normal file
View File

@ -0,0 +1,41 @@
export default class Deferred {
[Symbol.toStringTag]: 'Promise';
private promise: Promise<unknown>;
private _resolve: (value?: unknown) => void;
private _reject: (reason?: any) => void;
constructor() {
this._resolve = () => {};
this._reject = () => {};
this.promise = new Promise((resolve, reject) => {
this._resolve = resolve as (value?: unknown) => void;
this._reject = reject;
});
}
public then<TResult1, TResult2>(
onfulfilled?: ((value: unknown) => TResult1 | PromiseLike<TResult1>) | undefined | null,
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
): Promise<TResult1 | TResult2> {
return this.promise.then(onfulfilled, onrejected);
}
public catch<TResult>(
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
): Promise<unknown | TResult> {
return this.promise.catch(onrejected);
}
public finally(onfinally?: (() => void) | null | undefined): Promise<unknown> {
return this.promise.finally(onfinally);
}
public resolve(value?: unknown): void {
this._resolve(value);
}
public reject(reason?: any): void {
this._reject(reason);
}
}

View File

@ -143,6 +143,7 @@
"@rocket.chat/eslint-config": "^0.4.0", "@rocket.chat/eslint-config": "^0.4.0",
"@storybook/addon-storyshots": "5.3.21", "@storybook/addon-storyshots": "5.3.21",
"@storybook/react-native": "5.3.25", "@storybook/react-native": "5.3.25",
"@types/bytebuffer": "^5.0.43",
"@testing-library/react-native": "^9.0.0", "@testing-library/react-native": "^9.0.0",
"@types/ejson": "^2.1.3", "@types/ejson": "^2.1.3",
"@types/jest": "^26.0.24", "@types/jest": "^26.0.24",

View File

@ -4200,6 +4200,14 @@
dependencies: dependencies:
"@babel/types" "^7.3.0" "@babel/types" "^7.3.0"
"@types/bytebuffer@^5.0.43":
version "5.0.43"
resolved "https://registry.yarnpkg.com/@types/bytebuffer/-/bytebuffer-5.0.43.tgz#b5259fca1412106bcee0cabfbf7c104846d06738"
integrity sha512-vQnTYvy4LpSojHjKdmg4nXFI1BAiYPvZ/k3ouczZAQnbDprk1xqxJiFmFHyy8y6MuUq3slz5erNMtn6n87uVKw==
dependencies:
"@types/long" "*"
"@types/node" "*"
"@types/color-name@^1.1.1": "@types/color-name@^1.1.1":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
@ -4341,6 +4349,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.172.tgz#aad774c28e7bfd7a67de25408e03ee5a8c3d028a" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.172.tgz#aad774c28e7bfd7a67de25408e03ee5a8c3d028a"
integrity sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw== integrity sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw==
"@types/long@*":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9"
integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==
"@types/minimatch@*": "@types/minimatch@*":
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"