From e7f688327135e7261d6f9a43304de842533dc80d Mon Sep 17 00:00:00 2001 From: Holger Nahrstaedt <holgernahrstaedt@gmx.de> Date: Thu, 18 Jun 2020 13:47:26 +0200 Subject: [PATCH] Preparing release 0.24.0 * new beemstorage module * Config is handled by SqliteConfigurationStore or InRamConfigurationStore * Keys are handled by SqliteEncryptedKeyStore or InRamPlainKeyStore * Move aes to beemgraphenebase * Wallet.keys, Wallet.keyStorage, Wallet.token and Wallet.keyMap has been removed * Wallet.store has now the Key Interface that handles key management * Token handling has been removed from Wallet * Token storage has been move from wallet to SteemConnect/HiveSigner --- CHANGELOG.rst | 11 + beem/__init__.py | 1 - beem/blockchaininstance.py | 5 +- beem/cli.py | 64 +- beem/exceptions.py | 6 - beem/hive.py | 15 - beem/hivesigner.py | 135 ++++- beem/instance.py | 4 +- beem/message.py | 1 - beem/steem.py | 16 - beem/steemconnect.py | 135 ++++- beem/storage.py | 718 ++--------------------- beem/transactionbuilder.py | 2 +- beem/version.py | 2 +- beem/wallet.py | 478 +++++---------- beemapi/version.py | 2 +- beembase/version.py | 2 +- beemgraphenebase/__init__.py | 1 + {beem => beemgraphenebase}/aes.py | 0 beemgraphenebase/bip38.py | 5 + beemgraphenebase/version.py | 2 +- beemstorage/__init__.py | 38 ++ beemstorage/base.py | 302 ++++++++++ beemstorage/exceptions.py | 18 + beemstorage/interfaces.py | 257 ++++++++ beemstorage/masterpassword.py | 243 ++++++++ beemstorage/ram.py | 32 + beemstorage/sqlite.py | 352 +++++++++++ docs/beem.aes.rst | 7 - docs/beemgraphenebase.aes.rst | 7 + docs/beemstorage.base.rst | 7 + docs/beemstorage.exceptions.rst | 7 + docs/beemstorage.interfaces.rst | 7 + docs/beemstorage.masterpassword.rst | 7 + docs/beemstorage.ram.rst | 7 + docs/beemstorage.sqlite.rst | 7 + docs/modules.rst | 17 +- setup.py | 2 +- tests/beem/test_aes.py | 2 +- tests/beem/test_connection.py | 14 +- tests/beem/test_hive.py | 16 +- tests/beem/test_steem.py | 17 +- tests/beem/test_storage.py | 2 +- tests/beem/test_testnet.py | 4 +- tests/beem/test_txbuffers.py | 10 +- tests/beem/test_wallet.py | 27 - tests/beemapi/test_noderpc.py | 4 +- tests/beemstorage/__init__.py | 0 tests/beemstorage/test_keystorage.py | 111 ++++ tests/beemstorage/test_masterpassword.py | 74 +++ tests/beemstorage/test_sqlite.py | 41 ++ 51 files changed, 2082 insertions(+), 1162 deletions(-) rename {beem => beemgraphenebase}/aes.py (100%) create mode 100644 beemstorage/__init__.py create mode 100644 beemstorage/base.py create mode 100644 beemstorage/exceptions.py create mode 100644 beemstorage/interfaces.py create mode 100644 beemstorage/masterpassword.py create mode 100644 beemstorage/ram.py create mode 100644 beemstorage/sqlite.py delete mode 100644 docs/beem.aes.rst create mode 100644 docs/beemgraphenebase.aes.rst create mode 100644 docs/beemstorage.base.rst create mode 100644 docs/beemstorage.exceptions.rst create mode 100644 docs/beemstorage.interfaces.rst create mode 100644 docs/beemstorage.masterpassword.rst create mode 100644 docs/beemstorage.ram.rst create mode 100644 docs/beemstorage.sqlite.rst create mode 100644 tests/beemstorage/__init__.py create mode 100644 tests/beemstorage/test_keystorage.py create mode 100644 tests/beemstorage/test_masterpassword.py create mode 100644 tests/beemstorage/test_sqlite.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ed08ff4c..c2699566 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,16 @@ Changelog ========= +0.24.0 +------ +* new beemstorage module +* Config is handled by SqliteConfigurationStore or InRamConfigurationStore +* Keys are handled by SqliteEncryptedKeyStore or InRamPlainKeyStore +* Move aes to beemgraphenebase +* Wallet.keys, Wallet.keyStorage, Wallet.token and Wallet.keyMap has been removed +* Wallet.store has now the Key Interface that handles key management +* Token handling has been removed from Wallet +* Token storage has been move from wallet to SteemConnect/HiveSigner + 0.23.13 ------- * receiver parameter removed from beempy decrypt diff --git a/beem/__init__.py b/beem/__init__.py index 2a406665..5ce33201 100644 --- a/beem/__init__.py +++ b/beem/__init__.py @@ -4,7 +4,6 @@ from .hive import Hive from .version import version as __version__ __all__ = [ "steem", - "aes", "account", "amount", "asset", diff --git a/beem/blockchaininstance.py b/beem/blockchaininstance.py index f1bff90d..4eaf9fe0 100644 --- a/beem/blockchaininstance.py +++ b/beem/blockchaininstance.py @@ -15,14 +15,13 @@ import time from beemgraphenebase.py23 import bytes_types, integer_types, string_types, text_type from datetime import datetime, timedelta, date from beemapi.noderpc import NodeRPC -from beemapi.exceptions import NoAccessApi, NoApiWithName from beemgraphenebase.account import PrivateKey, PublicKey from beembase import transactions, operations from beemgraphenebase.chains import known_chains +from .storage import get_default_config_store from .account import Account from .amount import Amount from .price import Price -from .storage import get_default_config_storage from .version import version as beem_version from .exceptions import ( AccountExistsException, @@ -193,7 +192,7 @@ class BlockChainInstance(object): self.path = kwargs.get("path", None) # Store config for access through other Classes - self.config = get_default_config_storage() + self.config = kwargs.get("config_store", get_default_config_store(**kwargs)) if self.path is None: self.path = self.config["default_path"] diff --git a/beem/cli.py b/beem/cli.py index 44ffcc14..ed2c9d2c 100644 --- a/beem/cli.py +++ b/beem/cli.py @@ -108,7 +108,7 @@ def unlock_wallet(stm, password=None, allow_wif=True): return True if not stm.wallet.locked(): return True - if len(stm.wallet.keys) > 0: + if not stm.wallet.store.is_encrypted(): return True password_storage = stm.config["password_storage"] if not password and KEYRING_AVAILABLE and password_storage == "keyring": @@ -151,6 +151,45 @@ def unlock_wallet(stm, password=None, allow_wif=True): return True +def unlock_token_wallet(stm, sc2, password=None): + if stm.unsigned and stm.nobroadcast: + return True + if stm.use_ledger: + return True + if not sc2.locked(): + return True + if not sc2.store.is_encrypted(): + return True + password_storage = stm.config["password_storage"] + if not password and KEYRING_AVAILABLE and password_storage == "keyring": + password = keyring.get_password("beem", "wallet") + if not password and password_storage == "environment" and "UNLOCK" in os.environ: + password = os.environ.get("UNLOCK") + if bool(password): + sc2.unlock(password) + else: + password = click.prompt("Password to unlock wallet", confirmation_prompt=False, hide_input=True) + try: + sc2.unlock(password) + except: + raise exceptions.WrongMasterPasswordException("entered password is not a valid password") + + if sc2.locked(): + if password_storage == "keyring" or password_storage == "environment": + print("Wallet could not be unlocked with %s!" % password_storage) + password = click.prompt("Password to unlock wallet", confirmation_prompt=False, hide_input=True) + if bool(password): + unlock_token_wallet(stm, password=password) + if not sc2.locked(): + return True + else: + print("Wallet could not be unlocked!") + return False + else: + print("Wallet Unlocked!") + return True + + @shell(prompt='beempy> ', intro='Starting beempy... (use help to list all commands)', chain=True) # @click.group(chain=True) @click.option( @@ -561,6 +600,7 @@ def createwallet(wipe): password = keyring.set_password("beem", "wallet", password) elif password_storage == "environment": print("The new wallet password can be stored in the UNLOCK environment variable to skip password prompt!") + stm.wallet.wipe(True) stm.wallet.create(password) set_shared_blockchain_instance(stm) @@ -584,7 +624,7 @@ def walletinfo(unlock, lock): t.add_row(["created", stm.wallet.created()]) t.add_row(["locked", stm.wallet.locked()]) t.add_row(["Number of stored keys", len(stm.wallet.getPublicKeys())]) - t.add_row(["sql-file", stm.wallet.keyStorage.sqlDataBaseFile]) + t.add_row(["sql-file", stm.wallet.store.sqlite_file]) password_storage = stm.config["password_storage"] t.add_row(["password_storage", password_storage]) password = os.environ.get("UNLOCK") @@ -883,11 +923,12 @@ def addtoken(name, unsafe_import_token): stm = shared_blockchain_instance() if stm.rpc is not None: stm.rpc.rpcconnect() - if not unlock_wallet(stm): - return + sc2 = SteemConnect(blockchain_instance=stm) + if not unlock_token_wallet(stm, sc2): + return if not unsafe_import_token: unsafe_import_token = click.prompt("Enter private token", confirmation_prompt=False, hide_input=True) - stm.wallet.addToken(name, unsafe_import_token) + sc2.addToken(name, unsafe_import_token) set_shared_blockchain_instance(stm) @@ -906,9 +947,10 @@ def deltoken(confirm, name): stm = shared_blockchain_instance() if stm.rpc is not None: stm.rpc.rpcconnect() - if not unlock_wallet(stm): - return - stm.wallet.removeTokenFromPublicName(name) + sc2 = SteemConnect(blockchain_instance=stm) + if not unlock_token_wallet(stm, sc2): + return + sc2.removeTokenFromPublicName(name) set_shared_blockchain_instance(stm) @@ -950,10 +992,10 @@ def listtoken(): stm = shared_blockchain_instance() t = PrettyTable(["name", "scope", "status"]) t.align = "l" - if not unlock_wallet(stm): - return sc2 = SteemConnect(blockchain_instance=stm) - for name in stm.wallet.getPublicNames(): + if not unlock_token_wallet(stm, sc2): + return + for name in sc2.getPublicNames(): ret = sc2.me(username=name) if "error" in ret: t.add_row([name, "-", ret["error"]]) diff --git a/beem/exceptions.py b/beem/exceptions.py index 0586ee6e..baee5db5 100644 --- a/beem/exceptions.py +++ b/beem/exceptions.py @@ -12,12 +12,6 @@ class WalletExists(Exception): pass -class WalletLocked(Exception): - """ Wallet is locked - """ - pass - - class RPCConnectionRequired(Exception): """ An RPC connection is required """ diff --git a/beem/hive.py b/beem/hive.py index d2f411fb..ab383744 100644 --- a/beem/hive.py +++ b/beem/hive.py @@ -14,24 +14,9 @@ import ast import time from beemgraphenebase.py23 import bytes_types, integer_types, string_types, text_type from datetime import datetime, timedelta, date -from beemapi.noderpc import NodeRPC -from beemapi.exceptions import NoAccessApi, NoApiWithName -from beemgraphenebase.account import PrivateKey, PublicKey from beembase import transactions, operations from beemgraphenebase.chains import known_chains -from .account import Account from .amount import Amount -from .price import Price -from .storage import get_default_config_storage -from .version import version as beem_version -from .exceptions import ( - AccountExistsException, - AccountDoesNotExistsException -) -from .wallet import Wallet -from .steemconnect import SteemConnect -from .hivesigner import HiveSigner -from .transactionbuilder import TransactionBuilder from beem.blockchaininstance import BlockChainInstance from .utils import formatTime, resolve_authorperm, derive_permlink, sanitize_permlink, remove_from_dict, addTzInfo, formatToTimeStamp from beem.constants import STEEM_VOTE_REGENERATION_SECONDS, STEEM_100_PERCENT, STEEM_1_PERCENT, STEEM_RC_REGEN_TIME diff --git a/beem/hivesigner.py b/beem/hivesigner.py index 3a210501..80223920 100644 --- a/beem/hivesigner.py +++ b/beem/hivesigner.py @@ -11,10 +11,17 @@ except ImportError: from urlparse import urlparse, urljoin from urllib import urlencode import requests -from .storage import get_default_config_storage +import logging from six import PY2 from beem.instance import shared_blockchain_instance from beem.amount import Amount +from beem.exceptions import ( + MissingKeyError, + WalletExists +) +from beemstorage.exceptions import KeyAlreadyInStoreException, WalletLocked + +log = logging.getLogger(__name__) class HiveSigner(object): @@ -94,10 +101,130 @@ class HiveSigner(object): self.hs_oauth_base_url = kwargs.get("hs_oauth_base_url", config["hs_oauth_base_url"]) self.hs_api_url = kwargs.get("hs_api_url", config["hs_api_url"]) + if "token" in kwargs and len(kwargs["token"]) > 0: + from beemstorage import InRamPlainTokenStore + self.store = InRamPlainTokenStore() + self.setToken(kwargs["keys"]) + else: + """ If no keys are provided manually we load the SQLite + keyStorage + """ + from beemstorage import SqliteEncryptedTokenStore + self.store = kwargs.get( + "token_store", + SqliteEncryptedTokenStore(config=config, **kwargs), + ) + @property def headers(self): return {'Authorization': self.access_token} + def setToken(self, loadtoken): + """ This method is strictly only for in memory token that are + passed to Wallet/Steem with the ``token`` argument + """ + log.debug( + "Force setting of private token. Not using the wallet database!") + if not isinstance(loadtoken, (set)): + raise ValueError("token must be a dict variable!") + for name in loadtoken: + self.store.add(loadtoken[name], name) + + def is_encrypted(self): + """ Is the key store encrypted? + """ + return self.store.is_encrypted() + + def unlock(self, pwd): + """ Unlock the wallet database + """ + unlock_ok = None + if self.store.is_encrypted(): + unlock_ok = self.store.unlock(pwd) + return unlock_ok + + def lock(self): + """ Lock the wallet database + """ + lock_ok = False + if self.store.is_encrypted(): + lock_ok = self.store.lock() + return lock_ok + + def unlocked(self): + """ Is the wallet database unlocked? + """ + unlocked = True + if self.store.is_encrypted(): + unlocked = not self.store.locked() + return unlocked + + def locked(self): + """ Is the wallet database locked? + """ + if self.store.is_encrypted(): + return self.store.locked() + else: + return False + + def changePassphrase(self, new_pwd): + """ Change the passphrase for the wallet database + """ + self.store.change_password(new_pwd) + + def created(self): + """ Do we have a wallet database already? + """ + if len(self.store.getPublicKeys()): + # Already keys installed + return True + else: + return False + + def create(self, pwd): + """ Alias for :func:`newWallet` + + :param str pwd: Passphrase for the created wallet + """ + self.newWallet(pwd) + + def newWallet(self, pwd): + """ Create a new wallet database + + :param str pwd: Passphrase for the created wallet + """ + if self.created(): + raise WalletExists("You already have created a wallet!") + self.store.unlock(pwd) + + def addToken(self, name, token): + if str(name) in self.store: + raise KeyAlreadyInStoreException("Token already in the store") + self.store.add(str(token), str(name)) + + def getTokenForAccountName(self, name): + """ Obtain the private token for a given public name + + :param str name: Public name + """ + if str(name) not in self.store: + raise MissingKeyError + return self.store.getPrivateKeyForPublicKey(str(name)) + + def removeTokenFromPublicName(self, name): + """ Remove a token from the wallet database + + :param str name: token to be removed + """ + self.store.delete(str(name)) + + def getPublicNames(self): + """ Return all installed public token + """ + if self.store is None: + return + return self.store.getPublicNames() + def get_login_url(self, redirect_uri, **kwargs): """ Returns a login url for receiving token from HiveSigner """ @@ -127,7 +254,7 @@ class HiveSigner(object): "grant_type": "authorization_code", "code": code, "client_id": self.client_id, - "client_secret": self.blockchain.wallet.getTokenForAccountName(self.client_id), + "client_secret": self.getTokenForAccountName(self.client_id), } r = requests.post( @@ -166,7 +293,7 @@ class HiveSigner(object): if permission != "posting": self.access_token = None return - self.access_token = self.blockchain.wallet.getTokenForAccountName(username) + self.access_token = self.getTokenForAccountName(username) def broadcast(self, operations, username=None): """ Broadcast an operation @@ -210,7 +337,7 @@ class HiveSigner(object): "grant_type": "refresh_token", "refresh_token": code, "client_id": self.client_id, - "client_secret": self.blockchain.wallet.getTokenForAccountName(self.client_id), + "client_secret": self.getTokenForAccountName(self.client_id), "scope": scope, } diff --git a/beem/instance.py b/beem/instance.py index fb9e6ee4..bccf638f 100644 --- a/beem/instance.py +++ b/beem/instance.py @@ -30,8 +30,8 @@ def shared_blockchain_instance(): """ if not SharedInstance.instance: clear_cache() - from beem.storage import get_default_config_storage - default_chain = get_default_config_storage()["default_chain"] + from beem.storage import get_default_config_store + default_chain = get_default_config_store()["default_chain"] if default_chain == "steem": SharedInstance.instance = beem.Steem(**SharedInstance.config) else: diff --git a/beem/message.py b/beem/message.py index c54ae9ab..597ef9b4 100644 --- a/beem/message.py +++ b/beem/message.py @@ -14,7 +14,6 @@ from beemgraphenebase.account import PublicKey from beem.instance import shared_blockchain_instance from beem.account import Account from .exceptions import InvalidMessageSignature, WrongMemoKey, AccountDoesNotExistsException, InvalidMemoKeyException -from .storage import get_default_config_storage log = logging.getLogger(__name__) diff --git a/beem/steem.py b/beem/steem.py index 5fea22ed..541a977e 100644 --- a/beem/steem.py +++ b/beem/steem.py @@ -14,24 +14,8 @@ import ast import time from beemgraphenebase.py23 import bytes_types, integer_types, string_types, text_type from datetime import datetime, timedelta, date -from beemapi.noderpc import NodeRPC -from beemapi.exceptions import NoAccessApi, NoApiWithName -from beemgraphenebase.account import PrivateKey, PublicKey -from beembase import transactions, operations from beemgraphenebase.chains import known_chains -from .account import Account from .amount import Amount -from .price import Price -from .storage import get_default_config_storage -from .version import version as beem_version -from .exceptions import ( - AccountExistsException, - AccountDoesNotExistsException -) -from .wallet import Wallet -from .steemconnect import SteemConnect -from .hivesigner import HiveSigner -from .transactionbuilder import TransactionBuilder from .utils import formatTime, resolve_authorperm, derive_permlink, sanitize_permlink, remove_from_dict, addTzInfo, formatToTimeStamp from beem.constants import STEEM_VOTE_REGENERATION_SECONDS, STEEM_100_PERCENT, STEEM_1_PERCENT, STEEM_RC_REGEN_TIME from beem.blockchaininstance import BlockChainInstance diff --git a/beem/steemconnect.py b/beem/steemconnect.py index afc02a40..45c6ecd3 100644 --- a/beem/steemconnect.py +++ b/beem/steemconnect.py @@ -11,10 +11,17 @@ except ImportError: from urlparse import urlparse, urljoin from urllib import urlencode import requests -from .storage import get_default_config_storage +import logging from six import PY2 from beem.instance import shared_blockchain_instance from beem.amount import Amount +from beem.exceptions import ( + MissingKeyError, + WalletExists +) +from beemstorage.exceptions import KeyAlreadyInStoreException, WalletLocked + +log = logging.getLogger(__name__) class SteemConnect(object): @@ -93,11 +100,131 @@ class SteemConnect(object): self.scope = kwargs.get("scope", "login") self.oauth_base_url = kwargs.get("oauth_base_url", config["oauth_base_url"]) self.sc2_api_url = kwargs.get("sc2_api_url", config["sc2_api_url"]) + + if "token" in kwargs and len(kwargs["token"]) > 0: + from beemstorage import InRamPlainTokenStore + self.store = InRamPlainTokenStore() + self.setToken(kwargs["keys"]) + else: + """ If no keys are provided manually we load the SQLite + keyStorage + """ + from beemstorage import SqliteEncryptedTokenStore + self.store = kwargs.get( + "token_store", + SqliteEncryptedTokenStore(config=config, **kwargs), + ) @property def headers(self): return {'Authorization': self.access_token} + def setToken(self, loadtoken): + """ This method is strictly only for in memory token that are + passed to Wallet/Steem with the ``token`` argument + """ + log.debug( + "Force setting of private token. Not using the wallet database!") + if not isinstance(loadtoken, (set)): + raise ValueError("token must be a dict variable!") + for name in loadtoken: + self.store.add(loadtoken[name], name) + + def is_encrypted(self): + """ Is the key store encrypted? + """ + return self.store.is_encrypted() + + def unlock(self, pwd): + """ Unlock the wallet database + """ + unlock_ok = None + if self.store.is_encrypted(): + unlock_ok = self.store.unlock(pwd) + return unlock_ok + + def lock(self): + """ Lock the wallet database + """ + lock_ok = False + if self.store.is_encrypted(): + lock_ok = self.store.lock() + return lock_ok + + def unlocked(self): + """ Is the wallet database unlocked? + """ + unlocked = True + if self.store.is_encrypted(): + unlocked = not self.store.locked() + return unlocked + + def locked(self): + """ Is the wallet database locked? + """ + if self.store.is_encrypted(): + return self.store.locked() + else: + return False + + def changePassphrase(self, new_pwd): + """ Change the passphrase for the wallet database + """ + self.store.change_password(new_pwd) + + def created(self): + """ Do we have a wallet database already? + """ + if len(self.store.getPublicKeys()): + # Already keys installed + return True + else: + return False + + def create(self, pwd): + """ Alias for :func:`newWallet` + + :param str pwd: Passphrase for the created wallet + """ + self.newWallet(pwd) + + def newWallet(self, pwd): + """ Create a new wallet database + + :param str pwd: Passphrase for the created wallet + """ + if self.created(): + raise WalletExists("You already have created a wallet!") + self.store.unlock(pwd) + + def addToken(self, name, token): + if str(name) in self.store: + raise KeyAlreadyInStoreException("Token already in the store") + self.store.add(str(token), str(name)) + + def getTokenForAccountName(self, name): + """ Obtain the private token for a given public name + + :param str name: Public name + """ + if str(name) not in self.store: + raise MissingKeyError + return self.store.getPrivateKeyForPublicKey(str(name)) + + def removeTokenFromPublicName(self, name): + """ Remove a token from the wallet database + + :param str name: token to be removed + """ + self.store.delete(str(name)) + + def getPublicNames(self): + """ Return all installed public token + """ + if self.store is None: + return + return self.store.getPublicNames() + def get_login_url(self, redirect_uri, **kwargs): """ Returns a login url for receiving token from steemconnect """ @@ -127,7 +254,7 @@ class SteemConnect(object): "grant_type": "authorization_code", "code": code, "client_id": self.client_id, - "client_secret": self.steem.wallet.getTokenForAccountName(self.client_id), + "client_secret": self.getTokenForAccountName(self.client_id), } r = requests.post( @@ -166,7 +293,7 @@ class SteemConnect(object): if permission != "posting": self.access_token = None return - self.access_token = self.steem.wallet.getTokenForAccountName(username) + self.access_token = self.getTokenForAccountName(username) def broadcast(self, operations, username=None): """ Broadcast an operation @@ -210,7 +337,7 @@ class SteemConnect(object): "grant_type": "refresh_token", "refresh_token": code, "client_id": self.client_id, - "client_secret": self.steem.wallet.getTokenForAccountName(self.client_id), + "client_secret": self.getTokenForAccountName(self.client_id), "scope": scope, } diff --git a/beem/storage.py b/beem/storage.py index 30bc72bc..2b4c3408 100644 --- a/beem/storage.py +++ b/beem/storage.py @@ -10,7 +10,7 @@ import shutil import time import os import sqlite3 -from .aes import AESCipher +from beemgraphenebase.aes import AESCipher from appdirs import user_data_dir from datetime import datetime import logging @@ -22,684 +22,44 @@ from .nodelist import NodeList log = logging.getLogger(__name__) log.setLevel(logging.DEBUG) log.addHandler(logging.StreamHandler()) +from beemstorage import ( + SqliteConfigurationStore, + SqliteEncryptedKeyStore, +) timeformat = "%Y%m%d-%H%M%S" - -class DataDir(object): - """ This class ensures that the user's data is stored in its OS - preotected user directory: - - **OSX:** - - * `~/Library/Application Support/<AppName>` - - **Windows:** - - * `C:\\Documents and Settings\\<User>\\Application Data\\Local Settings\\<AppAuthor>\\<AppName>` - * `C:\\Documents and Settings\\<User>\\Application Data\\<AppAuthor>\\<AppName>` - - **Linux:** - - * `~/.local/share/<AppName>` - - Furthermore, it offers an interface to generated backups - in the `backups/` directory every now and then. - """ - appname = "beem" - appauthor = "beem" - storageDatabase = "beem.sqlite" - - data_dir = user_data_dir(appname, appauthor) - sqlDataBaseFile = os.path.join(data_dir, storageDatabase) - - def __init__(self): - #: Storage - self.mkdir_p() - - def mkdir_p(self): - """ Ensure that the directory in which the data is stored - exists - """ - if os.path.isdir(self.data_dir): - return - else: - try: - os.makedirs(self.data_dir) - except FileExistsError: - self.sqlDataBaseFile = ":memory:" - return - except OSError: - self.sqlDataBaseFile = ":memory:" - return - - def sqlite3_backup(self, backupdir): - """ Create timestamped database copy - """ - if self.sqlDataBaseFile == ":memory:": - return - if not os.path.isdir(backupdir): - os.mkdir(backupdir) - backup_file = os.path.join( - backupdir, - os.path.basename(self.storageDatabase) + - datetime.utcnow().strftime("-" + timeformat)) - self.sqlite3_copy(self.sqlDataBaseFile, backup_file) - config = get_default_config_storage() - config["lastBackup"] = datetime.utcnow().strftime(timeformat) - del config - - def sqlite3_copy(self, src, dst): - """Copy sql file from src to dst""" - if self.sqlDataBaseFile == ":memory:": - return - if not os.path.isfile(src): - return - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - # Lock database before making a backup - cursor.execute('begin immediate') - # Make new backup file - shutil.copyfile(src, dst) - log.info("Creating {}...".format(dst)) - # Unlock database - connection.rollback() - - def recover_with_latest_backup(self, backupdir="backups"): - """ Replace database with latest backup""" - file_date = 0 - if self.sqlDataBaseFile == ":memory:": - return - if not os.path.isdir(backupdir): - backupdir = os.path.join(self.data_dir, backupdir) - if not os.path.isdir(backupdir): - return - newest_backup_file = None - for filename in os.listdir(backupdir): - backup_file = os.path.join(backupdir, filename) - if os.stat(backup_file).st_ctime > file_date: - if os.path.isfile(backup_file): - file_date = os.stat(backup_file).st_ctime - newest_backup_file = backup_file - if newest_backup_file is not None: - self.sqlite3_copy(newest_backup_file, self.sqlDataBaseFile) - - def clean_data(self, backupdir="backups"): - """ Delete files older than 70 days - """ - if self.sqlDataBaseFile == ":memory:": - return - log.info("Cleaning up old backups") - backupdir = os.path.join(self.data_dir, backupdir) - for filename in os.listdir(backupdir): - backup_file = os.path.join(backupdir, filename) - if os.stat(backup_file).st_ctime < (time.time() - 70 * 86400): - if os.path.isfile(backup_file): - os.remove(backup_file) - log.info("Deleting {}...".format(backup_file)) - - def refreshBackup(self): - """ Make a new backup - """ - backupdir = os.path.join(self.data_dir, "backups") - self.sqlite3_backup(backupdir) - self.clean_data(backupdir) - - -class Key(DataDir): - """ This is the key storage that stores the public key and the - (possibly encrypted) private key in the `keys` table in the - SQLite3 database. - """ - __tablename__ = 'keys' - - def __init__(self): - super(Key, self).__init__() - - def exists_table(self): - """ Check if the database table exists - """ - query = ("SELECT name FROM sqlite_master " - "WHERE type='table' AND name=?", (self.__tablename__, )) - try: - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(*query) - return True if cursor.fetchone() else False - except sqlite3.OperationalError: - self.sqlDataBaseFile = ":memory:" - log.warning("Could not read(database: %s)" % (self.sqlDataBaseFile)) - return True - - def create_table(self): - """ Create the new table in the SQLite database - """ - query = ("CREATE TABLE {0} (" - "id INTEGER PRIMARY KEY AUTOINCREMENT," - "pub STRING(256)," - "wif STRING(256))".format(self.__tablename__)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(query) - connection.commit() - - def getPublicKeys(self, prefix="STM"): - """ Returns the public keys stored in the database - """ - query = ("SELECT pub from {0} ".format(self.__tablename__)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - try: - cursor.execute(query) - results = cursor.fetchall() - keys = [] - for x in results: - if prefix == x[0][:len(prefix)]: - keys.append(x[0]) - return keys - except sqlite3.OperationalError: - return [] - - def getPrivateKeyForPublicKey(self, pub): - """Returns the (possibly encrypted) private key that - corresponds to a public key - - :param str pub: Public key - - The encryption scheme is BIP38 - """ - query = ("SELECT wif from {0} WHERE pub=?".format(self.__tablename__), (pub,)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(*query) - key = cursor.fetchone() - if key: - return key[0] - else: - return None - - def updateWif(self, pub, wif): - """ Change the wif to a pubkey - - :param str pub: Public key - :param str wif: Private key - """ - query = ("UPDATE {0} SET wif=? WHERE pub=?".format(self.__tablename__), (wif, pub)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(*query) - connection.commit() - - def add(self, wif, pub): - """Add a new public/private key pair (correspondence has to be - checked elsewhere!) - - :param str pub: Public key - :param str wif: Private key - """ - if self.getPrivateKeyForPublicKey(pub): - raise ValueError("Key already in storage") - query = ("INSERT INTO {0} (pub, wif) VALUES (?, ?)".format(self.__tablename__), (pub, wif)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(*query) - connection.commit() - - def delete(self, pub): - """ Delete the key identified as `pub` - - :param str pub: Public key - """ - query = ("DELETE FROM {0} WHERE pub=?".format(self.__tablename__), (pub,)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(*query) - connection.commit() - - def wipe(self, sure=False): - """Purge the entire wallet. No keys will survive this!""" - if not sure: - log.error( - "You need to confirm that you are sure " - "and understand the implications of " - "wiping your wallet!" - ) - return - else: - query = ("DELETE FROM {0} ".format(self.__tablename__)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(query) - connection.commit() - - -class Token(DataDir): - """ This is the token storage that stores the public username and the - (possibly encrypted) token in the `token` table in the - SQLite3 database. - """ - __tablename__ = 'token' - - def __init__(self): - super(Token, self).__init__() - - def exists_table(self): - """ Check if the database table exists - """ - query = ("SELECT name FROM sqlite_master " - "WHERE type='table' AND name=?", (self.__tablename__, )) - try: - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(*query) - return True if cursor.fetchone() else False - except sqlite3.OperationalError: - self.sqlDataBaseFile = ":memory:" - log.warning("Could not read(database: %s)" % (self.sqlDataBaseFile)) - return True - - def create_table(self): - """ Create the new table in the SQLite database - """ - query = ("CREATE TABLE {0} (" - "id INTEGER PRIMARY KEY AUTOINCREMENT," - "name STRING(256)," - "token STRING(256))".format(self.__tablename__)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(query) - connection.commit() - - def getPublicNames(self): - """ Returns the public names stored in the database - """ - query = ("SELECT name from {0} ".format(self.__tablename__)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - try: - cursor.execute(query) - results = cursor.fetchall() - return [x[0] for x in results] - except sqlite3.OperationalError: - return [] - - def getTokenForPublicName(self, name): - """Returns the (possibly encrypted) private token that - corresponds to a public name - - :param str pub: Public name - - The encryption scheme is BIP38 - """ - query = ("SELECT token from {0} WHERE name=?".format(self.__tablename__), (name,)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(*query) - token = cursor.fetchone() - if token: - return token[0] - else: - return None - - def updateToken(self, name, token): - """ Change the token to a name - - :param str name: Public name - :param str token: Private token - """ - query = ("UPDATE {0} SET token=? WHERE name=?".format(self.__tablename__), (token, name)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(*query) - connection.commit() - - def add(self, name, token): - """Add a new public/private token pair (correspondence has to be - checked elsewhere!) - - :param str name: Public name - :param str token: Private token - """ - if self.getTokenForPublicName(name): - raise ValueError("Key already in storage") - query = ("INSERT INTO {0} (name, token) VALUES (?, ?)".format(self.__tablename__), (name, token)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(*query) - connection.commit() - - def delete(self, name): - """ Delete the key identified as `name` - - :param str name: Public name - """ - query = ("DELETE FROM {0} WHERE name=?".format(self.__tablename__), (name,)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(*query) - connection.commit() - - def wipe(self, sure=False): - """Purge the entire wallet. No keys will survive this!""" - if not sure: - log.error( - "You need to confirm that you are sure " - "and understand the implications of " - "wiping your wallet!" - ) - return - else: - query = ("DELETE FROM {0} ".format(self.__tablename__)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(query) - connection.commit() - - -class Configuration(DataDir): - """ This is the configuration storage that stores key/value - pairs in the `config` table of the SQLite3 database. - """ - __tablename__ = "config" - - #: Default configuration - nodelist = NodeList() - blockchain = "hive" - if blockchain == "hive": - nodes = nodelist.get_hive_nodes(testnet=False) - elif blockchain == "steem": - nodes = nodelist.get_steem_nodes(testnet=False) - else: - nodes = [] - config_defaults = { - "node": nodes, - "default_chain": blockchain, - "password_storage": "environment", - "rpcpassword": "", - "rpcuser": "", - "order-expiration": 7 * 24 * 60 * 60, - "client_id": "", - "sc2_client_id": None, - "hs_client_id": None, - "hot_sign_redirect_uri": None, - "sc2_api_url": "https://api.steemconnect.com/api/", - "oauth_base_url": "https://api.steemconnect.com/oauth2/", - "hs_api_url": "https://hivesigner.com/api/", - "hs_oauth_base_url": "https://hivesigner.com/oauth2/", - "default_canonical_url": "https://hive.blog", - "default_path": "48'/13'/0'/0'/0'"} - - def __init__(self): - super(Configuration, self).__init__() - - def exists_table(self): - """ Check if the database table exists - """ - query = ("SELECT name FROM sqlite_master " - "WHERE type='table' AND name=?", (self.__tablename__,)) - try: - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(*query) - return True if cursor.fetchone() else False - except sqlite3.OperationalError: - self.sqlDataBaseFile = ":memory:" - log.warning("Could not read(database: %s)" % (self.sqlDataBaseFile)) - return True - - def create_table(self): - """ Create the new table in the SQLite database - """ - query = ("CREATE TABLE {0} (" - "id INTEGER PRIMARY KEY AUTOINCREMENT," - "key STRING(256)," - "value STRING(256))".format(self.__tablename__)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - try: - cursor.execute(query) - connection.commit() - except sqlite3.OperationalError: - log.error("Could not write to database: %s" % (self.__tablename__)) - raise NoWriteAccess("Could not write to database: %s" % (self.__tablename__)) - - def checkBackup(self): - """ Backup the SQL database every 7 days - """ - if ("lastBackup" not in self.config or - self.config["lastBackup"] == ""): - print("No backup has been created yet!") - self.refreshBackup() - try: - if ( - datetime.utcnow() - - datetime.strptime(self.config["lastBackup"], - timeformat) - ).days > 7: - print("Backups older than 7 days!") - self.refreshBackup() - except: - self.refreshBackup() - - def _haveKey(self, key): - """ Is the key `key` available int he configuration? - """ - query = ("SELECT value FROM {0} WHERE key=?".format(self.__tablename__), (key,)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - try: - cursor.execute(*query) - return True if cursor.fetchone() else False - except sqlite3.OperationalError: - log.warning("Could not read %s (database: %s)" % (str(key), self.__tablename__)) - return False - - def __getitem__(self, key): - """ This method behaves differently from regular `dict` in that - it returns `None` if a key is not found! - """ - query = ("SELECT value FROM {0} WHERE key=?".format(self.__tablename__), (key,)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - try: - cursor.execute(*query) - result = cursor.fetchone() - if result: - return result[0] - else: - if key in self.config_defaults: - return self.config_defaults[key] - else: - return None - except sqlite3.OperationalError: - log.warning("Could not read %s (database: %s)" % (str(key), self.__tablename__)) - if key in self.config_defaults: - return self.config_defaults[key] - else: - return None - - def get(self, key, default=None): - """ Return the key if exists or a default value - """ - if key in self: - return self.__getitem__(key) - else: - return default - - def __contains__(self, key): - if self._haveKey(key) or key in self.config_defaults: - return True - else: - return False - - def __setitem__(self, key, value): - if self._haveKey(key): - query = ("UPDATE {0} SET value=? WHERE key=?".format(self.__tablename__), (value, key)) - else: - query = ("INSERT INTO {0} (key, value) VALUES (?, ?)".format(self.__tablename__), (key, value)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - try: - cursor.execute(*query) - connection.commit() - except sqlite3.OperationalError: - log.error("Could not write to %s (database: %s)" % (str(key), self.__tablename__)) - raise NoWriteAccess("Could not write to %s (database: %s)" % (str(key), self.__tablename__)) - - def delete(self, key): - """ Delete a key from the configuration store - """ - query = ("DELETE FROM {0} WHERE key=?".format(self.__tablename__), (key,)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - try: - cursor.execute(*query) - connection.commit() - except sqlite3.OperationalError: - log.error("Could not write to %s (database: %s)" % (str(key), self.__tablename__)) - raise NoWriteAccess("Could not write to %s (database: %s)" % (str(key), self.__tablename__)) - - def __iter__(self): - return iter(list(self.items())) - - def items(self): - query = ("SELECT key, value from {0} ".format(self.__tablename__)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(query) - r = {} - for key, value in cursor.fetchall(): - r[key] = value - return r - - def __len__(self): - query = ("SELECT id from {0} ".format(self.__tablename__)) - connection = sqlite3.connect(self.sqlDataBaseFile) - cursor = connection.cursor() - cursor.execute(query) - return len(cursor.fetchall()) - - -class MasterPassword(object): - """ The keys are encrypted with a Masterpassword that is stored in - the configurationStore. It has a checksum to verify correctness - of the password - """ - - password = "" # nosec - decrypted_master = "" - - #: This key identifies the encrypted master password stored in the confiration - config_key = "encrypted_master_password" - - def __init__(self, password): - """ The encrypted private keys in `keys` are encrypted with a - random encrypted masterpassword that is stored in the - configuration. - - The password is used to encrypt this masterpassword. To - decrypt the keys stored in the keys database, one must use - BIP38, decrypt the masterpassword from the configuration - store with the user password, and use the decrypted - masterpassword to decrypt the BIP38 encrypted private keys - from the keys storage! - - :param str password: Password to use for en-/de-cryption - """ - self.password = password - self.config = get_default_config_storage() - if self.config_key not in self.config: - self.newMaster() - self.saveEncrytpedMaster() - else: - self.decryptEncryptedMaster() - - def decryptEncryptedMaster(self): - """ Decrypt the encrypted masterpassword - """ - aes = AESCipher(self.password) - checksum, encrypted_master = self.config[self.config_key].split("$") - try: - decrypted_master = aes.decrypt(encrypted_master) - except: - raise WrongMasterPasswordException - if checksum != self.deriveChecksum(decrypted_master): - raise WrongMasterPasswordException - self.decrypted_master = decrypted_master - - def saveEncrytpedMaster(self): - """ Store the encrypted master password in the configuration - store - """ - self.config[self.config_key] = self.getEncryptedMaster() - - def newMaster(self): - """ Generate a new random masterpassword - """ - # make sure to not overwrite an existing key - if (self.config_key in self.config and - self.config[self.config_key]): - return - self.decrypted_master = hexlify(os.urandom(32)).decode("ascii") - - def deriveChecksum(self, s): - """ Derive the checksum - """ - checksum = hashlib.sha256(py23_bytes(s, "ascii")).hexdigest() - return checksum[:4] - - def getEncryptedMaster(self): - """ Obtain the encrypted masterkey - """ - if not self.decrypted_master: - raise Exception("master not decrypted") - aes = AESCipher(self.password) - return "{}${}".format(self.deriveChecksum(self.decrypted_master), - aes.encrypt(self.decrypted_master)) - - def changePassword(self, newpassword): - """ Change the password - """ - self.password = newpassword - self.saveEncrytpedMaster() - - @staticmethod - def wipe(sure=False): - """Remove all keys from configStorage""" - if not sure: - log.error( - "You need to confirm that you are sure " - "and understand the implications of " - "wiping your wallet!" - ) - return - else: - config = get_default_config_storage() - config.delete(MasterPassword.config_key) - - -def get_default_config_storage(): - configStorage = Configuration() - # Create Tables if database is brand new - if not configStorage.exists_table(): - configStorage.create_table() - return configStorage - - -def get_default_key_storage(): - keyStorage = Key() - - newKeyStorage = False - if not keyStorage.exists_table(): - newKeyStorage = True - keyStorage.create_table() - return keyStorage - - -def get_default_token_storage(): - tokenStorage = Token() - newTokenStorage = False - if not tokenStorage.exists_table(): - newTokenStorage = True - tokenStorage.create_table() - return tokenStorage +#: Default configuration +nodelist = NodeList() +blockchain = "hive" +if blockchain == "hive": + nodes = nodelist.get_hive_nodes(testnet=False) +elif blockchain == "steem": + nodes = nodelist.get_steem_nodes(testnet=False) +else: + nodes = [] + +SqliteConfigurationStore.setdefault("node", nodes) +SqliteConfigurationStore.setdefault("default_chain", blockchain) +SqliteConfigurationStore.setdefault("password_storage", "environment") +SqliteConfigurationStore.setdefault("rpcpassword", "") +SqliteConfigurationStore.setdefault("rpcuser", "") +SqliteConfigurationStore.setdefault("order-expiration", 7 * 24 * 60 * 60) +SqliteConfigurationStore.setdefault("client_id", "") +SqliteConfigurationStore.setdefault("sc2_client_id", None) +SqliteConfigurationStore.setdefault("hs_client_id", None) +SqliteConfigurationStore.setdefault("hot_sign_redirect_uri", None) +SqliteConfigurationStore.setdefault("sc2_api_url", "https://api.steemconnect.com/api/") +SqliteConfigurationStore.setdefault("oauth_base_url", "https://api.steemconnect.com/oauth2/") +SqliteConfigurationStore.setdefault("hs_api_url", "https://hivesigner.com/api/") +SqliteConfigurationStore.setdefault("hs_oauth_base_url", "https://hivesigner.com/oauth2/") +SqliteConfigurationStore.setdefault("default_canonical_url", "https://hive.blog") +SqliteConfigurationStore.setdefault("default_path", "48'/13'/0'/0'/0'") + + +def get_default_config_store(*args, **kwargs): + return SqliteConfigurationStore(*args, **kwargs) + + +def get_default_key_store(config, *args, **kwargs): + return SqliteEncryptedKeyStore(config=config, **kwargs) diff --git a/beem/transactionbuilder.py b/beem/transactionbuilder.py index 0f661b89..7fa915c3 100644 --- a/beem/transactionbuilder.py +++ b/beem/transactionbuilder.py @@ -21,9 +21,9 @@ from .exceptions import ( InsufficientAuthorityError, MissingKeyError, InvalidWifError, - WalletLocked, OfflineHasNoRPCException ) +from beemstorage.exceptions import WalletLocked from beem.instance import shared_blockchain_instance log = logging.getLogger(__name__) diff --git a/beem/version.py b/beem/version.py index 37facaf7..1f0a7ac7 100644 --- a/beem/version.py +++ b/beem/version.py @@ -1,2 +1,2 @@ """THIS FILE IS GENERATED FROM beem SETUP.PY.""" -version = '0.23.13' +version = '0.24.0' diff --git a/beem/wallet.py b/beem/wallet.py index 2fa65481..94341f4f 100644 --- a/beem/wallet.py +++ b/beem/wallet.py @@ -6,33 +6,18 @@ from builtins import str, bytes from builtins import object import logging import os -import hashlib -from beemgraphenebase import bip38 from beemgraphenebase.account import PrivateKey from beem.instance import shared_blockchain_instance from .account import Account -from .aes import AESCipher from .exceptions import ( MissingKeyError, InvalidWifError, WalletExists, - WalletLocked, - WrongMasterPasswordException, - NoWalletException, OfflineHasNoRPCException, - AccountDoesNotExistsException, + AccountDoesNotExistsException ) -from beemapi.exceptions import NoAccessApi -from beemgraphenebase.py23 import py23_bytes -from .storage import get_default_config_storage, get_default_key_storage, get_default_token_storage -try: - import keyring - if not isinstance(keyring.get_keyring(), keyring.backends.fail.Keyring): - KEYRING_AVAILABLE = True - else: - KEYRING_AVAILABLE = False -except ImportError: - KEYRING_AVAILABLE = False +from beemstorage.exceptions import KeyAlreadyInStoreException, WalletLocked + log = logging.getLogger(__name__) @@ -99,18 +84,7 @@ class Wallet(object): import format (wif) (starting with a ``5``). """ - masterpassword = None - - # Keys from database - configStorage = None - MasterPassword = None - keyStorage = None - tokenStorage = None - # Manually provided keys - keys = {} # struct with pubkey as key and wif as value - token = {} - keyMap = {} # wif pairs to force certain keys def __init__(self, blockchain_instance=None, *args, **kwargs): if blockchain_instance is None: @@ -123,28 +97,20 @@ class Wallet(object): # Compatibility after name change from wif->keys if "wif" in kwargs and "keys" not in kwargs: kwargs["keys"] = kwargs["wif"] - master_password_set = False + if "keys" in kwargs and len(kwargs["keys"]) > 0: + from beemstorage import InRamPlainKeyStore + self.store = InRamPlainKeyStore() self.setKeys(kwargs["keys"]) else: """ If no keys are provided manually we load the SQLite keyStorage """ - from .storage import MasterPassword - self.MasterPassword = MasterPassword - master_password_set = True - self.keyStorage = get_default_key_storage() - - if "token" in kwargs: - self.setToken(kwargs["token"]) - else: - """ If no keys are provided manually we load the SQLite - keyStorage - """ - if not master_password_set: - from .storage import MasterPassword - self.MasterPassword = MasterPassword - self.tokenStorage = get_default_token_storage() + from beemstorage import SqliteEncryptedKeyStore + self.store = kwargs.get( + "key_store", + SqliteEncryptedKeyStore(config=self.blockchain.config, **kwargs), + ) @property def prefix(self): @@ -163,105 +129,65 @@ class Wallet(object): def setKeys(self, loadkeys): """ This method is strictly only for in memory keys that are - passed to Wallet/Steem with the ``keys`` argument + passed to Wallet with the ``keys`` argument """ - log.debug( - "Force setting of private keys. Not using the wallet database!") - self.clear_local_keys() + log.debug("Force setting of private keys. Not using the wallet database!") if isinstance(loadkeys, dict): - Wallet.keyMap = loadkeys loadkeys = list(loadkeys.values()) - elif isinstance(loadkeys, tuple): - loadkeys = list(loadkeys) - elif not isinstance(loadkeys, list): + elif not isinstance(loadkeys, (list, set)): loadkeys = [loadkeys] - for wif in loadkeys: - if isinstance(wif, list): - for w in wif: - pub = self._get_pub_from_wif(w) - Wallet.keys[pub] = str(w) - else: - pub = self._get_pub_from_wif(wif) - Wallet.keys[pub] = str(wif) - - def setToken(self, loadtoken): - """ This method is strictly only for in memory token that are - passed to Wallet/Steem with the ``token`` argument - """ - log.debug( - "Force setting of private token. Not using the wallet database!") - self.clear_local_token() - if isinstance(loadtoken, dict): - Wallet.token = loadtoken - else: - raise ValueError("token must be a dict variable!") + pub = self.publickey_from_wif(wif) + self.store.add(str(wif), pub) - def unlock(self, pwd=None): - """ Unlock the wallet database + def is_encrypted(self): + """ Is the key store encrypted? """ - if not self.created(): - raise NoWalletException + return self.store.is_encrypted() - if not pwd: - self.tryUnlockFromEnv() - else: - if (self.masterpassword is None and self.blockchain.config[self.MasterPassword.config_key]): - self.masterpwd = self.MasterPassword(pwd) - self.masterpassword = self.masterpwd.decrypted_master - - def tryUnlockFromEnv(self): - """ Try to fetch the unlock password from UNLOCK environment variable and keyring when no password is given. - """ - password_storage = self.blockchain.config["password_storage"] - if password_storage == "environment" and "UNLOCK" in os.environ: - log.debug("Trying to use environmental variable to unlock wallet") - pwd = os.environ.get("UNLOCK") - self.unlock(pwd) - elif password_storage == "keyring" and KEYRING_AVAILABLE: - log.debug("Trying to use keyring to unlock wallet") - pwd = keyring.get_password("beem", "wallet") - self.unlock(pwd) - else: - raise WrongMasterPasswordException + def unlock(self, pwd): + """ Unlock the wallet database + """ + unlock_ok = None + if self.store.is_encrypted(): + unlock_ok = self.store.unlock(pwd) + return unlock_ok def lock(self): """ Lock the wallet database """ - self.masterpassword = None + lock_ok = False + if self.store.is_encrypted(): + lock_ok = self.store.lock() + return lock_ok def unlocked(self): """ Is the wallet database unlocked? """ - return not self.locked() + unlocked = True + if self.store.is_encrypted(): + unlocked = not self.store.locked() + return unlocked def locked(self): """ Is the wallet database locked? """ - if Wallet.keys: # Keys have been manually provided! + if self.store.is_encrypted(): + return self.store.locked() + else: return False - try: - self.tryUnlockFromEnv() - except WrongMasterPasswordException: - pass - return not bool(self.masterpassword) def changePassphrase(self, new_pwd): """ Change the passphrase for the wallet database """ - if self.locked(): - raise AssertionError() - self.masterpwd.changePassword(new_pwd) + self.store.change_password(new_pwd) def created(self): """ Do we have a wallet database already? """ - if len(self.getPublicKeys()): + if len(self.store.getPublicKeys()): # Already keys installed return True - elif self.MasterPassword.config_key in self.blockchain.config: - # no keys but a master password - return True else: return False @@ -279,190 +205,42 @@ class Wallet(object): """ if self.created(): raise WalletExists("You already have created a wallet!") - self.masterpwd = self.MasterPassword(pwd) - self.masterpassword = self.masterpwd.decrypted_master - self.masterpwd.saveEncrytpedMaster() - - def wipe(self, sure=False): - """ Purge all data in wallet database - """ - if not sure: - log.error( - "You need to confirm that you are sure " - "and understand the implications of " - "wiping your wallet!" - ) - return - else: - from .storage import ( - MasterPassword - ) - keyStorage = get_default_key_storage() - tokenStorage = get_default_token_storage() - MasterPassword.wipe(sure) - keyStorage.wipe(sure) - tokenStorage.wipe(sure) - self.clear_local_keys() + self.store.unlock(pwd) - def clear_local_keys(self): - """Clear all manually provided keys""" - Wallet.keys = {} - Wallet.keyMap = {} + def privatekey(self, key): + return PrivateKey(key, prefix=self.prefix) - def clear_local_token(self): - """Clear all manually provided token""" - Wallet.token = {} - - def encrypt_wif(self, wif): - """ Encrypt a wif key - """ - if self.locked(): - raise AssertionError() - return format( - bip38.encrypt(PrivateKey(wif, prefix=self.prefix), self.masterpassword), "encwif") - - def decrypt_wif(self, encwif): - """ decrypt a wif key - """ - try: - # Try to decode as wif - PrivateKey(encwif, prefix=self.prefix) - return encwif - except (ValueError, AssertionError): - pass - if self.locked(): - raise AssertionError() - return format(bip38.decrypt(encwif, self.masterpassword), "wif") - - def deriveChecksum(self, s): - """ Derive the checksum - """ - checksum = hashlib.sha256(py23_bytes(s, "ascii")).hexdigest() - return checksum[:4] - - def encrypt_token(self, token): - """ Encrypt a token key - """ - if self.locked(): - raise AssertionError() - aes = AESCipher(self.masterpassword) - return "{}${}".format(self.deriveChecksum(token), aes.encrypt(token)) - - def decrypt_token(self, enctoken): - """ decrypt a wif key - """ - if self.locked(): - raise AssertionError() - aes = AESCipher(self.masterpassword) - checksum, encrypted_token = enctoken.split("$") - try: - decrypted_token = aes.decrypt(encrypted_token) - except: - raise WrongMasterPasswordException - if checksum != self.deriveChecksum(decrypted_token): - raise WrongMasterPasswordException - return decrypted_token - - def _get_pub_from_wif(self, wif): - """ Get the pubkey as string, from the wif key as string - """ - # it could be either graphenebase or steem so we can't check - # the type directly - if isinstance(wif, PrivateKey): - wif = str(wif) - try: - return format(PrivateKey(wif).pubkey, self.prefix) - except: - raise InvalidWifError( - "Invalid Private Key Format. Please use WIF!") - - def addToken(self, name, token): - if self.tokenStorage: - if not self.created(): - raise NoWalletException - self.tokenStorage.add(name, self.encrypt_token(token)) - - def getTokenForAccountName(self, name): - """ Obtain the private token for a given public name - - :param str name: Public name - """ - if(Wallet.token): - if name in Wallet.token: - return Wallet.token[name] - else: - raise MissingKeyError("No private token for {} found".format(name)) - else: - # Test if wallet exists - if not self.created(): - raise NoWalletException - - if not self.unlocked(): - raise WalletLocked - - enctoken = self.tokenStorage.getTokenForPublicName(name) - if not enctoken: - raise MissingKeyError("No private token for {} found".format(name)) - return self.decrypt_token(enctoken) - - def removeTokenFromPublicName(self, name): - """ Remove a token from the wallet database - - :param str name: token to be removed - """ - if self.tokenStorage: - # Test if wallet exists - if not self.created(): - raise NoWalletException - self.tokenStorage.delete(name) + def publickey_from_wif(self, wif): + return str(self.privatekey(str(wif)).pubkey) def addPrivateKey(self, wif): """Add a private key to the wallet database :param str wif: Private key """ - pub = self._get_pub_from_wif(wif) - if isinstance(wif, PrivateKey): - wif = str(wif) - if self.keyStorage: - # Test if wallet exists - if not self.created(): - raise NoWalletException - self.keyStorage.add(self.encrypt_wif(wif), pub) + try: + pub = self.publickey_from_wif(wif) + except Exception: + raise InvalidWifError("Invalid Key format!") + if str(pub) in self.store: + raise KeyAlreadyInStoreException("Key already in the store") + self.store.add(str(wif), str(pub)) def getPrivateKeyForPublicKey(self, pub): """ Obtain the private key for a given public key :param str pub: Public Key """ - if(Wallet.keys): - if pub in Wallet.keys: - return Wallet.keys[pub] - else: - raise MissingKeyError("No private key for {} found".format(pub)) - else: - # Test if wallet exists - if not self.created(): - raise NoWalletException - - if not self.unlocked(): - raise WalletLocked - - encwif = self.keyStorage.getPrivateKeyForPublicKey(pub) - if not encwif: - raise MissingKeyError("No private key for {} found".format(pub)) - return self.decrypt_wif(encwif) + if str(pub) not in self.store: + raise MissingKeyError + return self.store.getPrivateKeyForPublicKey(str(pub)) def removePrivateKeyFromPublicKey(self, pub): """ Remove a key from the wallet database :param str pub: Public key """ - if self.keyStorage: - # Test if wallet exists - if not self.created(): - raise NoWalletException - self.keyStorage.delete(pub) + self.store.delete(str(pub)) def removeAccount(self, account): """ Remove all keys associated with a given account @@ -472,7 +250,7 @@ class Wallet(object): accounts = self.getAccounts() for a in accounts: if a["name"] == account: - self.removePrivateKeyFromPublicKey(a["pubkey"]) + self.store.delete(a["pubkey"]) def getKeyForAccount(self, name, key_type): """ Obtain `key_type` Private Key for an account from the wallet database @@ -483,34 +261,32 @@ class Wallet(object): """ if key_type not in ["owner", "active", "posting", "memo"]: raise AssertionError("Wrong key type") - if key_type in Wallet.keyMap: - return Wallet.keyMap.get(key_type) + + if self.rpc.get_use_appbase(): + account = self.rpc.find_accounts({'accounts': [name]}, api="database")['accounts'] else: - if self.rpc.get_use_appbase(): - account = self.rpc.find_accounts({'accounts': [name]}, api="database")['accounts'] - else: - account = self.rpc.get_account(name) - if not account: - return - if len(account) == 0: - return - if key_type == "memo": - key = self.getPrivateKeyForPublicKey( - account[0]["memo_key"]) - if key: - return key - else: - key = None - for authority in account[0][key_type]["key_auths"]: - try: - key = self.getPrivateKeyForPublicKey(authority[0]) - if key: - return key - except MissingKeyError: - key = None - if key is None: - raise MissingKeyError("No private key for {} found".format(name)) + account = self.rpc.get_account(name) + if not account: + return + if len(account) == 0: return + if key_type == "memo": + key = self.getPrivateKeyForPublicKey( + account[0]["memo_key"]) + if key: + return key + else: + key = None + for authority in account[0][key_type]["key_auths"]: + try: + key = self.getPrivateKeyForPublicKey(authority[0]) + if key: + return key + except MissingKeyError: + key = None + if key is None: + raise MissingKeyError("No private key for {} found".format(name)) + return def getKeysForAccount(self, name, key_type): """ Obtain a List of `key_type` Private Keys for an account from the wallet database @@ -521,36 +297,34 @@ class Wallet(object): """ if key_type not in ["owner", "active", "posting", "memo"]: raise AssertionError("Wrong key type") - if key_type in Wallet.keyMap: - return Wallet.keyMap.get(key_type) + + if self.rpc.get_use_appbase(): + account = self.rpc.find_accounts({'accounts': [name]}, api="database")['accounts'] else: - if self.rpc.get_use_appbase(): - account = self.rpc.find_accounts({'accounts': [name]}, api="database")['accounts'] - else: - account = self.rpc.get_account(name) - if not account: - return - if len(account) == 0: - return - if key_type == "memo": - key = self.getPrivateKeyForPublicKey( - account[0]["memo_key"]) - if key: - return [key] - else: - keys = [] - key = None - for authority in account[0][key_type]["key_auths"]: - try: - key = self.getPrivateKeyForPublicKey(authority[0]) - if key: - keys.append(key) - except MissingKeyError: - key = None - if key is None: - raise MissingKeyError("No private key for {} found".format(name)) - return keys + account = self.rpc.get_account(name) + if not account: + return + if len(account) == 0: return + if key_type == "memo": + key = self.getPrivateKeyForPublicKey( + account[0]["memo_key"]) + if key: + return [key] + else: + keys = [] + key = None + for authority in account[0][key_type]["key_auths"]: + try: + key = self.getPrivateKeyForPublicKey(authority[0]) + if key: + keys.append(key) + except MissingKeyError: + key = None + if key is None: + raise MissingKeyError("No private key for {} found".format(name)) + return keys + return def getOwnerKeyForAccount(self, name): """ Obtain owner Private Key for an account from the wallet database @@ -590,7 +364,7 @@ class Wallet(object): def getAccountFromPrivateKey(self, wif): """ Obtain account name from private key """ - pub = self._get_pub_from_wif(wif) + pub = self.publickey_from_wif(wif) return self.getAccountFromPublicKey(pub) def getAccountsFromPublicKey(self, pub): @@ -669,9 +443,9 @@ class Wallet(object): """ for authority in ["owner", "active", "posting"]: for key in account[authority]["key_auths"]: - if pub == key[0]: + if str(pub) == key[0]: return authority - if pub == account["memo_key"]: + if str(pub) == account["memo_key"]: return "memo" return None @@ -686,18 +460,36 @@ class Wallet(object): accounts.extend(self.getAllAccounts(pubkey)) return accounts - def getPublicKeys(self): + def getPublicKeys(self, current=False): """ Return all installed public keys + :param bool current: If true, returns only keys for currently + connected blockchain """ - if self.keyStorage: - return self.keyStorage.getPublicKeys(prefix=self.blockchain.prefix) - else: - return list(Wallet.keys.keys()) + pubkeys = self.store.getPublicKeys() + if not current: + return pubkeys + pubs = [] + for pubkey in pubkeys: + # Filter those keys not for our network + if pubkey[: len(self.prefix)] == self.prefix: + pubs.append(pubkey) + return pubs def getPublicNames(self): """ Return all installed public token """ - if self.tokenStorage: - return self.tokenStorage.getPublicNames() + if self.token_store is None: + return + return self.token_store.getPublicNames() + + def wipe(self, sure=False): + if not sure: + log.error( + "You need to confirm that you are sure " + "and understand the implications of " + "wiping your wallet!" + ) + return else: - return list(Wallet.token.keys()) + self.store.wipe() + self.store.wipe_masterpassword() diff --git a/beemapi/version.py b/beemapi/version.py index 37facaf7..1f0a7ac7 100644 --- a/beemapi/version.py +++ b/beemapi/version.py @@ -1,2 +1,2 @@ """THIS FILE IS GENERATED FROM beem SETUP.PY.""" -version = '0.23.13' +version = '0.24.0' diff --git a/beembase/version.py b/beembase/version.py index 37facaf7..1f0a7ac7 100644 --- a/beembase/version.py +++ b/beembase/version.py @@ -1,2 +1,2 @@ """THIS FILE IS GENERATED FROM beem SETUP.PY.""" -version = '0.23.13' +version = '0.24.0' diff --git a/beemgraphenebase/__init__.py b/beemgraphenebase/__init__.py index f7a36fbf..22f84273 100644 --- a/beemgraphenebase/__init__.py +++ b/beemgraphenebase/__init__.py @@ -8,6 +8,7 @@ from .version import version as __version__ # from . import dictionary as BrainKeyDictionary __all__ = ['account', + 'aes', 'base58', 'bip32' 'bip38', diff --git a/beem/aes.py b/beemgraphenebase/aes.py similarity index 100% rename from beem/aes.py rename to beemgraphenebase/aes.py diff --git a/beemgraphenebase/bip38.py b/beemgraphenebase/bip38.py index 0dd4b470..f804a95f 100644 --- a/beemgraphenebase/bip38.py +++ b/beemgraphenebase/bip38.py @@ -57,6 +57,11 @@ def encrypt(privkey, passphrase): :rtype: Base58 """ + if isinstance(privkey, str): + privkey = PrivateKey(privkey) + else: + privkey = PrivateKey(repr(privkey)) + privkeyhex = repr(privkey) # hex addr = format(privkey.bitcoin.address, "BTC") a = py23_bytes(addr, 'ascii') diff --git a/beemgraphenebase/version.py b/beemgraphenebase/version.py index 37facaf7..1f0a7ac7 100644 --- a/beemgraphenebase/version.py +++ b/beemgraphenebase/version.py @@ -1,2 +1,2 @@ """THIS FILE IS GENERATED FROM beem SETUP.PY.""" -version = '0.23.13' +version = '0.24.0' diff --git a/beemstorage/__init__.py b/beemstorage/__init__.py new file mode 100644 index 00000000..24c31a26 --- /dev/null +++ b/beemstorage/__init__.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# Load modules from other classes +# # Inspired by https://raw.githubusercontent.com/xeroc/python-graphenelib/master/graphenestorage/__init__.py +from .base import ( + InRamConfigurationStore, + InRamPlainKeyStore, + InRamEncryptedKeyStore, + InRamPlainTokenStore, + InRamEncryptedTokenStore, + SqliteConfigurationStore, + SqlitePlainKeyStore, + SqliteEncryptedKeyStore, + SqlitePlainTokenStore, + SqliteEncryptedTokenStore, +) +from .sqlite import SQLiteFile, SQLiteCommon + +__all__ = ["interfaces", "masterpassword", "base", "sqlite", "ram"] + + +def get_default_config_store(*args, **kwargs): + """ This method returns the default **configuration** store + that uses an SQLite database internally. + :params str appname: The appname that is used internally to distinguish + different SQLite files + """ + kwargs["appname"] = kwargs.get("appname", "beem") + return SqliteConfigurationStore(*args, **kwargs) + + +def get_default_key_store(*args, config, **kwargs): + """ This method returns the default **key** store + that uses an SQLite database internally. + :params str appname: The appname that is used internally to distinguish + different SQLite files + """ + kwargs["appname"] = kwargs.get("appname", "beem") + return SqliteEncryptedKeyStore(config=config, **kwargs) diff --git a/beemstorage/base.py b/beemstorage/base.py new file mode 100644 index 00000000..3934592a --- /dev/null +++ b/beemstorage/base.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +# Inspired by https://raw.githubusercontent.com/xeroc/python-graphenelib/master/graphenestorage/base.py +import logging + +from .masterpassword import MasterPassword +from .interfaces import KeyInterface, ConfigInterface, EncryptedKeyInterface, TokenInterface, EncryptedTokenInterface +from .ram import InRamStore +from .sqlite import SQLiteStore +from .exceptions import KeyAlreadyInStoreException + +log = logging.getLogger(__name__) + + +# Configuration +class InRamConfigurationStore(InRamStore, ConfigInterface): + """ A simple example that stores configuration in RAM. + + Internally, this works by simply inheriting + :class:`beemstorage.ram.InRamStore`. The interface is defined in + :class:`beemstorage.interfaces.ConfigInterface`. + """ + + pass + + +class SqliteConfigurationStore(SQLiteStore, ConfigInterface): + """ This is the configuration storage that stores key/value + pairs in the `config` table of the SQLite3 database. + + Internally, this works by simply inheriting + :class:`beemstorage.sqlite.SQLiteStore`. The interface is defined + in :class:`beemstorage.interfaces.ConfigInterface`. + """ + + #: The table name for the configuration + __tablename__ = "config" + #: The name of the 'key' column + __key__ = "key" + #: The name of the 'value' column + __value__ = "value" + + +# Keys +class InRamPlainKeyStore(InRamStore, KeyInterface): + """ A simple in-RAM Store that stores keys unencrypted in RAM + + Internally, this works by simply inheriting + :class:`beemstorage.ram.InRamStore`. The interface is defined in + :class:`beemstorage.interfaces.KeyInterface`. + """ + + def getPublicKeys(self): + return [k for k, v in self.items()] + + def getPrivateKeyForPublicKey(self, pub): + return self.get(str(pub), None) + + def add(self, wif, pub): + if str(pub) in self: + raise KeyAlreadyInStoreException + self[str(pub)] = str(wif) + + def delete(self, pub): + InRamStore.delete(self, str(pub)) + + +class SqlitePlainKeyStore(SQLiteStore, KeyInterface): + """ This is the key storage that stores the public key and the + **unencrypted** private key in the `keys` table in the SQLite3 + database. + + Internally, this works by simply inheriting + :class:`beemstorage.ram.InRamStore`. The interface is defined in + :class:`beemstorage.interfaces.KeyInterface`. + """ + + #: The table name for the configuration + __tablename__ = "keys" + #: The name of the 'key' column + __key__ = "pub" + #: The name of the 'value' column + __value__ = "wif" + + def getPublicKeys(self): + return [k for k, v in self.items()] + + def getPrivateKeyForPublicKey(self, pub): + return self[pub] + + def add(self, wif, pub): + if str(pub) in self: + raise KeyAlreadyInStoreException + self[str(pub)] = str(wif) + + def delete(self, pub): + SQLiteStore.delete(self, str(pub)) + + def is_encrypted(self): + """ Returns False, as we are not encrypted here + """ + return False + + +class KeyEncryption(MasterPassword, EncryptedKeyInterface): + """ This is an interface class that provides the methods required for + EncryptedKeyInterface and links them to the MasterPassword-provided + functionatlity, accordingly. + """ + + def __init__(self, *args, **kwargs): + EncryptedKeyInterface.__init__(self, *args, **kwargs) + MasterPassword.__init__(self, *args, **kwargs) + + # Interface to deal with encrypted keys + def getPublicKeys(self): + return [k for k, v in self.items()] + + def getPrivateKeyForPublicKey(self, pub): + wif = self.get(str(pub), None) + if wif: + return self.decrypt(wif) # From Masterpassword + + def add(self, wif, pub): + if str(pub) in self: + raise KeyAlreadyInStoreException + self[str(pub)] = self.encrypt(str(wif)) # From Masterpassword + + def is_encrypted(self): + return True + + +class InRamEncryptedKeyStore(InRamStore, KeyEncryption): + """ An in-RAM Store that stores keys **encrypted** in RAM. + + Internally, this works by simply inheriting + :class:`beemstorage.ram.InRamStore`. The interface is defined in + :class:`beemstorage.interfaces.KeyInterface`. + + .. note:: This module also inherits + :class:`beemstorage.masterpassword.MasterPassword` which offers + additional methods and deals with encrypting the keys. + """ + + def __init__(self, *args, **kwargs): + InRamStore.__init__(self, *args, **kwargs) + KeyEncryption.__init__(self, *args, **kwargs) + + +class SqliteEncryptedKeyStore(SQLiteStore, KeyEncryption): + """ This is the key storage that stores the public key and the + **encrypted** private key in the `keys` table in the SQLite3 database. + + Internally, this works by simply inheriting + :class:`beemstorage.ram.InRamStore`. The interface is defined in + :class:`beemstorage.interfaces.KeyInterface`. + + .. note:: This module also inherits + :class:`beemstorage.masterpassword.MasterPassword` which offers + additional methods and deals with encrypting the keys. + """ + + __tablename__ = "keys" + __key__ = "pub" + __value__ = "wif" + + def __init__(self, *args, **kwargs): + SQLiteStore.__init__(self, *args, **kwargs) + KeyEncryption.__init__(self, *args, **kwargs) + + +# Token +class InRamPlainTokenStore(InRamStore, TokenInterface): + """ A simple in-RAM Store that stores token unencrypted in RAM + + Internally, this works by simply inheriting + :class:`beemstorage.ram.InRamStore`. The interface is defined in + :class:`beemstorage.interfaces.TokenInterface`. + """ + + def getPublicNames(self): + return [k for k, v in self.items()] + + def getPrivateKeyForPublicKey(self, pub): + return self.get(str(pub), None) + + def add(self, token, name): + if str(name) in self: + raise KeyAlreadyInStoreException + self[str(name)] = str(token) + + def delete(self, name): + InRamStore.delete(self, str(name)) + + +class SqlitePlainTokenStore(SQLiteStore, TokenInterface): + """ This is the token storage that stores the public key and the + **unencrypted** private key in the `tokens` table in the SQLite3 + database. + + Internally, this works by simply inheriting + :class:`beemstorage.ram.InRamStore`. The interface is defined in + :class:`beemstorage.interfaces.TokenInterface`. + """ + + #: The table name for the configuration + __tablename__ = "token" + #: The name of the 'key' column + __key__ = "name" + #: The name of the 'value' column + __value__ = "token" + + def getPublicNames(self): + return [k for k, v in self.items()] + + def getPrivateKeyForPublicKey(self, name): + return self[name] + + def add(self, token, name): + if str(name) in self: + raise KeyAlreadyInStoreException + self[str(name)] = str(token) + + def updateToken(self, name, token): + self[str(name)] = str(token) # From Masterpassword + + def delete(self, name): + SQLiteStore.delete(self, str(name)) + + def is_encrypted(self): + """ Returns False, as we are not encrypted here + """ + return False + + +class TokenEncryption(MasterPassword, EncryptedTokenInterface): + """ This is an interface class that provides the methods required for + EncryptedTokenInterface and links them to the MasterPassword-provided + functionatlity, accordingly. + """ + + def __init__(self, *args, **kwargs): + EncryptedTokenInterface.__init__(self, *args, **kwargs) + MasterPassword.__init__(self, *args, **kwargs) + + # Interface to deal with encrypted keys + def getPublicNames(self): + return [k for k, v in self.items()] + + def getPrivateKeyForPublicKey(self, name): + token = self.get(str(name), None) + if token: + return self.decrypt_text(token) # From Masterpassword + + def add(self, token, name): + if str(name) in self: + raise KeyAlreadyInStoreException + self[str(name)] = self.encrypt_text(str(token)) # From Masterpassword + + def updateToken(self, name, token): + self[str(name)] = self.encrypt_text(str(token)) # From Masterpassword + + def is_encrypted(self): + return True + + +class InRamEncryptedTokenStore(InRamStore, TokenEncryption): + """ An in-RAM Store that stores token **encrypted** in RAM. + + Internally, this works by simply inheriting + :class:`beemstorage.ram.InRamStore`. The interface is defined in + :class:`beemstorage.interfaces.TokenInterface`. + + .. note:: This module also inherits + :class:`beemstorage.masterpassword.MasterPassword` which offers + additional methods and deals with encrypting the keys. + """ + + def __init__(self, *args, **kwargs): + InRamStore.__init__(self, *args, **kwargs) + TokenEncryption.__init__(self, *args, **kwargs) + + +class SqliteEncryptedTokenStore(SQLiteStore, TokenEncryption): + """ This is the key storage that stores the account name and the + **encrypted** token in the `token` table in the SQLite3 database. + + Internally, this works by simply inheriting + :class:`beemstorage.ram.InRamStore`. The interface is defined in + :class:`beemstorage.interfaces.TokenInterface`. + + .. note:: This module also inherits + :class:`beemstorage.masterpassword.MasterPassword` which offers + additional methods and deals with encrypting the token. + """ + + __tablename__ = "token" + __key__ = "name" + __value__ = "token" + + def __init__(self, *args, **kwargs): + SQLiteStore.__init__(self, *args, **kwargs) + TokenEncryption.__init__(self, *args, **kwargs) diff --git a/beemstorage/exceptions.py b/beemstorage/exceptions.py new file mode 100644 index 00000000..7b30abe7 --- /dev/null +++ b/beemstorage/exceptions.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Inspired by https://raw.githubusercontent.com/xeroc/python-graphenelib/master/graphenestorage/exceptions.py +class WalletLocked(Exception): + pass + + +class WrongMasterPasswordException(Exception): + """ The password provided could not properly unlock the wallet + """ + + pass + + +class KeyAlreadyInStoreException(Exception): + """ The key of a key/value pair is already in the store + """ + + pass diff --git a/beemstorage/interfaces.py b/beemstorage/interfaces.py new file mode 100644 index 00000000..9b3a77bb --- /dev/null +++ b/beemstorage/interfaces.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- +# Inspired by https://raw.githubusercontent.com/xeroc/python-graphenelib/master/graphenestorage/interfaces.py +class StoreInterface(dict): + + """ The store interface is the most general store that we can have. + + It inherits dict and thus behaves like a dictionary. As such any + key/value store can be used as store with or even without an adaptor. + + .. note:: This class defines ``defaults`` that are used to return + reasonable defaults for the library. + + .. warning:: If you are trying to obtain a value for a key that does + **not** exist in the store, the library will **NOT** raise but + return a ``None`` value. This represents the biggest difference to + a regular ``dict`` class. + + Methods that need to be implemented: + + * ``def setdefault(cls, key, value)`` + * ``def __init__(self, *args, **kwargs)`` + * ``def __setitem__(self, key, value)`` + * ``def __getitem__(self, key)`` + * ``def __iter__(self)`` + * ``def __len__(self)`` + * ``def __contains__(self, key)`` + + + .. note:: Configuration and Key classes are subclasses of this to allow + storing keys separate from configuration. + + """ + + defaults = {} + + @classmethod + def setdefault(cls, key, value): + """ Allows to define default values + """ + cls.defaults[key] = value + + def __init__(self, *args, **kwargs): + pass + + def __setitem__(self, key, value): + """ Sets an item in the store + """ + return dict.__setitem__(self, key, value) + + def __getitem__(self, key): + """ Gets an item from the store as if it was a dictionary + + .. note:: Special behavior! If a key is not found, ``None`` is + returned instead of raising an exception, unless a default + value is found, then that is returned. + """ + if key in self: + return dict.__getitem__(self, key) + elif key in self.defaults: + return self.defaults[key] + else: + return None + + def __iter__(self): + """ Iterates through the store + """ + return dict.__iter__(self) + + def __len__(self): + """ return lenght of store + """ + return dict.__len__(self) + + def __contains__(self, key): + """ Tests if a key is contained in the store. + """ + return dict.__contains__(self, key) + + def items(self): + """ Returns all items off the store as tuples + """ + return dict.items(self) + + def get(self, key, default=None): + """ Return the key if exists or a default value + """ + return dict.get(self, key, default) + + # Specific for this library + def delete(self, key): + """ Delete a key from the store + """ + raise NotImplementedError + + def wipe(self): + """ Wipe the store + """ + raise NotImplementedError + + +class KeyInterface(StoreInterface): + """ The KeyInterface defines the interface for key storage. + + .. note:: This class inherits + :class:`beemstorage.interfaces.StoreInterface` and defines + additional key-specific methods. + """ + + def is_encrypted(self): + """ Returns True/False to indicate required use of unlock + """ + return False + + # Interface to deal with encrypted keys + def getPublicKeys(self): + """ Returns the public keys stored in the database + """ + raise NotImplementedError + + def getPrivateKeyForPublicKey(self, pub): + """ Returns the (possibly encrypted) private key that + corresponds to a public key + + :param str pub: Public key + + The encryption scheme is BIP38 + """ + raise NotImplementedError + + def add(self, wif, pub=None): + """ Add a new public/private key pair (correspondence has to be + checked elsewhere!) + + :param str pub: Public key + :param str wif: Private key + """ + raise NotImplementedError + + def delete(self, pub): + """ Delete a pubkey/privatekey pair from the store + + :param str pub: Public key + """ + raise NotImplementedError + + +class EncryptedKeyInterface(KeyInterface): + """ The EncryptedKeyInterface extends KeyInterface to work with encrypted + keys + """ + + def is_encrypted(self): + """ Returns True/False to indicate required use of unlock + """ + return True + + def unlock(self, password): + """ Tries to unlock the wallet if required + + :param str password: Plain password + """ + raise NotImplementedError + + def locked(self): + """ is the wallet locked? + """ + return False + + def lock(self): + """ Lock the wallet again + """ + raise NotImplementedError + + +class ConfigInterface(StoreInterface): + """ The BaseKeyStore defines the interface for key storage + + .. note:: This class inherits + :class:`beemstorage.interfaces.StoreInterface` and defines + **no** additional configuration-specific methods. + """ + + pass + + +class TokenInterface(StoreInterface): + """ The TokenInterface defines the interface for token storage. + + .. note:: This class inherits + :class:`beemstorage.interfaces.StoreInterface` and defines + additional key-specific methods. + """ + + def is_encrypted(self): + """ Returns True/False to indicate required use of unlock + """ + return False + + # Interface to deal with encrypted keys + def getPublicKeys(self): + """ Returns the public keys stored in the database + """ + raise NotImplementedError + + def getPrivateKeyForPublicKey(self, pub): + """ Returns the (possibly encrypted) private key that + corresponds to a public key + + :param str pub: Public key + + The encryption scheme is BIP38 + """ + raise NotImplementedError + + def add(self, wif, pub=None): + """ Add a new public/private key pair (correspondence has to be + checked elsewhere!) + + :param str pub: Public key + :param str wif: Private key + """ + raise NotImplementedError + + def delete(self, pub): + """ Delete a pubkey/privatekey pair from the store + + :param str pub: Public key + """ + raise NotImplementedError + + +class EncryptedTokenInterface(TokenInterface): + """ The EncryptedKeyInterface extends KeyInterface to work with encrypted + tokens + """ + + def is_encrypted(self): + """ Returns True/False to indicate required use of unlock + """ + return True + + def unlock(self, password): + """ Tries to unlock the wallet if required + + :param str password: Plain password + """ + raise NotImplementedError + + def locked(self): + """ is the wallet locked? + """ + return False + + def lock(self): + """ Lock the wallet again + """ + raise NotImplementedError diff --git a/beemstorage/masterpassword.py b/beemstorage/masterpassword.py new file mode 100644 index 00000000..673a94f2 --- /dev/null +++ b/beemstorage/masterpassword.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# Inspired by https://raw.githubusercontent.com/xeroc/python-graphenelib/master/graphenestorage/masterpassword.py +import os +import hashlib +import logging +import warnings + +from binascii import hexlify +from beemgraphenebase.py23 import py23_bytes +from beemgraphenebase import bip38 +from beemgraphenebase.aes import AESCipher +from .exceptions import WrongMasterPasswordException, WalletLocked +try: + import keyring + if not isinstance(keyring.get_keyring(), keyring.backends.fail.Keyring): + KEYRING_AVAILABLE = True + else: + KEYRING_AVAILABLE = False +except ImportError: + KEYRING_AVAILABLE = False + +log = logging.getLogger(__name__) + + +class MasterPassword(object): + """ The keys are encrypted with a Masterpassword that is stored in + the configurationStore. It has a checksum to verify correctness + of the password + The encrypted private keys in `keys` are encrypted with a random + **masterkey/masterpassword** that is stored in the configuration + encrypted by the user-provided password. + + :param ConfigStore config: Configuration store to get access to the + encrypted master password + """ + + def __init__(self, config=None, **kwargs): + if config is None: + raise ValueError("If using encrypted store, a config store is required!") + self.config = config + self.password = None + self.decrypted_master = None + self.config_key = "encrypted_master_password" + + @property + def masterkey(self): + """ Contains the **decrypted** master key + """ + return self.decrypted_master + + def has_masterpassword(self): + """ Tells us if the config store knows an encrypted masterpassword + """ + return self.config_key in self.config + + def locked(self): + """ Is the store locked. E.g. Is a valid password known that can be + used to decrypt the master key? + """ + return not self.unlocked() + + def unlocked(self): + """ Is the store unlocked so that I can decrypt the content? + """ + if self.password is not None: + return bool(self.password) + else: + password_storage = self.config["password_storage"] + if ( + "UNLOCK" in os.environ + and os.environ["UNLOCK"] + and self.config_key in self.config + and self.config[self.config_key] + and password_storage == "environment" + ): + log.debug("Trying to use environmental " "variable to unlock wallet") + self.unlock(os.environ.get("UNLOCK")) + return bool(self.password) + elif ( + password_storage == "keyring" + and KEYRING_AVAILABLE + and self.config_key in self.config + and self.config[self.config_key] + ): + log.debug("Trying to use keyring to unlock wallet") + pwd = keyring.get_password("beem", "wallet") + self.unlock(pwd) + return bool(self.password) + return False + + def lock(self): + """ Lock the store so that we can no longer decrypt the content of the + store + """ + self.password = None + self.decrypted_master = None + + def unlock(self, password): + """ The password is used to encrypt this masterpassword. To + decrypt the keys stored in the keys database, one must use + BIP38, decrypt the masterpassword from the configuration + store with the user password, and use the decrypted + masterpassword to decrypt the BIP38 encrypted private keys + from the keys storage! + + :param str password: Password to use for en-/de-cryption + """ + self.password = password + if self.config_key in self.config and self.config[self.config_key]: + self._decrypt_masterpassword() + else: + self._new_masterpassword(password) + self._save_encrypted_masterpassword() + + def wipe_masterpassword(self): + """ Removes the encrypted masterpassword from config storage""" + if self.config_key in self.config and self.config[self.config_key]: + self.config[self.config_key] = None + + def _decrypt_masterpassword(self): + """ Decrypt the encrypted masterkey + """ + aes = AESCipher(self.password) + checksum, encrypted_master = self.config[self.config_key].split("$") + try: + decrypted_master = aes.decrypt(encrypted_master) + except Exception: + self._raise_wrongmasterpassexception() + if checksum != self._derive_checksum(decrypted_master): + self._raise_wrongmasterpassexception() + self.decrypted_master = decrypted_master + + def _raise_wrongmasterpassexception(self): + self.password = None + raise WrongMasterPasswordException + + def _save_encrypted_masterpassword(self): + self.config[self.config_key] = self._get_encrypted_masterpassword() + + def _new_masterpassword(self, password): + """ Generate a new random masterkey, encrypt it with the password and + store it in the store. + + :param str password: Password to use for en-/de-cryption + """ + # make sure to not overwrite an existing key + if self.config_key in self.config and self.config[self.config_key]: + raise Exception("Storage already has a masterpassword!") + + self.decrypted_master = hexlify(os.urandom(32)).decode("ascii") + + # Encrypt and save master + self.password = password + self._save_encrypted_masterpassword() + return self.masterkey + + def _derive_checksum(self, s): + """ Derive the checksum + + :param str s: Random string for which to derive the checksum + """ + checksum = hashlib.sha256(bytes(s, "ascii")).hexdigest() + return checksum[:4] + + def _get_encrypted_masterpassword(self): + """ Obtain the encrypted masterkey + + .. note:: The encrypted masterkey is checksummed, so that we can + figure out that a provided password is correct or not. The + checksum is only 4 bytes long! + """ + if not self.unlocked(): + raise WalletLocked + aes = AESCipher(self.password) + return "{}${}".format( + self._derive_checksum(self.masterkey), aes.encrypt(self.masterkey) + ) + + def change_password(self, newpassword): + """ Change the password that allows to decrypt the master key + """ + if not self.unlocked(): + raise WalletLocked + self.password = newpassword + self._save_encrypted_masterpassword() + + def decrypt(self, wif): + """ Decrypt the content according to BIP38 + + :param str wif: Encrypted key + """ + if not self.unlocked(): + raise WalletLocked + return format(bip38.decrypt(wif, self.masterkey), "wif") + + def deriveChecksum(self, s): + """ Derive the checksum + """ + checksum = hashlib.sha256(py23_bytes(s, "ascii")).hexdigest() + return checksum[:4] + + def encrypt_text(self, txt): + """ Encrypt the content according to BIP38 + + :param str wif: Unencrypted key + """ + if not self.unlocked(): + raise WalletLocked + aes = AESCipher(self.masterkey) + return "{}${}".format(self.deriveChecksum(txt), aes.encrypt(txt)) + + def decrypt_text(self, enctxt): + """ Decrypt the content according to BIP38 + + :param str wif: Encrypted key + """ + if not self.unlocked(): + raise WalletLocked + aes = AESCipher(self.masterkey) + checksum, encrypted_text = enctxt.split("$") + try: + decrypted_text = aes.decrypt(encrypted_text) + except: + raise WrongMasterPasswordException + if checksum != self.deriveChecksum(decrypted_text): + raise WrongMasterPasswordException + return decrypted_text + + def encrypt(self, wif): + """ Encrypt the content according to BIP38 + + :param str wif: Unencrypted key + """ + if not self.unlocked(): + raise WalletLocked + return format(bip38.encrypt(str(wif), self.masterkey), "encwif") + + def changePassword(self, newpassword): # pragma: no cover + warnings.warn( + "changePassword will be replaced by change_password in the future", + PendingDeprecationWarning, + ) + return self.change_password(newpassword) diff --git a/beemstorage/ram.py b/beemstorage/ram.py new file mode 100644 index 00000000..b27433ad --- /dev/null +++ b/beemstorage/ram.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Inspired by https://raw.githubusercontent.com/xeroc/python-graphenelib/master/graphenestorage/ram.py +from .interfaces import StoreInterface + + +# StoreInterface is done first, then dict which overwrites the interface +# methods +class InRamStore(StoreInterface): + """ The InRamStore inherits + :class:`beemstorage.interfaces.StoreInterface` and extends it by two + further calls for wipe and delete. + + The store is syntactically equivalent to a regular dictionary. + + .. warning:: If you are trying to obtain a value for a key that does + **not** exist in the store, the library will **NOT** raise but + return a ``None`` value. This represents the biggest difference to + a regular ``dict`` class. + """ + + # Specific for this library + def delete(self, key): + """ Delete a key from the store + """ + self.pop(key, None) + + def wipe(self): + """ Wipe the store + """ + keys = list(self.keys()).copy() + for key in keys: + self.delete(key) diff --git a/beemstorage/sqlite.py b/beemstorage/sqlite.py new file mode 100644 index 00000000..fff61293 --- /dev/null +++ b/beemstorage/sqlite.py @@ -0,0 +1,352 @@ +# -*- coding: utf-8 -*- +# Inspired by https://raw.githubusercontent.com/xeroc/python-graphenelib/master/graphenestorage/sqlite.py +import os +import sqlite3 +import logging +import shutil +from datetime import datetime +from appdirs import user_data_dir +import time + +from .interfaces import StoreInterface + +log = logging.getLogger(__name__) +timeformat = "%Y%m%d-%H%M%S" + + +class SQLiteFile: + """ This class ensures that the user's data is stored in its OS + preotected user directory: + + **OSX:** + + * `~/Library/Application Support/<AppName>` + + **Windows:** + + * `C:\\Documents and Settings\\<User>\\Application Data\\Local Settings\\<AppAuthor>\\<AppName>` + * `C:\\Documents and Settings\\<User>\\Application Data\\<AppAuthor>\\<AppName>` + + **Linux:** + + * `~/.local/share/<AppName>` + + Furthermore, it offers an interface to generated backups + in the `backups/` directory every now and then. + + .. note:: The file name can be overwritten when providing a keyword + argument ``profile``. + """ + + def __init__(self, *args, **kwargs): + appauthor = "beem" + appname = kwargs.get("appname", "beem") + self.data_dir = kwargs.get("data_dir", user_data_dir(appname, appauthor)) + + if "profile" in kwargs: + self.storageDatabase = "{}.sqlite".format(kwargs["profile"]) + else: + self.storageDatabase = "{}.sqlite".format(appname) + + self.sqlite_file = os.path.join(self.data_dir, self.storageDatabase) + + """ Ensure that the directory in which the data is stored + exists + """ + if os.path.isdir(self.data_dir): # pragma: no cover + return + else: # pragma: no cover + os.makedirs(self.data_dir) + + def sqlite3_backup(self, backupdir): + """ Create timestamped database copy + """ + if not os.path.isdir(backupdir): + os.mkdir(backupdir) + backup_file = os.path.join( + backupdir, + os.path.basename(self.storageDatabase) + + datetime.utcnow().strftime("-" + timeformat)) + self.sqlite3_copy(self.sqlite_file, backup_file) + + def sqlite3_copy(self, src, dst): + """Copy sql file from src to dst""" + if not os.path.isfile(src): + return + connection = sqlite3.connect(self.sqlite_file) + cursor = connection.cursor() + # Lock database before making a backup + cursor.execute('begin immediate') + # Make new backup file + shutil.copyfile(src, dst) + log.info("Creating {}...".format(dst)) + # Unlock database + connection.rollback() + + def recover_with_latest_backup(self, backupdir="backups"): + """ Replace database with latest backup""" + file_date = 0 + if not os.path.isdir(backupdir): + backupdir = os.path.join(self.data_dir, backupdir) + if not os.path.isdir(backupdir): + return + newest_backup_file = None + for filename in os.listdir(backupdir): + backup_file = os.path.join(backupdir, filename) + if os.stat(backup_file).st_ctime > file_date: + if os.path.isfile(backup_file): + file_date = os.stat(backup_file).st_ctime + newest_backup_file = backup_file + if newest_backup_file is not None: + self.sqlite3_copy(newest_backup_file, self.sqlite_file) + + def clean_data(self, backupdir="backups"): + """ Delete files older than 70 days + """ + log.info("Cleaning up old backups") + backupdir = os.path.join(self.data_dir, backupdir) + for filename in os.listdir(backupdir): + backup_file = os.path.join(backupdir, filename) + if os.stat(backup_file).st_ctime < (time.time() - 70 * 86400): + if os.path.isfile(backup_file): + os.remove(backup_file) + log.info("Deleting {}...".format(backup_file)) + + def refreshBackup(self): + """ Make a new backup + """ + backupdir = os.path.join(self.data_dir, "backups") + self.sqlite3_backup(backupdir) + self.clean_data(backupdir) + + +class SQLiteCommon(object): + """ This class abstracts away common sqlite3 operations. + + This class should not be used directly. + + When inheriting from this class, the following instance members must + be defined: + + * ``sqlite_file``: Path to the SQLite Database file + """ + def sql_fetchone(self, query): + connection = sqlite3.connect(self.sqlite_file) + try: + cursor = connection.cursor() + cursor.execute(*query) + result = cursor.fetchone() + finally: + connection.close() + return result + + def sql_fetchall(self, query): + connection = sqlite3.connect(self.sqlite_file) + try: + cursor = connection.cursor() + cursor.execute(*query) + results = cursor.fetchall() + finally: + connection.close() + return results + + def sql_execute(self, query, lastid=False): + connection = sqlite3.connect(self.sqlite_file) + try: + cursor = connection.cursor() + cursor.execute(*query) + connection.commit() + except: + connection.close() + raise + ret = None + try: + if lastid: + cursor = connection.cursor() + cursor.execute("SELECT last_insert_rowid();") + ret = cursor.fetchone()[0] + finally: + connection.close() + return ret + + +class SQLiteStore(SQLiteFile, SQLiteCommon, StoreInterface): + """ The SQLiteStore deals with the sqlite3 part of storing data into a + database file. + + .. note:: This module is limited to two columns and merely stores + key/value pairs into the sqlite database + + On first launch, the database file as well as the tables are created + automatically. + + When inheriting from this class, the following three class members must + be defined: + + * ``__tablename__``: Name of the table + * ``__key__``: Name of the key column + * ``__value__``: Name of the value column + """ + + #: + __tablename__ = None + __key__ = None + __value__ = None + + def __init__(self, *args, **kwargs): + #: Storage + SQLiteFile.__init__(self, *args, **kwargs) + StoreInterface.__init__(self, *args, **kwargs) + if self.__tablename__ is None or self.__key__ is None or self.__value__ is None: + raise ValueError("Values missing for tablename, key, or value!") + if not self.exists(): # pragma: no cover + self.create() + + def _haveKey(self, key): + """ Is the key `key` available? + """ + query = ( + "SELECT {} FROM {} WHERE {}=?".format( + self.__value__, + self.__tablename__, + self.__key__ + ), (key,)) + return True if self.sql_fetchone(query) else False + + def __setitem__(self, key, value): + """ Sets an item in the store + + :param str key: Key + :param str value: Value + """ + if self._haveKey(key): + query = ( + "UPDATE {} SET {}=? WHERE {}=?".format( + self.__tablename__, self.__value__, self.__key__ + ), + (value, key), + ) + else: + query = ( + "INSERT INTO {} ({}, {}) VALUES (?, ?)".format( + self.__tablename__, self.__key__, self.__value__ + ), + (key, value), + ) + self.sql_execute(query) + + def __getitem__(self, key): + """ Gets an item from the store as if it was a dictionary + + :param str value: Value + """ + query = ( + "SELECT {} FROM {} WHERE {}=?".format( + self.__value__, + self.__tablename__, + self.__key__ + ), (key,)) + result = self.sql_fetchone(query) + if result: + return result[0] + else: + if key in self.defaults: + return self.defaults[key] + else: + return None + + def __iter__(self): + """ Iterates through the store + """ + return iter(self.keys()) + + def keys(self): + query = ("SELECT {} from {}".format( + self.__key__, + self.__tablename__), ) + return [x[0] for x in self.sql_fetchall(query)] + + def __len__(self): + """ return lenght of store + """ + query = ("SELECT id from {}".format(self.__tablename__), ) + return len(self.sql_fetchall(query)) + + def __contains__(self, key): + """ Tests if a key is contained in the store. + + May test againsts self.defaults + + :param str value: Value + """ + if self._haveKey(key) or key in self.defaults: + return True + else: + return False + + def items(self): + """ returns all items off the store as tuples + """ + query = ("SELECT {}, {} from {}".format( + self.__key__, + self.__value__, + self.__tablename__), ) + r = [] + for key, value in self.sql_fetchall(query): + r.append((key, value)) + return r + + def get(self, key, default=None): + """ Return the key if exists or a default value + + :param str value: Value + :param str default: Default value if key not present + """ + if key in self: + return self.__getitem__(key) + else: + return default + + # Specific for this library + def delete(self, key): + """ Delete a key from the store + + :param str value: Value + """ + query = ( + "DELETE FROM {} WHERE {}=?".format( + self.__tablename__, + self.__key__ + ), (key,)) + self.sql_execute(query) + + def wipe(self): + """ Wipe the store + """ + query = ("DELETE FROM {}".format(self.__tablename__), ) + self.sql_execute(query) + + def exists(self): + """ Check if the database table exists + """ + query = ("SELECT name FROM sqlite_master " + + "WHERE type='table' AND name=?", + (self.__tablename__, )) + return True if self.sql_fetchone(query) else False + + def create(self): # pragma: no cover + """ Create the new table in the SQLite database + """ + query = (( + """ + CREATE TABLE {} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + {} STRING(256), + {} STRING(256) + )""" + ).format( + self.__tablename__, + self.__key__, + self.__value__ + ), ) + self.sql_execute(query) diff --git a/docs/beem.aes.rst b/docs/beem.aes.rst deleted file mode 100644 index e784b7cd..00000000 --- a/docs/beem.aes.rst +++ /dev/null @@ -1,7 +0,0 @@ -beem\.aes -========= - -.. automodule:: beem.aes - :members: - :undoc-members: - :show-inheritance: \ No newline at end of file diff --git a/docs/beemgraphenebase.aes.rst b/docs/beemgraphenebase.aes.rst new file mode 100644 index 00000000..817bc8f0 --- /dev/null +++ b/docs/beemgraphenebase.aes.rst @@ -0,0 +1,7 @@ +beemgraphenebase\.aes +===================== + +.. automodule:: beemgraphenebase.aes + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemstorage.base.rst b/docs/beemstorage.base.rst new file mode 100644 index 00000000..240b3c05 --- /dev/null +++ b/docs/beemstorage.base.rst @@ -0,0 +1,7 @@ +beemstorage\.base +================= + +.. automodule:: beemstorage.base + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemstorage.exceptions.rst b/docs/beemstorage.exceptions.rst new file mode 100644 index 00000000..9e3a20b1 --- /dev/null +++ b/docs/beemstorage.exceptions.rst @@ -0,0 +1,7 @@ +beemstorage\.exceptions +======================= + +.. automodule:: beemstorage.exceptions + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemstorage.interfaces.rst b/docs/beemstorage.interfaces.rst new file mode 100644 index 00000000..fb4b45fe --- /dev/null +++ b/docs/beemstorage.interfaces.rst @@ -0,0 +1,7 @@ +beemstorage\.interfaces +======================= + +.. automodule:: beemstorage.interfaces + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemstorage.masterpassword.rst b/docs/beemstorage.masterpassword.rst new file mode 100644 index 00000000..1d34c62c --- /dev/null +++ b/docs/beemstorage.masterpassword.rst @@ -0,0 +1,7 @@ +beemstorage\.masterpassword +=========================== + +.. automodule:: beemstorage.masterpassword + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemstorage.ram.rst b/docs/beemstorage.ram.rst new file mode 100644 index 00000000..3497f986 --- /dev/null +++ b/docs/beemstorage.ram.rst @@ -0,0 +1,7 @@ +beemstorage\.ram +================ + +.. automodule:: beemstorage.ram + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/beemstorage.sqlite.rst b/docs/beemstorage.sqlite.rst new file mode 100644 index 00000000..69afcc2b --- /dev/null +++ b/docs/beemstorage.sqlite.rst @@ -0,0 +1,7 @@ +beemstorage\.sqlite +=================== + +.. automodule:: beemstorage.sqlite + :members: + :undoc-members: + :show-inheritance: \ No newline at end of file diff --git a/docs/modules.rst b/docs/modules.rst index a2bc2d45..fbdb33e8 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -7,7 +7,6 @@ beem Modules .. toctree:: beem.account - beem.aes beem.amount beem.asciichart beem.asset @@ -71,6 +70,7 @@ beemgraphenebase Modules .. toctree:: beemgraphenebase.account + beemgraphenebase.aes beemgraphenebase.base58 beemgraphenebase.bip32 beemgraphenebase.bip38 @@ -79,4 +79,17 @@ beemgraphenebase Modules beemgraphenebase.objecttypes beemgraphenebase.operations beemgraphenebase.signedtransactions - beemgraphenebase.unsignedtransactions \ No newline at end of file + beemgraphenebase.unsignedtransactions + + +beemstorage Modules +------------------- + +.. toctree:: + + beemstorage.base + beemstorage.exceptions + beemstorage.interfaces + beemstorage.masterpassword + beemstorage.ram + beemstorage.sqlite diff --git a/setup.py b/setup.py index 8aabccd7..8a9029e2 100755 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ except LookupError: ascii = codecs.lookup('ascii') codecs.register(lambda name, enc=ascii: {True: enc}.get(name == 'mbcs')) -VERSION = '0.23.13' +VERSION = '0.24.0' tests_require = ['mock >= 2.0.0', 'pytest', 'pytest-mock', 'parameterized'] diff --git a/tests/beem/test_aes.py b/tests/beem/test_aes.py index d3688903..7ff65083 100644 --- a/tests/beem/test_aes.py +++ b/tests/beem/test_aes.py @@ -10,7 +10,7 @@ import random import unittest import base64 from pprint import pprint -from beem.aes import AESCipher +from beemgraphenebase.aes import AESCipher class Testcases(unittest.TestCase): diff --git a/tests/beem/test_connection.py b/tests/beem/test_connection.py index 6cfabbc3..f3f7f409 100644 --- a/tests/beem/test_connection.py +++ b/tests/beem/test_connection.py @@ -1,5 +1,5 @@ import unittest -from beem import Steem +from beem import Hive, Steem from beem.account import Account from beem.instance import set_shared_steem_instance, SharedInstance from beem.blockchainobject import BlockchainObject @@ -13,15 +13,15 @@ class Testcases(unittest.TestCase): def test_stm1stm2(self): nodelist = NodeList() - nodelist.update_nodes(steem_instance=Steem(node=nodelist.get_nodes(exclude_limited=False), num_retries=10)) + nodelist.update_nodes(steem_instance=Hive(node=nodelist.get_hive_nodes(), num_retries=10)) b1 = Steem( node="https://api.steemit.com", nobroadcast=True, num_retries=10 ) - node_list = nodelist.get_nodes(exclude_limited=True) + node_list = nodelist.get_hive_nodes() - b2 = Steem( + b2 = Hive( node=node_list, nobroadcast=True, num_retries=10 @@ -31,10 +31,10 @@ class Testcases(unittest.TestCase): def test_default_connection(self): nodelist = NodeList() - nodelist.update_nodes(steem_instance=Steem(node=nodelist.get_nodes(exclude_limited=False), num_retries=10)) + nodelist.update_nodes(steem_instance=Hive(node=nodelist.get_hive_nodes(), num_retries=10)) - b2 = Steem( - node=nodelist.get_nodes(exclude_limited=True), + b2 = Hive( + node=nodelist.get_hive_nodes(), nobroadcast=True, ) set_shared_steem_instance(b2) diff --git a/tests/beem/test_hive.py b/tests/beem/test_hive.py index 85a90282..35922886 100644 --- a/tests/beem/test_hive.py +++ b/tests/beem/test_hive.py @@ -12,18 +12,16 @@ import json from pprint import pprint from beem import Hive, exceptions from beem.amount import Amount -from beem.memo import Memo from beem.version import version as beem_version -from beem.wallet import Wallet -from beem.witness import Witness from beem.account import Account from beemgraphenebase.account import PrivateKey -from beem.instance import set_shared_steem_instance from beem.nodelist import NodeList # Py3 compatibility import sys wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +wif2 = "5JKu2dFfjKAcD6aP1HqBDxMNbdwtvPS99CaxBzvMYhY94Pt6RDS" +wif3 = "5K1daXjehgPZgUHz6kvm55ahEArBHfCHLy6ew8sT7sjDb76PU2P" class Testcases(unittest.TestCase): @@ -37,7 +35,7 @@ class Testcases(unittest.TestCase): nobroadcast=True, unsigned=True, data_refresh_time_seconds=900, - keys={"active": wif, "owner": wif, "memo": wif}, + keys={"active": wif, "owner": wif2, "memo": wif3}, num_retries=10) cls.account = Account("test", full=True, steem_instance=cls.bts) @@ -65,7 +63,7 @@ class Testcases(unittest.TestCase): nobroadcast=True, unsigned=True, data_refresh_time_seconds=900, - keys={"active": wif, "owner": wif, "memo": wif}, + keys={"active": wif, "owner": wif2, "memo": wif3}, num_retries=10) core_unit = "STM" name = ''.join(random.choice(string.ascii_lowercase) for _ in range(12)) @@ -134,7 +132,7 @@ class Testcases(unittest.TestCase): nobroadcast=True, unsigned=True, data_refresh_time_seconds=900, - keys={"active": wif, "owner": wif, "memo": wif}, + keys={"active": wif, "owner": wif2, "memo": wif3}, num_retries=10) core_unit = "STM" name = ''.join(random.choice(string.ascii_lowercase) for _ in range(12)) @@ -364,7 +362,7 @@ class Testcases(unittest.TestCase): bts = Hive(node=self.nodelist.get_hive_nodes(), offline=True, data_refresh_time_seconds=900, - keys={"active": wif, "owner": wif, "memo": wif}) + keys={"active": wif, "owner": wif2, "memo": wif3}) bts.refresh_data("feed_history") self.assertTrue(bts.get_feed_history(use_stored_data=False) is None) self.assertTrue(bts.get_feed_history(use_stored_data=True) is None) @@ -393,7 +391,7 @@ class Testcases(unittest.TestCase): bts = Hive(node=self.nodelist.get_hive_nodes(), nobroadcast=True, data_refresh_time_seconds=900, - keys={"active": wif, "owner": wif, "memo": wif}, + keys={"active": wif, "owner": wif2, "memo": wif3}, num_retries=10) self.assertTrue(bts.is_hive) self.assertTrue(bts.get_feed_history(use_stored_data=False) is not None) diff --git a/tests/beem/test_steem.py b/tests/beem/test_steem.py index 8e25ba59..8a730260 100644 --- a/tests/beem/test_steem.py +++ b/tests/beem/test_steem.py @@ -12,19 +12,16 @@ import json from pprint import pprint from beem import Steem, exceptions from beem.amount import Amount -from beem.memo import Memo from beem.version import version as beem_version -from beem.wallet import Wallet -from beem.witness import Witness from beem.account import Account from beemgraphenebase.account import PrivateKey -from beem.instance import set_shared_steem_instance from beem.nodelist import NodeList # Py3 compatibility import sys wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" - +wif2 = "5JKu2dFfjKAcD6aP1HqBDxMNbdwtvPS99CaxBzvMYhY94Pt6RDS" +wif3 = "5K1daXjehgPZgUHz6kvm55ahEArBHfCHLy6ew8sT7sjDb76PU2P" class Testcases(unittest.TestCase): @@ -37,7 +34,7 @@ class Testcases(unittest.TestCase): nobroadcast=True, unsigned=True, data_refresh_time_seconds=900, - keys={"active": wif, "owner": wif, "memo": wif}, + keys={"active": wif, "owner": wif2, "memo": wif3}, num_retries=10) cls.account = Account("test", full=True, steem_instance=cls.bts) @@ -65,7 +62,7 @@ class Testcases(unittest.TestCase): nobroadcast=True, unsigned=True, data_refresh_time_seconds=900, - keys={"active": wif, "owner": wif, "memo": wif}, + keys={"active": wif, "owner": wif2, "memo": wif3}, num_retries=10) core_unit = "STM" name = ''.join(random.choice(string.ascii_lowercase) for _ in range(12)) @@ -134,7 +131,7 @@ class Testcases(unittest.TestCase): nobroadcast=True, unsigned=True, data_refresh_time_seconds=900, - keys={"active": wif, "owner": wif, "memo": wif}, + keys={"active": wif, "owner": wif2, "memo": wif3}, num_retries=10) core_unit = "STM" name = ''.join(random.choice(string.ascii_lowercase) for _ in range(12)) @@ -364,7 +361,7 @@ class Testcases(unittest.TestCase): bts = Steem(node=self.nodelist.get_steem_nodes(), offline=True, data_refresh_time_seconds=900, - keys={"active": wif, "owner": wif, "memo": wif}) + keys={"active": wif, "owner": wif2, "memo": wif3}) bts.refresh_data("feed_history") self.assertTrue(bts.get_feed_history(use_stored_data=False) is None) self.assertTrue(bts.get_feed_history(use_stored_data=True) is None) @@ -393,7 +390,7 @@ class Testcases(unittest.TestCase): bts = Steem(node=self.nodelist.get_steem_nodes(), nobroadcast=True, data_refresh_time_seconds=900, - keys={"active": wif, "owner": wif, "memo": wif}, + keys={"active": wif, "owner": wif2, "memo": wif3}, num_retries=10) self.assertTrue(bts.get_feed_history(use_stored_data=False) is not None) self.assertTrue(bts.get_reward_funds(use_stored_data=False) is not None) diff --git a/tests/beem/test_storage.py b/tests/beem/test_storage.py index 7dd89f17..cca88694 100644 --- a/tests/beem/test_storage.py +++ b/tests/beem/test_storage.py @@ -49,7 +49,7 @@ class Testcases(unittest.TestCase): cls.wallet = Wallet(steem_instance=cls.stm) cls.wallet.wipe(True) - cls.wallet.newWallet(pwd="TestingOneTwoThree") + cls.wallet.newWallet("TestingOneTwoThree") cls.wallet.unlock(pwd="TestingOneTwoThree") cls.wallet.addPrivateKey(wif) diff --git a/tests/beem/test_testnet.py b/tests/beem/test_testnet.py index baaa3bb3..b26963f8 100644 --- a/tests/beem/test_testnet.py +++ b/tests/beem/test_testnet.py @@ -12,9 +12,9 @@ from beem import Steem from beem.exceptions import ( InsufficientAuthorityError, MissingKeyError, - InvalidWifError, - WalletLocked + InvalidWifError ) +from beemstorage.exceptions import WalletLocked from beemapi import exceptions from beem.amount import Amount from beem.witness import Witness diff --git a/tests/beem/test_txbuffers.py b/tests/beem/test_txbuffers.py index c977e6de..63838310 100644 --- a/tests/beem/test_txbuffers.py +++ b/tests/beem/test_txbuffers.py @@ -17,14 +17,16 @@ from beem.amount import Amount from beem.exceptions import ( InsufficientAuthorityError, MissingKeyError, - InvalidWifError, - WalletLocked + InvalidWifError ) +from beemstorage.exceptions import WalletLocked from beemapi import exceptions from beem.wallet import Wallet from beem.utils import formatTimeFromNow from beem.nodelist import NodeList wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +wif2 = "5JKu2dFfjKAcD6aP1HqBDxMNbdwtvPS99CaxBzvMYhY94Pt6RDS" +wif3 = "5K1daXjehgPZgUHz6kvm55ahEArBHfCHLy6ew8sT7sjDb76PU2P" class Testcases(unittest.TestCase): @@ -36,14 +38,14 @@ class Testcases(unittest.TestCase): node_list = nodelist.get_nodes(exclude_limited=True) cls.stm = Steem( node=node_list, - keys={"active": wif, "owner": wif, "memo": wif}, + keys={"active": wif, "owner": wif2, "memo": wif3}, nobroadcast=True, num_retries=10 ) cls.steemit = Steem( node="https://api.steemit.com", nobroadcast=True, - keys={"active": wif, "owner": wif, "memo": wif}, + keys={"active": wif, "owner": wif2, "memo": wif3}, num_retries=10 ) set_shared_steem_instance(cls.stm) diff --git a/tests/beem/test_wallet.py b/tests/beem/test_wallet.py index 7278cf51..78c8dc07 100644 --- a/tests/beem/test_wallet.py +++ b/tests/beem/test_wallet.py @@ -138,30 +138,3 @@ class Testcases(unittest.TestCase): ): self.wallet.getPostingKeysForAccount("test") - def test_encrypt(self): - stm = self.stm - self.wallet.steem = stm - self.wallet.unlock(pwd="TestingOneTwoThree") - self.wallet.masterpassword = "TestingOneTwoThree" - self.assertEqual([self.wallet.encrypt_wif("5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd"), - self.wallet.encrypt_wif("5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR")], - ["6PRN5mjUTtud6fUXbJXezfn6oABoSr6GSLjMbrGXRZxSUcxThxsUW8epQi", - "6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg"]) - self.wallet.masterpassword = "Satoshi" - self.assertEqual([self.wallet.encrypt_wif("5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5")], - ["6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq"]) - self.wallet.masterpassword = "TestingOneTwoThree" - - def test_deencrypt(self): - stm = self.stm - self.wallet.steem = stm - self.wallet.unlock(pwd="TestingOneTwoThree") - self.wallet.masterpassword = "TestingOneTwoThree" - self.assertEqual([self.wallet.decrypt_wif("6PRN5mjUTtud6fUXbJXezfn6oABoSr6GSLjMbrGXRZxSUcxThxsUW8epQi"), - self.wallet.decrypt_wif("6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg")], - ["5HqUkGuo62BfcJU5vNhTXKJRXuUi9QSE6jp8C3uBJ2BVHtB8WSd", - "5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR"]) - self.wallet.masterpassword = "Satoshi" - self.assertEqual([self.wallet.decrypt_wif("6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq")], - ["5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5"]) - self.wallet.masterpassword = "TestingOneTwoThree" diff --git a/tests/beemapi/test_noderpc.py b/tests/beemapi/test_noderpc.py index 43f15745..d15e41c9 100644 --- a/tests/beemapi/test_noderpc.py +++ b/tests/beemapi/test_noderpc.py @@ -21,6 +21,8 @@ from beem.nodelist import NodeList import sys wif = "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3" +wif2 = "5JKu2dFfjKAcD6aP1HqBDxMNbdwtvPS99CaxBzvMYhY94Pt6RDS" +wif3 = "5K1daXjehgPZgUHz6kvm55ahEArBHfCHLy6ew8sT7sjDb76PU2P" core_unit = "STM" @@ -38,7 +40,7 @@ class Testcases(unittest.TestCase): cls.appbase = Steem( node=cls.nodes, nobroadcast=True, - keys={"active": wif, "owner": wif, "memo": wif}, + keys={"active": wif, "owner": wif2, "memo": wif3}, num_retries=10 ) cls.rpc = NodeRPC(urls=cls.nodes_steemit) diff --git a/tests/beemstorage/__init__.py b/tests/beemstorage/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/beemstorage/test_keystorage.py b/tests/beemstorage/test_keystorage.py new file mode 100644 index 00000000..ee86c5b1 --- /dev/null +++ b/tests/beemstorage/test_keystorage.py @@ -0,0 +1,111 @@ +from builtins import chr +from builtins import range +from builtins import str +import unittest +import hashlib +from binascii import hexlify, unhexlify +import os +import json +from pprint import pprint +from beemstorage import SqliteEncryptedKeyStore, InRamEncryptedKeyStore, InRamPlainKeyStore, SqlitePlainKeyStore, InRamConfigurationStore +from beemstorage import InRamEncryptedTokenStore, InRamPlainTokenStore +from beemstorage.exceptions import WalletLocked, KeyAlreadyInStoreException +from beemgraphenebase.account import PrivateKey +from beemgraphenebase.bip38 import SaltException + + + +def pubprivpair(wif): + return (str(wif), str(PrivateKey(wif).pubkey)) + + +class Testcases(unittest.TestCase): + + def test_inramkeystore(self): + self.do_keystore(InRamPlainKeyStore()) + + def test_inramencryptedkeystore(self): + self.do_keystore( + InRamEncryptedKeyStore(config=InRamConfigurationStore()) + ) + + def test_sqlitekeystore(self): + s = SqlitePlainKeyStore(profile="testing") + s.wipe() + self.do_keystore(s) + self.assertFalse(s.is_encrypted()) + + def test_sqliteencryptedkeystore(self): + self.do_keystore( + SqliteEncryptedKeyStore( + profile="testing", config=InRamConfigurationStore() + ) + ) + + def do_keystore(self, keys): + keys.wipe() + password = "foobar" + + if isinstance( + keys, (SqliteEncryptedKeyStore, InRamEncryptedKeyStore) + ): + keys.config.wipe() + with self.assertRaises(WalletLocked): + keys.decrypt( + "6PRViepa2zaXXGEQTYUsoLM1KudLmNBB1t812jtdKx1TEhQtvxvmtEm6Yh" + ) + with self.assertRaises(WalletLocked): + keys.encrypt( + "6PRViepa2zaXXGEQTYUsoLM1KudLmNBB1t812jtdKx1TEhQtvxvmtEm6Yh" + ) + with self.assertRaises(WalletLocked): + keys._get_encrypted_masterpassword() + + # set the first MasterPassword here! + keys._new_masterpassword(password) + keys.lock() + keys.unlock(password) + assert keys.unlocked() + assert keys.is_encrypted() + + with self.assertRaises(SaltException): + keys.decrypt( + "6PRViepa2zaXXGEQTYUsoLM1KudLmNBB1t812jtdKx1TEhQtvxvmtEm6Yh" + ) + + keys.add(*pubprivpair("5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3")) + # Duplicate key + with self.assertRaises(KeyAlreadyInStoreException): + keys.add( + *pubprivpair("5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3") + ) + self.assertIn( + "STM6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV", + keys.getPublicKeys(), + ) + + self.assertEqual( + keys.getPrivateKeyForPublicKey( + "STM6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV" + ), + "5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3", + ) + self.assertEqual(keys.getPrivateKeyForPublicKey("GPH6MRy"), None) + self.assertEqual(len(keys.getPublicKeys()), 1) + keys.add(*pubprivpair("5Hqr1Rx6v3MLAvaYCxLYqaSEsm4eHaDFkLksPF2e1sDS7omneaZ")) + self.assertEqual(len(keys.getPublicKeys()), 2) + self.assertEqual( + keys.getPrivateKeyForPublicKey( + "STM5u9tEsKaqtCpKibrXJAMhaRUVBspB5pr9X34PPdrSbvBb6ajZY" + ), + "5Hqr1Rx6v3MLAvaYCxLYqaSEsm4eHaDFkLksPF2e1sDS7omneaZ", + ) + keys.delete("STM5u9tEsKaqtCpKibrXJAMhaRUVBspB5pr9X34PPdrSbvBb6ajZY") + self.assertEqual(len(keys.getPublicKeys()), 1) + + if isinstance( + keys, (SqliteEncryptedKeyStore, InRamEncryptedKeyStore) + ): + keys.lock() + keys.wipe() + keys.config.wipe() diff --git a/tests/beemstorage/test_masterpassword.py b/tests/beemstorage/test_masterpassword.py new file mode 100644 index 00000000..74222813 --- /dev/null +++ b/tests/beemstorage/test_masterpassword.py @@ -0,0 +1,74 @@ +from builtins import chr +from builtins import range +from builtins import str +import unittest +import hashlib +from binascii import hexlify, unhexlify +import os +import json +from pprint import pprint +from beemstorage.base import InRamConfigurationStore, InRamEncryptedKeyStore +from beemstorage.exceptions import WrongMasterPasswordException + + +class Testcases(unittest.TestCase): + def test_masterpassword(self): + password = "foobar" + config = InRamConfigurationStore() + keys = InRamEncryptedKeyStore(config=config) + self.assertFalse(keys.has_masterpassword()) + master = keys._new_masterpassword(password) + self.assertEqual( + len(master), + len("66eaab244153031e8172e6ffed321" "7288515ddb63646bbefa981a654bdf25b9f"), + ) + with self.assertRaises(Exception): + keys._new_masterpassword(master) + + keys.lock() + + with self.assertRaises(Exception): + keys.change_password("foobar") + + keys.unlock(password) + self.assertEqual(keys.decrypted_master, master) + + new_pass = "new_secret_password" + keys.change_password(new_pass) + keys.lock() + keys.unlock(new_pass) + self.assertEqual(keys.decrypted_master, master) + + def test_wrongmastermass(self): + config = InRamConfigurationStore() + keys = InRamEncryptedKeyStore(config=config) + keys._new_masterpassword("foobar") + keys.lock() + with self.assertRaises(WrongMasterPasswordException): + keys.unlock("foobar2") + + def test_masterpwd(self): + with self.assertRaises(Exception): + InRamEncryptedKeyStore() + config = InRamConfigurationStore() + config["password_storage"] = "environment" + keys = InRamEncryptedKeyStore(config=config) + self.assertTrue(keys.locked()) + keys.unlock("foobar") + keys.password = "FOoo" + with self.assertRaises(Exception): + keys._decrypt_masterpassword() + keys.lock() + + with self.assertRaises(WrongMasterPasswordException): + keys.unlock("foobar2") + + with self.assertRaises(Exception): + keys._get_encrypted_masterpassword() + + self.assertFalse(keys.unlocked()) + + os.environ["UNLOCK"] = "foobar" + self.assertTrue(keys.unlocked()) + + self.assertFalse(keys.locked()) diff --git a/tests/beemstorage/test_sqlite.py b/tests/beemstorage/test_sqlite.py new file mode 100644 index 00000000..f02af06f --- /dev/null +++ b/tests/beemstorage/test_sqlite.py @@ -0,0 +1,41 @@ +from builtins import chr +from builtins import range +from builtins import str +import unittest +import hashlib +from binascii import hexlify, unhexlify +import os +import json +from pprint import pprint +from beemstorage.sqlite import SQLiteStore + +class MyStore(SQLiteStore): + __tablename__ = "testing" + __key__ = "key" + __value__ = "value" + + defaults = {"default": "value"} + + +class Testcases(unittest.TestCase): + def test_init(self): + store = MyStore() + self.assertEqual(store.storageDatabase, "beem.sqlite") + store = MyStore(profile="testing") + self.assertEqual(store.storageDatabase, "testing.sqlite") + + directory = "/tmp/temporaryFolder" + expected = os.path.join(directory, "testing.sqlite") + + store = MyStore(profile="testing", data_dir=directory) + self.assertEqual(store.sqlite_file, expected) + + def test_initialdata(self): + store = MyStore() + store["foobar"] = "banana" + self.assertEqual(store["foobar"], "banana") + + self.assertIsNone(store["empty"]) + + self.assertEqual(store["default"], "value") + self.assertEqual(len(store), 1) \ No newline at end of file -- GitLab