util.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. /* eslint-disable @typescript-eslint/camelcase */
  2. // Copyright 2017-2019 @polkadot/app-123code authors & contributors
  3. // This software may be modified and distributed under the terms
  4. // of the Apache-2.0 license. See the LICENSE file for details.
  5. import { EthereumAddress, EcdsaSignature } from '@polkadot/types/interfaces';
  6. import secp256k1 from 'secp256k1/elliptic';
  7. import { createType } from '@polkadot/types';
  8. import { assert, hexToU8a, stringToU8a, u8aToBuffer, u8aConcat } from '@polkadot/util';
  9. import { keccakAsHex, keccakAsU8a } from '@polkadot/util-crypto';
  10. interface RecoveredSignature {
  11. error: Error | null;
  12. ethereumAddress: EthereumAddress | null;
  13. signature: EcdsaSignature | null;
  14. }
  15. interface SignatureParts {
  16. recovery: number;
  17. signature: Buffer;
  18. }
  19. // converts an Ethereum address to a checksum representation
  20. export function addrToChecksum (_address: string): string {
  21. const address = _address.toLowerCase();
  22. const hash = keccakAsHex(address.substr(2)).substr(2);
  23. let result = '0x';
  24. for (let n = 0; n < 40; n++) {
  25. result = `${result}${
  26. parseInt(hash[n], 16) > 7
  27. ? address[n + 2].toUpperCase()
  28. : address[n + 2]
  29. }`;
  30. }
  31. return result;
  32. }
  33. // convert a give public key to an Ethereum address (the last 20 bytes of an _exapnded_ key keccack)
  34. export function publicToAddr (publicKey: Uint8Array): string {
  35. return addrToChecksum(`0x${keccakAsHex(publicKey).slice(-40)}`);
  36. }
  37. // hash a message for use in signature recovery, adding the standard Ethereum header
  38. export function hashMessage (message: string): Buffer {
  39. const expanded = stringToU8a(`\x19Ethereum Signed Message:\n${message.length.toString()}${message}`);
  40. const hashed = keccakAsU8a(expanded);
  41. return u8aToBuffer(hashed);
  42. }
  43. // split is 65-byte signature into the r, s (combined) and recovery number (derived from v)
  44. export function sigToParts (_signature: string): SignatureParts {
  45. const signature = hexToU8a(_signature);
  46. assert(signature.length === 65, `Invalid signature length, expected 65 found ${signature.length}`);
  47. let v = signature[64];
  48. if (v < 27) {
  49. v += 27;
  50. }
  51. const recovery = v - 27;
  52. assert(recovery === 0 || recovery === 1, 'Invalid signature v value');
  53. return {
  54. recovery,
  55. signature: u8aToBuffer(signature.slice(0, 64))
  56. };
  57. }
  58. // recover an address from a given message and a recover/signature combination
  59. export function recoverAddress (message: string, { recovery, signature }: SignatureParts): string {
  60. const msgHash = hashMessage(message);
  61. const senderPubKey = secp256k1.recover(msgHash, signature, recovery);
  62. return publicToAddr(
  63. secp256k1.publicKeyConvert(senderPubKey, false).slice(1)
  64. );
  65. }
  66. // recover an address from a signature JSON (as supplied by e.g. MyCrypto)
  67. export function recoverFromJSON (signatureJson: string | null): RecoveredSignature {
  68. try {
  69. const { msg, sig } = JSON.parse(signatureJson || '{}');
  70. if (!msg || !sig) {
  71. throw new Error('Invalid signature object');
  72. }
  73. const parts = sigToParts(sig);
  74. return {
  75. error: null,
  76. ethereumAddress: createType('EthereumAddress', recoverAddress(msg, parts)),
  77. signature: createType('EcdsaSignature', u8aConcat(parts.signature, new Uint8Array([parts.recovery])))
  78. };
  79. } catch (error) {
  80. console.error(error);
  81. return {
  82. error,
  83. ethereumAddress: null,
  84. signature: null
  85. };
  86. }
  87. }