Gav Wood
10 years ago
8 changed files with 612 additions and 410 deletions
@ -0,0 +1,220 @@ |
|||
/*
|
|||
This file is part of cpp-ethereum. |
|||
|
|||
cpp-ethereum is free software: you can redistribute it and/or modify |
|||
it under the terms of the GNU General Public License as published by |
|||
the Free Software Foundation, either version 3 of the License, or |
|||
(at your option) any later version. |
|||
|
|||
cpp-ethereum is distributed in the hope that it will be useful, |
|||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
GNU General Public License for more details. |
|||
|
|||
You should have received a copy of the GNU General Public License |
|||
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
|||
*/ |
|||
/** @file SecretStore.cpp
|
|||
* @author Gav Wood <i@gavwood.com> |
|||
* @date 2014 |
|||
*/ |
|||
|
|||
#include "SecretStore.h" |
|||
#include <thread> |
|||
#include <mutex> |
|||
#include <boost/filesystem.hpp> |
|||
#include <libdevcore/Log.h> |
|||
#include <libdevcore/Guards.h> |
|||
#include <test/JsonSpiritHeaders.h> |
|||
#include "SHA3.h" |
|||
#include "FileSystem.h" |
|||
using namespace std; |
|||
using namespace dev; |
|||
namespace js = json_spirit; |
|||
namespace fs = boost::filesystem; |
|||
|
|||
SecretStore::SecretStore() |
|||
{ |
|||
load(); |
|||
} |
|||
|
|||
SecretStore::~SecretStore() |
|||
{ |
|||
} |
|||
|
|||
bytes SecretStore::secret(h128 const& _uuid, function<std::string()> const& _pass) const |
|||
{ |
|||
auto rit = m_cached.find(_uuid); |
|||
if (rit != m_cached.end()) |
|||
return rit->second; |
|||
auto it = m_keys.find(_uuid); |
|||
if (it == m_keys.end()) |
|||
return bytes(); |
|||
bytes key = decrypt(it->second.first, _pass()); |
|||
if (!key.empty()) |
|||
m_cached[_uuid] = key; |
|||
return key; |
|||
} |
|||
|
|||
h128 SecretStore::importSecret(bytes const& _s, std::string const& _pass) |
|||
{ |
|||
h128 r = h128::random(); |
|||
m_cached[r] = _s; |
|||
m_keys[r] = make_pair(encrypt(_s, _pass), std::string()); |
|||
save(); |
|||
return r; |
|||
} |
|||
|
|||
void SecretStore::kill(h128 const& _uuid) |
|||
{ |
|||
m_cached.erase(_uuid); |
|||
if (m_keys.count(_uuid)) |
|||
{ |
|||
boost::filesystem::remove(m_keys[_uuid].second); |
|||
m_keys.erase(_uuid); |
|||
} |
|||
} |
|||
|
|||
void SecretStore::clearCache() const |
|||
{ |
|||
m_cached.clear(); |
|||
} |
|||
|
|||
void SecretStore::save(std::string const& _keysPath) |
|||
{ |
|||
fs::path p(_keysPath); |
|||
boost::filesystem::create_directories(p); |
|||
for (auto& k: m_keys) |
|||
{ |
|||
std::string uuid = toUUID(k.first); |
|||
std::string filename = (p / uuid).string() + ".json"; |
|||
js::mObject v; |
|||
js::mValue crypto; |
|||
js::read_string(k.second.first, crypto); |
|||
v["crypto"] = crypto; |
|||
v["id"] = uuid; |
|||
v["version"] = 2; |
|||
writeFile(filename, js::write_string(js::mValue(v), true)); |
|||
if (!k.second.second.empty() && k.second.second != filename) |
|||
boost::filesystem::remove(k.second.second); |
|||
k.second.second = filename; |
|||
} |
|||
} |
|||
|
|||
void SecretStore::load(std::string const& _keysPath) |
|||
{ |
|||
fs::path p(_keysPath); |
|||
js::mValue v; |
|||
for (fs::directory_iterator it(p); it != fs::directory_iterator(); ++it) |
|||
if (is_regular_file(it->path())) |
|||
{ |
|||
cdebug << "Reading" << it->path(); |
|||
js::read_string(contentsString(it->path().string()), v); |
|||
if (v.type() == js::obj_type) |
|||
{ |
|||
js::mObject o = v.get_obj(); |
|||
int version = o.count("Version") ? stoi(o["Version"].get_str()) : o.count("version") ? o["version"].get_int() : 0; |
|||
if (version == 2) |
|||
m_keys[fromUUID(o["id"].get_str())] = make_pair(js::write_string(o["crypto"], false), it->path().string()); |
|||
else |
|||
cwarn << "Cannot read key version" << version; |
|||
} |
|||
// else
|
|||
// cwarn << "Invalid JSON in key file" << it->path().string();
|
|||
} |
|||
} |
|||
|
|||
std::string SecretStore::encrypt(bytes const& _v, std::string const& _pass) |
|||
{ |
|||
js::mObject ret; |
|||
|
|||
// KDF info
|
|||
unsigned dklen = 16; |
|||
unsigned iterations = 262144; |
|||
bytes salt = h256::random().asBytes(); |
|||
ret["kdf"] = "pbkdf2"; |
|||
{ |
|||
js::mObject params; |
|||
params["prf"] = "hmac-sha256"; |
|||
params["c"] = (int)iterations; |
|||
params["salt"] = toHex(salt); |
|||
params["dklen"] = (int)dklen; |
|||
ret["kdfparams"] = params; |
|||
} |
|||
bytes derivedKey = pbkdf2(_pass, salt, iterations, dklen); |
|||
|
|||
// cipher info
|
|||
ret["cipher"] = "aes-128-cbc"; |
|||
h128 key(sha3(h128(derivedKey, h128::AlignRight)), h128::AlignRight); |
|||
h128 iv = h128::random(); |
|||
{ |
|||
js::mObject params; |
|||
params["iv"] = toHex(iv.ref()); |
|||
ret["cipherparams"] = params; |
|||
} |
|||
|
|||
// cipher text
|
|||
bytes cipherText = encryptSymNoAuth(key, iv, &_v); |
|||
ret["ciphertext"] = toHex(cipherText); |
|||
|
|||
// and mac.
|
|||
h256 mac = sha3(bytesConstRef(&derivedKey).cropped(derivedKey.size() - 16).toBytes() + cipherText); |
|||
ret["mac"] = toHex(mac.ref()); |
|||
|
|||
return js::write_string((js::mValue)ret, true); |
|||
} |
|||
|
|||
bytes SecretStore::decrypt(std::string const& _v, std::string const& _pass) |
|||
{ |
|||
js::mObject o; |
|||
{ |
|||
js::mValue ov; |
|||
js::read_string(_v, ov); |
|||
o = ov.get_obj(); |
|||
} |
|||
|
|||
// derive key
|
|||
bytes derivedKey; |
|||
if (o["kdf"].get_str() == "pbkdf2") |
|||
{ |
|||
auto params = o["kdfparams"].get_obj(); |
|||
if (params["prf"].get_str() != "hmac-sha256") |
|||
{ |
|||
cwarn << "Unknown PRF for PBKDF2" << params["prf"].get_str() << "not supported."; |
|||
return bytes(); |
|||
} |
|||
unsigned iterations = params["c"].get_int(); |
|||
bytes salt = fromHex(params["salt"].get_str()); |
|||
derivedKey = pbkdf2(_pass, salt, iterations, params["dklen"].get_int()); |
|||
} |
|||
else |
|||
{ |
|||
cwarn << "Unknown KDF" << o["kdf"].get_str() << "not supported."; |
|||
return bytes(); |
|||
} |
|||
|
|||
bytes cipherText = fromHex(o["ciphertext"].get_str()); |
|||
|
|||
// check MAC
|
|||
h256 mac(o["mac"].get_str()); |
|||
h256 macExp = sha3(bytesConstRef(&derivedKey).cropped(derivedKey.size() - 16).toBytes() + cipherText); |
|||
if (mac != macExp) |
|||
{ |
|||
cwarn << "Invalid key - MAC mismatch; expected" << toString(macExp) << ", got" << toString(mac); |
|||
return bytes(); |
|||
} |
|||
|
|||
// decrypt
|
|||
if (o["cipher"].get_str() == "aes-128-cbc") |
|||
{ |
|||
auto params = o["cipherparams"].get_obj(); |
|||
h128 key(sha3(h128(derivedKey, h128::AlignRight)), h128::AlignRight); |
|||
h128 iv(params["iv"].get_str()); |
|||
return decryptSymNoAuth(key, iv, &cipherText); |
|||
} |
|||
else |
|||
{ |
|||
cwarn << "Unknown cipher" << o["cipher"].get_str() << "not supported."; |
|||
return bytes(); |
|||
} |
|||
} |
@ -0,0 +1,56 @@ |
|||
/*
|
|||
This file is part of cpp-ethereum. |
|||
|
|||
cpp-ethereum is free software: you can redistribute it and/or modify |
|||
it under the terms of the GNU General Public License as published by |
|||
the Free Software Foundation, either version 3 of the License, or |
|||
(at your option) any later version. |
|||
|
|||
cpp-ethereum is distributed in the hope that it will be useful, |
|||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
GNU General Public License for more details. |
|||
|
|||
You should have received a copy of the GNU General Public License |
|||
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
|||
*/ |
|||
/** @file SecretStore.h
|
|||
* @author Gav Wood <i@gavwood.com> |
|||
* @date 2014 |
|||
*/ |
|||
|
|||
#pragma once |
|||
|
|||
#include <functional> |
|||
#include <mutex> |
|||
#include "Common.h" |
|||
#include "FileSystem.h" |
|||
|
|||
namespace dev |
|||
{ |
|||
|
|||
class SecretStore |
|||
{ |
|||
public: |
|||
SecretStore(); |
|||
~SecretStore(); |
|||
|
|||
bytes secret(h128 const& _uuid, std::function<std::string()> const& _pass) const; |
|||
h128 importSecret(bytes const& _s, std::string const& _pass); |
|||
void kill(h128 const& _uuid); |
|||
|
|||
// Clear any cached keys.
|
|||
void clearCache() const; |
|||
|
|||
private: |
|||
void save(std::string const& _keysPath = getDataDir("web3") + "/keys"); |
|||
void load(std::string const& _keysPath = getDataDir("web3") + "/keys"); |
|||
static std::string encrypt(bytes const& _v, std::string const& _pass); |
|||
static bytes decrypt(std::string const& _v, std::string const& _pass); |
|||
|
|||
mutable std::map<h128, bytes> m_cached; |
|||
std::map<h128, std::pair<std::string, std::string>> m_keys; |
|||
}; |
|||
|
|||
} |
|||
|
@ -0,0 +1,203 @@ |
|||
/*
|
|||
This file is part of cpp-ethereum. |
|||
|
|||
cpp-ethereum is free software: you can redistribute it and/or modify |
|||
it under the terms of the GNU General Public License as published by |
|||
the Free Software Foundation, either version 3 of the License, or |
|||
(at your option) any later version. |
|||
|
|||
cpp-ethereum is distributed in the hope that it will be useful, |
|||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
GNU General Public License for more details. |
|||
|
|||
You should have received a copy of the GNU General Public License |
|||
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
|||
*/ |
|||
/** @file KeyManager.cpp
|
|||
* @author Gav Wood <i@gavwood.com> |
|||
* @date 2014 |
|||
*/ |
|||
|
|||
#include "KeyManager.h" |
|||
#include <thread> |
|||
#include <mutex> |
|||
#include <boost/filesystem.hpp> |
|||
#include <libdevcore/Log.h> |
|||
#include <libdevcore/Guards.h> |
|||
#include <libdevcore/RLP.h> |
|||
using namespace std; |
|||
using namespace dev; |
|||
namespace fs = boost::filesystem; |
|||
|
|||
KeyManager::KeyManager(std::string const& _keysFile): |
|||
m_keysFile(_keysFile) |
|||
{} |
|||
|
|||
KeyManager::~KeyManager() |
|||
{} |
|||
|
|||
bool KeyManager::exists() const |
|||
{ |
|||
return !contents(m_keysFile + ".salt").empty() && !contents(m_keysFile).empty(); |
|||
} |
|||
|
|||
void KeyManager::create(std::string const& _pass) |
|||
{ |
|||
m_password = asString(h256::random().asBytes()); |
|||
write(_pass, m_keysFile); |
|||
} |
|||
|
|||
bool KeyManager::load(std::string const& _pass) |
|||
{ |
|||
try { |
|||
bytes salt = contents(m_keysFile + ".salt"); |
|||
bytes encKeys = contents(m_keysFile); |
|||
m_key = h128(pbkdf2(_pass, salt, 262144, 16)); |
|||
bytes bs = decryptSymNoAuth(m_key, h128(), &encKeys); |
|||
RLP s(bs); |
|||
unsigned version = (unsigned)s[0]; |
|||
if (version == 1) |
|||
{ |
|||
for (auto const& i: s[1]) |
|||
m_keyInfo[m_addrLookup[(Address)i[0]] = (h128)i[1]] = KeyInfo{(h256)i[2], (std::string)i[3]}; |
|||
for (auto const& i: s[2]) |
|||
m_passwordInfo[(h256)i[0]] = (std::string)i[1]; |
|||
m_password = (string)s[3]; |
|||
} |
|||
m_cachedPasswords[hashPassword(m_password)] = m_password; |
|||
return true; |
|||
} |
|||
catch (...) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
Secret KeyManager::secret(Address const& _address, function<std::string()> const& _pass) const |
|||
{ |
|||
auto it = m_addrLookup.find(_address); |
|||
if (it == m_addrLookup.end()) |
|||
return Secret(); |
|||
return secret(it->second, _pass); |
|||
} |
|||
|
|||
Secret KeyManager::secret(h128 const& _uuid, function<std::string()> const& _pass) const |
|||
{ |
|||
return Secret(m_store.secret(_uuid, [&](){ |
|||
auto kit = m_keyInfo.find(_uuid); |
|||
if (kit != m_keyInfo.end()) |
|||
{ |
|||
auto it = m_cachedPasswords.find(kit->second.passHash); |
|||
if (it != m_cachedPasswords.end()) |
|||
return it->second; |
|||
} |
|||
std::string p = _pass(); |
|||
m_cachedPasswords[hashPassword(p)] = p; |
|||
return p; |
|||
})); |
|||
} |
|||
|
|||
h128 KeyManager::uuid(Address const& _a) const |
|||
{ |
|||
auto it = m_addrLookup.find(_a); |
|||
if (it == m_addrLookup.end()) |
|||
return h128(); |
|||
return it->second; |
|||
} |
|||
|
|||
Address KeyManager::address(h128 const& _uuid) const |
|||
{ |
|||
for (auto const& i: m_addrLookup) |
|||
if (i.second == _uuid) |
|||
return i.first; |
|||
return Address(); |
|||
} |
|||
|
|||
h128 KeyManager::import(Secret const& _s, string const& _info, std::string const& _pass, string const& _passInfo) |
|||
{ |
|||
Address addr = KeyPair(_s).address(); |
|||
auto passHash = hashPassword(_pass); |
|||
m_cachedPasswords[passHash] = _pass; |
|||
m_passwordInfo[passHash] = _passInfo; |
|||
auto uuid = m_store.importSecret(_s.asBytes(), _pass); |
|||
m_keyInfo[uuid] = KeyInfo{passHash, _info}; |
|||
m_addrLookup[addr] = uuid; |
|||
write(m_keysFile); |
|||
return uuid; |
|||
} |
|||
|
|||
void KeyManager::importExisting(h128 const& _uuid, std::string const& _info, std::string const& _pass, std::string const& _passInfo) |
|||
{ |
|||
bytes key = m_store.secret(_uuid, [&](){ return _pass; }); |
|||
if (key.empty()) |
|||
return; |
|||
Address a = KeyPair(Secret(key)).address(); |
|||
auto passHash = hashPassword(_pass); |
|||
if (!m_passwordInfo.count(passHash)) |
|||
m_passwordInfo[passHash] = _passInfo; |
|||
if (!m_cachedPasswords.count(passHash)) |
|||
m_cachedPasswords[passHash] = _pass; |
|||
m_addrLookup[a] = _uuid; |
|||
m_keyInfo[_uuid].passHash = passHash; |
|||
m_keyInfo[_uuid].info = _info; |
|||
write(m_keysFile); |
|||
} |
|||
|
|||
void KeyManager::kill(Address const& _a) |
|||
{ |
|||
auto id = m_addrLookup[_a]; |
|||
m_addrLookup.erase(_a); |
|||
m_keyInfo.erase(id); |
|||
m_store.kill(id); |
|||
} |
|||
|
|||
std::map<Address, std::pair<std::string, std::string>> KeyManager::keys() const |
|||
{ |
|||
std::map<Address, std::pair<std::string, std::string>> ret; |
|||
for (auto const& i: m_addrLookup) |
|||
if (m_keyInfo.count(i.second) > 0) |
|||
ret[i.first] = make_pair(m_keyInfo.at(i.second).info, m_passwordInfo.at(m_keyInfo.at(i.second).passHash)); |
|||
return ret; |
|||
} |
|||
|
|||
h256 KeyManager::hashPassword(std::string const& _pass) const |
|||
{ |
|||
// TODO SECURITY: store this a bit more securely; Scrypt perhaps?
|
|||
return h256(pbkdf2(_pass, asBytes(m_password), 262144, 32)); |
|||
} |
|||
|
|||
bool KeyManager::write(std::string const& _keysFile) const |
|||
{ |
|||
if (!m_key) |
|||
return false; |
|||
write(m_key, _keysFile); |
|||
return true; |
|||
} |
|||
|
|||
void KeyManager::write(std::string const& _pass, std::string const& _keysFile) const |
|||
{ |
|||
bytes salt = h256::random().asBytes(); |
|||
writeFile(_keysFile + ".salt", salt); |
|||
auto key = h128(pbkdf2(_pass, salt, 262144, 16)); |
|||
write(key, _keysFile); |
|||
} |
|||
|
|||
void KeyManager::write(h128 const& _key, std::string const& _keysFile) const |
|||
{ |
|||
RLPStream s(4); |
|||
s << 1; |
|||
s.appendList(m_addrLookup.size()); |
|||
for (auto const& i: m_addrLookup) |
|||
if (m_keyInfo.count(i.second)) |
|||
{ |
|||
auto ki = m_keyInfo.at(i.second); |
|||
s.appendList(4) << i.first << i.second << ki.passHash << ki.info; |
|||
} |
|||
s.appendList(m_passwordInfo.size()); |
|||
for (auto const& i: m_passwordInfo) |
|||
s.appendList(2) << i.first << i.second; |
|||
s.append(m_password); |
|||
|
|||
writeFile(_keysFile, encryptSymNoAuth(_key, h128(), &s.out())); |
|||
m_key = _key; |
|||
} |
@ -0,0 +1,108 @@ |
|||
/*
|
|||
This file is part of cpp-ethereum. |
|||
|
|||
cpp-ethereum is free software: you can redistribute it and/or modify |
|||
it under the terms of the GNU General Public License as published by |
|||
the Free Software Foundation, either version 3 of the License, or |
|||
(at your option) any later version. |
|||
|
|||
cpp-ethereum is distributed in the hope that it will be useful, |
|||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
GNU General Public License for more details. |
|||
|
|||
You should have received a copy of the GNU General Public License |
|||
along with cpp-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
|||
*/ |
|||
/** @file KeyManager.h
|
|||
* @author Gav Wood <i@gavwood.com> |
|||
* @date 2014 |
|||
*/ |
|||
|
|||
#pragma once |
|||
|
|||
#include <functional> |
|||
#include <mutex> |
|||
#include <libdevcrypto/SecretStore.h> |
|||
#include <libdevcrypto/FileSystem.h> |
|||
|
|||
namespace dev |
|||
{ |
|||
|
|||
class UnknownPassword: public Exception {}; |
|||
|
|||
struct KeyInfo |
|||
{ |
|||
h256 passHash; |
|||
std::string info; |
|||
}; |
|||
|
|||
static const auto DontKnowThrow = [](){ BOOST_THROW_EXCEPTION(UnknownPassword()); return std::string(); }; |
|||
|
|||
// TODO: This one is specifically for Ethereum, but we can make it generic in due course.
|
|||
// TODO: hidden-partition style key-store.
|
|||
/**
|
|||
* @brief High-level manager of keys for Ethereum. |
|||
* Usage: |
|||
* |
|||
* Call exists() to check whether there is already a database. If so, get the master password from |
|||
* the user and call load() with it. If not, get a new master password from the user (get them to type |
|||
* it twice and keep some hint around!) and call create() with it. |
|||
*/ |
|||
class KeyManager |
|||
{ |
|||
public: |
|||
KeyManager(std::string const& _keysFile = getDataDir("ethereum") + "/keys.info"); |
|||
~KeyManager(); |
|||
|
|||
void setKeysFile(std::string const& _keysFile) { m_keysFile = _keysFile; } |
|||
std::string const& keysFile() const { return m_keysFile; } |
|||
|
|||
bool exists() const; |
|||
void create(std::string const& _pass); |
|||
bool load(std::string const& _pass); |
|||
void save(std::string const& _pass) const { write(_pass, m_keysFile); } |
|||
|
|||
std::map<Address, std::pair<std::string, std::string>> keys() const; |
|||
|
|||
h128 uuid(Address const& _a) const; |
|||
Address address(h128 const& _uuid) const; |
|||
|
|||
h128 import(Secret const& _s, std::string const& _info, std::string const& _pass, std::string const& _passInfo); |
|||
h128 import(Secret const& _s, std::string const& _info) { return import(_s, _info, m_password, std::string()); } |
|||
|
|||
SecretStore& store() { return m_store; } |
|||
void importExisting(h128 const& _uuid, std::string const& _info, std::string const& _pass, std::string const& _passInfo); |
|||
|
|||
Secret secret(Address const& _address, std::function<std::string()> const& _pass = DontKnowThrow) const; |
|||
Secret secret(h128 const& _uuid, std::function<std::string()> const& _pass = DontKnowThrow) const; |
|||
|
|||
void kill(h128 const& _id) { kill(address(_id)); } |
|||
void kill(Address const& _a); |
|||
|
|||
private: |
|||
h256 hashPassword(std::string const& _pass) const; |
|||
|
|||
// Only use if previously loaded ok.
|
|||
// @returns false if wasn't previously loaded ok.
|
|||
bool write(std::string const& _keysFile) const; |
|||
void write(std::string const& _pass, std::string const& _keysFile) const; |
|||
void write(h128 const& _key, std::string const& _keysFile) const; |
|||
|
|||
// Ethereum keys.
|
|||
std::map<Address, h128> m_addrLookup; |
|||
std::map<h128, KeyInfo> m_keyInfo; |
|||
std::map<h256, std::string> m_passwordInfo; |
|||
|
|||
// Passwords that we're storing.
|
|||
mutable std::map<h256, std::string> m_cachedPasswords; |
|||
|
|||
// The default password for keys in the keystore - protected by the master password.
|
|||
std::string m_password; |
|||
|
|||
SecretStore m_store; |
|||
mutable h128 m_key; |
|||
mutable std::string m_keysFile; |
|||
}; |
|||
|
|||
} |
Loading…
Reference in new issue