Skip to content

Commit 88804b2

Browse files
authored
[NEW][Enterprise] Second layer encryption for data transport (alpha) (RocketChat#21692)
1 parent 5b9872e commit 88804b2

24 files changed

Lines changed: 1297 additions & 103 deletions

File tree

.github/workflows/build_and_test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ jobs:
108108
if: steps.cache-nodemodules.outputs.cache-hit != 'true' || steps.cache-cypress.outputs.cache-hit != 'true'
109109
run: |
110110
meteor npm install
111+
cd ./ee/server/services
112+
npm install
113+
cd -
111114
112115
- run: meteor npm run lint
113116

.meteor/packages

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# 'meteor add' and 'meteor remove' will edit this file for you,
44
# but you can also edit it by hand.
55

6+
rocketchat:ddp
67
rocketchat:mongo-config
78

89
accounts-facebook@1.3.2

.meteor/versions

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ reactive-dict@1.3.0
115115
reactive-var@1.0.11
116116
reload@1.3.1
117117
retry@1.1.0
118+
rocketchat:ddp@0.0.1
118119
rocketchat:i18n@0.0.1
119120
rocketchat:livechat@0.0.1
120121
rocketchat:mongo-config@0.0.1

app/api/server/default/info.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,15 @@ API.default.addRoute('info', { authRequired: false }, {
1717
});
1818
},
1919
});
20+
21+
API.default.addRoute('ecdh_proxy/initEncryptedSession', { authRequired: false }, {
22+
post() {
23+
return {
24+
statusCode: 406,
25+
body: {
26+
success: false,
27+
error: 'Not Acceptable',
28+
},
29+
};
30+
},
31+
});

app/utils/client/lib/RestApiClient.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export const APIClient = {
6161
return query;
6262
},
6363

