diff --git a/.npmignore b/.npmignore index 0520d2e..4b4363a 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,3 @@ src/ !/dist/** +.github/ \ No newline at end of file diff --git a/README.md b/README.md index 4c91d6b..32e4af4 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 -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'; @@ -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, }, }, @@ -75,7 +47,7 @@ server.createServer('127.0.0.1', 8080, { const client = new remoteEnvClient(); client.connect('127.0.0.1', 8080, { auth: { - encryption: { + rsa: { publicKey, privateKey, }, @@ -83,26 +55,33 @@ client.connect('127.0.0.1', 8080, { }); ``` -- 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. diff --git a/package-lock.json b/package-lock.json index 3ff2159..c52cd4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vientorepublic/remote-env", - "version": "0.1.4", + "version": "0.1.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vientorepublic/remote-env", - "version": "0.1.4", + "version": "0.1.7", "license": "MIT", "dependencies": { "dotenv": "^16.4.5" diff --git a/package.json b/package.json index d36157a..7ff135f 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/client.ts b/src/client.ts index 41b8411..a910598 100644 --- a/src/client.ts +++ b/src/client.ts @@ -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; @@ -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.'); } } @@ -89,16 +83,16 @@ export class remoteEnvClient { */ public getEnv(key: string): Promise { 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(':')); @@ -106,7 +100,6 @@ export class remoteEnvClient { 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( @@ -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); } diff --git a/src/server.ts b/src/server.ts index 4e9db60..3838835 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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 }); @@ -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( { @@ -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) { diff --git a/src/types.d.ts b/src/types.d.ts index 3d7279f..5526022 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,7 +1,7 @@ export interface IServerConfig { auth?: { - password?: string; - encryption?: { + key?: Buffer; + rsa?: { publicKey: string; }; }; @@ -9,8 +9,8 @@ export interface IServerConfig { export interface IClientConfig { auth?: { - password?: string; - encryption?: { + key?: Buffer; + rsa?: { publicKey: string; privateKey: string; };