Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ jobs:
if: steps.cache-nodemodules.outputs.cache-hit != 'true' || steps.cache-cypress.outputs.cache-hit != 'true'
run: |
meteor npm install
cd ./ee/server/services
npm install
cd -

- run: meteor npm run lint

Expand Down
1 change: 1 addition & 0 deletions .meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# 'meteor add' and 'meteor remove' will edit this file for you,
# but you can also edit it by hand.

rocketchat:ddp
rocketchat:mongo-config

[email protected]
Expand Down
1 change: 1 addition & 0 deletions .meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ [email protected]
[email protected]
[email protected]
[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
rocketchat:[email protected]
Expand Down
12 changes: 12 additions & 0 deletions app/api/server/default/info.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,15 @@ API.default.addRoute('info', { authRequired: false }, {
});
},
});

API.default.addRoute('ecdh_proxy/initEncryptedSession', { authRequired: false }, {
post() {
return {
statusCode: 406,
body: {
success: false,
error: 'Not Acceptable',
},
};
},
});
3 changes: 2 additions & 1 deletion app/utils/client/lib/RestApiClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const APIClient = {
return query;
},

_jqueryCall(method, endpoint, params, body, headers = {}) {
_jqueryCall(method, endpoint, params, body, headers = {}, dataType) {
const query = APIClient._generateQueryFromParams(params);

return new Promise(function _rlRestApiGet(resolve, reject) {
Expand All @@ -73,6 +73,7 @@ export const APIClient = {
...APIClient.getCredentials(),
}, headers),
data: JSON.stringify(body),
dataType,
success: function _rlGetSuccess(result) {
resolve(result);
},
Expand Down
1 change: 1 addition & 0 deletions client/main.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import '../ee/client/ecdh';
import './polyfills';

import './lib/meteorCallWrapper';
Expand Down
12 changes: 12 additions & 0 deletions client/types/meteor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ declare module 'meteor/meteor' {
// eslint-disable-next-line @typescript-eslint/camelcase
_livedata_data(message: IDDPUpdatedMessage): void;

_stream: {
eventCallbacks: {
message: Array<(data: string) => void>;
};
socket: {
onmessage: (data: { type: string; data: string }) => void;
_didMessage: (data: string) => void;
send: (data: string) => void;
};
_launchConnectionAsync: () => void;
};

onMessage(message: string): void;
}

Expand Down
26 changes: 26 additions & 0 deletions ee/app/ecdh/ClientSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Session } from './Session';

export class ClientSession extends Session {
async init(): Promise<string> {
const sodium = await this.sodium();

const clientKeypair = await sodium.crypto_box_keypair();
this.secretKey = await sodium.crypto_box_secretkey(clientKeypair);
this.publicKey = await sodium.crypto_box_publickey(clientKeypair);

return this.publicKey.toString(this.stringFormatKey);
}

async setServerKey(serverPublic: string): Promise<void> {
const sodium = await this.sodium();

const [decryptKey, encryptKey] = await sodium.crypto_kx_client_session_keys(
this.publicKey,
this.secretKey,
this.publicKeyFromString(serverPublic),
);

this.decryptKey = decryptKey;
this.encryptKey = encryptKey;
}
}
30 changes: 30 additions & 0 deletions ee/app/ecdh/ServerSession.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Session } from './Session';

export type ProcessString = (text: string[]) => string;
export type ProcessBuffer = (text: Buffer) => Buffer[];

export class ServerSession extends Session {
async init(clientPublic: string): Promise<void> {
const sodium = await this.sodium();

const staticSeed = process.env.STATIC_SEED;

if (!staticSeed?.trim()) {
console.error('STATIC_SEED environment variable is required');
process.exit(1);
}

const serverKeypair = await sodium.crypto_kx_seed_keypair(staticSeed + clientPublic);
this.secretKey = await sodium.crypto_box_secretkey(serverKeypair);
this.publicKey = await sodium.crypto_box_publickey(serverKeypair);

const [decryptKey, encryptKey] = await sodium.crypto_kx_server_session_keys(
this.publicKey,
this.secretKey,
this.publicKeyFromString(clientPublic),
);

this.decryptKey = decryptKey;
this.encryptKey = encryptKey;
}
}
70 changes: 70 additions & 0 deletions ee/app/ecdh/Session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { SodiumPlus, X25519PublicKey, X25519SecretKey, CryptographyKey } from 'sodium-plus';

let sodium: SodiumPlus;

export class Session {
// Encoding for the key exchange, no requirements to be small
protected readonly stringFormatKey: BufferEncoding = 'base64';

// Encoding for the transfer of encrypted data, should be smaller as possible
protected readonly stringFormatEncryptedData: BufferEncoding = 'base64';

// Encoding before the encryption to keep unicode chars
protected readonly stringFormatRawData: BufferEncoding = 'base64';

protected decryptKey: CryptographyKey;

protected encryptKey: CryptographyKey;

protected secretKey: X25519SecretKey;

public publicKey: X25519PublicKey;

async sodium(): Promise<SodiumPlus> {
return sodium || SodiumPlus.auto();
}

get publicKeyString(): string {
return this.publicKey.toString(this.stringFormatKey);
}

publicKeyFromString(text: string): X25519PublicKey {
return new X25519PublicKey(Buffer.from(text, this.stringFormatKey));
}

async encryptToBuffer(plaintext: string | Buffer): Promise<Buffer> {
const sodium = await this.sodium();
const nonce = await sodium.randombytes_buf(24);

const ciphertext = await sodium.crypto_secretbox(
Buffer.from(plaintext).toString(this.stringFormatRawData),
nonce,
this.encryptKey,
);

return Buffer.concat([nonce, ciphertext]);
}

async encrypt(plaintext: string | Buffer): Promise<string> {
const buffer = await this.encryptToBuffer(plaintext);
return buffer.toString(this.stringFormatEncryptedData);
}

async decryptToBuffer(data: string | Buffer): Promise<Buffer> {
const sodium = await this.sodium();
const buffer = Buffer.from(Buffer.isBuffer(data) ? data.toString() : data, this.stringFormatEncryptedData);

const decrypted = await sodium.crypto_secretbox_open(
buffer.slice(24),
buffer.slice(0, 24),
this.decryptKey,
);

return Buffer.from(decrypted.toString(), this.stringFormatRawData);
}

async decrypt(data: string | Buffer): Promise<string> {
const buffer = await this.decryptToBuffer(data);
return buffer.toString();
}
}
72 changes: 72 additions & 0 deletions ee/client/ecdh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Meteor } from 'meteor/meteor';

