cfx-simple-keyring
Version:
A simple standard interface for a series of Conflux private keys.
496 lines (409 loc) • 20 kB
JavaScript
const assert = require('assert')
const ethUtil = require('cfx-util')
const sigUtil = require('cfx-sig-util')
const SimpleKeyring = require('../')
const EthereumTx = require('ethereumjs-tx').Transaction
const { expect } = require('chai')
const TYPE_STR = 'Simple Key Pair'
// Sample account:
const testAccount = {
key: '0xb8a9c05beeedb25df85f8d641538cbffedf67216048de9c678ee26260eb91952',
address: '0x11560cd3bac62cc6d7e6380600d9317363400896',
}
describe('simple-keyring', () => {
let keyring
beforeEach(() => {
keyring = new SimpleKeyring()
})
describe('Keyring.type', () => {
it('is a class property that returns the type string.', () => {
const type = SimpleKeyring.type
assert.equal(type, TYPE_STR)
})
})
describe('#type', () => {
it('returns the correct value', () => {
const type = keyring.type
assert.equal(type, TYPE_STR)
})
})
describe('#serialize empty wallets.', () => {
it('serializes an empty array', async () => {
const output = await keyring.serialize()
assert.deepEqual(output, [])
})
})
describe('#deserialize a private key', () => {
it('serializes what it deserializes', async () => {
await keyring.deserialize([testAccount.key])
assert.equal(keyring.wallets.length, 1, 'has one wallet')
const serialized = await keyring.serialize()
assert.equal(serialized[0], ethUtil.stripHexPrefix(testAccount.key))
const accounts = await keyring.getAccounts()
assert.deepEqual(accounts, [testAccount.address], 'accounts match expected')
})
})
describe('#constructor with a private key', () => {
it('has the correct addresses', async () => {
const keyring = new SimpleKeyring([testAccount.key])
const accounts = await keyring.getAccounts()
assert.deepEqual(accounts, [testAccount.address], 'accounts match expected')
})
})
describe('#signTransaction', () => {
const address = '0x1858e7d8b79fc3e6d989636721584498926da38a'
const privateKey = '0x7dd98753d7b4394095de7d176c58128e2ed6ee600abe97c9f6d9fd65015d9b18'
it('returns a signed tx object', async () => {
await keyring.deserialize([ privateKey ])
const txParams = {
from: address,
nonce: '0x00',
gasPrice: '0x09184e72a000',
gasLimit: '0x2710',
to: address,
value: '0x1000',
}
const tx = new EthereumTx(txParams)
const signed = await keyring.signTransaction(address, tx)
assert.ok(signed.raw, 'has a raw signature')
})
})
describe('#signMessage', () => {
const address = '0x1858e7d8b79fc3e6d989636721584498926da38a'
const message = '0x879a053d4800c6354e76c7985a865d2922c82fb5b3f4577b2fe08b998954f2e0'
const privateKey = '0x7dd98753d7b4394095de7d176c58128e2ed6ee600abe97c9f6d9fd65015d9b18'
const expectedResult = '0x28fcb6768e5110144a55b2e6ce9d1ea5a58103033632d272d2b5cf506906f7941a00b539383fd872109633d8c71c404e13dba87bc84166ee31b0e36061a69e161c'
it('passes the dennis test', async () => {
await keyring.deserialize([ privateKey ])
const result = await keyring.signMessage(address, message)
assert.equal(result, expectedResult)
})
it('reliably can decode messages it signs', async () => {
const message = 'hello there!'
const msgHashHex = ethUtil.bufferToHex(ethUtil.keccak(message))
await keyring.deserialize([ privateKey ])
await keyring.addAccounts(9)
const addresses = await keyring.getAccounts()
const signatures = await Promise.all(addresses.map(async (address) => {
return await keyring.signMessage(address, msgHashHex)
}))
signatures.forEach((sgn, index) => {
const address = addresses[index]
const r = ethUtil.toBuffer(sgn.slice(0,66))
const s = ethUtil.toBuffer('0x' + sgn.slice(66,130))
const v = ethUtil.bufferToInt(ethUtil.toBuffer('0x' + sgn.slice(130,132)))
const m = ethUtil.toBuffer(msgHashHex)
const pub = ethUtil.ecrecover(m, v, r, s)
const adr = '0x' + ethUtil.pubToAddress(pub).toString('hex')
assert.equal(adr, address, 'recovers address from signature correctly')
})
})
})
describe('#addAccounts', () => {
describe('with no arguments', () => {
it('creates a single wallet', async () => {
await keyring.addAccounts()
assert.equal(keyring.wallets.length, 1)
})
})
describe('with a numeric argument', () => {
it('creates that number of wallets', async () => {
await keyring.addAccounts(3)
assert.equal(keyring.wallets.length, 3)
})
})
})
describe('#getAccounts', () => {
it('calls getAddress on each wallet', async () => {
// Push a mock wallet
const desiredOutput = '0x18a3462427bcc9133bb46e88bcbe39cd7ef0e761'
keyring.wallets.push({
getAddress() {
return ethUtil.toBuffer(desiredOutput)
}
})
const output = await keyring.getAccounts()
assert.equal(output[0], desiredOutput)
assert.equal(output.length, 1)
})
})
describe('#removeAccount', () => {
describe('if the account exists', () => {
it('should remove that account', async () => {
await keyring.addAccounts()
const addresses = await keyring.getAccounts()
keyring.removeAccount(addresses[0])
const addressesAfterRemoval = await keyring.getAccounts()
assert.equal(addressesAfterRemoval.length, addresses.length -1)
})
})
describe('if the account does not exist', () => {
it('should throw an error', (done) => {
const unexistingAccount = '0x0000000000000000000000000000000000000000'
expect(_ => {
keyring.removeAccount(unexistingAccount)
}).to.throw(`Address ${unexistingAccount} not found in this keyring`)
done()
})
})
})
describe('#signPersonalMessage', () => {
it('returns the expected value', async () => {
const address = '0x1e93f9bacbcffc8ee6663f2647917ed7a20a57bb'
const privateKey = new Buffer('6969696969696969696969696969696969696969696969696969696969696969', 'hex')
const privKeyHex = ethUtil.bufferToHex(privateKey)
const message = '0x68656c6c6f20776f726c64'
const signature = '0x16f85b247cedca22c62bc3a180b394e018c274ab1cc946f7ba3fa815ecc83f946e3d8ba19c9780fa836ea250f5990268e9406538156750d8665902d89851dc261c'
await keyring.deserialize([privKeyHex])
const sig = await keyring.signPersonalMessage(address, message)
assert.equal(sig, signature, 'signature matches')
const restored = sigUtil.recoverPersonalSignature({
data: message,
sig,
})
assert.equal(restored, address, 'recovered address')
})
})
describe('#signTypedData', () => {
const address = '0x19c76e6ad8f28bb1004902578fb108c507be341b'
const privKeyHex = '4af1bceebf7f3634ec3cff8a2c38e51178d5d4ce585c52d6043e5e2cc3418bb0'
const expectedSig = '0x49e75d475d767de7fcc67f521e0d86590723d872e6111e51c393e8c1e2f21d032dfaf5833af158915f035db6af4f37bf2d5d29781cd81f28a44c5cb4b9d241531b'
const privKey = Buffer.from(privKeyHex, 'hex')
const typedData = [
{
type: 'string',
name: 'message',
value: 'Hi, Alice!'
}
]
const msgParams = { data: typedData }
it('returns the expected value', async () => {
await keyring.deserialize([privKeyHex])
const sig = await keyring.signTypedData(address, typedData)
const signedParams = Object.create(msgParams)
signedParams.sig = sig;
assert.equal(sig, expectedSig, 'produced correct signature.')
const restored = sigUtil.recoverTypedSignatureLegacy(signedParams)
assert.equal(restored, address, 'recovered address')
})
})
describe('#signTypedData_v1', () => {
const address = '0x19c76e6ad8f28bb1004902578fb108c507be341b'
const privKeyHex = '4af1bceebf7f3634ec3cff8a2c38e51178d5d4ce585c52d6043e5e2cc3418bb0'
const expectedSig = '0x49e75d475d767de7fcc67f521e0d86590723d872e6111e51c393e8c1e2f21d032dfaf5833af158915f035db6af4f37bf2d5d29781cd81f28a44c5cb4b9d241531b'
const privKey = Buffer.from(privKeyHex, 'hex')
const typedData = [
{
type: 'string',
name: 'message',
value: 'Hi, Alice!'
}
]
const msgParams = { data: typedData }
it('returns the expected value', async () => {
await keyring.deserialize([privKeyHex])
const sig = await keyring.signTypedData_v1(address, typedData)
const signedParams = Object.create(msgParams)
signedParams.sig = sig;
assert.equal(sig, expectedSig, 'produced correct signature.')
const restored = sigUtil.recoverTypedSignatureLegacy(signedParams)
assert.equal(restored, address, 'recovered address')
})
it('works via version paramter', async () => {
await keyring.deserialize([privKeyHex])
const sig = await keyring.signTypedData(address, typedData)
const signedParams = Object.create(msgParams)
signedParams.sig = sig;
assert.equal(sig, expectedSig, 'produced correct signature.')
const restored = sigUtil.recoverTypedSignatureLegacy(signedParams)
assert.equal(restored, address, 'recovered address')
})
})
describe('#signTypedData_v3', () => {
const address = '0x19c76e6ad8f28bb1004902578fb108c507be341b'
const privKeyHex = '0x4af1bceebf7f3634ec3cff8a2c38e51178d5d4ce585c52d6043e5e2cc3418bb0'
it('returns the expected value', async () => {
const typedData = {
types: {
EIP712Domain: []
},
domain: {},
primaryType: 'EIP712Domain',
message: {}
}
await keyring.deserialize([privKeyHex])
const sig = await keyring.signTypedData_v3(address, typedData)
const restored = sigUtil.recoverTypedSignature({ data: typedData, sig: sig })
assert.equal(restored, address, 'recovered address')
})
it('works via the version parameter', async () => {
const typedData = {
types: {
EIP712Domain: []
},
domain: {},
primaryType: 'EIP712Domain',
message: {}
}
await keyring.deserialize([privKeyHex])
const sig = await keyring.signTypedData(address, typedData, { version: 'V3' })
const restored = sigUtil.recoverTypedSignature({ data: typedData, sig: sig })
assert.equal(restored, address, 'recovered address')
})
})
describe('#signTypedData_v3 signature verification', () => {
const privKeyHex = 'c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4'
const expectedSig = '0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c'
it('returns the expected value', async () => {
const typedData = {"data":{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","domain":{"name":"Ether Mail","version":"1","chainId":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}}}
await keyring.deserialize([privKeyHex])
const addresses = await keyring.getAccounts()
const address = addresses[0]
const sig = await keyring.signTypedData_v3(address, typedData.data)
assert.equal(sig, expectedSig, 'verified signature')
const signedData = Object.create(typedData)
signedData.sig = sig
const restored = sigUtil.recoverTypedSignature(signedData)
assert.equal(restored, address, 'recovered address')
})
})
describe('#signTypedData_v4', () => {
const address = '0x19c76e6ad8f28bb1004902578fb108c507be341b'
const privKeyHex = '0x4af1bceebf7f3634ec3cff8a2c38e51178d5d4ce585c52d6043e5e2cc3418bb0'
it('returns the expected value', async () => {
const typedData = {
types: {
EIP712Domain: []
},
domain: {},
primaryType: 'EIP712Domain',
message: {}
}
await keyring.deserialize([privKeyHex])
const sig = await keyring.signTypedData_v4(address, typedData)
const restored = sigUtil.recoverTypedSignature({ data: typedData, sig: sig })
assert.equal(restored, address, 'recovered address')
})
})
describe('#decryptMessage', () => {
it('returns the expected value', async () => {
const address = '0x1e93f9bacbcffc8ee6663f2647917ed7a20a57bb'
const privateKey = new Buffer('6969696969696969696969696969696969696969696969696969696969696969', 'hex')
const privKeyHex = ethUtil.bufferToHex(privateKey)
const message = 'Hello world!'
const encryptedMessage = sigUtil.encrypt(sigUtil.getEncryptionPublicKey(privateKey), {'data': message}, 'x25519-xsalsa20-poly1305')
await keyring.deserialize([privKeyHex])
const decryptedMessage = await keyring.decryptMessage(address, encryptedMessage)
assert.equal(message, decryptedMessage, 'signature matches')
})
})
describe('#encryptionPublicKey', () => {
it('returns the expected value', async () => {
const address = '0x1e93f9bacbcffc8ee6663f2647917ed7a20a57bb'
const privateKey = new Buffer('6969696969696969696969696969696969696969696969696969696969696969', 'hex')
const publicKey = 'GxuMqoE2oHsZzcQtv/WMNB3gCH2P6uzynuwO1P0MM1U='
const privKeyHex = ethUtil.bufferToHex(privateKey)
await keyring.deserialize([privKeyHex])
const encryptionPublicKey = await keyring.getEncryptionPublicKey(address, privateKey)
assert.equal(publicKey, encryptionPublicKey, 'public keys matches')
})
})
describe('#signTypedData_v4 signature verification', () => {
const privKeyHex = 'c85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4'
const expectedSig = '0x65cbd956f2fae28a601bebc9b906cea0191744bd4c4247bcd27cd08f8eb6b71c78efdf7a31dc9abee78f492292721f362d296cf86b4538e07b51303b67f749061b'
it('returns the expected value', async () => {
const typedData = {"data":{"types":{"EIP712Domain":[{"name":"name","type":"string"},{"name":"version","type":"string"},{"name":"chainId","type":"uint256"},{"name":"verifyingContract","type":"address"}],"Person":[{"name":"name","type":"string"},{"name":"wallets","type":"address[]"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}],"Group":[{"name":"name","type":"string"},{"name":"members","type":"Person[]"}]},"domain":{"name":"Ether Mail","version":"1","chainId":1,"verifyingContract":"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"},"primaryType":"Mail","message":{"from":{"name":"Cow","wallets":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"]},"to":[{"name":"Bob","wallets":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB","0xB0BdaBea57B0BDABeA57b0bdABEA57b0BDabEa57","0xB0B0b0b0b0b0B000000000000000000000000000"]}],"contents":"Hello, Bob!"}}}
await keyring.deserialize([privKeyHex])
const addresses = await keyring.getAccounts()
const address = addresses[0]
const sig = await keyring.signTypedData_v4(address, typedData.data)
assert.equal(sig, expectedSig, 'verified signature')
const signedData = Object.create(typedData)
signedData.sig = sig
const restored = sigUtil.recoverTypedSignature_v4(signedData)
assert.equal(restored, address, 'recovered address')
})
})
describe('getAppKeyAddress', function () {
it('should return a public address custom to the provided app key origin', async function () {
const address = testAccount.address
const keyring = new SimpleKeyring([testAccount.key])
const appKeyAddress = await keyring.getAppKeyAddress(address, 'someapp.origin.io')
assert.notEqual(address, appKeyAddress)
assert(ethUtil.isValidAddress(appKeyAddress))
})
it('should return different addresses when provided different app key origins', async function () {
const address = testAccount.address
const keyring = new SimpleKeyring([testAccount.key])
const appKeyAddress1 = await keyring.getAppKeyAddress(address, 'someapp.origin.io')
assert(ethUtil.isValidAddress(appKeyAddress1))
const appKeyAddress2 = await keyring.getAppKeyAddress(address, 'anotherapp.origin.io')
assert(ethUtil.isValidAddress(appKeyAddress2))
assert.notEqual(appKeyAddress1, appKeyAddress2)
})
it('should return the same address when called multiple times with the same params', async function () {
const address = testAccount.address
const keyring = new SimpleKeyring([testAccount.key])
const appKeyAddress1 = await keyring.getAppKeyAddress(address, 'someapp.origin.io')
assert(ethUtil.isValidAddress(appKeyAddress1))
const appKeyAddress2 = await keyring.getAppKeyAddress(address, 'someapp.origin.io')
assert(ethUtil.isValidAddress(appKeyAddress2))
assert.equal(appKeyAddress1, appKeyAddress2)
})
it('should throw error if the provided origin is not a string', async function () {
const address = testAccount.address
const keyring = new SimpleKeyring([testAccount.key])
try {
await keyring.getAppKeyAddress(address, [])
} catch (error) {
assert(error instanceof Error, 'Value thrown is not an error')
return
}
assert.fail('Should have thrown error')
})
it('should throw error if the provided origin is an empty string', async function () {
const address = testAccount.address
const keyring = new SimpleKeyring([testAccount.key])
try {
await keyring.getAppKeyAddress(address, '')
} catch (error) {
assert(error instanceof Error, 'Value thrown is not an error')
return
}
assert.fail('Should have thrown error')
})
})
describe('signing methods withAppKeyOrigin option', function () {
it('should signPersonalMessage with the expected key when passed a withAppKeyOrigin', async function () {
const address = testAccount.address
const message = '0x68656c6c6f20776f726c64'
const privateKeyHex = '4fbe006f0e9c2374f53eb1aef1b6970d20206c61ea05ad9591ef42176eb842c0'
const privateKeyBuffer = new Buffer(privateKeyHex, 'hex')
const expectedSig = sigUtil.personalSign(privateKeyBuffer, { data: message })
const keyring = new SimpleKeyring([testAccount.key])
const sig = await keyring.signPersonalMessage(address, message, {
withAppKeyOrigin: 'someapp.origin.io',
})
assert.equal(expectedSig, sig, 'sign with app key generated private key')
})
it('should signTypedData_v3 with the expected key when passed a withAppKeyOrigin', async function () {
const address = testAccount.address
const typedData = {
types: {
EIP712Domain: []
},
domain: {},
primaryType: 'EIP712Domain',
message: {}
}
const privateKeyHex = '4fbe006f0e9c2374f53eb1aef1b6970d20206c61ea05ad9591ef42176eb842c0'
const privateKeyBuffer = new Buffer(privateKeyHex, 'hex')
const expectedSig = sigUtil.signTypedData(privateKeyBuffer, { data: typedData })
const keyring = new SimpleKeyring([testAccount.key])
const sig = await keyring.signTypedData_v3(address, typedData, {
withAppKeyOrigin: 'someapp.origin.io',
})
assert.equal(expectedSig, sig, 'sign with app key generated private key')
})
})
})