diff --git a/CMakeLists.txt b/CMakeLists.txt index 2de569aca17..c6100be0d66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1400,7 +1400,6 @@ else () subdir: crypto #]===============================] src/ripple/crypto/impl/GenerateDeterministicKey.cpp - src/ripple/crypto/impl/KeyType.cpp src/ripple/crypto/impl/RFC1751.cpp src/ripple/crypto/impl/csprng.cpp src/ripple/crypto/impl/ec_key.cpp diff --git a/src/ripple/crypto/KeyType.h b/src/ripple/crypto/KeyType.h index 9e2f809ae50..32195c7647d 100644 --- a/src/ripple/crypto/KeyType.h +++ b/src/ripple/crypto/KeyType.h @@ -21,21 +21,41 @@ #define RIPPLE_CRYPTO_KEYTYPE_H_INCLUDED #include +#include namespace ripple { enum class KeyType { - invalid = -1, - unknown = invalid, - secp256k1 = 0, ed25519 = 1, }; -KeyType keyTypeFromString (std::string const& s); +inline +boost::optional +keyTypeFromString (std::string const& s) +{ + if (s == "secp256k1") + return KeyType::secp256k1; + + if (s == "ed25519") + return KeyType::ed25519; + + return {}; +} -const char* to_string (KeyType type); +inline +char const* +to_string (KeyType type) +{ + if (type == KeyType::secp256k1) + return "secp256k1"; + + if (type == KeyType::ed25519) + return "ed25519"; + + return "INVALID"; +} template inline diff --git a/src/ripple/crypto/impl/KeyType.cpp b/src/ripple/crypto/impl/KeyType.cpp deleted file mode 100644 index 9d0df6d94bd..00000000000 --- a/src/ripple/crypto/impl/KeyType.cpp +++ /dev/null @@ -1,39 +0,0 @@ -//------------------------------------------------------------------------------ -/* - This file is part of rippled: https://github.com/ripple/rippled - Copyright (c) 2015 Ripple Labs Inc. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted, provided that the above - copyright notice and this permission notice appear in all copies. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -*/ -//============================================================================== - -#include - -namespace ripple { - -KeyType keyTypeFromString (std::string const& s) -{ - if (s == "secp256k1") return KeyType::secp256k1; - if (s == "ed25519" ) return KeyType::ed25519; - - return KeyType::invalid; -} - -const char* to_string (KeyType type) -{ - return type == KeyType::secp256k1 ? "secp256k1" - : type == KeyType::ed25519 ? "ed25519" - : "INVALID"; -} - -} diff --git a/src/ripple/rpc/handlers/WalletPropose.cpp b/src/ripple/rpc/handlers/WalletPropose.cpp index a9f0e88109c..bb3277ec1c4 100644 --- a/src/ripple/rpc/handlers/WalletPropose.cpp +++ b/src/ripple/rpc/handlers/WalletPropose.cpp @@ -70,9 +70,9 @@ Json::Value doWalletPropose (RPC::Context& context) Json::Value walletPropose (Json::Value const& params) { + boost::optional keyType; boost::optional seed; - - KeyType keyType = KeyType::secp256k1; + bool rippleLibSeed = false; if (params.isMember (jss::key_type)) { @@ -85,25 +85,55 @@ Json::Value walletPropose (Json::Value const& params) keyType = keyTypeFromString ( params[jss::key_type].asString()); - if (keyType == KeyType::invalid) + if (!keyType) return rpcError(rpcINVALID_PARAMS); } - if (params.isMember (jss::passphrase) || - params.isMember (jss::seed) || - params.isMember (jss::seed_hex)) + // ripple-lib encodes seed used to generate an Ed25519 wallet in a + // non-standard way. While we never encode seeds that way, we try + // to detect such keys to avoid user confusion. { - Json::Value err; - seed = RPC::getSeedFromRPC (params, err); - if (!seed) - return err; + if (params.isMember(jss::passphrase)) + seed = RPC::parseRippleLibSeed(params[jss::passphrase]); + else if (params.isMember(jss::seed)) + seed = RPC::parseRippleLibSeed(params[jss::seed]); + + if(seed) + { + rippleLibSeed = true; + + // If the user *explicitly* requests a key type other than + // Ed25519 we return an error. + if (keyType.value_or(KeyType::ed25519) != KeyType::ed25519) + return rpcError(rpcBAD_SEED); + + keyType = KeyType::ed25519; + } } - else + + if (!seed) { - seed = randomSeed (); + if (params.isMember(jss::passphrase) || + params.isMember(jss::seed) || + params.isMember(jss::seed_hex)) + { + Json::Value err; + + seed = RPC::getSeedFromRPC(params, err); + + if (!seed) + return err; + } + else + { + seed = randomSeed(); + } } - auto const publicKey = generateKeyPair (keyType, *seed).first; + if (!keyType) + keyType = KeyType::secp256k1; + + auto const publicKey = generateKeyPair (*keyType, *seed).first; Json::Value obj (Json::objectValue); @@ -116,13 +146,13 @@ Json::Value walletPropose (Json::Value const& params) obj[jss::master_key] = seed1751; obj[jss::account_id] = toBase58(calcAccountID(publicKey)); obj[jss::public_key] = toBase58(TokenType::AccountPublic, publicKey); - obj[jss::key_type] = to_string (keyType); + obj[jss::key_type] = to_string (*keyType); obj[jss::public_key_hex] = strHex (publicKey); // If a passphrase was specified, and it was hashed and used as a seed // run a quick entropy check and add an appropriate warning, because // "brain wallets" can be easily attacked. - if (params.isMember (jss::passphrase)) + if (!rippleLibSeed && params.isMember (jss::passphrase)) { auto const passphrase = params[jss::passphrase].asString(); diff --git a/src/ripple/rpc/impl/RPCHelpers.cpp b/src/ripple/rpc/impl/RPCHelpers.cpp index 1d59d4268ae..00a83cedec4 100644 --- a/src/ripple/rpc/impl/RPCHelpers.cpp +++ b/src/ripple/rpc/impl/RPCHelpers.cpp @@ -516,6 +516,25 @@ readLimitField(unsigned int& limit, Tuning::LimitRange const& range, return boost::none; } +boost::optional +parseRippleLibSeed(Json::Value const& value) +{ + // ripple-lib encodes seed used to generate an Ed25519 wallet in a + // non-standard way. While rippled never encode seeds that way, we + // try to detect such keys to avoid user confusion. + if (!value.isString()) + return boost::none; + + auto const result = decodeBase58Token(value.asString(), TokenType::None); + + if (result.size() == 18 && + static_cast(result[0]) == std::uint8_t(0xE1) && + static_cast(result[1]) == std::uint8_t(0x4B)) + return Seed(makeSlice(result.substr(2))); + + return boost::none; +} + boost::optional getSeedFromRPC(Json::Value const& params, Json::Value& error) { @@ -623,7 +642,7 @@ keypairForSignature(Json::Value const& params, Json::Value& error) return { }; } - KeyType keyType = KeyType::secp256k1; + boost::optional keyType; boost::optional seed; if (has_key_type) @@ -635,10 +654,9 @@ keypairForSignature(Json::Value const& params, Json::Value& error) return { }; } - keyType = keyTypeFromString ( - params[jss::key_type].asString()); + keyType = keyTypeFromString(params[jss::key_type].asString()); - if (keyType == KeyType::invalid) + if (!keyType) { error = RPC::invalid_field_error(jss::key_type); return { }; @@ -651,20 +669,47 @@ keypairForSignature(Json::Value const& params, Json::Value& error) std::string(jss::key_type) + " is used."); return { }; } - - seed = getSeedFromRPC (params, error); } - else + + // ripple-lib encodes seed used to generate an Ed25519 wallet in a + // non-standard way. While we never encode seeds that way, we try + // to detect such keys to avoid user confusion. + if (secretType != jss::seed_hex.c_str()) { - if (! params[jss::secret].isString()) + seed = RPC::parseRippleLibSeed(params[secretType]); + + if (seed) { - error = RPC::expected_field_error ( - jss::secret, "string"); - return { }; + // If the user passed in an Ed25519 seed but *explicitly* + // requested another key type, return an error. + if (keyType.value_or(KeyType::ed25519) != KeyType::ed25519) + { + error = RPC::make_error (rpcBAD_SEED, + "Specified seed is for an Ed25519 wallet."); + return { }; + } + + keyType = KeyType::ed25519; } + } + + if (!keyType) + keyType = KeyType::secp256k1; + + if (!seed) + { + if (has_key_type) + seed = getSeedFromRPC(params, error); + else + { + if (!params[jss::secret].isString()) + { + error = RPC::expected_field_error(jss::secret, "string"); + return {}; + } - seed = parseGenericSeed ( - params[jss::secret].asString ()); + seed = parseGenericSeed(params[jss::secret].asString()); + } } if (!seed) @@ -672,7 +717,7 @@ keypairForSignature(Json::Value const& params, Json::Value& error) if (!contains_error (error)) { error = RPC::make_error (rpcBAD_SEED, - RPC::invalid_field_message (secretType)); + RPC::invalid_field_message (secretType)); } return { }; @@ -681,7 +726,7 @@ keypairForSignature(Json::Value const& params, Json::Value& error) if (keyType != KeyType::secp256k1 && keyType != KeyType::ed25519) LogicError ("keypairForSignature: invalid key type"); - return generateKeyPair (keyType, *seed); + return generateKeyPair (*keyType, *seed); } std::pair diff --git a/src/ripple/rpc/impl/RPCHelpers.h b/src/ripple/rpc/impl/RPCHelpers.h index 11471faf545..74dc6b166bf 100644 --- a/src/ripple/rpc/impl/RPCHelpers.h +++ b/src/ripple/rpc/impl/RPCHelpers.h @@ -112,6 +112,9 @@ readLimitField(unsigned int& limit, Tuning::LimitRange const&, Context const&); boost::optional getSeedFromRPC(Json::Value const& params, Json::Value& error); +boost::optional +parseRippleLibSeed(Json::Value const& params); + std::pair keypairForSignature(Json::Value const& params, Json::Value& error); diff --git a/src/ripple/unity/crypto.cpp b/src/ripple/unity/crypto.cpp index cbd27db37cf..4529a601e94 100644 --- a/src/ripple/unity/crypto.cpp +++ b/src/ripple/unity/crypto.cpp @@ -20,7 +20,6 @@ #include #include -#include #include #include #include diff --git a/src/test/rpc/KeyGeneration_test.cpp b/src/test/rpc/KeyGeneration_test.cpp index 7fd59dea2da..cb2cf2c051b 100644 --- a/src/test/rpc/KeyGeneration_test.cpp +++ b/src/test/rpc/KeyGeneration_test.cpp @@ -107,7 +107,6 @@ class WalletPropose_test : public ripple::TestSuite BEAST_EXPECT(! contains_error (result)); BEAST_EXPECT(result.isMember (jss::account_id)); - BEAST_EXPECT(result.isMember (jss::master_key)); BEAST_EXPECT(result.isMember (jss::master_seed)); BEAST_EXPECT(result.isMember (jss::master_seed_hex)); BEAST_EXPECT(result.isMember (jss::public_key)); @@ -133,7 +132,6 @@ class WalletPropose_test : public ripple::TestSuite BEAST_EXPECT(! contains_error (result)); expectEquals (result[jss::account_id], s.account_id); - expectEquals (result[jss::master_key], s.master_key); expectEquals (result[jss::master_seed], s.master_seed); expectEquals (result[jss::master_seed_hex], s.master_seed_hex); expectEquals (result[jss::public_key], s.public_key); @@ -709,6 +707,86 @@ class WalletPropose_test : public ripple::TestSuite } } + void testRippleLibEd25519() + { + testcase ("ripple-lib encoded Ed25519 keys"); + + auto test = [this](char const* seed, char const* addr) + { + { + Json::Value params; + Json::Value error; + + params[jss::passphrase] = seed; + + auto ret = keypairForSignature(params, error); + + BEAST_EXPECT(!contains_error(error)); + BEAST_EXPECT(ret.first.size() != 0); + BEAST_EXPECT(toBase58(calcAccountID(ret.first)) == addr); + } + + { + Json::Value params; + Json::Value error; + + params[jss::key_type] = "secp256k1"; + params[jss::passphrase] = seed; + + auto ret = keypairForSignature(params, error); + + BEAST_EXPECT(contains_error(error)); + BEAST_EXPECT(error[jss::error_message] == + "Specified seed is for an Ed25519 wallet."); + } + + { + Json::Value params; + Json::Value error; + + params[jss::key_type] = "ed25519"; + params[jss::seed] = seed; + + auto ret = keypairForSignature(params, error); + + BEAST_EXPECT(!contains_error(error)); + BEAST_EXPECT(ret.first.size() != 0); + BEAST_EXPECT(toBase58(calcAccountID(ret.first)) == addr); + } + + { + Json::Value params; + Json::Value error; + + params[jss::key_type] = "secp256k1"; + params[jss::seed] = seed; + + auto ret = keypairForSignature(params, error); + + BEAST_EXPECT(contains_error(error)); + BEAST_EXPECT(error[jss::error_message] == + "Specified seed is for an Ed25519 wallet."); + } + }; + + test("sEdVWZmeUDgQdMEFKTK9kYVX71FKB7o", "r34XnDB2zS11NZ1wKJzpU1mjWExGVugTaQ"); + test("sEd7zJoVnqg1FxB9EuaHC1AB5UPfHWz", "rDw51qRrBEeMw7Na1Nh79LN7HYZDo7nZFE"); + test("sEdSxVntbihdLyabbfttMCqsaaucVR9", "rwiyBDfAYegXZyaQcN2L1vAbKRYn2wNFMq"); + test("sEdSVwJjEXTYCztqDK4JD9WByH3otDX", "rQJ4hZzNGkLQhLtKPCmu1ywEw1ai2vgUJN"); + test("sEdV3jXjKuUoQTSr1Rb4yw8Kyn9r46U", "rERRw2Pxbau4tevE61V5vZUwD7Rus5Y6vW"); + test("sEdVeUZjuYT47Uy51FQCnzivsuWyiwB", "rszewT5gRjUgWNEmnfMjvVYzJCkhvWY32i"); + test("sEd7MHTewdw4tFYeS7rk7XT4qHiA9jH", "rBB2rvnf4ztwjgNhinFXQJ91nAZjkFgR3p"); + test("sEd7A5jFBSdWbNeKGriQvLr1thBScJh", "rLAXz8Nz7aDivz7PwThsLFqaKrizepNCdA"); + test("sEdVPU9M2uyzVNT4Yb5Dn4tUtYjbFAw", "rHbHRFPCxD5fnn98TBzsQHJ7SsRq7eHkRj"); + test("sEdVfF2zhAmS8gfMYzJ4yWBMeR4BZKc", "r9PsneKHcAE7kUfiTixomM5Mnwi28tCc7h"); + test("sEdTjRtcsQkwthDXUSLi9DHNyJcR8GW", "rM4soF4XS3wZrmLurvE6ZmudG16Lk5Dur5"); + test("sEdVNKeu1Lhpfh7Nf6tRDbxnmMyZ4Dv", "r4ZwJxq6FDtWjapDtCGhjG6mtNm1nWdJcD"); + test("sEd7bK4gf5BHJ1WbaEWx8pKMA9MLHpC", "rD6tnn51m4o1uXeEK9CFrZ3HR7DcFhiYnp"); + test("sEd7jCh3ppnQMsLdGcZ6TZayZaHhBLg", "rTcBkiRQ1EfFQ4FCCwqXNHpn1yUTAACkj"); + test("sEdTFJezurQwSJAbkLygj2gQXBut2wh", "rnXaMacNbRwcJddbbPbqdcpSUQcfzFmrR8"); + test("sEdSWajfQAAWFuDvVZF3AiGucReByLt", "rBJtow6V3GTdsWMamrxetRDwWs6wwTxcKa"); + } + void run() override { testKeyType (boost::none, secp256k1_strings); @@ -721,6 +799,9 @@ class WalletPropose_test : public ripple::TestSuite testKeypairForSignature (std::string("secp256k1"), secp256k1_strings); testKeypairForSignature (std::string("ed25519"), ed25519_strings); testKeypairForSignature (std::string("secp256k1"), strong_brain_strings); + + testRippleLibEd25519(); + testKeypairForSignatureErrors (); } };