diff --git a/README.md b/README.md index 11cd619..393abdc 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,20 @@ Or add a checksum to an existing address: Eth::Utils.format_address "0x4bc787699093f11316e819b5692be04a712c4e69" # => "0x4bc787699093f11316e819B5692be04A712C4E69" ``` +### Personal Signatures + +You can recover public keys and generate web3/metamask-compatible signatures: + +```ruby +# Generate signature +key.personal_sign('hello world') + +# Recover signature +message = 'test' +signature = '0x3eb24bd327df8c2b614c3f652ec86efe13aa721daf203820241c44861a26d37f2bffc6e03e68fc4c3d8d967054c9cb230ed34339b12ef89d512b42ae5bf8c2ae1c' +Eth::Key.personal_recover(message, signature) # => 043e5b33f0080491e21f9f5f7566de59a08faabf53edbc3c32aaacc438552b25fdde531f8d1053ced090e9879cbf2b0d1c054e4b25941dab9254d2070f39418afc +``` + ### Configure In order to prevent replay attacks, you must specify which Ethereum chain your transactions are created for. See [EIP 155](https://github.com/ethereum/EIPs/issues/155) for more detail. diff --git a/lib/eth/key.rb b/lib/eth/key.rb index fa6949a..d9e8da7 100644 --- a/lib/eth/key.rb +++ b/lib/eth/key.rb @@ -16,6 +16,10 @@ def self.decrypt(data, password) new priv: priv end + def self.personal_recover(message, signature) + bin_signature = Utils.hex_to_bin(signature).bytes.rotate(-1).pack('c*') + OpenSsl.recover_compact(Utils.keccak256(Utils.prefix_message(message)), bin_signature) + end def initialize(priv: nil) @private_key = MoneyTree::PrivateKey.new key: priv @@ -55,6 +59,10 @@ def verify_signature(message, signature) public_hex == OpenSsl.recover_compact(hash, signature) end + def personal_sign(message) + Utils.bin_to_hex(sign(Utils.prefix_message(message)).bytes.rotate(1).pack('c*')) + end + private diff --git a/lib/eth/utils.rb b/lib/eth/utils.rb index 0ad6588..ece7369 100644 --- a/lib/eth/utils.rb +++ b/lib/eth/utils.rb @@ -51,6 +51,10 @@ def bin_to_prefixed_hex(binary) prefix_hex bin_to_hex(binary) end + def prefix_message(message) + "\x19Ethereum Signed Message:\n#{message.length}#{message}" + end + def public_key_to_address(hex) bytes = hex_to_bin(hex) address_bytes = Utils.keccak256(bytes[1..-1])[-20..-1] diff --git a/spec/eth/key_spec.rb b/spec/eth/key_spec.rb index 64c165b..86b9df6 100644 --- a/spec/eth/key_spec.rb +++ b/spec/eth/key_spec.rb @@ -48,6 +48,29 @@ end end + describe "#personal_sign" do + let(:message) { "Hi Mom!" } + + it "signs a message so that the public key can be recovered with personal_recover" do + 10.times do + signature = key.personal_sign message + expect(Eth::Key.personal_recover message, signature).to eq(key.public_hex) + end + end + end + + describe ".personal_recover" do + let(:message) { "test" } + let(:signature) { "3eb24bd327df8c2b614c3f652ec86efe13aa721daf203820241c44861a26d37f2bffc6e03e68fc4c3d8d967054c9cb230ed34339b12ef89d512b42ae5bf8c2ae1c" } + let(:public_hex) { "043e5b33f0080491e21f9f5f7566de59a08faabf53edbc3c32aaacc438552b25fdde531f8d1053ced090e9879cbf2b0d1c054e4b25941dab9254d2070f39418afc" } + + it "it can recover a public key from a signature generated with web3/metamask" do + 10.times do + expect(Eth::Key.personal_recover message, signature).to eq(public_hex) + end + end + end + describe "#verify_signature" do let(:priv) { '5a37533acfa3ff9386aed01e16c0e7a79038ce05cc383e290d360b8ce9cd6fdf' } let(:signature) { hex_to_bin "1ce2f13b4123a23a4a280ac4adcba1ffa3f3848f494dc1de440af43f677e0e01260fb4667ed117d555659b249702c8215162b3f0ee09628813a4ef83616f99f180" }