Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: log error when ledger signing failed #3212

Merged
merged 13 commits into from
Aug 2, 2024
7 changes: 7 additions & 0 deletions packages/neuron-wallet/.env
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,10 @@ MULTISIG_CODE_HASH=0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc963
CKB_NODE_ASSUME_VALID_TARGET='0x6dd077b407d019a0bce0cbad8c34e69a524ae4b2599b9feda2c7491f3559d32c'
CKB_NODE_ASSUME_VALID_TARGET_BLOCK_NUMBER=13007704
CKB_NODE_DATA_SIZE=56

# openssl rsa -in key.pem -outform PEM -pubout -out public.pem
# LOG_ENCRYPTION_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
# 256-bit RSA KEY IN PEM FORMAT
# -----END PUBLIC KEY-----"
# TODO
LOG_ENCRYPTION_PUBLIC_KEY="-----BEGIN RSA PUBLIC KEY-----\n base64 RSA public key here \n-----END RSA PUBLIC KEY-----"
Keith-CY marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion packages/neuron-wallet/src/services/hardware/hardware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export abstract class Hardware {
i => witnessesArgs[0].lockArgs.slice(0, 42) === Multisig.hash([i.blake160])
)!.blake160
const serializedMultiSign: string = Multisig.serialize([blake160])
const witnesses = await TransactionSender.signSingleMultiSignScript(
const witnesses = TransactionSender.signSingleMultiSignScript(
path,
serializedWitnesses,
txHash,
Expand Down
31 changes: 25 additions & 6 deletions packages/neuron-wallet/src/services/hardware/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { hd } from '@ckb-lumos/lumos'
import logger from '../../utils/logger'
import NetworksService from '../../services/networks'
import { generateRPC } from '../../utils/ckb-rpc'
import LogEncryption from '../log-encryption'

const UNCOMPRESSED_KEY_LENGTH = 130
const compressPublicKey = (key: string) => {
Expand Down Expand Up @@ -78,12 +79,30 @@ export default class Ledger extends Hardware {
context = txs.map(i => rpc.paramsFormatter.toRawTransaction(i.transaction))
}

const signature = await this.ledgerCKB!.signTransaction(
path === hd.AccountExtendedPublicKey.pathForReceiving(0) ? this.defaultPath : path,
rawTx,
witnesses,
context,
this.defaultPath
const hdPath = path === hd.AccountExtendedPublicKey.pathForReceiving(0) ? this.defaultPath : path
const signature = await this.ledgerCKB!.signTransaction(hdPath, rawTx, witnesses, context, this.defaultPath).catch(
error => {
const errorMessage = error instanceof Error ? error.message : String(error)
const encryption = LogEncryption.getInstance()
logger.error(
encryption.encrypt(
JSON.stringify([
'Ledger: failed to sign the transaction ',
errorMessage,
' HD path:',
hdPath,
' raw transaction:',
JSON.stringify(rawTx),
' witnesses:',
JSON.stringify(witnesses),
' context:',
JSON.stringify(context),
])
)
)

return Promise.reject(error)
}
)

return signature
Expand Down
73 changes: 73 additions & 0 deletions packages/neuron-wallet/src/services/log-encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { randomBytes, createCipheriv, createHash, publicEncrypt, createPublicKey } from 'node:crypto'
import { machineIdSync } from '../utils/machineid'
import logger from '../utils/logger'

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

/**
* A determinable AES key for encrypting log message
* @private
*/
private static localLogKey: Uint8Array

/**
* The RSA encrypted {@link localLogKey} in base64 format
* @private
*/
private static encryptedLogKey: string

private constructor() {}

static getInstance(): LogEncryption {
if (!LogEncryption.instance) {
const adminPublicKey = process.env.LOG_ENCRYPTION_PUBLIC_KEY
if (!adminPublicKey) {
throw new Error('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)
}

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)
}

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')

if (prependIV) {
return `[iv:${Buffer.from(iv).toString('base64')}] ${encryptedMsg}`
}

return encryptedMsg
}
}

const JSONSerializer = (_key: string, value: any) => {
if (typeof value === 'bigint') {
return String(value) + 'n'
}
return value
}

logger.info(LogEncryption.getInstance().encrypt('hello world'))
yanguoyu marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion packages/neuron-wallet/src/services/transaction-sender.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ export default class TransactionSender {
return tx
}

public static async signSingleMultiSignScript(
public static signSingleMultiSignScript(
privateKeyOrPath: string,
witnesses: (string | WitnessArgs)[],
txHash: string,
Expand Down
68 changes: 68 additions & 0 deletions packages/neuron-wallet/src/utils/machineid.ts
yanguoyu marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// https://github.com/automation-stack/node-machine-id/blob/f580f9f20668582e9087d92cea2511c972f2e6aa/index.js
import { execSync } from 'child_process'
import { createHash } from 'crypto'

const { platform } = process
const win32RegBinPath: Record<string, string> = {
native: '%windir%\\System32',
mixed: '%windir%\\sysnative\\cmd.exe /c %windir%\\System32',
}
const guid: Record<string, string> = {
darwin: 'ioreg -rd1 -c IOPlatformExpertDevice',
win32:
`${win32RegBinPath[isWindowsProcessMixedOrNativeArchitecture()]}\\REG.exe ` +
'QUERY HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography ' +
'/v MachineGuid',
linux: '( cat /var/lib/dbus/machine-id /etc/machine-id 2> /dev/null || hostname ) | head -n 1 || :',
freebsd: 'kenv -q smbios.system.uuid || sysctl -n kern.hostuuid',
}

function isWindowsProcessMixedOrNativeArchitecture(): string {
// detect if the node binary is the same arch as the Windows OS.
// or if this is 32 bit node on 64 bit windows.
if (process.platform !== 'win32') {
return ''
}
if (process.arch === 'ia32' && Object.prototype.hasOwnProperty.call(process.env, 'PROCESSOR_ARCHITEW6432')) {
return 'mixed'
}
return 'native'
}

function hash(guid: string): string {
return createHash('sha256').update(guid).digest('hex')
}

function expose(result: string): string {
switch (platform) {
case 'darwin':
return result
.split('IOPlatformUUID')[1]
.split('\n')[0]
.replace(/=|\s+|"/gi, '')
.toLowerCase()
case 'win32':
return result
.toString()
.split('REG_SZ')[1]
.replace(/\r+|\n+|\s+/gi, '')
.toLowerCase()
case 'linux':
return result
.toString()
.replace(/\r+|\n+|\s+/gi, '')
.toLowerCase()
case 'freebsd':
return result
.toString()
.replace(/\r+|\n+|\s+/gi, '')
.toLowerCase()
default:
throw new Error(`Unsupported platform: ${process.platform}`)
}
}

export function machineIdSync(original: boolean): string {
let id: string = expose(execSync(guid[platform]).toString())
return original ? id : hash(id)
}
16 changes: 16 additions & 0 deletions scripts/admin/decrypt-log/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
## Log 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==
```

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`
13 changes: 13 additions & 0 deletions scripts/admin/decrypt-log/example.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Search the "LogEncryption key" in the log file
# example log: [2024-07-31T11:15:06.811Z] [info] LogEncryption key LFWL6pdZrEgMwhuyL6ViGYKy/ZSilpeksZW3gpvGEqTg+4tzKk0Sjep8/Emzy1t5tyGEI6fs0BJVVkgmiAVCozotDQJVmUUtAZkdNok7Y9rnZxIaTsyLciXUyWXyqckW7WJriNKmpzxUSj9PBH+U69irdeqmwNTysJ3Qv4y7wSdSG4mZ9/WOOH3S4S27NmJ9ZeO1PNaXZWMz2i7baA0erYAkl9zyPtgg3QSlYrSqk91mkOGgCrqJebC6d63+516wIskNk/NWPt0GA+KXIlDNketIFgu6SOBopLorhXi69mX/7q5XU/Cmv8+4nYrdnhqd+hReJg3MIK8tJuZvxNXy6w==
# example: LFWL6pdZrEgMwhuyL6ViGYKy/ZSilpeksZW3gpvGEqTg+4tzKk0Sjep8/Emzy1t5tyGEI6fs0BJVVkgmiAVCozotDQJVmUUtAZkdNok7Y9rnZxIaTsyLciXUyWXyqckW7WJriNKmpzxUSj9PBH+U69irdeqmwNTysJ3Qv4y7wSdSG4mZ9/WOOH3S4S27NmJ9ZeO1PNaXZWMz2i7baA0erYAkl9zyPtgg3QSlYrSqk91mkOGgCrqJebC6d63+516wIskNk/NWPt0GA+KXIlDNketIFgu6SOBopLorhXi69mX/7q5XU/Cmv8+4nYrdnhqd+hReJg3MIK8tJuZvxNXy6w==
CLIENT_ENCRYPTED_KEY=

# PEM formated RSA private key

Check warning on line 6 in scripts/admin/decrypt-log/example.env

View workflow job for this annotation

GitHub Actions / Check spell

"formated" should be "formatted".

Check warning on line 6 in scripts/admin/decrypt-log/example.env

View workflow job for this annotation

GitHub Actions / Check spell

"formated" should be "formatted".
# openssl genrsa -out key.pem 2048
# example: -----BEGIN PRIVATE KEY----- base64 content here ----END PRIVATE KEY-----
ADMIN_PRIVATE_KEY=

# The log message
# example: [2024-07-31T11:15:06.811Z] [info] [iv:OJ9oGf7yL3K1jWYx7ABWHg==] /b4lCkOpL/kt7DHoyaDlOg==
LOG_MESSAGE=
20 changes: 20 additions & 0 deletions scripts/admin/decrypt-log/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createDecipheriv, privateDecrypt } from "node:crypto";

const CLIENT_ENCRYPTED_KEY = process.env.CLIENT_ENCRYPTED_KEY!;
const ADMIN_PRIVATE_KEY = process.env.ADMIN_PRIVATE_KEY!;
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"));
Loading