| .env | |||||
| node_modules |
| const crypto = require('crypto') | |||||
| const { signTypedData } = require('@metamask/eth-sig-util') | |||||
| const { TransactionFactory } = require('@ethereumjs/tx') | |||||
| const { Common } = require('@ethereumjs/common') | |||||
| const { | |||||
| hashPersonalMessage, | |||||
| toBuffer, | |||||
| ecsign, | |||||
| addHexPrefix, | |||||
| pubToAddress, | |||||
| ecrecover | |||||
| } = require('@ethereumjs/util') | |||||
| function chainConfig(chain, hardfork) { | |||||
| const chainId = BigInt(chain) | |||||
| return Common.isSupportedChainId(chainId) | |||||
| ? new Common({ chain: chainId, hardfork }) | |||||
| : Common.custom({ chainId: chainId }, { baseChain: 'mainnet', hardfork }) | |||||
| } | |||||
| class HotSignerWorker { | |||||
| constructor() { | |||||
| this.token = crypto.randomBytes(32).toString('hex') | |||||
| //process.send({ type: 'token', token: this.token }) | |||||
| } | |||||
| handleMessage({ id, method, params, token }) { | |||||
| // Define (pseudo) callback | |||||
| const pseudoCallback = (error, result) => { | |||||
| // Add correlation id to response | |||||
| const response = { id, error, result, type: 'rpc' } | |||||
| // Send response to parent process | |||||
| process.send(response) | |||||
| } | |||||
| // Verify token | |||||
| if (!crypto.timingSafeEqual(Buffer.from(token), Buffer.from(this.token))) | |||||
| return pseudoCallback('Invalid token') | |||||
| // If method exists -> execute | |||||
| if (this[method]) return this[method](params, pseudoCallback) | |||||
| // Else return error | |||||
| pseudoCallback(`Invalid method: '${method}'`) | |||||
| } | |||||
| signMessage(key, message, pseudoCallback) { | |||||
| // Hash message | |||||
| const hash = hashPersonalMessage(toBuffer(message)) | |||||
| // Sign message | |||||
| const signed = ecsign(hash, key) | |||||
| // Return serialized signed message | |||||
| const hex = Buffer.concat([signed.r, signed.s, Buffer.from([Number(signed.v)])]).toString('hex') | |||||
| pseudoCallback(null, addHexPrefix(hex)) | |||||
| } | |||||
| signTypedData(key, typedMessage, pseudoCallback) { | |||||
| try { | |||||
| const { data, version } = typedMessage | |||||
| const signature = signTypedData({ privateKey: key, data, version }) | |||||
| pseudoCallback(null, signature) | |||||
| } catch (e) { | |||||
| pseudoCallback(e.message) | |||||
| } | |||||
| } | |||||
| signTransaction(key, rawTx, pseudoCallback) { | |||||
| if (!rawTx.chainId) { | |||||
| console.error(`invalid chain id ${rawTx.chainId} for transaction`) | |||||
| return pseudoCallback('could not determine chain id for transaction') | |||||
| } | |||||
| const chainId = parseInt(rawTx.chainId, 16) | |||||
| const hardfork = parseInt(rawTx.type) === 2 ? 'london' : 'berlin' | |||||
| const common = chainConfig(chainId, hardfork) | |||||
| const tx = TransactionFactory.fromTxData(rawTx, { common }) | |||||
| const signedTx = tx.sign(key) | |||||
| const serialized = signedTx.serialize().toString('hex') | |||||
| pseudoCallback(null, addHexPrefix(serialized)) | |||||
| } | |||||
| verifyAddress({ index, address }, pseudoCallback) { | |||||
| const message = '0x' + crypto.randomBytes(32).toString('hex') | |||||
| this.signMessage({ index, message }, (err, signedMessage) => { | |||||
| // Handle signing errors | |||||
| if (err) return pseudoCallback(err) | |||||
| // Signature -> buffer | |||||
| const signature = Buffer.from(signedMessage.replace('0x', ''), 'hex') | |||||
| // Ensure correct length | |||||
| if (signature.length !== 65) | |||||
| return pseudoCallback(new Error('Frame verifyAddress signature has incorrect length')) | |||||
| // Verify address | |||||
| let v = signature[64] | |||||
| v = BigInt(v === 0 || v === 1 ? v + 27 : v) | |||||
| const r = toBuffer(signature.slice(0, 32)) | |||||
| const s = toBuffer(signature.slice(32, 64)) | |||||
| const hash = hashPersonalMessage(toBuffer(message)) | |||||
| const verifiedAddress = '0x' + pubToAddress(ecrecover(hash, v, r, s)).toString('hex') | |||||
| // Return result | |||||
| pseudoCallback(null, verifiedAddress.toLowerCase() === address.toLowerCase()) | |||||
| }) | |||||
| } | |||||
| _encrypt(string, password) { | |||||
| const salt = crypto.randomBytes(16) | |||||
| const iv = crypto.randomBytes(16) | |||||
| const cipher = crypto.createCipheriv('aes-256-cbc', this._hashPassword(password, salt), iv) | |||||
| const encrypted = Buffer.concat([cipher.update(string), cipher.final()]) | |||||
| return salt.toString('hex') + ':' + iv.toString('hex') + ':' + encrypted.toString('hex') | |||||
| } | |||||
| _decrypt(string, password) { | |||||
| const parts = string.split(':') | |||||
| const salt = Buffer.from(parts.shift(), 'hex') | |||||
| const iv = Buffer.from(parts.shift(), 'hex') | |||||
| const decipher = crypto.createDecipheriv('aes-256-cbc', this._hashPassword(password, salt), iv) | |||||
| const encryptedString = Buffer.from(parts.join(':'), 'hex') | |||||
| const decrypted = Buffer.concat([decipher.update(encryptedString), decipher.final()]) | |||||
| return decrypted.toString() | |||||
| } | |||||
| _hashPassword(password, salt) { | |||||
| try { | |||||
| return crypto.scryptSync(password, salt, 32, { N: 32768, r: 8, p: 1, maxmem: 36000000 }) | |||||
| } catch (e) { | |||||
| console.error('Error during hashPassword', e) // TODO: Handle Error | |||||
| } | |||||
| } | |||||
| } | |||||
| module.exports = HotSignerWorker |
| * Frame decryption tool | |||||
| Tool to recover seeds stored in [Frame](https://frame.sh/) | |||||
| * Instructions | |||||
| Get the encrypted seed phrase (or key) from ~\~/.config/frame/signers/*.json~ file. Get the value ~encryptedSeed~ (or ~encryptedKeys~): | |||||
| #+BEGIN_SRC | |||||
| jq '.encryptedSeed ~/.config/frame/signers/1234.json | |||||
| #+END_SRC | |||||
| Create an ~.env~ file with: | |||||
| #+BEGIN_SRC | |||||
| export PASSWORD='password-used-in-frame' | |||||
| export ENCRYPTED='the-value-obtained-above' | |||||
| export NUM_KEYS=num-of-keys-and-addresses-you-want-to-derive | |||||
| #+END_SRC | |||||
| Run: | |||||
| #+BEGIN_SRC | |||||
| npm install | |||||
| npm start | |||||
| #+END_SRC | |||||
| Note that you will get the hex string seed value obtained from ~bip39.mnemonicToSeed~, which is irreversible. (I.e., it is impossible to recover the original mnemonic phrase). But you can recover private keys and private addresses. |
| const { computeAddress } = require('ethers').utils; | |||||
| const hdKey = require('hdkey'); | |||||
| const HotSignerWorker = require('../HotSigner/worker'); | |||||
| class SeedSignerWorker extends HotSignerWorker { | |||||
| constructor() { | |||||
| super() | |||||
| this.seed = null | |||||
| //process.on('message', (message) => this.handleMessage(message)) | |||||
| } | |||||
| unlock({ encryptedSeed, password }, pseudoCallback) { | |||||
| try { | |||||
| this.seed = this._decrypt(encryptedSeed, password) | |||||
| pseudoCallback(null) | |||||
| } catch (e) { | |||||
| pseudoCallback('Invalid password') | |||||
| } | |||||
| } | |||||
| lock(_, pseudoCallback) { | |||||
| this.seed = null | |||||
| pseudoCallback(null) | |||||
| } | |||||
| encryptSeed({ seed, password }, pseudoCallback) { | |||||
| pseudoCallback(null, this._encrypt(seed.toString('hex'), password)) | |||||
| } | |||||
| signMessage({ index, message }, pseudoCallback) { | |||||
| // Make sure signer is unlocked | |||||
| if (!this.seed) return pseudoCallback('Signer locked') | |||||
| // Derive private key | |||||
| const key = this._derivePrivateKey(index) | |||||
| // Sign message | |||||
| super.signMessage(key, message, pseudoCallback) | |||||
| } | |||||
| signTypedData({ index, typedMessage }, pseudoCallback) { | |||||
| // Make sure signer is unlocked | |||||
| if (!this.seed) return pseudoCallback('Signer locked') | |||||
| // Derive private key | |||||
| const key = this._derivePrivateKey(index) | |||||
| // Sign message | |||||
| super.signTypedData(key, typedMessage, pseudoCallback) | |||||
| } | |||||
| signTransaction({ index, rawTx }, pseudoCallback) { | |||||
| // Make sure signer is unlocked | |||||
| if (!this.seed) return pseudoCallback('Signer locked') | |||||
| // Derive private key | |||||
| const key = this._derivePrivateKey(index) | |||||
| // Sign transaction | |||||
| super.signTransaction(key, rawTx, pseudoCallback) | |||||
| } | |||||
| _derivePrivateKey(index) { | |||||
| let key = hdKey.fromMasterSeed(Buffer.from(this.seed, 'hex')) | |||||
| key = key.derive("m/44'/60'/0'/0/" + index) | |||||
| return key.privateKey | |||||
| } | |||||
| _deriveAddress(index) { | |||||
| const key = hdKey.fromMasterSeed(Buffer.from(this.seed, 'hex')); | |||||
| const publicKey = key.derive("m/44'/60'/0'/0/" + index).publicKey; | |||||
| const address = computeAddress(publicKey); | |||||
| return address; | |||||
| } | |||||
| } | |||||
| //const seedSignerWorker = new SeedSignerWorker() // eslint-disable-line | |||||
| module.exports = SeedSignerWorker |
| const SeedSignerWorker = require('./SeedSigner/worker.js'); | |||||
| // Create an instance of the worker | |||||
| const worker = new SeedSignerWorker(); | |||||
| // Example usage of _encrypt and _decrypt | |||||
| const password = process.env.PASSWORD ? process.env.PASSWORD : "1234"; | |||||
| // console.log('password: ', password) | |||||
| const originalEncrypted = process.env.ENCRYPTED; | |||||
| const NUM_KEYS = process.env.NUM_KEYS ? process.env.NUM_KEYS : 1; | |||||
| if (!originalEncrypted) { | |||||
| const originalMessage = 'attract rapid earn couch also first limb beyond defense truth yard final'; | |||||
| console.log('Original message:', originalMessage); | |||||
| // Encrypt the message | |||||
| let encrypted = worker._encrypt(originalMessage, password); | |||||
| console.log('Encrypted:', encrypted); | |||||
| // Decrypt the message | |||||
| let decrypted = worker._decrypt(encrypted, password); | |||||
| console.log('Decrypted:', decrypted); | |||||
| // Verify | |||||
| console.log('Match:', originalMessage === decrypted); | |||||
| } else { | |||||
| console.log('Encrypted:', originalEncrypted); | |||||
| const seed = worker._decrypt(originalEncrypted, password); | |||||
| console.log('Decrypted seed:', seed); | |||||
| worker.seed = seed; | |||||
| for (let i=0; i < NUM_KEYS; i++) { | |||||
| const privateKey = worker._derivePrivateKey(i); | |||||
| console.log(`Private Key ${i}: ${privateKey.toString('hex')}`); | |||||
| const address = worker._deriveAddress(i); | |||||
| console.log(`Address ${i}: ${address.toString('hex')}`); | |||||
| } | |||||
| } | |||||
| { | |||||
| "name": "frame-decryption", | |||||
| "version": "1.0.0", | |||||
| "description": "Frame decryption tool", | |||||
| "main": "main.js", | |||||
| "scripts": { | |||||
| "start": "node main.js" | |||||
| }, | |||||
| "dependencies": { | |||||
| "@ethereumjs/common": "^4.1.0", | |||||
| "@ethereumjs/tx": "^5.1.0", | |||||
| "@ethereumjs/util": "^9.0.1", | |||||
| "@metamask/eth-sig-util": "^7.0.1", | |||||
| "ethers": "5.7.2", | |||||
| "hdkey": "2.1.0" | |||||
| } | |||||
| } |