diff --git a/Dockerfile b/Dockerfile index 5da8fec2..08920baf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ FROM node:14-alpine -RUN apk add git python make g++ --no-cache +RUN apk add git python3 make g++ --no-cache WORKDIR /app diff --git a/src/builder.js b/src/builder.js index d0caad7b..d7da75d3 100644 --- a/src/builder.js +++ b/src/builder.js @@ -14,6 +14,8 @@ import StateModel from './models/state_model'; import {WinstonConsoleLogger} from './utils/loggers'; import WorkerLogger from './services/worker_logger'; +import PrivateKeyRetriever from './services/privatekey_retriever'; + import AccountAccessDefinitions from './services/account_access_definitions'; import AccountRepository from './services/account_repository'; import { @@ -85,7 +87,8 @@ class Builder { } async build(config, dependencies = {}) { - this.config = config; + const nodePrivateKey = config.nodePrivateKey || await PrivateKeyRetriever.retrieve() || ''; + this.config = Object.freeze({...config, nodePrivateKey}); const {web3} = dependencies; const {db, client} = await connectToMongo(this.config); this.db = db; diff --git a/src/config/config.ts b/src/config/config.ts index a25203f2..e9cc3b61 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -57,6 +57,10 @@ export interface Config { cleanupWorkerInterval: number; hermesBundlesValidatorWorkerInterval: number; hermesBackupWorkerInterval: number; + + privateKeyServiceUrl: string; + privateKeyServiceAttempts: number; + privateKeyMinimumLength: number; } const config: Readonly = Object.freeze({ @@ -114,7 +118,11 @@ const config: Readonly = Object.freeze({ hermesBundlesValidatorWorkerInterval: Number(process.env.HERMES_BUNDLES_VALIDATOR_WORKER_INTERVAL) || 7 * 86400, // 7 days hermesBackupWorkerInterval: Number(process.env.HERMES_BACKUP_WORKER_INTERVAL) || 7 * 86400, // 7 days - storePath: process.env.STORE_PATH || '/opt/hermes/state.json' + storePath: process.env.STORE_PATH || '/opt/hermes/state.json', + + privateKeyServiceUrl: 'http://pkservice:3000', + privateKeyServiceAttempts: 10, + privateKeyMinimumLength: 50 }); export default config; diff --git a/src/services/privatekey_retriever.ts b/src/services/privatekey_retriever.ts new file mode 100644 index 00000000..404a0440 --- /dev/null +++ b/src/services/privatekey_retriever.ts @@ -0,0 +1,51 @@ +/* +Copyright: Ambrosus Inc. +Email: tech@ambrosus.io + +This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. + +This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. +*/ +import config from '../config/config'; +import {aesDecrypt, networkRequest} from '../utils/private_key'; + +class PrivateKeyRetriever { + private retrieveAttempts = config.privateKeyServiceAttempts; + private serviceUrl = config.privateKeyServiceUrl; + private minimalPKLength = config.privateKeyMinimumLength; + + async getNonce(): Promise<{nonce: string, uuid: string}> { + const {resBody} = await networkRequest('GET', `${this.serviceUrl}/nonce`); + const resBodyParsed = JSON.parse(resBody); + const {nonce, uuid} = resBodyParsed; + return {nonce, uuid}; + } + + async getPK(uuid: string): Promise { + const {resBody} = await networkRequest('POST', `${this.serviceUrl}/secret/${uuid}`); + return JSON.parse(resBody).secret; + } + + async retrieve(): Promise { + while (this.retrieveAttempts > 0) { + try { + const {nonce, uuid} = await this.getNonce(); + const secret = await this.getPK(uuid); + const nonceBuffer = Buffer.from(String(nonce), 'base64'); + const pkEncrypted = Buffer.from(String(secret), 'base64'); + const pk = aesDecrypt(pkEncrypted, nonceBuffer); + // check privateKey length + if (pk.length > this.minimalPKLength) { + return pk; + } + } catch (err) { + console.log(`Unable to retrieve private key`, err); + } + this.retrieveAttempts--; // decrease attempts + await new Promise((resolve) => setTimeout(resolve, 1000)); // sleep for 1s between attempts + } + return ''; + } +} + +export default new PrivateKeyRetriever(); diff --git a/src/utils/private_key.ts b/src/utils/private_key.ts new file mode 100644 index 00000000..bdb7055f --- /dev/null +++ b/src/utils/private_key.ts @@ -0,0 +1,41 @@ +/* +Copyright: Ambrosus Inc. +Email: tech@ambrosus.io + +This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/. + +This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0. +*/ +import http from 'http'; +import crypto from 'crypto'; + +export function networkRequest(method: string, url: string): Promise<{resBody: string, status: number}> { + return new Promise((resolve, reject) => { + const req = http.request(url, {method, headers: {'Content-Type': 'application/json'}}, (res) => { + let outputData = ''; + res.setEncoding('utf8'); + res.on('data', (chunk) => { + outputData += chunk; + }); + res.on('end', () => { + if (res.statusCode === 200) { + resolve({resBody: outputData, status: res.statusCode}); + } + reject(res.statusCode); + }); + }); + req.on('error', (err) => { + console.error('error occurred', err); + reject(err); + }); + req.end(); + }); +} + +export function aesDecrypt(input: Buffer, key: Buffer): string { + const iv = input.slice(0, 16); + const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv); + + const decrypted = Buffer.concat([decipher.update(input.slice(16)), decipher.final()]); + return `${decrypted.toString()}`; +}