import { APIClient } from '../../app/utils/client';
import { ClientSession } from '../app/ecdh/ClientSession';

let resolveSession: (value: ClientSession | void) => void;
const sessionPromise = new Promise<ClientSession | void>((resolve) => {
resolveSession = resolve;
});

function init(session: ClientSession): void {
Meteor.connection._stream._launchConnectionAsync();

const _didMessage = Meteor.connection._stream.socket._didMessage.bind(
Meteor.connection._stream.socket,
);
const send = Meteor.connection._stream.socket.send.bind(Meteor.connection._stream.socket);

Meteor.connection._stream.socket._didMessage = async (data): Promise<void> => {
const decryptedData = await session.decrypt(data);
decryptedData.split('\n').forEach((d) => _didMessage(d));
};

Meteor.connection._stream.socket.send = async (data): Promise<void> => {
send(await session.encrypt(data));
};
}

async function initEncryptedSession(): Promise<void> {
const session = new ClientSession();
const clientPublicKey = await session.init();

try {
const response = await fetch('/api/ecdh_proxy/initEncryptedSession', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ clientPublicKey }),
});

if (response.status !== 200) {
resolveSession();
return Meteor.connection._stream._launchConnectionAsync();
}

await session.setServerKey(await response.text());
resolveSession(session);
init(session);
} catch (e) {
console.log(e);
resolveSession();
Meteor.connection._stream._launchConnectionAsync();
}
}

initEncryptedSession();

const _jqueryCall = APIClient._jqueryCall.bind(APIClient);

