Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keystore updates & disabling #13

Merged
merged 4 commits into from
Aug 9, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,6 @@ ENV/

# Rope project settings
.ropeproject

# vim swap files
.*.sw*
52 changes: 32 additions & 20 deletions pacli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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.
'''
Expand Down Expand Up @@ -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")
Expand All @@ -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:
Expand Down Expand Up @@ -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()
25 changes: 14 additions & 11 deletions pacli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
116 changes: 63 additions & 53 deletions pacli/keystore.py
Original file line number Diff line number Diff line change
@@ -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 <privkey> with <label>"""
mykey = Kutil(network=self.network, wif=privkey)

@classmethod
def __getattr__(self, name):
return getattr(self.provider, name)
if label not in self.privkeys.keys():
self.privkeys[label] = []

@classmethod
def importprivkey(self, privkey: str, label: str) -> int:
"""import <privkey> with <label>"""
mykey = Kutil(wif=privkey)
if mykey.privkey not in [key['privkey'] for key in self.privkeys[label]]:
self.privkeys[label].append({ "privkey": mykey.privkey,
"address": mykey.address })

if label not in self.privkeys.keys():
self.privkeys[label] = []
def getaddressesbyaccount(self, label: str) -> list:
if label in self.privkeys.keys():
return [key["address"] for key in self.privkeys[label]]

if mykey._privkey not in [key['privkey'] for key in self.privkeys[label]]:
self.privkeys[label].append({"privkey":mykey._privkey,"address":mykey.address})
def listaccounts(self) -> dict:
return {key:0 for key in self.privkeys.keys()}

@classmethod
def getaddressesbyaccount(self, label: str) -> list:
if label in self.privkeys.keys():
return [key["address"] for key in self.privkeys[label]]
def dumpprivkeys(self) -> dict:
return self.privkeys

@classmethod
def listaccounts(self) -> dict:
return {key:0 for key in self.privkeys.keys()}
return LocalKeyProvider

@classmethod
def dumpprivkeys(self) -> dict:
return self.privkeys
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
pypeerassets>=0.1.1
pypeerassets>=0.2.1
terminaltables>=3.1.0
gnupg>=2.1
appdirs