diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..41a3429 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +unsafe-perm = true +package-lock=false diff --git a/README.md b/README.md index e91aa27..89e22ba 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ const EthCrypto = require('eth-crypto'); - [createIdentity()](#createidentity) - [publicKeyByPrivateKey()](#publickeybyprivatekey) -- [addressByPublicKey()](#addressbypublickey) +- [publicKey.toAddress()](#addressbypublickey) - [sign()](#sign) - [recover()](#recover) - [recoverPublicKey()](#recoverpublickey) @@ -78,17 +78,39 @@ Derives the publicKey from a privateKey and returns it as hex-string. // > 'bf1cc3154424dc22191941d9f4f50b063a2b663a2337e5548abea633c1d06ece...' ``` -### addressByPublicKey() +### publicKey.toAddress() Derives the ethereum-address from the publicKey. ```javascript - const address = EthCrypto.addressByPublicKey( + const address = EthCrypto.publicKey.toAddress( 'bf1cc3154424dc22191941d9f4f50b063a2b663a2337e5548abea633c1d06ece...' ); // > '0x3f243FdacE01Cfd9719f7359c94BA11361f32471' ``` +### publicKey.compress() + +Compresses an uncompressed publicKey. + +```javascript + const address = EthCrypto.publicKey.compress( + '04a34d6aef3eb42335fb3cacb59...' + ); + // > '03a34d6aef3eb42335fb3cacb59478c0b44c0bbeb8bb4ca427dbc7044157a5d24b' // compressed keys start with '02' or '03' +``` + +### publicKey.decompress() + +Decompresses a compressed publicKey. + +```javascript + const address = EthCrypto.publicKey.decompress( + '03a34d6aef3eb42335fb3c...' + ); + // > 'a34d6aef3eb42335fb3cacb5947' // non-compressed keys start with '04' or no prefix +``` + ### sign() Signs the hash with the privateKey. Returns the signature as hex-string. diff --git a/package.json b/package.json index a68bfbc..92947c0 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,6 @@ "ethereumjs-tx": "1.3.4", "ethereumjs-util": "5.1.5", "secp256k1": "3.5.0", - "web3": "1.0.0-beta.30" + "web3": "1.0.0-beta.34" } } diff --git a/src/address-by-public-key.js b/src/address-by-public-key.js deleted file mode 100644 index e29531d..0000000 --- a/src/address-by-public-key.js +++ /dev/null @@ -1,15 +0,0 @@ -import EthUtil from 'ethereumjs-util'; -import { - web3 -} from './util'; - -/** - * generates the ethereum-adress of the publicKey - * We create the checksum-adress which is case-sensitive - * @returns {string} address - */ -export default function addressByPublicKey(publicKey) { - const addressBuffer = EthUtil.pubToAddress(new Buffer(publicKey, 'hex')); - const checkSumAdress = web3.utils.toChecksumAddress(addressBuffer.toString('hex')); - return checkSumAdress; -} diff --git a/src/create-identity.js b/src/create-identity.js index a5f5360..53bbc6d 100644 --- a/src/create-identity.js +++ b/src/create-identity.js @@ -1,20 +1,13 @@ -import { - web3 -} from './util'; import publicKeyByPrivateKey from './public-key-by-private-key'; +import Account from 'eth-lib/lib/account'; + /** * creates a new object with * private-, public-Key and address */ export default function createIdentity() { - const account = web3.eth.accounts.create(); - - const identity = { - address: account.address, - privateKey: account.privateKey, - publicKey: publicKeyByPrivateKey(account.privateKey) - }; - + const identity = Account.create(); + identity.publicKey = publicKeyByPrivateKey(identity.privateKey); return identity; } diff --git a/src/encrypt-with-public-key.js b/src/encrypt-with-public-key.js index 95723e7..bbacab8 100644 --- a/src/encrypt-with-public-key.js +++ b/src/encrypt-with-public-key.js @@ -1,7 +1,13 @@ import eccrypto from 'eccrypto'; +import { + decompress +} from './public-key'; export default async function encryptWithPublicKey(publicKey, message) { + // ensure its an uncompressed publicKey + publicKey = decompress(publicKey); + // re-add the compression-flag const pubString = '04' + publicKey; diff --git a/src/hash.js b/src/hash.js index 481dc20..d7b1da6 100644 --- a/src/hash.js +++ b/src/hash.js @@ -13,16 +13,3 @@ export function keccak256(params) { } export const SIGN_PREFIX = '\x19Ethereum Signed Message:\n32'; - -/** - * hashes the given hash with the web3-prefix - * '\x19Ethereum Signed Message:\n32' - */ -export function prefixedHash(hash) { - if (!web3.utils.isHexStrict(hash)) - throw new Error('EthCrypto.hash.prefixedHash(): please insert an hash'); - return web3.eth.accounts.hashMessage({ - type: 'bytes32', - value: hash - }); -} diff --git a/src/hex.js b/src/hex.js index fa32ed3..a9e7e7f 100644 --- a/src/hex.js +++ b/src/hex.js @@ -4,10 +4,13 @@ * @link https://stackoverflow.com/a/40471908/3443137 */ -import * as util from './util'; +import { + removeTrailing0x, + addTrailing0x +} from './util'; export function compress(hex, base64 = false) { - hex = util.removeTrailing0x(hex); + hex = removeTrailing0x(hex); // if base64:true, we use our own function because it results in a smaller output if (base64 === true) @@ -29,7 +32,7 @@ export function decompress(compressedString, base64 = false) { // if base64:true, we use our own function because it results in a smaller output if (base64 === true) { const ret = new Buffer(compressedString, 'base64').toString('hex'); - return util.addTrailing0x(ret); + return addTrailing0x(ret); } let hex = ''; @@ -38,5 +41,5 @@ export function decompress(compressedString, base64 = false) { hex += ((i == 0 ? '' : '000') + compressedString.charCodeAt(i).toString(16)).slice(-4); } hex = hex.toLowerCase(); - return util.addTrailing0x(hex); + return addTrailing0x(hex); } diff --git a/src/index.js b/src/index.js index a3c0c07..b731b8e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ -import addressByPublicKey from './address-by-public-key'; + import createIdentity from './create-identity'; +import * as publicKey from './public-key'; import decryptWithPrivateKey from './decrypt-with-private-key'; import encryptWithPublicKey from './encrypt-with-public-key'; import publicKeyByPrivateKey from './public-key-by-private-key'; @@ -15,8 +16,8 @@ import * as vrs from './vrs'; import * as util from './util'; export { - addressByPublicKey, createIdentity, + publicKey, decryptWithPrivateKey, encryptWithPublicKey, publicKeyByPrivateKey, @@ -33,8 +34,8 @@ export { }; export default { - addressByPublicKey, createIdentity, + publicKey, decryptWithPrivateKey, encryptWithPublicKey, publicKeyByPrivateKey, diff --git a/src/public-key.js b/src/public-key.js new file mode 100644 index 0000000..8a694c8 --- /dev/null +++ b/src/public-key.js @@ -0,0 +1,48 @@ +import { + publicKeyConvert +} from 'secp256k1'; +import ethUtil from 'ethereumjs-util'; + +export function compress(startsWith04) { + + // add trailing 04 if not done before + const testBuffer = new Buffer(startsWith04, 'hex'); + if (testBuffer.length === 64) startsWith04 = '04' + startsWith04; + + + return publicKeyConvert( + new Buffer(startsWith04, 'hex'), + true + ).toString('hex'); +}; + +export function decompress(startsWith02Or03) { + + // if already decompressed an not has trailing 04 + const testBuffer = new Buffer(startsWith02Or03, 'hex'); + if (testBuffer.length === 64) startsWith02Or03 = '04' + startsWith02Or03; + + let decompressed = publicKeyConvert( + new Buffer(startsWith02Or03, 'hex'), + false + ).toString('hex'); + + // remove trailing 04 + decompressed = decompressed.substring(2); + return decompressed; +}; + +/** + * generates the ethereum-adress of the publicKey + * We create the checksum-adress which is case-sensitive + * @returns {string} address + */ +export function toAddress(publicKey) { + + // normalize key + publicKey = decompress(publicKey); + + const addressBuffer = ethUtil.pubToAddress(new Buffer(publicKey, 'hex')); + const checkSumAdress = ethUtil.toChecksumAddress(addressBuffer.toString('hex')); + return checkSumAdress; +} diff --git a/src/recover-public-key.js b/src/recover-public-key.js index 19ddcda..a3ab403 100644 --- a/src/recover-public-key.js +++ b/src/recover-public-key.js @@ -1,7 +1,11 @@ -import * as secp256k1 from 'secp256k1'; +import { + recover +} from 'secp256k1'; import * as vrs from './vrs'; -import * as util from './util'; +import { + removeTrailing0x +} from './util'; /** @@ -15,12 +19,12 @@ export default function recoverPublicKey(signature, hash) { let sigOnly = signature.substring(0, signature.length - 1); - sigOnly = util.removeTrailing0x(sigOnly); + sigOnly = removeTrailing0x(sigOnly); const recoveryNumber = vals.v === '0x1c' ? 1 : 0; - let pubKey = secp256k1.recover( - new Buffer(util.removeTrailing0x(hash), 'hex'), + let pubKey = recover( + new Buffer(removeTrailing0x(hash), 'hex'), new Buffer(sigOnly, 'hex'), recoveryNumber, false diff --git a/src/recover.js b/src/recover.js index 3da86ce..26880d9 100644 --- a/src/recover.js +++ b/src/recover.js @@ -1,5 +1,7 @@ import recoverPublicKey from './recover-public-key'; -import addressByPublicKey from './address-by-public-key'; +import { + toAddress as addressByPublicKey +} from './public-key'; /** * returns the adress with which the messageHash was signed diff --git a/src/sign-transaction.js b/src/sign-transaction.js index cbdac49..ea0c821 100644 --- a/src/sign-transaction.js +++ b/src/sign-transaction.js @@ -1,6 +1,8 @@ import Tx from 'ethereumjs-tx'; import publicKeyByPrivateKey from './public-key-by-private-key'; -import addressByPublicKey from './address-by-public-key'; +import { + toAddress as addressByPublicKey +} from './public-key'; export default function signTransaction( rawTx, diff --git a/src/sign.js b/src/sign.js index e63ed21..98d2fb7 100644 --- a/src/sign.js +++ b/src/sign.js @@ -1,5 +1,10 @@ -import * as secp256k1 from 'secp256k1'; -import * as util from './util'; +import { + sign as secp256k1_sign +} from 'secp256k1'; +import { + addTrailing0x, + removeTrailing0x +} from './util'; /** * signs the given message @@ -9,13 +14,13 @@ import * as util from './util'; * @return {string} hexString */ export default function sign(privateKey, hash) { - hash = util.addTrailing0x(hash); + hash = addTrailing0x(hash); if (hash.length !== 66) throw new Error('EthCrypto.sign(): Can only sign hashes, given: ' + hash); - const sigObj = secp256k1.sign( - new Buffer(util.removeTrailing0x(hash), 'hex'), - new Buffer(util.removeTrailing0x(privateKey), 'hex') + const sigObj = secp256k1_sign( + new Buffer(removeTrailing0x(hash), 'hex'), + new Buffer(removeTrailing0x(privateKey), 'hex') ); const recoveryId = sigObj.recovery === 1 ? '1c' : '1b'; diff --git a/test/unit.test.js b/test/unit.test.js index 70e1cc3..edfdb0c 100644 --- a/test/unit.test.js +++ b/test/unit.test.js @@ -36,21 +36,6 @@ describe('unit.test.js', () => { }); }); }); - describe('.addressByPublicKey()', () => { - describe('positive', () => { - it('should generate the correct address', () => { - const address = EthCrypto.addressByPublicKey(TEST_DATA.publicKey); - assert.equal(address, TEST_DATA.address); - }); - }); - describe('negative', () => { - assert.throws( - () => EthCrypto.addressByPublicKey( - AsyncTestUtil.randomString(12) - ) - ); - }); - }); describe('.sign()', () => { describe('positive', () => { it('should sign the data', () => { @@ -116,6 +101,20 @@ describe('unit.test.js', () => { assert.equal(typeof encrypted.ciphertext, 'string'); assert.equal(typeof encrypted.mac, 'string'); }); + it('should also work with compressed keys', async () => { + const message = AsyncTestUtil.randomString(12); + const ident = EthCrypto.createIdentity(); + const compressed = EthCrypto.publicKey.compress(ident.publicKey); + const encrypted = await EthCrypto.encryptWithPublicKey( + compressed, + message + ); + const decrypted = await EthCrypto.decryptWithPrivateKey( + ident.privateKey, + encrypted + ); + assert.equal(decrypted, message); + }); }); describe('negative', () => { it('should throw when non-key given', async () => { @@ -124,7 +123,8 @@ describe('unit.test.js', () => { () => EthCrypto.encryptWithPublicKey( AsyncTestUtil.randomString(12), message - ) + ), + 'RangeError' ); }); }); @@ -146,6 +146,72 @@ describe('unit.test.js', () => { }); describe('negative', () => {}); }); + describe('.publicKey', () => { + describe('.compress()', () => { + it('should compress the key', () => { + const uncompressed = 'a34d6aef3eb42335fb3cacb59478c0b44c0bbeb8bb4ca427dbc7044157a5d24b4adf14868d8449c9b3e50d3d6338f3e5a2d3445abe679cddbe75cb893475806f'; + const compressed = EthCrypto.publicKey.compress(uncompressed); + assert.equal(typeof compressed, 'string'); + assert.ok(compressed.startsWith('03')); + }); + it('should also work with trailing 04', () => { + const uncompressed = '04a34d6aef3eb42335fb3cacb59478c0b44c0bbeb8bb4ca427dbc7044157a5d24b4adf14868d8449c9b3e50d3d6338f3e5a2d3445abe679cddbe75cb893475806f'; + const compressed = EthCrypto.publicKey.compress(uncompressed); + assert.equal(typeof compressed, 'string'); + assert.ok(compressed.startsWith('03')); + }); + it('should also work when compressed already given', () => { + const uncompressed = '03a34d6aef3eb42335fb3cacb59478c0b44c0bbeb8bb4ca427dbc7044157a5d24b'; + const compressed = EthCrypto.publicKey.compress(uncompressed); + assert.equal(typeof compressed, 'string'); + assert.ok(compressed.startsWith('03')); + }); + }); + describe('.decompress()', () => { + it('should decompress', () => { + const compressed = '03a34d6aef3eb42335fb3cacb59478c0b44c0bbeb8bb4ca427dbc7044157a5d24b'; + const uncompressed = EthCrypto.publicKey.decompress(compressed); + assert.equal(typeof uncompressed, 'string'); + const buf = new Buffer(uncompressed, 'hex'); + assert.equal(buf.length, 64); + }); + it('should work when already uncompressed', () => { + const compressed = '04a34d6aef3eb42335fb3cacb59478c0b44c0bbeb8bb4ca427dbc7044157a5d24b4adf14868d8449c9b3e50d3d6338f3e5a2d3445abe679cddbe75cb893475806f'; + const uncompressed = EthCrypto.publicKey.decompress(compressed); + assert.equal(typeof uncompressed, 'string'); + const buf = new Buffer(uncompressed, 'hex'); + assert.equal(buf.length, 64); + }); + it('should work when already uncompressed (no04)', () => { + const compressed = 'a34d6aef3eb42335fb3cacb59478c0b44c0bbeb8bb4ca427dbc7044157a5d24b4adf14868d8449c9b3e50d3d6338f3e5a2d3445abe679cddbe75cb893475806f'; + const uncompressed = EthCrypto.publicKey.decompress(compressed); + assert.equal(typeof uncompressed, 'string'); + const buf = new Buffer(uncompressed, 'hex'); + assert.equal(buf.length, 64); + }); + }); + describe('.toAddress()', () => { + describe('positive', () => { + it('should generate the correct address', () => { + const address = EthCrypto.publicKey.toAddress(TEST_DATA.publicKey); + assert.equal(address, TEST_DATA.address); + }); + it('should work with compressed key', () => { + const ident = EthCrypto.createIdentity(); + const compressed = EthCrypto.publicKey.compress(ident.publicKey); + const address = EthCrypto.publicKey.toAddress(compressed); + assert.equal(address, ident.address); + }); + }); + describe('negative', () => { + assert.throws( + () => EthCrypto.publicKey.toAddress( + AsyncTestUtil.randomString(12) + ) + ); + }); + }); + }); describe('.signTransaction()', () => { describe('positive', () => { it('should sign our transaction', () => { diff --git a/typings/index.d.ts b/typings/index.d.ts index ea5376e..0b025f7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -7,8 +7,13 @@ export function createIdentity(): { address: string }; +export type publicKey = { + compress(publicKey: string): string; + decompress(publicKey: string): string; + toAddress(publicKey: string): string; +}; + export function publicKeyByPrivateKey(privateKey: string): string; -export function addressByPublicKey(publicKey: string): string; export type Signature = { v: string, @@ -67,8 +72,6 @@ export type TypedValue = { export type hash = { keccak256(params: TypedValue[]): string; - prefixedHash(msg: string): string; - SIGN_PREFIX: string; }; export type util = { @@ -86,8 +89,8 @@ export type hex = { export function publicKeyToAddress(publicKey: string): string; declare const _default: { - addressByPublicKey, createIdentity, + publicKey, decryptWithPrivateKey, encryptWithPublicKey, publicKeyByPrivateKey,