diff --git a/man/sphinx.md b/man/sphinx.md index 5e23e70..af6e4df 100644 --- a/man/sphinx.md +++ b/man/sphinx.md @@ -340,12 +340,12 @@ Get a TOTP PIN: ### minisign -Create a new key, and store the public key at /tmp/minisig.pub: +Create a new key and store the public key at /tmp/minisig.pub: ```sh % getpwd \ - | sphinx create minisig://user example.com \ - | pipe2tmpfile minisign -R -s @@keyfile@@ -p /tmp/minisig.pub + | sphinx create minisig://user example.com >/tmp/minisig.pub ``` +`Create` and `Change` SPHINX operations automatically return a public key. Sign a file `filetosign`: ```sh @@ -353,15 +353,16 @@ Sign a file `filetosign`: | sphinx get minisig://user example.com \ | pipe2tmpfile minisign -S -s @@keyfile@@ -m filetosign ``` +The `Get` SPHINX operations return a private key. ### Age Generate an AGE key and store the public key: ```sh % getpwd \ - | sphinx create age://user example.com \ - | sphage pubkey >/tmp/age.pub + | sphinx create age://user example.com >/tmp/age.pub ``` +`Create` and `Change` SPHINX operations automatically return a public key. Decrypt a file using an AKE key from SPHINX: ```sh @@ -369,15 +370,16 @@ Decrypt a file using an AKE key from SPHINX: | sphinx get age://user localhost \ | pipe2tmpfile age --decrypt -i @@keyfile@@ encryptedfile ``` +The `Get` SPHINX operations return a private key. ### SSH-ED25519 Create key and save public key: ```sh % getpwd \ - | sphinx create ssh-ed25519://test asdf \ - | pipe2tmpfile ssh-keygen -e -f @@keyfile@@ >pubkey + | sphinx create ssh-ed25519://test asdf >pubkey ``` +`Create` and `Change` SPHINX operations automatically return a public key. Sign a file: ```sh @@ -385,6 +387,7 @@ Sign a file: | sphinx get ssh-ed25519://test asdf \ | pipe2tmpfile ssh-keygen -Y sign -n file -f @@keyfile@@ content.txt > content.txt.sig ``` +The `Get` SPHINX operations return a private key. Verify file with public key: ```sh diff --git a/pwdsphinx/converter.py b/pwdsphinx/converter.py index c7e431a..681b158 100755 --- a/pwdsphinx/converter.py +++ b/pwdsphinx/converter.py @@ -31,7 +31,7 @@ def load_converters(): load_converters() -def convert(rwd, user, host, *opts): +def convert(rwd, user, host, op, *opts): if '://' not in user: return bin2pass.derive(rwd, *opts) @@ -40,7 +40,7 @@ def convert(rwd, user, host, *opts): rwd = bin2pass.derive(rwd, *opts) schema, name = user.split("://",1) - return converters[schema](rwd, name, host, *opts) + return converters[schema](rwd, name, host, op, *opts) def convertedBy(user): for k in converters.keys(): diff --git a/pwdsphinx/converters/minisig.py b/pwdsphinx/converters/minisig.py index 4f782ed..cd8aebb 100755 --- a/pwdsphinx/converters/minisig.py +++ b/pwdsphinx/converters/minisig.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import sys, base64, pysodium, binascii +from pwdsphinx.consts import * # usage # getpwd | env/bin/sphinx create minisig://test asdf | pipe2tmpfile minisign -R -s @@keyfile@@ -p /tmp/minisig.pub @@ -20,16 +21,24 @@ to derive pubkey from secret key """ -def convert(rwd, user, host, *opts): - seed=rwd[:32] - kid=rwd[32:40] - pk,sk=pysodium.crypto_sign_seed_keypair(seed) - +def privkey(sk, kid): raw = (binascii.unhexlify("456400004232") + b'\x00' * 48 + kid + sk + b'\x00' * 32) - return f"untrusted comment: minisign encrypted secret key\n{base64.b64encode(raw).decode('utf8')}" + return f"untrusted comment: minisign secret key\n{base64.b64encode(raw).decode('utf8')}" + +def pubkey(pk, kid): + raw = (b'Ed' + kid + pk) + return f"untrusted comment: minisign public key\n{base64.b64encode(raw).decode('utf8')}" + +def convert(rwd, user, host, op, *opts): + seed=rwd[:32] + kid=rwd[32:40] + pk,sk=pysodium.crypto_sign_seed_keypair(seed) + if op in {CREATE, CHANGE}: + return pubkey(pk, kid) + return privkey(sk, kid) schema = {'minisig': convert} diff --git a/pwdsphinx/converters/raw.py b/pwdsphinx/converters/raw.py index 70ea0f2..624dc00 100755 --- a/pwdsphinx/converters/raw.py +++ b/pwdsphinx/converters/raw.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -def convert(rwd, user, host, *opts): +def convert(rwd, user, host, op, *opts): size = opts[1] # rwd[:] does not copy the underlying data, and thus # a clearmem() not only wipes the original, but also the copy... diff --git a/pwdsphinx/converters/sphage.py b/pwdsphinx/converters/sphage.py index df9a28c..95be578 100755 --- a/pwdsphinx/converters/sphage.py +++ b/pwdsphinx/converters/sphage.py @@ -28,6 +28,7 @@ import sys, base64, pysodium from enum import Enum +from pwdsphinx.consts import * class Encoding(Enum): """Enumeration type to list the various supported encodings.""" @@ -131,7 +132,10 @@ def encode(hrp, data): return None return ret -def convert(rwd, user, host, *opts): +def convert(rwd, user, host, op, *opts): + if op in {CREATE, CHANGE}: + pk = pysodium.crypto_scalarmult_base(rwd[:32]) + return encode("age", pk) return encode('age-secret-key-', rwd[:32]).upper() schema = {'age': convert} diff --git a/pwdsphinx/converters/ssh-ed25519.py b/pwdsphinx/converters/ssh-ed25519.py index 55f3d33..b41c22d 100755 --- a/pwdsphinx/converters/ssh-ed25519.py +++ b/pwdsphinx/converters/ssh-ed25519.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import sys, base64, pysodium, binascii, struct +from pwdsphinx.consts import * # usage # create key and save pubkey @@ -14,7 +15,7 @@ def split_by_n(iterable, n): return zip_longest(*[iter(iterable)]*n, fillvalue='') -def convert(rwd, user, host, *opts): +def privkey(rwd, user, host): seed=rwd[:32] pk,sk=pysodium.crypto_sign_seed_keypair(seed) @@ -62,4 +63,20 @@ def convert(rwd, user, host, *opts): '\n'.join(''.join(l) for l in split_by_n(base64.b64encode(raw).decode('utf8'), 70)) + "\n-----END OPENSSH PRIVATE KEY-----") +def pubkey(rwd, user, host): + seed=rwd[:32] + pk,sk=pysodium.crypto_sign_seed_keypair(seed) + + raw = (binascii.unhexlify("0000000b" # int length = 11 + "7373682d65643235353139" # string key type = ssh-ed25519 + "00000020") + # int length = 32 + pk) + + return f"ssh-ed25519 {base64.b64encode(raw).decode('utf8')} {user}@{host}" + +def convert(rwd, user, host, op, *opts): + if op in {CREATE, CHANGE}: + return pubkey(rwd, user, host) + return privkey(rwd,user, host) + schema = {'ssh-ed25519': convert} diff --git a/pwdsphinx/sphinx.py b/pwdsphinx/sphinx.py index 4e7a6f1..e5e5ab6 100755 --- a/pwdsphinx/sphinx.py +++ b/pwdsphinx/sphinx.py @@ -575,7 +575,7 @@ def create(m, pwd, user, host, char_classes='uld', symbols=bin2pass.symbols, siz xormask = pysodium.randombytes(64) candidate = convert( xor(pysodium.crypto_generichash(PASS_CTX, rwd, outlen=64),xormask), - user, host, + user, host, CREATE, char_classes,size,symbols) if 1 <= size < 8: break # too much of a bias especially for ulsd when size < 5 if 'u' in char_classes and len(_uppers.intersection(candidate)) == 0: @@ -604,13 +604,13 @@ def create(m, pwd, user, host, char_classes='uld', symbols=bin2pass.symbols, siz rwd = xor(pysodium.crypto_generichash(PASS_CTX, rwd, outlen=64),xormask) - ret = convert(rwd,user,host,char_classes,size,symbols) + ret = convert(rwd,user,host,CREATE,char_classes,size,symbols) clearmem(rwd) return ret def try_v1get(pwd, host, user): rwd, classes, size, symbols = v1sphinx.get(pwd, user, host) - ret = convert(rwd,user,host,classes,size,symbols) + ret = convert(rwd,user,GET,host,classes,size,symbols) if not user.startswith("otp://"): target = ret else: @@ -696,7 +696,7 @@ def get(m, pwd, user, host): rwd = xor(pysodium.crypto_generichash(PASS_CTX, rwd, outlen=64),xormask) - ret = convert(rwd,user,host,classes,size,symbols) + ret = convert(rwd,user,GET,host,classes,size,symbols) clearmem(rwd) return ret @@ -807,7 +807,7 @@ def change(m, oldpwd, newpwd, user, host, classes='uld', symbols=bin2pass.symbol m.close() rwd = xor(pysodium.crypto_generichash(PASS_CTX, rwd, outlen=64),xormask) - ret = convert(rwd,user,host,classes,size,symbols) + ret = convert(rwd,user,host,CHANGE,classes,size,symbols) clearmem(rwd) return ret diff --git a/tests/test_conv.py b/tests/test_conv.py index 2cc1a52..6ea0328 100755 --- a/tests/test_conv.py +++ b/tests/test_conv.py @@ -2,6 +2,7 @@ import unittest from pwdsphinx.converter import convert +from pwdsphinx.consts import * from pwdsphinx import bin2pass char_classes = 'uld' @@ -11,29 +12,29 @@ class TestConverters(unittest.TestCase): def test_bin2pass(self): rwd = b'\xaa' * 32 - pwd = convert(rwd, "asdf", "host", char_classes, size, symbols) + pwd = convert(rwd, "asdf", "host", GET, char_classes, size, symbols) self.assertEqual(pwd, '2UH@/%XoTb+T-RT*tipUqT+b\'lYQ*kUiPOdq@sK') def test_bin2pass_8char(self): rwd = b'\xaa' * 32 - pwd = convert(rwd, "asdf", "host", char_classes, 8, symbols) + pwd = convert(rwd, "asdf", "host", GET, char_classes, 8, symbols) self.assertEqual(pwd, 'iPOdq@sK') def test_raw(self): rwd = b'\xaa' * 32 - pwd = convert(rwd, "raw://asdf", "host", char_classes, len(rwd), symbols) + pwd = convert(rwd, "raw://asdf", "host", GET, char_classes, len(rwd), symbols) self.assertEqual(pwd, rwd) def test_otp(self): pwd = 'A' * 16 rwd, classes, symbols = bin2pass.pass2bin(pwd) - pwd = convert(rwd, "otp://asdf", "host", classes, len(pwd), symbols) + pwd = convert(rwd, "otp://asdf", "host", GET, classes, len(pwd), symbols) self.assertIsInstance(pwd, str) self.assertEqual(len(pwd), 6) def test_age(self): rwd = b'\x55' * 32 - pwd = convert(rwd, "age://asdf", "host", char_classes, size, symbols) + pwd = convert(rwd, "age://asdf", "host", GET, char_classes, size, symbols) self.assertEqual(pwd, 'AGE-SECRET-KEY-1242424242424242424242424242424242424242424242424242S6JN5PD') from pwdsphinx.converters.sphage import decode self.assertEqual(rwd, bytes(decode('age-secret-key-', pwd)))