diff --git a/.gitignore b/.gitignore index 72364f9..156bd4b 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,6 @@ ENV/ # Rope project settings .ropeproject + +# vim swap files +.*.sw* diff --git a/pacli/__main__.py b/pacli/__main__.py index 8f733ba..43e79a8 100644 --- a/pacli/__main__.py +++ b/pacli/__main__.py @@ -10,7 +10,7 @@ import json import logging -from pacli.keystore import read_keystore, write_keystore, KeyedProvider +from pacli.keystore import GpgKeystore, as_local_key_provider conf_dir = user_config_dir("pacli") conf_file = os.path.join(conf_dir, "pacli.conf") @@ -56,7 +56,7 @@ def set_up(provider): if not Settings.production: if not provider.listtransactions("PATEST"): pa.pautils.load_p2th_privkeys_into_local_node(provider, prod=False) - else: + elif Settings.provider != 'holy': pa.pautils.load_p2th_privkeys_into_local_node(provider,keyfile) def default_account_utxo(provider, amount): @@ -335,7 +335,7 @@ def new_deck(provider, deck, broadcast): ''' Spawn a new PeerAssets deck. - pacli deck -new '{"name": "test", "number_of_decimals": 1, "issue_mode": "ONCE"}' + pacli deck --new '{"name": "test", "number_of_decimals": 1, "issue_mode": "ONCE"}' Will return deck span txid. ''' @@ -780,7 +780,8 @@ def cli(): parser.add_argument("--newaddress", action="store_true", help="generate a new address and import to wallet") parser.add_argument("--status", action="store_true", help="show pacli status") - parser.add_argument("--addressbalance", action="store", nargs=2, help="check card balance of the address") + parser.add_argument("--addressbalance", action="store", + metavar=('DECK_ID', 'ADDRESS'), nargs=2, help="check card balance of the address") deck = subparsers.add_parser('deck', help='Deck manipulation.') deck.add_argument("--list", action="store_true", help="list decks") @@ -807,28 +808,37 @@ def cli(): return parser.parse_args() -def main(): - first_run() +def configured_provider(Settings): + " resolve settings into configured provider " - try: - load_conf() - except: - raise + if Settings.provider.lower() == "rpcnode": + Provider = pa.RpcNode + kwargs = dict(testnet=Settings.testnet) - mypg = None - password = None - mykeys = "" - mykeys = read_keystore(Settings,keyfile) + elif Settings.provider.lower() == "holy": + Provider = pa.Holy + kwargs = dict(network=Settings.network) - if Settings.provider.lower() == "rpcnode": - provider = pa.RpcNode(testnet=Settings.testnet) - if Settings.provider.lower() == "holy": - provider = pa.Holy(network=Settings.network) + else: raise Exception('invalid provider') + + if Settings.keystore.lower() == "gnupg": + Provider = as_local_key_provider(Provider) + kwargs['keystore'] = keystore = GpgKeystore(Settings, keyfile) - provider = KeyedProvider(provider,keysJson=mykeys) + provider = Provider(**kwargs) set_up(provider) + return provider + + +def main(): + + first_run() + load_conf() + + provider = configured_provider(Settings) + args = cli() if args.status: @@ -878,7 +888,9 @@ def main(): if args.info: vote_info(provider, args.info) - write_keystore(Settings,keyfile,provider.dumpprivkeys()) + if (hasattr(provider, 'keystore')): + # could possibly make this direct behavior of dumprivkeys + provider.keystore.write(provider.dumpprivkeys()) if __name__ == "__main__": main() diff --git a/pacli/config.py b/pacli/config.py index e1a7365..2dac2fd 100644 --- a/pacli/config.py +++ b/pacli/config.py @@ -12,21 +12,24 @@ def write_default_config(conf_file=None): with open(conf_file, 'w') as configfile: config.write(configfile) +optional = { + "keystore": "none", + "gnupgdir": "", + "gnupgagent": "", + "gnupgkey": "" + } + +required = { "network", "production", "loglevel", "change" } + def read_conf(conf_file): config = configparser.ConfigParser() config.read(conf_file) try: - settings = { - "network": config["settings"]["network"], - "production": config["settings"]["production"], - "loglevel": config["settings"]["loglevel"], - "change": config["settings"]["change"], - "provider": config["settings"]["provider"], - "keystore": config["settings"]["keystore"], - "gnupgdir": config["settings"]["gnupgdir"], - "gnupgagent": config["settings"]["gnupgagent"], - "gnupgkey": config["settings"]["gnupgkey"] - } + settings = dict(config["settings"]) + assert set(settings.keys()).issuperset(required) + for k, v in optional.items(): + settings[k] = settings.get(k, v) + except: print("config is outdated, saving current default config to",conf_file+".sample") write_default_config(conf_file+".sample") diff --git a/pacli/keystore.py b/pacli/keystore.py index 96f260f..e2117cc 100644 --- a/pacli/keystore.py +++ b/pacli/keystore.py @@ -1,77 +1,87 @@ -import gnupg -import getpass -import sys +import sys, pickle +from binascii import hexlify, unhexlify +import gnupg, getpass from pypeerassets.kutil import Kutil -mypg = None +class GpgKeystore: + """ + Implements reading and writing from gpg encrypted file. + python-gnupg is a wrapper around the gpg cli, which makes it inherently fragile + Uses pickle because private keys are binary + """ + + def __init__(self, Settings, keyfile): + assert Settings.keystore == "gnupg" -def read_keystore(Settings,keyfile) -> str: - mykeys = "" + self._key = Settings.gnupgkey + self._keyfile = keyfile - if Settings.keystore == "gnupg" and Settings.provider != "rpcnode": - mypg = gnupg.GPG(binary='/usr/bin/gpg',homedir=Settings.gnupgdir,use_agent=bool(Settings.gnupgagent=="True"),keyring='pubring.gpg',secring='secring.gpg') + self._init_settings = dict( + homedir=Settings.gnupgdir, + use_agent=bool(Settings.gnupgagent == "True"), + keyring='pubring.gpg', + secring='secring.gpg') + self.gpg = gnupg.GPG(**self._init_settings) + + def read(self) -> dict: password = getpass.getpass("Input gpg key password:") - fd = open(keyfile) - data = fd.read() + contents = open(self._keyfile, 'rb').read() - if len(data)>0: - mykeys = str(mypg.decrypt(data,passphrase=password)) - fd.close() - else: - print("using rpcnode") + if not len(contents): + return {} - return mykeys + decrypted = self.gpg.decrypt(contents, passphrase=password) -def write_keystore(Settings,keyfile,keys): + assert decrypted.ok, decrypted.status + return pickle.loads(unhexlify(str(decrypted).encode())) - if mypg: - data = str(mypg.encrypt(str(keys),Settings.gnupgkey)) - fd = open(keyfile,"w") - fd.write(data) - fd.close() + def write(self, data: dict) -> str: + encrypted = self.gpg.encrypt(hexlify(pickle.dumps(data)).decode(), self._key) + assert encrypted.ok, encrypted.status + keyfile = open(self._keyfile, "w") + keyfile.write(str(encrypted)) + keyfile.close() -class KeyedProvider: +def as_local_key_provider(Provider): """ - Keystore class + factory for subclassing Providers, + allowing for local key management and isinstance checks """ - @classmethod - def __init__(self, provider, keysJson: str=""): + class LocalKeyProvider(Provider): + """ - : + Wraps a provider, shadowing it's private key management and deferring other logic. + Uses an in-memory store to handle + importprivkey, getaddressesbyaccount, listaccounts, and dumpprivkeys """ - self.provider = provider - if keysJson != "": - self.privkeys = eval(keysJson) - else: - self.privkeys = {} + def __init__(self, keystore: GpgKeystore, **kwargs): + super(Provider, self).__init__(**kwargs) + self.keystore = keystore + self.privkeys = keystore.read() + + def importprivkey(self, privkey: str, label: str) -> int: + """import with