Skip to content

Commit

Permalink
feat: use random aes key to simplify implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
homura committed Aug 1, 2024
1 parent 139a71c commit 6d1dcc9
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 133 deletions.
113 changes: 73 additions & 40 deletions packages/neuron-wallet/src/services/log-encryption.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,101 @@
import { randomBytes, createCipheriv, createHash, publicEncrypt, createPublicKey } from 'node:crypto'
import { machineIdSync } from '../utils/machineid'
import { randomBytes, createCipheriv, publicEncrypt, privateDecrypt, createDecipheriv } from 'node:crypto'
import logger from '../utils/logger'

export default class LogEncryption {
private static instance: LogEncryption
private static algorithm = 'aes-256-cbc'
export const DEFAULT_ALGORITHM = 'aes-256-cbc'

export default class LogEncryption {
/**
* A determinable AES key for encrypting log message
* We use CBC mode here to prevent pattern-discerned
* > a one-bit change in a plaintext or initialization vector (IV) affects all following ciphertext blocks
* @private
*/
private static localLogKey: Uint8Array
private readonly algorithm = DEFAULT_ALGORITHM

private readonly adminPublicKey: string

/**
* The RSA encrypted {@link localLogKey} in base64 format
* @private
*
* @param adminPublicKey a PEM-formatted RSA public key
*/
private static encryptedLogKey: string
constructor(adminPublicKey: string) {
this.adminPublicKey = adminPublicKey
}

private constructor() {}
/**
* Encrypt a message
* @param message
*/
encrypt(message: unknown): string {
if (message == null) return ''
if (!this.adminPublicKey) return 'The admin public key does not exist, skip encrypting message'

const localLogKey = randomBytes(32)
const iv = randomBytes(16)

const cipher = createCipheriv(this.algorithm, localLogKey, iv)
const serializedMessage = typeof message === 'string' ? message : JSON.stringify(message, JSONSerializer)

const encryptedLogKey = publicEncrypt(this.adminPublicKey, localLogKey).toString('base64')
const encryptedMsg = Buffer.concat([cipher.update(serializedMessage), cipher.final()]).toString('base64')

return `[key:${encryptedLogKey}] [iv:${iv.toString('base64')}] ${encryptedMsg}`
}

private static instance: LogEncryption

static getInstance(): LogEncryption {
if (!LogEncryption.instance) {
const adminPublicKey = process.env.LOG_ENCRYPTION_PUBLIC_KEY
const adminPublicKey = process.env.LOG_ENCRYPTION_PUBLIC_KEY ?? ''
if (!adminPublicKey) {
throw new Error('LOG_ENCRYPTION_PUBLIC_KEY is required to create LogEncryption instance')
logger.warn('LOG_ENCRYPTION_PUBLIC_KEY is required to create LogEncryption instance')
}

const localLogKey = Buffer.from(createHash('sha256').update(machineIdSync(false)).digest())
LogEncryption.localLogKey = localLogKey
LogEncryption.encryptedLogKey = publicEncrypt(createPublicKey(adminPublicKey!), localLogKey).toString('base64')

LogEncryption.instance = new LogEncryption()
logger.info('LogEncryption key', LogEncryption.encryptedLogKey)
LogEncryption.instance = new LogEncryption(adminPublicKey)
}

return LogEncryption.instance
}
}

/**
* Encrypt a message
* @param message
* @param iv
*/
encrypt(message: unknown, iv?: Uint8Array): string {
if (message == null) return ''
let prependIV = false
if (!iv) {
prependIV = true
iv = randomBytes(16)
}
export class LogDecryption {
private readonly adminPrivateKey: string

const cipher = createCipheriv(LogEncryption.algorithm, LogEncryption.localLogKey, iv)
const serializedMessage = typeof message === 'string' ? message : JSON.stringify(message, JSONSerializer)
const encryptedMsg = Buffer.concat([cipher.update(serializedMessage), cipher.final()]).toString('base64')
constructor(adminPrivateKey: string) {
this.adminPrivateKey = adminPrivateKey
}

if (prependIV) {
return `[iv:${Buffer.from(iv).toString('base64')}] ${encryptedMsg}`
}
decrypt(encryptedMessage: string): string {
const { iv, key, content } = parseMessage(encryptedMessage)

const decipher = createDecipheriv(
DEFAULT_ALGORITHM,
privateDecrypt(this.adminPrivateKey, Buffer.from(key, 'base64')),
Buffer.from(iv, 'base64')
)

return Buffer.concat([decipher.update(content, 'base64'), decipher.final()]).toString('utf-8')
}
}

return encryptedMsg
function parseMessage(message: string) {
const result: Record<string, string> = {}
const regex = /\[([^\]:]+):([^\]]+)]/g
let match
let lastIndex = 0

while ((match = regex.exec(message)) !== null) {
const [, key, value] = match
result[key.trim()] = value.trim()
lastIndex = regex.lastIndex
}

// Extract remaining content after the last bracket
const remainingContent = message.slice(lastIndex).trim()
if (remainingContent) {
result.content = remainingContent
}

return result
}

const JSONSerializer = (_key: string, value: any) => {
Expand All @@ -69,5 +104,3 @@ const JSONSerializer = (_key: string, value: any) => {
}
return value
}

logger.info(LogEncryption.getInstance().encrypt('hello world'))
68 changes: 0 additions & 68 deletions packages/neuron-wallet/src/utils/machineid.ts

This file was deleted.

19 changes: 19 additions & 0 deletions packages/neuron-wallet/tests/services/log-encryption.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import LogEncryption, { LogDecryption } from '../../src/services/log-encryption'
import { generateKeyPairSync } from 'node:crypto'