APIClient._jqueryCall = async (method, endpoint, params, body, headers = {}): Promise<any> => {
const session = await sessionPromise;

if (!session) {
return _jqueryCall(method, endpoint, params, body, headers);
}

const result = await _jqueryCall(method, endpoint, params, body, headers, 'text');
const decrypted = await session.decrypt(result);
const parsed = JSON.parse(decrypted);
return parsed;
};
6 changes: 4 additions & 2 deletions ee/server/services/.config/traefik/servers/localhost.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ http:
routers:
router2:
rule: Host(`localhost`) && PathPrefix(`/sockjs/`, `/websocket/`)
service: ddp-streamer-service@docker
# service: ddp-streamer-service@docker
service: ecdh-proxy-socket-service@docker
priority: 2
entryPoints:
- web
router1:
rule: Host(`localhost`)
service: service1
service: ecdh-proxy-http-service@docker
# service: service1
priority: 1
entryPoints:
- web
Expand Down
4 changes: 2 additions & 2 deletions ee/server/services/ddp-streamer/configureServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const loginServiceConfigurationCollection = 'meteor_accounts_loginServiceConfigu
const loginServiceConfigurationPublication = 'meteor.loginServiceConfiguration';
const loginServices = new Map<string, any>();

MeteorService.getLoginServiceConfiguration().then((records) => records.forEach((record) => loginServices.set(record._id, record)));
MeteorService.getLoginServiceConfiguration().then((records = []) => records.forEach((record) => loginServices.set(record._id, record)));

server.publish(loginServiceConfigurationPublication, async function() {
loginServices.forEach((record) => this.added(loginServiceConfigurationCollection, record._id, record));
Expand Down Expand Up @@ -43,7 +43,7 @@ server.publish(loginServiceConfigurationPublication, async function() {

const autoUpdateRecords = new Map<string, AutoUpdateRecord>();

MeteorService.getLastAutoUpdateClientVersions().then((records) => {
MeteorService.getLastAutoUpdateClientVersions().then((records = []) => {
records.forEach((record) => autoUpdateRecords.set(record._id, record));
});

Expand Down
39 changes: 39 additions & 0 deletions ee/server/services/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,45 @@ services:
traefik.http.services.ddp-streamer-service.loadbalancer.server.port: 3000
traefik.http.routers.ddp-streamer-service.service: ddp-streamer-service

ecdh-proxy-socket-service:
container_name: ecdh-proxy-socket-service
build:
context: .
args:
SERVICE: ecdh-proxy
image: rocketchat/ecdh-proxy-service:latest
env_file: .config/services/service.env
environment:
PROXY_HOST: ddp-streamer-service
STATIC_SEED: my seed
depends_on:
- nats
- traefik
- ddp-streamer-service
labels:
traefik.enable: true
traefik.http.services.ecdh-proxy-socket-service.loadbalancer.server.port: 3000
traefik.http.routers.ecdh-proxy-socket-service.service: ecdh-proxy-socket-service

ecdh-proxy-http-service:
container_name: ecdh-proxy-http-service
build:
context: .
args:
SERVICE: ecdh-proxy
image: rocketchat/ecdh-proxy-service:latest
env_file: .config/services/service.env
environment:
PROXY_HOST: host.docker.internal
STATIC_SEED: my seed
depends_on:
- nats
- traefik
labels:
traefik.enable: true
traefik.http.services.ecdh-proxy-http-service.loadbalancer.server.port: 3000
traefik.http.routers.ecdh-proxy-http-service.service: ecdh-proxy-http-service

stream-hub-service:
container_name: stream-hub-service
build:
Expand Down
7 changes: 7 additions & 0 deletions ee/server/services/ecdh-proxy/ECDHProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ServiceClass } from '../../../../server/sdk/types/ServiceClass';

import './lib/server';

export class ECDHProxy extends ServiceClass {
protected name = 'ecdh-proxy';
}
12 changes: 12 additions & 0 deletions ee/server/services/ecdh-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# ECDH Proxy (alpha)

This services aims to be in front of the HTTP and Webscoket services and provide a second layer of encryption based on [ECDH](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman) algorithm.

## Configuration

All the configuration for this service is done via environment variables:

- **STATIC_SEED**: The static seed to compose the encryption. **Required**
- **PORT**: The port this service will expose the HTTP server. Default: `4000`
- **PROXY_HOST**: The host this service will proxy the requests to after decoding. Default `localhost`
- **PROXY_PORT**: The port this service will proxy the requests to after decoding. Default `3000`
Loading