From c84b59e2cf9a9074f96234a65c0e03ad406d7980 Mon Sep 17 00:00:00 2001 From: Yann Autissier Date: Tue, 27 Sep 2022 16:33:07 +0200 Subject: [PATCH] new input/output format * add JWK input/output format * bump version v0.0.5 --- Makefile | 13 ++- docker/dpgpid/Dockerfile | 26 +++-- keygen | 297 +++++++++++++++++++++++++++++++---------------- requirements.txt | 1 + specs/keygen_spec.sh | 34 +++++- 5 files changed, 254 insertions(+), 117 deletions(-) diff --git a/Makefile b/Makefile index b25062c..cd78517 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,9 @@ BINDIR ?= $(PREFIX)/bin PREFIX ?= /usr/local MYOS ?= ../myos -MYOS_REPOSITORY ?= $(patsubst %/dpgpid,%/myos,$(shell git config --get remote.origin.url 2>/dev/null)) +MYOS_REPOSITORY ?= $(patsubst %/$(THIS),%/myos,$(THIS_REPOSITORY)) +THIS ?= $(lastword $(subst /, ,$(THIS_REPOSITORY))) +THIS_REPOSITORY ?= $(shell git config --get remote.origin.url 2>/dev/null) $(MYOS): -@git clone $(MYOS_REPOSITORY) $(MYOS) -include $(MYOS)/make/include.mk @@ -12,11 +14,16 @@ default: tests all: install tests -install: +install: $(if $(shell which pip3),pip3-install,pip3-not-found) mkdir -p "$(BINDIR)" install dpgpid "$(BINDIR)/dpgpid" install keygen "$(BINDIR)/keygen" - pip install -r requirements.txt + +pip3-install: + pip3 install -r requirements.txt + +pip3-not-found: + printf "WARNING: pip3 not found, please manually install python modules from requirements.txt\n" shellcheck-%: @shellcheck $*/*.sh diff --git a/docker/dpgpid/Dockerfile b/docker/dpgpid/Dockerfile index 1cd1b9d..6afa542 100644 --- a/docker/dpgpid/Dockerfile +++ b/docker/dpgpid/Dockerfile @@ -8,9 +8,20 @@ ARG PROCESSOR_ARCHITECTURE=$(uname -m) ARG PYTHON_RELEASE=3.10 WORKDIR /opt/dpgpid -COPY requirements.txt ./ + RUN apk upgrade --no-cache \ - && apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/community/ --virtual .build-deps \ + && apk add --no-cache \ + bash \ + ca-certificates \ + gettext \ + gpg \ + gpg-agent \ + libc6-compat \ + libsodium \ + make + +COPY requirements.txt ./ +RUN apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/community/ --virtual .build-deps \ build-base \ libffi-dev \ py3-gpgme \ @@ -27,16 +38,7 @@ RUN apk upgrade --no-cache \ |awk 'system("[ -e /lib/"$1" -o -e /usr/lib/"$1" -o -e ./lib/python'"${PYTHON_RELEASE}"'/site-packages/*/"$1" ]") == 0 { next } { print "so:" $1 }' \ |xargs -rt apk add --no-cache --repository http://dl-cdn.alpinelinux.org/alpine/edge/community/ -RUN apk add --no-cache \ - bash \ - ca-certificates \ - gettext \ - gpg \ - gpg-agent \ - libc6-compat \ - libsodium \ - make \ - && OS="$(echo ${OPERATING_SYSTEM} |awk '{print tolower($0)}')"; \ +RUN OS="$(echo ${OPERATING_SYSTEM} |awk '{print tolower($0)}')"; \ ARCH="$(echo ${PROCESSOR_ARCHITECTURE})"; \ wget -qO - https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.${OS}.${ARCH}.tar.xz \ |tar --strip-components 1 -C ./bin -xJf - \ diff --git a/keygen b/keygen index f0d4f06..d33e573 100755 --- a/keygen +++ b/keygen @@ -31,6 +31,7 @@ from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.primitives import serialization import duniterpy.key import gpg +from jwcrypto import jwk import logging as log import nacl.bindings import nacl.encoding @@ -44,7 +45,7 @@ import sys import time import warnings -__version__='0.0.4' +__version__='0.0.5' class keygen: def __init__(self): @@ -56,14 +57,15 @@ class keygen: "-d", "--debug", action="store_true", - help="show debug informations (WARNING: including SECRET KEY value)", + help="show debug informations (WARNING: including SECRET KEY)", ) self.parser.add_argument( "-f", "--format", - dest="format", + choices=['ewif', 'jwk', 'nacl','pb2','pem','pubsec','seed','wif'], default=None, - help="output file format: [ewif|nacl|pb2|pem|pubsec|seed|wif], default: pem (pkcs8)", + dest="format", + help="output file format, default: pem (pkcs8)", ) self.parser.add_argument( "-g", @@ -75,8 +77,8 @@ class keygen: "-i", "--input", dest="input", - default=None, - help="read ed25519 key from file INPUT, autodetect format: [credentials|ewif|nacl|mnemonic|pb2|pubsec|seed|wif]", + help="read ed25519 key from file FILE, autodetect format: {credentials,ewif,jwk,nacl,mnemonic,pb2,pubsec,seed,wif}", + metavar='FILE', ) self.parser.add_argument( "-k", @@ -95,7 +97,8 @@ class keygen: "--output", dest="output", default=None, - help="write ed25519 key to file OUTPUT", + help="write ed25519 key to file FILE", + metavar='FILE', ) self.parser.add_argument( "-p", @@ -113,14 +116,15 @@ class keygen: "-s", "--secret", action="store_true", - help="show secret key", + help="show only secret key", ) self.parser.add_argument( "-t", "--type", - dest="type", + choices=['b58mh','b64mh','base58','base64','duniter','ipfs','jwk'], default="base58", - help="output text format: [b58mh|b64mh|base58|base64|duniter|ipfs], default: base58", + dest="type", + help="output text format, default: base58", ) self.parser.add_argument( "-v", @@ -144,12 +148,8 @@ class keygen: def _check_args(self, args): log.debug("keygen._check_args(%s)" % args) - if self.input is None: - 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") + if self.input is None and self.username is None: + self.parser.error('keygen requires an input file or a username') def _cleanup(self): log.debug("keygen._cleanup()") @@ -181,12 +181,16 @@ class keygen: if hasattr(self, 'ipfs_privkey') and self.ipfs_privkey: clearmem(self.ipfs_privkey) log.debug("cleared: keygen.ipfs_privkey") + if hasattr(self, 'jwk'): + if hasattr(self.jwk, 'd') and self.jwk.d: + clearmem(self.jwk.d) + log.debug("cleared: keygen.jwk.d") 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, 'pgp_secret_armor') and self.pgp_secret_armor: + clearmem(self.pgp_secret_armor) + log.debug("cleared: keygen.pgp_secret_armor") 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) @@ -203,7 +207,7 @@ class keygen: def _invalid_type(self): log.debug("keygen._invalid_type()") - self.parser.error(f"type: {self.type} is not valid.") + self.parser.error(f"type {self.type} is not valid.") def _load_config(self): log.debug("keygen._load_config()") @@ -223,48 +227,74 @@ class keygen: def _output_file(self): log.debug("keygen._output_file()") - 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(1) - 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_protobuf'): - self.protobuf_from_ed25519() - with open(self.output, "wb") as fh: - fh.write(self.ed25519_secret_protobuf) - elif self.format == 'pubsec': - 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() - with open(self.output, "w") as fh: - fh.write(self.ed25519_secret_pem_pkcs8) + try: + if self.format == 'dewif': + if not hasattr(self, 'duniterpy'): + self.duniterpy_from_ed25519_seed_bytes() + if not self.password: + with pynentry.PynEntry() as p: + p.description = f"""Data in DEWIF 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(1) + self.duniterpy.save_dewif_v1_file(self.output, self.password) + elif self.format == 'ewif': + if not hasattr(self, 'duniterpy'): + self.duniterpy_from_ed25519_seed_bytes() + 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(1) + self.duniterpy.save_ewif_file(self.output, self.password) + elif self.format == 'jwk': + if not hasattr(self, 'jwk'): + self.jwk_from_ed25519() + with open(self.output, "w") as file: + file.write(self.jwk.export()) + elif self.format == 'nacl': + if not hasattr(self, 'duniterpy'): + self.duniterpy_from_ed25519_seed_bytes() + self.duniterpy.save_private_key(self.output) + elif self.format == 'pb2': + if not hasattr(self, 'ed25519_secret_protobuf'): + self.protobuf_from_ed25519() + with open(self.output, "wb") as file: + file.write(self.ed25519_secret_protobuf) + elif self.format == 'pubsec': + if not hasattr(self, 'duniterpy'): + self.duniterpy_from_ed25519_seed_bytes() + self.duniterpy.save_pubsec_file(self.output) + elif self.format == 'seed': + if not hasattr(self, 'duniterpy'): + self.duniterpy_from_ed25519_seed_bytes() + self.duniterpy.save_seedhex_file(self.output) + elif self.format == 'wif': + if not hasattr(self, 'duniterpy'): + self.duniterpy_from_ed25519_seed_bytes() + self.duniterpy.save_wif_file(self.output) + else: + if not hasattr(self, 'ed25519_secret_pem_pkcs8'): + self.pem_pkcs8_from_ed25519() + with open(self.output, "w") as file: + file.write(self.ed25519_secret_pem_pkcs8) + except Exception as e: + log.error(f'Unable to output file {self.output}: {e}') + self._cleanup() + exit(2) def _output_text(self, public_key, secret_key, public_key_prefix, secret_key_prefix): log.debug("keygen._output_text()") @@ -388,6 +418,11 @@ class keygen: self.b64mh_from_protobuf() self._output(self.ed25519_public_b58mh, self.ed25519_secret_b64mh, 'PeerID: ', 'PrivKEY: ') + def do_jwk(self): + log.debug("keygen.do_jwk()") + self.jwk_from_ed25519() + self._output(self.jwk.export_public(), self.jwk.export_private(), 'pub: ', 'sec: ') + def duniterpy_from_credentials(self): log.debug("keygen.duniterpy_from_credentials()") try: @@ -418,12 +453,12 @@ class keygen: exit(2) log.debug("keygen.duniterpy.seed: %s" % self.duniterpy.seed) - def duniterpy_from_ed25519(self): - log.debug("keygen.duniterpy_from_ed25519()") + def duniterpy_from_ed25519_seed_bytes(self): + log.debug("keygen.duniterpy_from_ed25519_seed_bytes()") try: self.duniterpy = duniterpy.key.SigningKey(self.ed25519_seed_bytes) except Exception as e: - log.error(f'Unable to get duniterpy from ed25519: {e}') + log.error(f'Unable to get duniterpy from ed25519 seed bytes: {e}') self._cleanup() exit(2) log.debug("keygen.duniterpy.seed: %s" % self.duniterpy.seed) @@ -436,17 +471,18 @@ class keygen: 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_jwk = re.compile('^\\s*{\\s*"crv":\\s*"Ed25519",\\s*"d":\\s*"(.)+",\\s*"kty":\\s*"OKP",\\s*"x":\\s*"(.)+"\\s*}') + 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_ssb = re.compile('\\s*{\\s*"curve":\\s*"ed25519",\\s*"public":\\s*"(.+)\\.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. + p.description = f"""Data in EWIF file is encrypted. Please enter a password to decrypt seed. """ p.prompt = 'Passphrase:' @@ -457,13 +493,18 @@ class keygen: self._cleanup() exit(1) self.duniterpy = duniterpy.key.SigningKey.from_ewif_file(self.input, self.password) + elif re.search(regex_jwk, line): + log.info("input file format detected: jwk") + self.jwk_from_json(line) + self.ed25519_seed_bytes_from_jwk() + self.duniterpy_from_ed25519_seed_bytes() 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_seed_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() + self.ed25519_seed_bytes_from_pem(''.join(lines).encode()) + self.duniterpy_from_ed25519_seed_bytes() elif re.search(regex_pubsec, line): log.info("input file format detected: pubsec") self.duniterpy = duniterpy.key.SigningKey.from_pubsec_file(self.input) @@ -495,12 +536,28 @@ class keygen: lines = file.readlines() if len(lines) > 0: line = lines[0].strip() + regex_dewif = re.compile(b'^\x00\x00\x00\x01\x00\x00\x00\x01') regex_pb2 = re.compile(b'^\x08\x01\x12@') + if re.search(regex_dewif, line): + log.info("input file format detected: dewif") + if not self.password: + with pynentry.PynEntry() as p: + p.description = f"""Data in DEWIF file is encrypted. + 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(1) + self.duniterpy = duniterpy.key.SigningKey.from_dewif_file(self.input, self.password) if re.search(regex_pb2, line): log.info("input file format detected: pb2") self.ed25519_secret_protobuf = line - self.ed25519_from_protobuf() - self.duniterpy_from_ed25519() + self.ed25519_seed_bytes_from_protobuf() + self.duniterpy_from_ed25519_seed_bytes() else: raise NotImplementedError('unknown input file format.') else: @@ -551,16 +608,12 @@ class keygen: def ed25519_from_duniterpy(self): log.debug("keygen.ed25519_from_duniterpy()") try: - self.ed25519_public_bytes = base58.b58decode(self.duniterpy.pubkey) - self.ed25519_secret_bytes = self.duniterpy.sk - self.ed25519_seed_bytes = self.ed25519_secret_bytes[:32] - except Exception as e: + self.ed25519_seed_bytes_from_duniterpy() + self.ed25519_from_seed_bytes() + except: log.error(f'Unable to get ed25519 from duniterpy: {e}') self._cleanup() exit(2) - log.debug("keygen.ed25519_seed_bytes=%s" % 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_from_gpg(self): log.debug("keygen.ed25519_from_gpg()") @@ -607,28 +660,15 @@ class keygen: self.ed25519_seed_bytes_from_pgpy() self.ed25519_from_seed_bytes() except Exception as e: - log.error(f'Unable to get ed25519 from pgpy: {e}') + log.error(f'Unable to get ed25519 seed bytes from pgpy: {e}') self._cleanup() exit(2) - def ed25519_from_protobuf(self): - log.debug("keygen.ed25519_from_protobuf()") - try: - self.ed25519_secret_bytes = self.ed25519_secret_protobuf.lstrip(b'\x08\x01\x12@') - self.ed25519_seed_bytes = self.ed25519_secret_bytes[:32] - self.ed25519_public_bytes = self.ed25519_secret_bytes.lstrip(self.ed25519_seed_bytes) - except Exception as e: - log.error(f'Unable to get ed25519 from protobuf: {e}') - self._cleanup() - exit(2) - log.debug("keygen.ed25519_seed_bytes=%s" % 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_from_seed_bytes(self): log.debug("keygen.ed25519_from_seed_bytes()") try: self.ed25519_public_bytes, self.ed25519_secret_bytes = nacl.bindings.crypto_sign_seed_keypair(self.ed25519_seed_bytes) + self.ed25519 = ed25519.Ed25519PrivateKey.from_private_bytes(self.ed25519_seed_bytes) except Exception as e: log.error(f'Unable to get ed25519 from seed bytes: {e}') self._cleanup() @@ -636,6 +676,34 @@ class keygen: 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_duniterpy(self): + log.debug("keygen.ed25519_seed_bytes_from_duniterpy()") + try: + self.ed25519_seed_bytes = self.duniterpy.sk[:32] + except Exception as e: + log.error(f'Unable to get ed25519 seed bytes from duniterpy: {e}') + self._cleanup() + exit(2) + log.debug("keygen.ed25519_seed_bytes=%s" % self.ed25519_seed_bytes) + + def ed25519_seed_bytes_from_jwk(self): + log.debug("keygen.ed25519_seed_bytes_from_jwk()") + try: + self.ed25519_seed_bytes = self.jwk._okp_pri().private_bytes(encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption()) + except Exception as e: + log.error(f'Unable to get ed25519 seed bytes from jwk: {e}') + self._cleanup() + exit(2) + + def ed25519_seed_bytes_from_pem(self, pem): + log.debug("keygen.ed25519_seed_bytes_from_pem()") + try: + self.ed25519_seed_bytes = serialization.load_pem_private_key(pem, password=None).private_bytes(encoding=serialization.Encoding.Raw, format=serialization.PrivateFormat.Raw, encryption_algorithm=serialization.NoEncryption()) + except Exception as e: + log.error(f'Unable to get ed25519 seed bytes from pem: {e}') + self._cleanup() + exit(2) + def ed25519_seed_bytes_from_pgpy(self): log.debug("keygen.ed25519_seed_bytes_from_pgpy()") try: @@ -649,21 +717,49 @@ class keygen: 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) else: - raise NotImplementedError(f"Getting seed from {self.pgpy_key_type} key is not implemented") + raise NotImplementedError(f"getting seed from {self.pgpy_key_type} key is not implemented") except Exception as e: log.error(f'Unable to get ed25519 seed bytes from pgpy: {e}') self._cleanup() exit(2) log.debug("keygen.ed25519_seed_bytes=%s" % self.ed25519_seed_bytes) + def ed25519_seed_bytes_from_protobuf(self): + log.debug("keygen.ed25519_seed_bytes_from_protobuf()") + try: + self.ed25519_seed_bytes = self.ed25519_secret_protobuf.lstrip(b'\x08\x01\x12@')[:32] + except Exception as e: + log.error(f'Unable to get ed25519 seed bytes from protobuf: {e}') + self._cleanup() + exit(2) + log.debug("keygen.ed25519_seed_bytes=%s" % self.ed25519_seed_bytes) + 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 + def jwk_from_ed25519(self): + log.debug("keygen.jwk_from_ed25519()") + try: + self.jwk = jwk.JWK.from_pyca(self.ed25519) + except Exception as e: + log.error(f'Unable to get jwk from ed25519: {e}') + self._cleanup() + exit(2) + + def jwk_from_json(self, json): + log.debug("keygen.jwk_from_json()") + try: + self.jwk = jwk.JWK.from_json(json) + except Exception as e: + log.error(f'Unable to get jwk from json: {e}') + self._cleanup() + exit(2) + def pem_pkcs8_from_ed25519(self): log.debug("keygen.pem_pkcs8_from_ed25519()") try: - self.ed25519_secret_pem_pkcs8 = ed25519.Ed25519PrivateKey.from_private_bytes(self.ed25519_seed_bytes).private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption()).decode('ascii') + self.ed25519_secret_pem_pkcs8 = self.ed25519.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption()).decode('ascii') except Exception as e: log.error(f'Unable to get pem pkcs8 from ed25519: {e}') self._cleanup() @@ -690,9 +786,10 @@ class keygen: log.debug("keygen.gpg_seckey.last_update=%s" % self.gpg_seckey.last_update) if self.password: self.gpg.set_pinentry_mode(gpg.constants.PINENTRY_MODE_LOOPBACK) - 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: + self.pgp_public_armor = self.gpg.key_export(self.gpg_seckey.fpr) + self.pgp_secret_armor = self.gpg.key_export_secret(self.gpg_seckey.fpr) + log.debug("keygen.pgp_secret_armor=%s" % self.pgp_secret_armor) + if not self.pgp_secret_armor: 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) @@ -700,7 +797,7 @@ class keygen: # remove CryptographyDeprecationWarning about deprecated # SymmetricKeyAlgorithm IDEA, CAST5 and Blowfish (PGPy v0.5.4) warnings.simplefilter('ignore') - self.pgpy, _ = pgpy.PGPKey.from_blob(self.pgp_secret_armored) + self.pgpy, _ = pgpy.PGPKey.from_blob(self.pgp_secret_armor) except Exception as e: log.error(f'Unable to get pgpy from gpg: {e}') self._cleanup() diff --git a/requirements.txt b/requirements.txt index b6be266..ca8ca03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ base58==2.1.1 cryptography==3.4.8 duniterpy==1.1.0 +jwcrypto==1.4.2 pgpy==0.5.4 pynentry==0.1.6 pynacl==1.5.0 diff --git a/specs/keygen_spec.sh b/specs/keygen_spec.sh index 291e495..6372d82 100644 --- a/specs/keygen_spec.sh +++ b/specs/keygen_spec.sh @@ -4,6 +4,7 @@ set -eu CRED_FILE="${SHELLSPEC_TMPBASE}/credentials" DUBP_FILE="${SHELLSPEC_TMPBASE}/mnemonic" EWIF_FILE="${SHELLSPEC_TMPBASE}/username.ewif" +JWK_FILE="${SHELLSPEC_TMPBASE}/username.jwk" NACL_FILE="${SHELLSPEC_TMPBASE}/username.nacl" PB2_FILE="${SHELLSPEC_TMPBASE}/username.pb2" PEM_FILE="${SHELLSPEC_TMPBASE}/username.pem" @@ -57,7 +58,7 @@ Describe 'keygen' Describe '--version:' It 'prints version' When run keygen --version - The output should include 'v0.0.4' + The output should include 'v0.0.5' The status should be success The stderr should equal "" End @@ -166,6 +167,15 @@ Describe 'keygen' The stderr should equal "" End End + Describe '-pkt jwk username password:' + It 'prints prefixed jwk public and secret keys for user "username" and password "password"' + When run keygen -pkt jwk username password + The output should include 'pub: {"crv":"Ed25519","kty":"OKP","x":"NJoTbvcP-m51-XwxrmWqHaOpI1ZD0USwLjqAmV8Boas"}' + The output should include 'sec: {"crv":"Ed25519","d":"D5eoJaNGoKM172hTdADv3psQf5P6vGDI9D8SRe8TYy8","kty":"OKP","x":"NJoTbvcP-m51-XwxrmWqHaOpI1ZD0USwLjqAmV8Boas"}' + The status should be success + The stderr should equal "" + End + End Describe '-pkm "tongue cute mail ...":' It 'prints prefixed base58 public and secret keys for mnemonic "tongue cute mail ..."' When run keygen -pkm "tongue cute mail fossil great frozen same social weasel impact brush kind" @@ -197,6 +207,26 @@ Describe 'keygen' End rm -f "${DUBP_FILE}" End + Describe "-f jwk -o ${JWK_FILE} username password:" + rm -f "${JWK_FILE}" + It 'writes secret key to a JWK file for user "username" and password "password"' + When run keygen -f jwk -o "${JWK_FILE}" username password + The path "${JWK_FILE}" should exist + The contents of file "${JWK_FILE}" should include '{"crv":"Ed25519","d":"D5eoJaNGoKM172hTdADv3psQf5P6vGDI9D8SRe8TYy8","kty":"OKP","x":"NJoTbvcP-m51-XwxrmWqHaOpI1ZD0USwLjqAmV8Boas"}' + The status should be success + The stderr should equal "" + End + End + Describe "-pki ${JWK_FILE}:" + It 'prints prefixed base58 public and secret keys for ed25519 key read from JWK file"' + When run keygen -pki "${JWK_FILE}" -v + The output should include 'pub: 4YLU1xQ9jzb7LzC6d91VZrYTEKS9N2j93Nnvcee6wxZG' + The output should include 'sec: K5heSX4xGUPtRbxcZh6zbgaKbDv8FeVc9JuSNWtUs7C1oGNKqv7kQJ3DHdouTPzoW4duKKnuLQK8LbHKfN9fkjC' + The status should be success + The stderr should include 'input file format detected: jwk' + End + rm -f "${JWK_FILE}" + End Describe "-f nacl -o ${NACL_FILE} username password:" rm -f "${NACL_FILE}" It 'writes secret key to a libnacl file for user "username" and password "password"' @@ -277,7 +307,7 @@ Describe 'keygen' rm -f "${PB2_FILE}" It 'writes protobuf2 secret key to a pb2 file for user "username" and password "password"' decode_pb2() { - xxd -ps "${PB2_FILE}" + xxd -p "${PB2_FILE}" } not_xxd() { ! which xxd >/dev/null 2>&1