describe('Test LogEncryption', () => {
it('encrypted message should be able to decrypt', () => {
const { publicKey: adminPublicKey, privateKey: adminPrivateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
})

const encryption = new LogEncryption(adminPublicKey.export({ format: 'pem', type: 'pkcs1' }).toString())
const decryption = new LogDecryption(adminPrivateKey.export({ format: 'pem', type: 'pkcs1' }).toString())

const message = 'hello'
const encryptedMessage = encryption.encrypt(message)
const decryptedMessage = decryption.decrypt(encryptedMessage)

expect(decryptedMessage).toBe(message)
})
})
21 changes: 13 additions & 8 deletions scripts/admin/decrypt-log/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
## Log Decryption
## Log Encryption & Decryption

## Encryption

An environment variable `LOG_ENCRYPTION_PUBLIC_KEY` must be set to enable log encryption when releasing Neuron. If the variable is not set, a placeholder message will be left in the log file.

## Decryption

An encrypted log looks the following block

```
[2024-07-31T11:15:06.811Z] [info] LogEncryption key LFWL6pdZrEgMwhuyL6ViGYKy/ZSilpeksZW3gpvGEqTg+4tzKk0Sjep8/Emzy1t5tyGEI6fs0BJVVkgmiAVCozotDQJVmUUtAZkdNok7Y9rnZxIaTsyLciXUyWXyqckW7WJriNKmpzxUSj9PBH+U69irdeqmwNTysJ3Qv4y7wSdSG4mZ9/WOOH3S4S27NmJ9ZeO1PNaXZWMz2i7baA0erYAkl9zyPtgg3QSlYrSqk91mkOGgCrqJebC6d63+516wIskNk/NWPt0GA+KXIlDNketIFgu6SOBopLorhXi69mX/7q5XU/Cmv8+4nYrdnhqd+hReJg3MIK8tJuZvxNXy6w==
[2024-07-31T11:15:06.811Z] [info] [iv:OJ9oGf7yL3K1jWYx7ABWHg==] /b4lCkOpL/kt7DHoyaDlOg==
[2024-07-31T11:15:06.811Z] [info] [key:sWFKSuG+GzC52QlqDUcLhCvWFevSR8JjcvlIwCmB6U750UbO59zQZlQFyIUCBMH2Vamdr/ScZaF00wObzyi2BERMkKCQ9XY1ELcQSvCaAjUy4251B4MIyrnYPu4Bf+bca5U/906ko37G6dZMDNCcm2J5pm3+0TvqwXFA+BDXsAeZ7YWXpNha+WTMbQJiGj+ltbjIlodXhtqGWBhkLHgeZtfpM/OQDclOUfSP4SDva1LUvjdkQjnmUB+5dLumEAQpm7u7mroXl5eMTpVhyVtULm+QkQ4aA/D9Q/Y1dGUxl8jU2zcgL1h8Uhrb9FMpCaLyu13gGZr42HlFVU4j/VzD/g==] [iv:/jDhuN6b/qEetyHnU2WPDw==] 0+B+gimzrZgbxfxBTtznyA==
```

To decrypt the message

1. Create an `.env` at this folder(`/decrypt-log`)
2. Config `CLIENT_ENCRYPTED_KEY`: Search the keyword `LogEncryption key` in the log file, copy the value to `.env`. This is an encrypted AES key used for decrypting the encrypted log message
3. Config `ADMIN_PRIVATE_KEY`: copy the PEM format RSA private key to the `.env` file
4. Config `LOG_MESSAGE`: copy the encrypted, base64 encoded log message to the `.env` file
5. Run `node --experimental-strip-types --env-file .env run.ts` or `bun run.ts`
```sh
export LOG_MESSAGE="<log message similar the above mentioned>"
export ADMIN_PRIVATE_KEY="<pem formatted rsa private key>"
bun run.ts
```
24 changes: 7 additions & 17 deletions scripts/admin/decrypt-log/run.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import { createDecipheriv, privateDecrypt } from "node:crypto";
import { LogDecryption } from "../../../packages/neuron-wallet/src/services/log-encryption";

const CLIENT_ENCRYPTED_KEY = process.env.CLIENT_ENCRYPTED_KEY!;
const ADMIN_PRIVATE_KEY = process.env.ADMIN_PRIVATE_KEY!;
const ADMIN_PRIVATE_KEY = process.env
.ADMIN_PRIVATE_KEY!.split(/\r?\n/)
.map((line) => line.trim())
.join("\n");
const LOG_MESSAGE = process.env.LOG_MESSAGE!;

const ALGORITHM = "aes-256-cbc";

let [_original, _date, _level, iv, message] = LOG_MESSAGE.match(/(\[.+])\s*(\[.+])\s*(\[iv:.+])\s*(.+)/)!;

// recovery the client log key
const encryptedClientKey = Buffer.from(CLIENT_ENCRYPTED_KEY, "base64");
const clientKey = privateDecrypt(ADMIN_PRIVATE_KEY, encryptedClientKey);

const decodedIV = Buffer.from(iv.substring("[iv:".length, iv.length - 1), "base64");
const decipher = createDecipheriv(ALGORITHM, clientKey, decodedIV);

const decryptedLog = Buffer.concat([decipher.update(message, "base64"), decipher.final()]);

console.log(decryptedLog.toString("utf-8"));
const decryption = new LogDecryption(ADMIN_PRIVATE_KEY);
console.log(decryption.decrypt(LOG_MESSAGE));

0 comments on commit 6d1dcc9

Please sign in to comment.