@ -27,7 +27,7 @@ import argparse
import base58
import base64
import configparser
import cryptography.hazmat.primitives.asymmetric.ed25519 as ed25519
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
import duniterpy.key
import gpg
@ -44,15 +44,15 @@ import sys
import time
import warnings
__version__='0.0.3 '
__version__='0.0.4 '
class keygen:
def __init__(self):
# desc: generate ed25519 keys suitables for duniter or ipfs
# desc: generate ed25519 keys suitables for duniter or ipfs
self.parser = argparse.ArgumentParser(description="""
Generate ed25519 keys suitables for duniter or ipfs.
It converts your Duniter username and password or a GPG key to a duniter
pub/sec key or an IPFS PeerID/PrivateKEY.""")
Generate ed25519 keys suitables for duniter and ipfs.
It converts an ed25519 key, duniter username/password, or a GPG key, to
a duniter wallet or an IPFS PeerID/PrivateKEY.""")
self.parser.add_argument(
"-d",
"--debug",
@ -64,20 +64,20 @@ class keygen:
"--format",
dest="format",
default=None,
help="output file format: [pb2|pem|pubsec], default: pem (pkcs8)",
help="output file format: [ewif|nacl| pb2|pem|pubsec|seed|wif ], default: pem (pkcs8)",
)
self.parser.add_argument(
"-g",
"--gpg",
action="store_true",
help="use gpg key from user id matched by username option as input ",
help="use gpg key with uid matched by username ",
)
self.parser.add_argument(
"-i",
"--input",
dest="input",
default=None,
help="read public and secret keys in pubsec format from file INPUT ",
help="read ed25519 key from file INPUT, autodetect format: [credentials|ewif|libnacl|mnemonic|pubsec|seedhex|wif] ",
)
self.parser.add_argument(
"-k",
@ -86,17 +86,23 @@ class keygen:
help="show public and secret keys",
)
self.parser.add_argument(
"-m",
"--mnemonic",
action="store_true",
help="use username as a DUBP mnemonic passphrase",
)
self.parser.add_argument(
"-o",
"--output",
dest="output",
default=None,
help="write public and secret keys to file OUTPUT",
help="write ed25519 key to file OUTPUT",
)
self.parser.add_argument(
"-p",
"--prefix",
action="store_true",
help="prefix key with key type",
help="prefix output text with key type",
)
self.parser.add_argument(
"-q",
@ -143,22 +149,24 @@ class keygen:
if self.password is None:
if self.username is None:
self.parser.error(f"keygen requires an input file or username args")
if self.format not in [None, 'ewif', 'nacl', 'pb2', 'pem', 'pubsec', 'seed', 'wif']:
self.parser.error(f"format not valid")
def _cleanup(self):
log.debug("keygen._cleanup()")
if hasattr(self, 'armored_pgp_secret_key') and self.armored_pgp_secret_key:
clearmem(self.armored_pgp_secret_key)
log.debug("cleared: keygen.armored_pgp_secret_key")
if hasattr(self, 'duniterpy'):
if hasattr(self.duniterpy, 'seed') and self.duniterpy.seed:
clearmem(self.duniterpy.seed)
log.debug("cleared: keygen.duniterpy.seed")
if hasattr(self.duniterpy, 'sk') and self.duniterpy.sk:
clearmem(self.duniterpy.sk)
log.debug("cleared: keygen.duniterpy.sk")
if hasattr(self, 'ed25519_secret_base58') and self.ed25519_secret_base58:
clearmem(self.ed25519_secret_base58)
log.debug("cleared: keygen.ed25519_secret_base58")
if hasattr(self, 'ed25519_secret_base64') and self.ed25519_secret_base64:
clearmem(self.ed25519_secret_base64)
log.debug("cleared: keygen.ed25519_secret_base64")
if hasattr(self, 'duniter'):
if hasattr(self.duniter, 'sk') and self.duniterpy.sk:
clearmem(self.duniterpy.sk)
log.debug("cleared: keygen.duniterpy.sk")
if hasattr(self, 'ed25519_secret_bytes') and self.ed25519_secret_bytes:
clearmem(self.ed25519_secret_bytes)
log.debug("cleared: keygen.ed25519_secret_bytes")
@ -168,12 +176,18 @@ class keygen:
if hasattr(self, 'ed25519_secret_protobuf2') and self.ed25519_secret_protobuf2:
clearmem(self.ed25519_secret_protobuf2)
log.debug("cleared: keygen.ed25515_secret_protobuf2")
if hasattr(self, 'ed25519_seed_bytes') and self.ed25519_seed_bytes:
clearmem(self.ed25519_seed_bytes)
log.debug("cleared: keygen.ed25519_seed_bytes")
if hasattr(self, 'ipfs_privkey') and self.ipfs_privkey:
clearmem(self.ipfs_privkey)
log.debug("cleared: keygen.ipfs_privkey")
if hasattr(self, 'password') and self.password:
clearmem(self.password)
log.debug("cleared: keygen.password")
if hasattr(self, 'pgp_secret_armored') and self.pgp_secret_armored:
clearmem(self.pgp_secret_armored)
log.debug("cleared: keygen.pgp_secret_armored")
if hasattr(self, 'pgpy'):
if hasattr(self.pgpy._key.keymaterial, 'p') and self.pgpy._key.keymaterial.p and not isinstance(self.pgpy._key.keymaterial.p, pgpy.packet.fields.ECPoint):
clearmem(self.pgpy._key.keymaterial.p)
@ -184,12 +198,9 @@ class keygen:
if hasattr(self.pgpy._key.keymaterial, 's') and self.pgpy._key.keymaterial.s:
clearmem(self.pgpy._key.keymaterial.s)
log.debug("cleared: keygen.pgpy._key.material.s")
if hasattr(self, 'pgp_key_seed') and self.pgp_key_seed:
clearmem(self.pgp_key_seed)
log.debug("cleared: keygen.pgp_key_seed")
if hasattr(self, 'pgp_key_value') and self.pgp_key_value:
clearmem(self.pgp_key_value)
log.debug("cleared: keygen.pgp_key_value")
if hasattr(self, 'username') and self.username:
clearmem(self.username)
log.debug("cleared: keygen.username")
def _invalid_type(self):
log.debug("keygen._invalid_type()")
@ -213,22 +224,43 @@ class keygen:
def _output_file(self):
log.debug("keygen._output_file()")
if self.format == 'pb2':
if self.format == 'ewif':
if not hasattr(self, 'duniterpy'):
self.duniterpy_from_ed25519()
if not self.password:
with pynentry.PynEntry() as p:
p.description = f"""Data in EWIF file needs to be encrypted.
Please enter a password to encrypt seed.
"""
p.prompt = 'Passphrase:'
try:
self.password = p.get_pin()
except pynentry.PinEntryCancelled:
log.warning('Cancelled! Goodbye.')
self._cleanup()
exit(2)
self.duniterpy.save_ewif_file(self.output, self.password)
elif self.format == 'nacl':
if not hasattr(self, 'duniterpy'):
self.duniterpy_from_ed25519()
self.duniterpy.save_private_key(self.output)
elif self.format == 'pb2':
if not hasattr(self, 'ed25519_secret_protobuf2'):
self.protobuf2_from_ed25519()
with open(self.output, "wb") as fh:
fh.write(self.ed25519_secret_protobuf2)
elif self.format == 'pubsec':
if not hasattr(self, 'ed25519_public_base58') or not hasattr(self, 'ed25519_secret_base58'):
self.base58_from_ed25519()
with open(self.output, "w") as fh:
fh.write(
f"""Type: PubSec
Version: 1
pub: {self.ed25519_public_base58}
sec: {self.ed25519_secret_base58}
"""
)
if not hasattr(self, 'duniterpy'):
self.duniterpy_from_ed25519()
self.duniterpy.save_pubsec_file(self.output)
elif self.format == 'seed':
if not hasattr(self, 'duniterpy'):
self.duniterpy_from_ed25519()
self.duniterpy.save_seedhex_file(self.output)
elif self.format == 'wif':
if not hasattr(self, 'duniterpy'):
self.duniterpy_from_ed25519()
self.duniterpy.save_wif_file(self.output)
else:
if not hasattr(self, 'ed25519_secret_pem_pkcs8'):
self.pem_pkcs8_from_ed25519()
@ -316,8 +348,8 @@ sec: {self.ed25519_secret_base58}
self.ipfs_from_protobuf2()
self._output(self.ipfs_peerid, self.ipfs_privkey, 'PeerID: ', 'PrivKEY: ')
def duniterpy_from_salt_and_password (self):
log.debug("keygen.duniterpy_from_salt_and_password ()")
def duniterpy_from_credentials (self):
log.debug("keygen.duniterpy_from_credentials ()")
scrypt_params = duniterpy.key.scrypt_params.ScryptParams(
int(self.config.get('scrypt', 'n')) if self.config.has_option('scrypt', 'n') else 4096,
int(self.config.get('scrypt', 'r')) if self.config.has_option('scrypt', 'r') else 16,
@ -339,17 +371,107 @@ sec: {self.ed25519_secret_base58}
self.password,
scrypt_params
)
log.debug("keygen.duniterpy.seed: %s" % self.duniterpy.seed)
def duniterpy_from_ed25519(self):
log.debug("keygen.duniterpy_from_ed25519()")
self.duniterpy = duniterpy.key.SigningKey(self.ed25519_secret_bytes[:32])
log.debug("keygen.duniterpy.seed: %s" % self.duniterpy.seed)
def duniterpy_from_file(self):
log.debug("keygen.duniterpy_from_file()")
try:
with open(self.input, 'r') as file:
lines = file.readlines()
if len(lines) > 0:
line = lines[0].strip()
regex_ewif = re.compile('^Type: EWIF$')
regex_nacl = re.compile('^\\s*{\\s*"priv":\\s*"[0-9a-fA-F]+",\\s*"verify":\\s*"[0-9a-fA-F]+",\\s*"sign":\\s*"[0-9a-fA-F]+"\\s*}$')
regex_pem = re.compile('^-----BEGIN PRIVATE KEY-----$')
regex_pubsec = re.compile('^Type: PubSec$')
regex_seed = re.compile('^[0-9a-fA-F]{64}$')
regex_ssb = re.compile('\\s*{\\s*"curve": "ed25519",\\s*"public": "(.+)\\.ed25519",\\s*"private":\\s*"(.+)\\.ed25519",\\s*"id":\\s*"@(.+).ed25519"\\s*}')
regex_wif = re.compile('^Type: WIF$')
if re.search(regex_ewif, line):
log.info("input file format detected: ewif")
if not self.password:
with pynentry.PynEntry() as p:
p.description = f"""Data in EWIF file needs to be decrypted.
Please enter a password to decrypt seed.
"""
p.prompt = 'Passphrase:'
try:
self.password = p.get_pin()
except pynentry.PinEntryCancelled:
log.warning('Cancelled! Goodbye.')
self._cleanup()
exit(2)
self.duniterpy = duniterpy.key.SigningKey.from_ewif_file(self.input, self.password)
elif re.search(regex_nacl, line):
log.info("input file format detected: nacl")
self.duniterpy = duniterpy.key.SigningKey.from_private_key(self.input)
elif re.search(regex_pem, line):
log.info("input file format detected: pem")
self.ed25519_secret_bytes = serialization.load_pem_private_key(''.join(lines).encode(), password=None).private_bytes(encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption())
self.duniterpy_from_ed25519()
## at this stage, self.ed25519_secret_bytes contains only the 32 seed bytes and not the 32 seed bytes + 32 public bytes as it should
# we need to call self.ed25519_from_duniterpy() later to correctly build the self.ed25519_secret_bytes
elif re.search(regex_pubsec, line):
log.info("input file format detected: pubsec")
self.duniterpy = duniterpy.key.SigningKey.from_pubsec_file(self.input)
elif re.search(regex_seed, line):
log.info("input file format detected: seed")
self.duniterpy = duniterpy.key.SigningKey.from_seedhex_file(self.input)
elif re.search(regex_ssb, line):
log.info("input file format detected: ssb")
self.duniterpy = duniterpy.key.SigningKey.from_ssb_file(self.input)
elif re.search(regex_wif, line):
log.info("input file format detected: wif")
self.duniterpy = duniterpy.key.SigningKey.from_wif_file(self.input)
elif len(line.split(' ')) == 12:
log.info("input file format detected: mnemonic")
self.username = line
self.duniterpy_from_mnemonic()
elif len(lines) > 1:
log.info("input file format detected: credentials")
self.username = line
self.password = lines[1].strip()
self.duniterpy_from_credentials()
else:
raise NotImplementedError(f"""unable to detect input file format.""")
except Exception as e:
log.error(f"""Unable to open file {self.input}: {e}""")
self._cleanup()
exit(1)
log.debug("keygen.duniterpy.seed: %s" % self.duniterpy.seed)
def duniterpy_from_mnemonic(self):
log.debug("keygen.duniterpy_from_mnemonic()")
scrypt_params = duniterpy.key.scrypt_params.ScryptParams(
int(self.config.get('scrypt', 'n')) if self.config.has_option('scrypt', 'n') else 4096,
int(self.config.get('scrypt', 'r')) if self.config.has_option('scrypt', 'r') else 16,
int(self.config.get('scrypt', 'p')) if self.config.has_option('scrypt', 'p') else 1,
int(self.config.get('scrypt', 'sl')) if self.config.has_option('scrypt', 'sl') else 32,
)
self.duniterpy = duniterpy.key.SigningKey.from_dubp_mnemonic(
self.username,
scrypt_params
)
log.debug("keygen.duniterpy.seed: %s" % self.duniterpy.seed)
def ed25519(self, args):
log.debug("keygen.ed25519(%s)" % args)
if args.gpg is True:
if args.gpg:
self.ed25519_from_gpg()
elif self.input is None:
self.duniterpy_from_salt_and_password()
self.ed25519_from_duniterpy()
else:
self.base58_from_pubsec()
self.ed25519_from_base58()
if self.input:
self.duniterpy_from_file()
else:
if self.mnemonic:
self.duniterpy_from_mnemonic()
else:
self.duniterpy_from_credentials()
self.ed25519_from_duniterpy()
def ed25519_from_base58(self):
log.debug("keygen.ed25519_from_base58()")
@ -387,13 +509,11 @@ sec: {self.ed25519_secret_base58}
log.debug("keygen.gpg_seckey.uids=%s" % self.gpg_seckey.uids)
log.debug("keygen.gpg_seckey.owner_trust=%s" % self.gpg_seckey.owner_trust)
log.debug("keygen.gpg_seckey.last_update=%s" % self.gpg_seckey.last_update)
self.armored_pgp_public_key = self.gpg.key_export(self.gpg_seckey.fpr)
log.debug("keygen.armored_pgp_public_key=%s" % self.armored_pgp_public_key)
if self.password:
self.gpg.set_pinentry_mode(gpg.constants.PINENTRY_MODE_LOOPBACK)
self.armored_pgp_secret_key = self.gpg.key_export_secret(self.gpg_seckey.fpr)
log.debug("keygen.armored_pgp_secret_key=%s" % self.armored_pgp_secret_key )
if not self.armored_pgp_secret_key :
self.pgp_secret_armored = self.gpg.key_export_secret(self.gpg_seckey.fpr)
log.debug("keygen.pgp_secret_armored=%s" % self.pgp_secret_armored )
if not self.pgp_secret_armored :
log.error(f"""Unable to export gpg secret key id "{self.gpg_seckey.fpr}" of user "{self.username}". Please check your password!""")
self._cleanup()
exit(2)
@ -401,7 +521,7 @@ sec: {self.ed25519_secret_base58}
# remove CryptographyDeprecationWarning about deprecated
# SymmetricKeyAlgorithm IDEA, CAST5 and Blowfish (PGPy v0.5.4)
warnings.simplefilter('ignore')
self.pgpy, _ = pgpy.PGPKey.from_blob(self.armored_pgp_secret_key )
self.pgpy, _ = pgpy.PGPKey.from_blob(self.pgp_secret_armored )
def ed25519_from_pgpy(self):
log.debug("keygen.ed25519_from_pgpy()")
@ -428,17 +548,35 @@ sec: {self.ed25519_secret_base58}
with self.pgpy.unlock(self.password):
assert self.pgpy.is_unlocked
log.debug("keygen.pgpy.is_unlocked=%s" % self.pgpy.is_unlocked)
self.pgp_key_seed _from_pgpy()
self.ed25519_seed_bytes _from_pgpy()
except Exception as e:
log.error(f"""Unable to unlock pgp secret key id "{self.pgpy.fingerprint.keyid}" of user "{self.username}". Please check your password!""")
self._cleanup()
exit(2)
else:
self.pgp_key_seed _from_pgpy()
self.ed25519_public_bytes, self.ed25519_secret_bytes = nacl.bindings.crypto_sign_seed_keypair(self.pgp_key_seed )
self.ed25519_seed_bytes _from_pgpy()
self.ed25519_public_bytes, self.ed25519_secret_bytes = nacl.bindings.crypto_sign_seed_keypair(self.ed25519_seed_bytes )
log.debug("keygen.ed25519_public_bytes=%s" % self.ed25519_public_bytes)
log.debug("keygen.ed25519_secret_bytes=%s" % self.ed25519_secret_bytes)
def ed25519_seed_bytes_from_pgpy(self):
log.debug("keygen.ed25519_seed_bytes_from_pgpy()")
self.pgpy_key_type()
if self.pgpy_key_type == 'RSA':
log.debug("keygen.pgpy._key.keymaterial.p=%s" % self.pgpy._key.keymaterial.p)
log.debug("keygen.pgpy._key.keymaterial.q=%s" % self.pgpy._key.keymaterial.q)
# custom seed: use sha256 hash of (p + q)
self.ed25519_seed_bytes = nacl.bindings.crypto_hash_sha256(long_to_bytes(self.pgpy._key.keymaterial.p + self.pgpy._key.keymaterial.q))
p = long_to_bytes(self.pgpy._key.keymaterial.p)
q = long_to_bytes(self.pgpy._key.keymaterial.q)
log.debug("keygen.ed25519_seed_bytes=%s" % self.ed25519_seed_bytes)
elif self.pgpy_key_type in ('ECDSA', 'EdDSA', 'ECDH'):
log.debug("keygen.pgpy._key.keymaterial.s=%s" % self.pgpy._key.keymaterial.s)
self.ed25519_seed_bytes = long_to_bytes(self.pgpy._key.keymaterial.s)
log.debug("keygen.ed25519_seed_bytes=%s" % self.ed25519_seed_bytes)
else:
raise NotImplementedError(f"Getting seed from {self.pgpy_key_type} key is not implemented")
def gpg_passphrase_cb(self, uid_hint, passphrase_info, prev_was_bad):
log.debug("keygen.gpg_passphrase_cb(%s, %s, %s)" % (uid_hint, passphrase_info, prev_was_bad))
return self.password
@ -460,32 +598,6 @@ sec: {self.ed25519_secret_base58}
self.ed25519_secret_pem_pkcs8 = ed25519.Ed25519PrivateKey.from_private_bytes(self.ed25519_secret_bytes[:32]).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption()).decode('ascii')
log.debug("keygen.ed25519_secret_pem_pkcs8=%s" % self.ed25519_secret_pem_pkcs8)
def pgp_key_seed_from_pgpy(self):
log.debug("keygen.pgp_key_seed_from_pgpy()")
self.pgpy_key_type()
if self.pgpy_key_type == 'RSA':
log.debug("keygen.pgpy._key.keymaterial.p=%s" % self.pgpy._key.keymaterial.p)
log.debug("keygen.pgpy._key.keymaterial.q=%s" % self.pgpy._key.keymaterial.q)
# custom seed: use sha256 hash of (p + q)
self.pgp_key_seed = nacl.bindings.crypto_hash_sha256(long_to_bytes(self.pgpy._key.keymaterial.p + self.pgpy._key.keymaterial.q))
p = long_to_bytes(self.pgpy._key.keymaterial.p)
q = long_to_bytes(self.pgpy._key.keymaterial.q)
self.pgp_key_value = "".join([f"{c:02x}" for c in p]) + "".join([f"{c:02x}" for c in q])
self.pgp_key_size = (len(p) + len(q)) * 8
log.debug("keygen.pgp_key_seed=%s" % self.pgp_key_seed)
log.debug("keygen.pgp_key_value=%s" % self.pgp_key_value)
log.debug("keygen.pgp_key_size=%s" % self.pgp_key_size)
elif self.pgpy_key_type in ('ECDSA', 'EdDSA', 'ECDH'):
log.debug("keygen.pgpy._key.keymaterial.s=%s" % self.pgpy._key.keymaterial.s)
self.pgp_key_seed = long_to_bytes(self.pgpy._key.keymaterial.s)
self.pgp_key_value = "".join([f"{c:02x}" for c in self.pgp_key_seed])
self.pgp_key_size = len(self.pgp_key_seed)*8
log.debug("keygen.pgp_key_seed=%s" % self.pgp_key_seed)
log.debug("keygen.pgp_key_value=%s" % self.pgp_key_value)
log.debug("keygen.pgp_key_size=%s" % self.pgp_key_size)
else:
raise NotImplementedError(f"Getting seed from {self.pgpy_key_type} key is not implemented")
def pgpy_key_type(self):
log.debug("keygen.pgpy_key_type()")
if isinstance(self.pgpy._key.keymaterial, pgpy.packet.fields.RSAPriv):