64-
_jqueryCall(method, endpoint, params, body, headers = {}) {
64+
_jqueryCall(method, endpoint, params, body, headers = {}, dataType) {
6565
const query = APIClient._generateQueryFromParams(params);
6666

6767
return new Promise(function _rlRestApiGet(resolve, reject) {
@@ -73,6 +73,7 @@ export const APIClient = {
7373
...APIClient.getCredentials(),
7474
}, headers),
7575
data: JSON.stringify(body),
76+
dataType,
7677
success: function _rlGetSuccess(result) {
7778
resolve(result);
7879
},

client/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import '../ee/client/ecdh';
12
import './polyfills';
23

34
import './lib/meteorCallWrapper';

client/types/meteor.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ declare module 'meteor/meteor' {
2020
// eslint-disable-next-line @typescript-eslint/camelcase
2121
_livedata_data(message: IDDPUpdatedMessage): void;
2222

23+
_stream: {
24+
eventCallbacks: {
25+
message: Array<(data: string) => void>;
26+
};
27+
socket: {
28+
onmessage: (data: { type: string; data: string }) => void;
29+
_didMessage: (data: string) => void;
30+
send: (data: string) => void;
31+
};
32+
_launchConnectionAsync: () => void;
33+
};
34+
2335
onMessage(message: string): void;
2436
}
2537

ee/app/ecdh/ClientSession.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Session } from './Session';
2+
3+
export class ClientSession extends Session {
4+
async init(): Promise<string> {
5+
const sodium = await this.sodium();
6+
7+
const clientKeypair = await sodium.crypto_box_keypair();
8+
this.secretKey = await sodium.crypto_box_secretkey(clientKeypair);
9+
this.publicKey = await sodium.crypto_box_publickey(clientKeypair);
10+
11+
return this.publicKey.toString(this.stringFormatKey);
12+
}
13+
14+
async setServerKey(serverPublic: string): Promise<void> {
15+
const sodium = await this.sodium();
16+
17+
const [decryptKey, encryptKey] = await sodium.crypto_kx_client_session_keys(
18+
this.publicKey,
19+
this.secretKey,
20+
this.publicKeyFromString(serverPublic),
21+
);
22+
23+
this.decryptKey = decryptKey;
24+
this.encryptKey = encryptKey;
25+
}
26+
}

ee/app/ecdh/ServerSession.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Session } from './Session';
2+
3+
export type ProcessString = (text: string[]) => string;
4+
export type ProcessBuffer = (text: Buffer) => Buffer[];
5+
6+
export class ServerSession extends Session {
7+
async init(clientPublic: string): Promise<void> {
8+
const sodium = await this.sodium();
9+
10+
const staticSeed = process.env.STATIC_SEED;
11+
12+
if (!staticSeed?.trim()) {
13+
console.error('STATIC_SEED environment variable is required');
14+
process.exit(1);
15+
}
16+
17+
const serverKeypair = await sodium.crypto_kx_seed_keypair(staticSeed + clientPublic);
18+
this.secretKey = await sodium.crypto_box_secretkey(serverKeypair);
19+
this.publicKey = await sodium.crypto_box_publickey(serverKeypair);
20+
21+
const [decryptKey, encryptKey] = await sodium.crypto_kx_server_session_keys(
22+
this.publicKey,
23+
this.secretKey,
24+
this.publicKeyFromString(clientPublic),
25+
);
26+
27+
this.decryptKey = decryptKey;
28+
this.encryptKey = encryptKey;
29+
}
30+
}

ee/app/ecdh/Session.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { SodiumPlus, X25519PublicKey, X25519SecretKey, CryptographyKey } from 'sodium-plus';
2+
3+
let sodium: SodiumPlus;
4+
5+
export class Session {
6+
// Encoding for the key exchange, no requirements to be small
7+
protected readonly stringFormatKey: BufferEncoding = 'base64';
8+
9+
// Encoding for the transfer of encrypted data, should be smaller as possible
10+
protected readonly stringFormatEncryptedData: BufferEncoding = 'base64';
11+
12+
// Encoding before the encryption to keep unicode chars
13+
protected readonly stringFormatRawData: BufferEncoding = 'base64';
14+
15+
protected decryptKey: CryptographyKey;
16+
17+
protected encryptKey: CryptographyKey;
18+
19+
protected secretKey: X25519SecretKey;
20+
21+
public publicKey: X25519PublicKey;
22+
23+
async sodium(): Promise<SodiumPlus> {
24+
return sodium || SodiumPlus.auto();
25+
}
26+
27+
get publicKeyString(): string {
28+
return this.publicKey.toString(this.stringFormatKey);
29+
}
30+
31+
publicKeyFromString(text: string): X25519PublicKey {
32+
return new X25519PublicKey(Buffer.from(text, this.stringFormatKey));
33+
}
34+
35+
async encryptToBuffer(plaintext: string | Buffer): Promise<Buffer> {
36+
const sodium = await this.sodium();
37+
const nonce = await sodium.randombytes_buf(24);
38+
39+
const ciphertext = await sodium.crypto_secretbox(
40+
Buffer.from(plaintext).toString(this.stringFormatRawData),
41+
nonce,
42+
this.encryptKey,
43+
);
44+
45+
return Buffer.concat([nonce, ciphertext]);
46+
}
47+
48+
async encrypt(plaintext: string | Buffer): Promise<string> {
49+
const buffer = await this.encryptToBuffer(plaintext);
50+
return buffer.toString(this.stringFormatEncryptedData);
51+
}
52+
53+
async decryptToBuffer(data: string | Buffer): Promise<Buffer> {
54+
const sodium = await this.sodium();
55+
const buffer = Buffer.from(Buffer.isBuffer(data) ? data.toString() : data, this.stringFormatEncryptedData);
56+
57+
const decrypted = await sodium.crypto_secretbox_open(
58+
buffer.slice(24),
59+
buffer.slice(0, 24),
60+
this.decryptKey,
61+
);
62+
63+
return Buffer.from(decrypted.toString(), this.stringFormatRawData);
64+
}
65+
66+
async decrypt(data: string | Buffer): Promise<string> {
67+
const buffer = await this.decryptToBuffer(data);
68+
return buffer.toString();
69+
}
70+
}

0 commit comments

Comments
 (0)