diff --git a/packages/keyring/src/index.spec.ts b/packages/keyring/src/index.spec.ts index 15b9eb95f..daa98640c 100644 --- a/packages/keyring/src/index.spec.ts +++ b/packages/keyring/src/index.spec.ts @@ -228,13 +228,43 @@ describe('keypair', (): void => { expect(pair.address).toEqual('0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887'); }); + it('encodes a pair toJSON', (): void => { + const pair = keyring.createFromUri(PHRASE); + const json = pair.toJson('password'); + + expect(json.address).toEqual('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077'); + expect(json.encoding).toEqual({ + content: ['pkcs8', 'ethereum'], + type: ['scrypt', 'xsalsa20-poly1305'], + version: '3' + }); + }); + it('allows adding from JSON', (): void => { const pair = keyring.addFromJson( - JSON.parse('{"address":"34wtVv7QQ5NsJQDcyWYnz9J5XrNgYE3VcRfKQ6scSt7o1ixi","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') + JSON.parse('{"address":"KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') ); - expect(pair.publicKey).toEqual(hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')); + expect(pair.publicKey).toEqual(hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')); expect(pair.address).toEqual('0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887'); + + pair.decodePkcs8('password'); + + expect(pair.isLocked).toBe(false); + expect(pair.publicKey).toEqual(hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')); + expect(pair.address).toBe('0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887'); + }); + + it('allows for signing/verification', (): void => { + const MESSAGE = stringToU8a('just some test message'); + const signer = keyring.createFromUri(PHRASE); + const verifier = keyring.addFromJson( + JSON.parse('{"address":"KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou","encoded":"U8qFEaghhmNV2PgFhjqzmhyUy37Ok7abfFU2MNsBd0sAgAAAAQAAAAgAAAA3+NniKogzNphiMNueB1X0sGA07B6CaXWfpXPx45iSXoTTprwzU5mOoSqUWO0GKHROI72LN+uJ8Yfv6Ll6JOOV3VPKfoVoFmYm+zDrrMPa0gk5E5kUuSijxADcE6zUrliPVr0Ix/qaghu5SJ7RtWDQLBf4Hp86SJ8Gg6gTSSk=","encoding":{"content":["pkcs8","ethereum"],"type":["scrypt","xsalsa20-poly1305"],"version":"3"},"meta":{}}') + ); + + const signature = signer.sign(MESSAGE); + + expect(verifier.verify(MESSAGE, signature)).toBe(true); }); }); diff --git a/packages/keyring/src/pair/decode.ts b/packages/keyring/src/pair/decode.ts index 4fb9eb962..061cdef01 100644 --- a/packages/keyring/src/pair/decode.ts +++ b/packages/keyring/src/pair/decode.ts @@ -8,7 +8,7 @@ import { PairInfo } from './types'; import { assert, stringToU8a, u8aFixLength } from '@polkadot/util'; import { naclDecrypt, scryptEncode, scryptFromU8a } from '@polkadot/util-crypto'; -import { NONCE_LENGTH, PKCS8_DIVIDER, PKCS8_HEADER, PUB_LENGTH, SEC_LENGTH, SEED_LENGTH, SCRYPT_LENGTH } from './defaults'; +import { ENCODING, NONCE_LENGTH, PKCS8_DIVIDER, PKCS8_HEADER, PUB_LENGTH, SEC_LENGTH, SEED_LENGTH, SCRYPT_LENGTH } from './defaults'; const SEED_OFFSET = PKCS8_HEADER.length; @@ -43,7 +43,7 @@ function decodePkcs8 (encoded: Uint8Array): DecodeResult { }; } -export default function decode (passphrase?: string, encrypted?: Uint8Array | null, encType: KeyringPair$JsonEncodingTypes[] = ['scrypt', 'xsalsa20-poly1305']): DecodeResult { +export default function decode (passphrase?: string, encrypted?: Uint8Array | null, encType: KeyringPair$JsonEncodingTypes[] = ENCODING): DecodeResult { assert(encrypted, 'No encrypted data available to decode'); assert(passphrase || !encType.includes('xsalsa20-poly1305'), 'Password required to decode encypted data'); diff --git a/packages/keyring/src/pair/defaults.ts b/packages/keyring/src/pair/defaults.ts index c52e52f20..5b4e3df59 100644 --- a/packages/keyring/src/pair/defaults.ts +++ b/packages/keyring/src/pair/defaults.ts @@ -2,6 +2,9 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { KeyringPair$JsonEncodingTypes } from '../types'; + +const ENCODING: KeyringPair$JsonEncodingTypes[] = ['scrypt', 'xsalsa20-poly1305']; const NONCE_LENGTH = 24; const PKCS8_DIVIDER = new Uint8Array([161, 35, 3, 33, 0]); const PKCS8_HEADER = new Uint8Array([48, 83, 2, 1, 1, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]); @@ -12,6 +15,7 @@ const SEED_LENGTH = 32; const SCRYPT_LENGTH = SALT_LENGTH + (3 * 4); export { + ENCODING, NONCE_LENGTH, PKCS8_DIVIDER, PKCS8_HEADER, diff --git a/packages/keyring/src/pair/index.ts b/packages/keyring/src/pair/index.ts index 457cf0a88..b4a7e3afb 100644 --- a/packages/keyring/src/pair/index.ts +++ b/packages/keyring/src/pair/index.ts @@ -6,8 +6,8 @@ import { Keypair, KeypairType } from '@polkadot/util-crypto/types'; import { KeyringPair, KeyringPair$Json, KeyringPair$JsonEncodingTypes, KeyringPair$Meta, SignOptions } from '../types'; import { PairInfo } from './types'; -import { assert, u8aConcat } from '@polkadot/util'; -import { blake2AsU8a, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, naclKeypairFromSeed as naclFromSeed, naclSign, naclVerify, schnorrkelKeypairFromSeed as schnorrkelFromSeed, schnorrkelSign, schnorrkelVerify, secp256k1Compress, secp256k1KeypairFromSeed as secp256k1FromSeed, secp256k1Sign, secp256k1Verify } from '@polkadot/util-crypto'; +import { assert, u8aConcat, u8aToHex } from '@polkadot/util'; +import { blake2AsU8a, ethereumEncode, keccakAsU8a, keyExtractPath, keyFromPath, naclKeypairFromSeed as naclFromSeed, naclSign, naclVerify, schnorrkelKeypairFromSeed as schnorrkelFromSeed, schnorrkelSign, schnorrkelVerify, secp256k1Expand, secp256k1KeypairFromSeed as secp256k1FromSeed, secp256k1Sign, secp256k1Verify, secp256k1Compress } from '@polkadot/util-crypto'; import decode from './decode'; import encode from './encode'; @@ -35,54 +35,28 @@ const TYPE_PREFIX = { }; const TYPE_SIGNATURE = { - ecdsa: (message: Uint8Array, pair: Partial) => - secp256k1Sign(message, pair, 'blake2'), + ecdsa: (m: Uint8Array, p: Partial) => secp256k1Sign(m, p, 'blake2'), ed25519: naclSign, - ethereum: (message: Uint8Array, pair: Partial) => - secp256k1Sign(message, pair, 'keccak'), + ethereum: (m: Uint8Array, p: Partial) => secp256k1Sign(m, p, 'keccak'), sr25519: schnorrkelSign }; +const TYPE_ADDRESS = { + ecdsa: (p: Uint8Array) => p.length > 32 ? blake2AsU8a(p) : p, + ed25519: (p: Uint8Array) => p, + ethereum: (p: Uint8Array) => keccakAsU8a(secp256k1Expand(p)), + sr25519: (p: Uint8Array) => p +}; + const TYPE_VERIFY = { - ecdsa: (message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array) => - secp256k1Verify(message, signature, blake2AsU8a(publicKey), 'blake2'), + ecdsa: (m: Uint8Array, s: Uint8Array, p: Uint8Array) => secp256k1Verify(m, s, TYPE_ADDRESS.ecdsa(p), 'blake2'), ed25519: naclVerify, - ethereum: (message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array) => - secp256k1Verify(message, signature, keccakAsU8a(publicKey), 'keccak'), + ethereum: (m: Uint8Array, s: Uint8Array, p: Uint8Array) => secp256k1Verify(m, s, TYPE_ADDRESS.ethereum(p), 'keccak', true), sr25519: schnorrkelVerify }; function isEmpty (u8a: Uint8Array): boolean { - return u8a.reduce((count, u8): number => count + u8, 0) === 0; -} - -function fromSeed (type: KeypairType, seed: Uint8Array): Keypair { - return TYPE_FROM_SEED[type](seed); -} - -function sign (type: KeypairType, message: Uint8Array, pair: Partial, { withType = false }: SignOptions = {}): Uint8Array { - return u8aConcat( - // for multi-signatures, i.e. with indicator, append the signature type as per - // the MultiSignature enum - withType - ? TYPE_PREFIX[type] - : SIG_TYPE_NONE, - TYPE_SIGNATURE[type](message, pair) - ); -} - -function verify (type: KeypairType, message: Uint8Array, signature: Uint8Array, publicKey: Uint8Array): boolean { - return TYPE_VERIFY[type](message, signature, publicKey); -} - -function getAddress (type: KeypairType, publicKey: Uint8Array, isCompressed = false): Uint8Array { - if (type === 'ecdsa' && publicKey.length > 32) { - return blake2AsU8a(publicKey); - } else if (type === 'ethereum' && isCompressed) { - return secp256k1Compress(publicKey).slice(-32); - } - - return publicKey; + return u8a.reduce((count, u8) => count + u8, 0) === 0; } // Not 100% correct, since it can be a Uint8Array, but an invalid one - just say "undefined" is anything non-valid @@ -129,11 +103,14 @@ export default function createPair ({ toSS58, type }: Setup, { publicKey, secret return encoded; }; + const encodeAddress = (): string => + type === 'ethereum' + ? ethereumEncode(TYPE_ADDRESS[type](publicKey)) + : toSS58(TYPE_ADDRESS[type](publicKey)); + return { get address (): string { - return type === 'ethereum' - ? ethereumEncode(getAddress(type, publicKey)) - : toSS58(getAddress(type, publicKey)); + return encodeAddress(); }, get isLocked (): boolean { return isLocked(secretKey); @@ -155,7 +132,7 @@ export default function createPair ({ toSS58, type }: Setup, { publicKey, secret publicKey = decoded.publicKey; secretKey = decoded.secretKey; } else { - const pair = fromSeed(type, decoded.secretKey); + const pair = TYPE_FROM_SEED[type](decoded.secretKey); publicKey = pair.publicKey; secretKey = pair.secretKey; @@ -177,14 +154,24 @@ export default function createPair ({ toSS58, type }: Setup, { publicKey, secret setMeta: (additional: KeyringPair$Meta): void => { meta = { ...meta, ...additional }; }, - sign: (message: Uint8Array, options?: SignOptions): Uint8Array => { + sign: (message: Uint8Array, options: SignOptions = {}): Uint8Array => { assert(!isLocked(secretKey), 'Cannot sign with a locked key pair'); - return sign(type, message, { publicKey, secretKey }, options); + return u8aConcat( + options.withType + ? TYPE_PREFIX[type] + : SIG_TYPE_NONE, + TYPE_SIGNATURE[type](message, { publicKey, secretKey }) + ); + }, + toJson: (passphrase?: string): KeyringPair$Json => { + const address = ['ecdsa', 'ethereum'].includes(type) + ? u8aToHex(secp256k1Compress(publicKey)) + : encodeAddress(); + + return toJson(type, { address, meta }, recode(passphrase), !!passphrase); }, - toJson: (passphrase?: string): KeyringPair$Json => - toJson(type, { address: toSS58(getAddress(type, publicKey, true)), meta }, recode(passphrase), !!passphrase), verify: (message: Uint8Array, signature: Uint8Array): boolean => - verify(type, message, signature, publicKey) + TYPE_VERIFY[type](message, signature, publicKey) }; } diff --git a/packages/keyring/src/pair/toJson.ts b/packages/keyring/src/pair/toJson.ts index 47ffc7fb8..d48cd4699 100644 --- a/packages/keyring/src/pair/toJson.ts +++ b/packages/keyring/src/pair/toJson.ts @@ -3,10 +3,12 @@ // of the Apache-2.0 license. See the LICENSE file for details. import { KeypairType } from '@polkadot/util-crypto/types'; -import { KeyringPair$Json, KeyringPair$Meta } from '../types'; +import { KeyringPair$Json, KeyringPair$JsonEncodingTypes, KeyringPair$Meta } from '../types'; import { base64Encode } from '@polkadot/util-crypto'; +import { ENCODING } from './defaults'; + interface PairStateJson { address: string; meta: KeyringPair$Meta; @@ -14,6 +16,9 @@ interface PairStateJson { // version 2 - nonce, encoded (previous) // version 3 - salt, nonce, encoded +const VERSION = '3'; + +const ENC_NONE: KeyringPair$JsonEncodingTypes[] = ['none']; export default function toJson (type: KeypairType, { address, meta }: PairStateJson, encoded: Uint8Array, isEncrypted: boolean): KeyringPair$Json { return { @@ -22,9 +27,9 @@ export default function toJson (type: KeypairType, { address, meta }: PairStateJ encoding: { content: ['pkcs8', type], type: isEncrypted - ? ['scrypt', 'xsalsa20-poly1305'] - : ['none'], - version: '3' + ? ENCODING + : ENC_NONE, + version: VERSION }, meta }; diff --git a/packages/util-crypto/src/address/checkChecksum.ts b/packages/util-crypto/src/address/checkChecksum.ts index b5c52353d..af5e53acc 100644 --- a/packages/util-crypto/src/address/checkChecksum.ts +++ b/packages/util-crypto/src/address/checkChecksum.ts @@ -5,7 +5,7 @@ import sshash from './sshash'; export default function checkChecksum (decoded: Uint8Array): [boolean, number] { - const isPublicKey = decoded.length === 35; + const isPublicKey = [35, 36].includes(decoded.length); // non-publicKeys has 1 byte checksums, else default to 2 const length = decoded.length - (isPublicKey ? 2 : 1); diff --git a/packages/util-crypto/src/address/decode.spec.ts b/packages/util-crypto/src/address/decode.spec.ts index e411a03c2..b0f0e6d32 100644 --- a/packages/util-crypto/src/address/decode.spec.ts +++ b/packages/util-crypto/src/address/decode.spec.ts @@ -2,7 +2,7 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import { stringToU8a, u8aToHex } from '@polkadot/util'; +import { stringToU8a, u8aToHex, hexToU8a } from '@polkadot/util'; import testingPairs, { TestKeyringMap } from '../../../keyring/src/testingPairs'; import decode from './decode'; @@ -74,6 +74,14 @@ describe('decodeAddress', (): void => { ).toEqual(new Uint8Array([42, 44, 10, 0, 0, 0, 0, 0])); }); + it('decodes a 33-byte address', (): void => { + expect( + decode('KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou') + ).toEqual( + hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077') + ); + }); + it.skip('allows invalid prefix (in list)', (): void => { expect( (): Uint8Array => decode('6GfvWUvHvU8otbZ7sFhXH4eYeMcKdUkL61P3nFy52efEPVUx') diff --git a/packages/util-crypto/src/address/defaults.ts b/packages/util-crypto/src/address/defaults.ts index fb0bd1e30..1f6090b23 100644 --- a/packages/util-crypto/src/address/defaults.ts +++ b/packages/util-crypto/src/address/defaults.ts @@ -5,9 +5,9 @@ import { allowedSS58 } from './ss58'; const defaults = { - allowedDecodedLengths: [1, 2, 4, 8, 32], + allowedDecodedLengths: [1, 2, 4, 8, 32, 33], // publicKey has prefix + 2 checksum bytes, short only prefix + 1 checksum byte - allowedEncodedLengths: [3, 4, 6, 10, 35], + allowedEncodedLengths: [3, 4, 6, 10, 35, 36], allowedPrefix: allowedSS58, prefix: 42 }; diff --git a/packages/util-crypto/src/address/encode.spec.ts b/packages/util-crypto/src/address/encode.spec.ts index 74b65f5cb..885ba0dfd 100644 --- a/packages/util-crypto/src/address/encode.spec.ts +++ b/packages/util-crypto/src/address/encode.spec.ts @@ -67,11 +67,17 @@ describe('encode', (): void => { ).toEqual('as7QnGMf'); }); - it('enodes a 8-byte address', (): void => { + it('encodes a 8-byte address', (): void => { expect( encode( new Uint8Array([42, 44, 10, 0, 0, 0, 0, 0]), 68 ) ).toEqual('4q7qY5RBG7Z4wv'); }); + + it('encodes an 33-byte address', (): void => { + expect( + encode('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077') + ).toEqual('KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou'); + }); }); diff --git a/packages/util-crypto/src/address/encode.ts b/packages/util-crypto/src/address/encode.ts index 8281c46f3..574119845 100644 --- a/packages/util-crypto/src/address/encode.ts +++ b/packages/util-crypto/src/address/encode.ts @@ -19,7 +19,7 @@ export default function encode (_key: Uint8Array | string, ss58Format: Prefix = assert(defaults.allowedDecodedLengths.includes(key.length), `Expected a valid key to convert, with length ${defaults.allowedDecodedLengths.join(', ')}`); - const isPublicKey = key.length === 32; + const isPublicKey = [32, 33].includes(key.length); const input = u8aConcat(new Uint8Array([ss58Format]), key); const hash = sshash(input); diff --git a/packages/util-crypto/src/ethereum/encode.spec.ts b/packages/util-crypto/src/ethereum/encode.spec.ts index 3b3253aa3..396bc1bde 100644 --- a/packages/util-crypto/src/ethereum/encode.spec.ts +++ b/packages/util-crypto/src/ethereum/encode.spec.ts @@ -2,6 +2,7 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. +import { keccakAsU8a } from '../keccak'; import { ethereumEncode } from './'; describe('formatAddress', () => { @@ -35,12 +36,6 @@ describe('formatAddress', () => { const ADDRESS = '0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887'; it('encodes a compressed publicKey', (): void => { - expect( - ethereumEncode('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077') - ).toEqual(ADDRESS); - }); - - it('encodes a compressed publicKey (indicator)', (): void => { expect( ethereumEncode('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077') ).toEqual(ADDRESS); @@ -48,13 +43,15 @@ describe('formatAddress', () => { it('encodes an expanded publicKey', (): void => { expect( - ethereumEncode('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af') + ethereumEncode('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af') ).toEqual(ADDRESS); }); - it('encodes an expanded publicKey (indicator + x/y)', (): void => { + it('encodes a pre-hashed key', (): void => { expect( - ethereumEncode('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af') + ethereumEncode( + keccakAsU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af') + ) ).toEqual(ADDRESS); }); }); diff --git a/packages/util-crypto/src/ethereum/encode.ts b/packages/util-crypto/src/ethereum/encode.ts index d026d2c78..b9f33e22a 100644 --- a/packages/util-crypto/src/ethereum/encode.ts +++ b/packages/util-crypto/src/ethereum/encode.ts @@ -7,20 +7,24 @@ import { assert, u8aToHex, u8aToU8a } from '@polkadot/util'; import { keccakAsU8a } from '../keccak'; import { secp256k1Expand } from '../secp256k1'; +function getH160 (u8a: Uint8Array): Uint8Array { + if ([33, 65].includes(u8a.length)) { + u8a = keccakAsU8a(secp256k1Expand(u8a)); + } + + return u8a.slice(-20); +} + export default function ethereumEncode (addressOrPublic?: string | Uint8Array): string { if (!addressOrPublic) { return '0x'; } - let u8aAddress = u8aToU8a(addressOrPublic); + const u8aAddress = u8aToU8a(addressOrPublic); - assert([20, 32, 33, 64, 65].includes(u8aAddress.length), 'Invalid address or publicKey passed'); + assert([20, 32, 33, 65].includes(u8aAddress.length), 'Invalid address or publicKey passed'); - if (u8aAddress.length > 20) { - u8aAddress = keccakAsU8a(secp256k1Expand(u8aAddress)).slice(-20); - } - - const address = u8aToHex(u8aAddress, -1, false); + const address = u8aToHex(getH160(u8aAddress), -1, false); const hash = u8aToHex(keccakAsU8a(address), -1, false); let result = ''; diff --git a/packages/util-crypto/src/secp256k1/compress.spec.ts b/packages/util-crypto/src/secp256k1/compress.spec.ts index 905f7c7d7..f64c51fcf 100644 --- a/packages/util-crypto/src/secp256k1/compress.spec.ts +++ b/packages/util-crypto/src/secp256k1/compress.spec.ts @@ -10,7 +10,7 @@ describe('secp256k1Compress', (): void => { it('compresses a known key', (): void => { expect( secp256k1Compress( - hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af') + hexToU8a('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af') ) ).toEqual( hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077') diff --git a/packages/util-crypto/src/secp256k1/compress.ts b/packages/util-crypto/src/secp256k1/compress.ts index 064762afc..dbaf03ded 100644 --- a/packages/util-crypto/src/secp256k1/compress.ts +++ b/packages/util-crypto/src/secp256k1/compress.ts @@ -3,16 +3,15 @@ // of the Apache-2.0 license. See the LICENSE file for details. import elliptic from 'elliptic'; - -import sanitizePublic from './sanitizePublic'; +import { assert } from '@polkadot/util'; const EC = elliptic.ec; const ec = new EC('secp256k1'); export default function secp256k1Compress (publicKey: Uint8Array): Uint8Array { + assert([33, 65].includes(publicKey.length), 'Invalid publicKey provided'); + return new Uint8Array( - ec.keyFromPublic( - sanitizePublic(publicKey) - ).getPublic().encodeCompressed() + ec.keyFromPublic(publicKey).getPublic().encodeCompressed() ); } diff --git a/packages/util-crypto/src/secp256k1/expand.spec.ts b/packages/util-crypto/src/secp256k1/expand.spec.ts index 2d4de7a92..8c8de3e99 100644 --- a/packages/util-crypto/src/secp256k1/expand.spec.ts +++ b/packages/util-crypto/src/secp256k1/expand.spec.ts @@ -17,16 +17,6 @@ describe('secp256k1Expand', (): void => { ); }); - it('expands a known key (no indicator)', (): void => { - expect( - secp256k1Expand( - hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077') - ) - ).toEqual( - hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af') - ); - }); - it('expands a known full key', (): void => { expect( secp256k1Expand( @@ -36,14 +26,4 @@ describe('secp256k1Expand', (): void => { hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af') ); }); - - it('expands a known full key (no indicator)', (): void => { - expect( - secp256k1Expand( - hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af') - ) - ).toEqual( - hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af') - ); - }); }); diff --git a/packages/util-crypto/src/secp256k1/expand.ts b/packages/util-crypto/src/secp256k1/expand.ts index 027d8f518..9a45443a0 100644 --- a/packages/util-crypto/src/secp256k1/expand.ts +++ b/packages/util-crypto/src/secp256k1/expand.ts @@ -3,15 +3,15 @@ // of the Apache-2.0 license. See the LICENSE file for details. import elliptic from 'elliptic'; -import { bnToU8a, u8aConcat } from '@polkadot/util'; - -import sanitizePublic from './sanitizePublic'; +import { assert, bnToU8a, u8aConcat } from '@polkadot/util'; const EC = elliptic.ec; const ec = new EC('secp256k1'); export default function secp256k1Expand (publicKey: Uint8Array): Uint8Array { - const expanded = ec.keyFromPublic(sanitizePublic(publicKey)).getPublic(); + assert([33, 65].includes(publicKey.length), 'Invalid publicKey provided'); + + const expanded = ec.keyFromPublic(publicKey).getPublic(); return u8aConcat( bnToU8a(expanded.getX(), { bitLength: 256, isLe: false }), diff --git a/packages/util-crypto/src/secp256k1/sanitizePublic.ts b/packages/util-crypto/src/secp256k1/sanitizePublic.ts deleted file mode 100644 index daba63b21..000000000 --- a/packages/util-crypto/src/secp256k1/sanitizePublic.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2017-2020 @polkadot/util-crypto authors & contributors -// This software may be modified and distributed under the terms -// of the Apache-2.0 license. See the LICENSE file for details. - -import { assert, u8aConcat } from '@polkadot/util'; - -const INDICATOR_COMP = new Uint8Array([3]); -const INDICATOR_FULL = new Uint8Array([4]); - -export default function sanitizePublic (publicKey: Uint8Array): Uint8Array { - assert([32, 33, 64, 65].includes(publicKey.length), 'Invalid publicKey provided'); - - return [33, 65].includes(publicKey.length) - ? publicKey - : publicKey.length === 32 - ? u8aConcat(INDICATOR_COMP, publicKey) - : u8aConcat(INDICATOR_FULL, publicKey); -} diff --git a/packages/util-crypto/src/secp256k1/verify.ts b/packages/util-crypto/src/secp256k1/verify.ts index 39c894e7f..4f01c6828 100644 --- a/packages/util-crypto/src/secp256k1/verify.ts +++ b/packages/util-crypto/src/secp256k1/verify.ts @@ -5,8 +5,9 @@ import { HashType } from './types'; import elliptic from 'elliptic'; -import { assert, u8aToU8a } from '@polkadot/util'; +import { assert, u8aEq, u8aToU8a } from '@polkadot/util'; +import secp256k1Expand from './expand'; import hasher from './hasher'; const EC = elliptic.ec; @@ -16,21 +17,19 @@ const ec = new EC('secp256k1'); * @name secp256k1Verify * @description Verifies the signature of `message`, using the supplied pair */ -export default function secp256k1Verify (message: Uint8Array | string, signature: Uint8Array | string, address: Uint8Array | string, hashType: HashType = 'blake2'): boolean { - const signatureU8a = u8aToU8a(signature); +export default function secp256k1Verify (message: Uint8Array | string, signature: Uint8Array | string, address: Uint8Array | string, hashType: HashType = 'blake2', isExpanded = false): boolean { + const u8a = u8aToU8a(signature); - assert(signatureU8a.length === 65, `Expected signature with 65 bytes, ${signatureU8a.length} found instead`); + assert(u8a.length === 65, `Expected signature with 65 bytes, ${u8a.length} found instead`); - const sig = { - r: signatureU8a.slice(0, 32), - s: signatureU8a.slice(32, 64) - }; - const recovery = signatureU8a[64]; const publicKey = new Uint8Array( // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - ec.recoverPubKey(hasher(hashType, message), sig, recovery) + ec.recoverPubKey(hasher(hashType, message), { r: u8a.slice(0, 32), s: u8a.slice(32, 64) }, u8a[64]) .encodeCompressed() ); - return Buffer.compare(hasher(hashType, publicKey), u8aToU8a(address)) === 0; + return u8aEq( + hasher(hashType, isExpanded ? secp256k1Expand(publicKey) : publicKey), + u8aToU8a(address) + ); }