| @@ -0,0 +1,2 @@ | |||
| .env | |||
| node_modules | |||
| @@ -0,0 +1,134 @@ | |||
| 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 | |||
| @@ -0,0 +1,26 @@ | |||
| * 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. | |||
| @@ -0,0 +1,72 @@ | |||
| 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 | |||
| @@ -0,0 +1,43 @@ | |||
| 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')}`); | |||
| } | |||
| } | |||
| @@ -0,0 +1,17 @@ | |||
| { | |||
| "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" | |||
| } | |||
| } | |||