Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Commit

Permalink
Merge pull request #29 from vientorepublic/dev
Browse files Browse the repository at this point in the history
add chacha20-poly1305 encryption
  • Loading branch information
vientorepublic authored Jul 7, 2024
2 parents 77d3a98 + 108dcf2 commit 5fe234d
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 105 deletions.
1 change: 1 addition & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
src/
!/dist/**
.github/
59 changes: 19 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ This project aims for minimal dependencies & light weight.

Helps you easily share environment variables in various distributed service structures.

# Warning

This project is currently in development.

**If you expose remote-env to an external network, you must specify an authentication method for security purposes.**

# CI Build Versions

- ES2020 Node.js 20.x
Expand All @@ -30,32 +24,10 @@ This project is currently in development.
npm install @vientorepublic/remote-env
```

## Example usage (Typescript & ESM)

```javascript
import { remoteEnvProvider, remoteEnvClient } from '@vientorepublic/remote-env';

// For CommonJS:
// const { remoteEnvProvider, remoteEnvClient } = require('@vientorepublic/remote-env');

const server = new remoteEnvProvider();
server.createServer('127.0.0.1', 8080);

const client = new remoteEnvClient();
client.connect('127.0.0.1', 8080);

// getEnv(key: string): Promise<string | null>
const value = await client.getEnv('KEY');
console.log(value);

client.close();
server.close();
```

## Protect with rsa public key encryption

> [!NOTE]
> This option is highly recommended as it encrypts your data with the RSA algorithm.
> For security purposes, you must specify an encryption method. The plain text method is no longer supported.
## Example: Protect with rsa public key encryption

```javascript
import { readFileSync } from 'node:fs';
Expand All @@ -66,7 +38,7 @@ const privateKey = readFileSync('private_key.pem', 'utf8');
const server = new remoteEnvProvider();
server.createServer('127.0.0.1', 8080, {
auth: {
encryption: {
rsa: {
publicKey,
},
},
Expand All @@ -75,34 +47,41 @@ server.createServer('127.0.0.1', 8080, {
const client = new remoteEnvClient();
client.connect('127.0.0.1', 8080, {
auth: {
encryption: {
rsa: {
publicKey,
privateKey,
},
},
});
```

- Generate RSA 2048Bit Private Key: `openssl genrsa -out private_key.pem 2048`
- Generate rsa 2048bit private key: `openssl genrsa -out private_key.pem 2048`
- Extract public key from private key: `openssl rsa -in private_key.pem -out public_key.pem -pubout`

## Protect with password authentication

> [!WARNING]
> Password Authentication will be deprecated. This option does not encrypt your data.
## Example: Protect with chacha20-poly1305 encryption

```javascript
import { readFileSync } from 'node:fs';

const key = Buffer.from(readFileSync('secretkey'));

const server = new remoteEnvProvider();
server.createServer('127.0.0.1', 8080, {
auth: {
password: 'my-supersecret-password@!',
key,
},
});

const client = new remoteEnvClient();
client.connect('127.0.0.1', 8080, {
auth: {
password: 'my-supersecret-password@!',
key,
},
});
```

- Generate ChaCha20-Poly1305 32byte key: `openssl rand 32 > secretkey`

# License

This project is released under the MIT License.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vientorepublic/remote-env",
"version": "0.1.7",
"version": "0.1.8",
"description": "Remote environment variable server/client for Node.js",
"main": "./dist/index.js",
"scripts": {
Expand Down
80 changes: 47 additions & 33 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { connect, Socket } from 'net';
import { IClientConfig } from './types';
import { privateDecrypt, constants } from 'crypto';
import { privateDecrypt, constants, createDecipheriv } from 'crypto';

/**
* Remote-env client instance. To connect to a server, call `connect()` and provide server information in the parameter.
*
* Usage: https://github.com/vientorepublic/remote-env?tab=readme-ov-file#example-usage-typescript--esm
* @author Doyeon Kim - https://github.com/vientorepublic
*/
export class remoteEnvClient {
public client: Socket;
private password?: string;
private key?: Buffer;
private publicKey?: string;
private privateKey?: string;

Expand All @@ -26,35 +24,31 @@ export class remoteEnvClient {
public connect(
address: string,
port: number,
config?: IClientConfig,
config: IClientConfig,
callback?: () => any,
): void {
if (!address || !port) {
throw new Error('address, port is required.');
}
if (config.auth) {
if (config.auth.password && config.auth.encryption) {
throw new Error(
'Password and encryption options cannot be used together.',
);
}
// Plain text based password protection
if (config.auth.password) {
console.warn(
'[WARN]',
'Password protection will be deprecated. Specify the RSA Public/Private Key in the `encryption` option.',
);
this.password = config.auth.password;
if (!config.auth) {
throw new Error('Authentication options are not set');
}
if (config.auth.key && config.auth.rsa) {
throw new Error('key and rsa options cannot be used together.');
}
if (config.auth.key) {
if (config.auth.key.length !== 32) {
throw new Error('ChaCha20-Poly1305 must have a key length of 32 bytes');
}
// RSA public key encryption
const option = config.auth.encryption;
if (option) {
if (option.publicKey && option.privateKey) {
this.publicKey = option.publicKey;
this.privateKey = option.privateKey;
} else {
throw new Error('RSA Public/Private Key is missing.');
}
this.key = config.auth.key;
}
const option = config.auth.rsa;
if (option) {
if (option.publicKey && option.privateKey) {
this.publicKey = option.publicKey;
this.privateKey = option.privateKey;
} else {
throw new Error('RSA Public/Private Key is missing.');
}
}

Expand Down Expand Up @@ -89,24 +83,23 @@ export class remoteEnvClient {
*/
public getEnv(key: string): Promise<string | null> {
return new Promise((resolve, reject) => {
// [0]: Password Type (PWD, RSA, PLAIN)
// [1]?: Password
// [0]: Request Type (CHA-POLY, RSA)
// [1]?: RSA Public Key
// [2]: Dotenv Key
const data: string[] = [];
if (this.password) {
data.push('PWD', this.password);
if (this.key) {
data.push('CHA-POLY');
} else if (this.publicKey) {
data.push('RSA', this.publicKey);
} else {
data.push('PLAIN');
throw new Error('Authentication options are not set');
}
data.push(key);
this.client.write(data.join(':'));
this.client.on('error', (err) => reject(err));
this.client.on('data', (e) => {
const data = e.toString().split(':');
if (data[0] === 'ERROR') resolve(null);
else if (data[0] === 'PLAIN') resolve(data[1]);
else if (data[0] === 'RSA') {
const payload = Buffer.from(data[1], 'base64');
const decrypted = privateDecrypt(
Expand All @@ -117,6 +110,27 @@ export class remoteEnvClient {
payload,
).toString();
resolve(decrypted);
} else if (data[0] === 'CHA-POLY') {
const decipher = createDecipheriv(
'chacha20-poly1305',
this.key,
Buffer.from(data[1].substring(0, 24), 'hex'),
{
authTagLength: 16,
},
);
decipher.setAuthTag(Buffer.from(data[1].substring(24, 56), 'hex'));
const decrypted = [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
decipher.update(
Buffer.from(data[1].substring(56), 'hex'),
'binary',
'utf-8',
),
decipher.final('utf-8'),
].join('');
resolve(decrypted);
} else {
resolve(null);
}
Expand Down
65 changes: 40 additions & 25 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { publicEncrypt, constants } from 'node:crypto';
import {
publicEncrypt,
constants,
createCipheriv,
randomBytes,
} from 'node:crypto';
import { createServer, Server } from 'net';
import { IServerConfig } from './types';
import { config } from 'dotenv';

/**
* Remote-env server instance. To open a server, call `createServer()`
*
* Usage: https://github.com/vientorepublic/remote-env?tab=readme-ov-file#example-usage-typescript--esm
* @author Doyeon Kim - https://github.com/vientorepublic
*/
export class remoteEnvProvider {
public server: Server;
public path?: string;
private password?: string;
private key?: Buffer;
constructor(path?: string) {
this.path = path;
config({ path: this.path ?? null });
Expand All @@ -24,34 +27,42 @@ export class remoteEnvProvider {
console.log(`IP Address: ${address}, Port: ${port}`);

socket.on('data', (e) => {
// [0]: Password Type (PWD, RSA, PLAIN)
// [1]?: Password
// [0]: Response Type (CHA-POLY, RSA)
// [1]?: RSA Public Key
// [2]: Dotenv Key
const data = e.toString().split(':');
const value: string[] = [];

if (data.length <= 1 || data.length > 3) return;

if (data[0] === 'PLAIN') {
// ChaCha20-Poly1305 Encryption
if (data[0] === 'CHA-POLY') {
const env = this.getEnv(data[1]);
if (!env) {
socket.write('ERROR');
return;
}
value.push('PLAIN', env);
const iv = randomBytes(12);
const cipher = createCipheriv('chacha20-poly1305', this.key, iv, {
authTagLength: 16,
});
const encrypted = Buffer.concat([
cipher.update(env, 'utf8'),
cipher.final(),
]);
const tag = cipher.getAuthTag();
const final = Buffer.concat([iv, tag, encrypted]).toString('hex');
value.push('CHA-POLY', final);
socket.write(value.join(':'));
}

const env = this.getEnv(data[2]);
if (!env) {
socket.write('ERROR');
return;
}
if (data[0] === 'PWD' && this.password === data[1]) {
value.push('PLAIN', env);
socket.write(value.join(':'));
}
// RSA Public Key Encryption
if (data[0] === 'RSA' && data.length === 3) {
const env = this.getEnv(data[2]);
if (!env) {
socket.write('ERROR');
return;
}
const valueBuf = Buffer.from(env, 'utf-8');
const encrypted = publicEncrypt(
{
Expand Down Expand Up @@ -90,19 +101,23 @@ export class remoteEnvProvider {
public createServer(
address: string,
port: number,
config?: IServerConfig,
config: IServerConfig,
callback?: () => any,
): void {
if (!address || !port) {
throw new Error('address, port is required.');
}
if (config && config.auth) {
this.password = config.auth.password;
} else {
console.warn(
'[WARN]',
'Authentication method is not defined. Use it caution.',
);
if (!config.auth) {
throw new Error('Authentication options are not set');
}
if (config.auth.key && config.auth.rsa) {
throw new Error('key and rsa options cannot be used together.');
}
if (config.auth.key) {
if (config.auth.key.length !== 32) {
throw new Error('ChaCha20-Poly1305 must have a key length of 32 bytes');
}
this.key = config.auth.key;
}
this.server.listen(port, address, () => {
if (callback) {
Expand Down
Loading

0 comments on commit 5fe234d

Please sign in to comment.