Skip to content

Commit

Permalink
[enh] create/challenge ops return pubkeys for age/minisign/ssh-ed2551…
Browse files Browse the repository at this point in the history
…9 converters
  • Loading branch information
stef committed Feb 13, 2025
1 parent e165dbc commit 088d4f4
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 28 deletions.
17 changes: 10 additions & 7 deletions man/sphinx.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,51 +340,54 @@ 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
% getpwd \
| 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
% getpwd \
| 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
% getpwd \
| 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
Expand Down
4 changes: 2 additions & 2 deletions pwdsphinx/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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():
Expand Down
21 changes: 15 additions & 6 deletions pwdsphinx/converters/minisig.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}
2 changes: 1 addition & 1 deletion pwdsphinx/converters/raw.py
Original file line number Diff line number Diff line change
@@ -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...
Expand Down
6 changes: 5 additions & 1 deletion pwdsphinx/converters/sphage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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}
Expand Down
19 changes: 18 additions & 1 deletion pwdsphinx/converters/ssh-ed25519.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python3

import sys, base64, pysodium, binascii, struct
from pwdsphinx.consts import *

# usage
# create key and save pubkey
Expand All @@ -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)

Expand Down Expand Up @@ -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}
10 changes: 5 additions & 5 deletions pwdsphinx/sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions tests/test_conv.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import unittest
from pwdsphinx.converter import convert
from pwdsphinx.consts import *
from pwdsphinx import bin2pass

char_classes = 'uld'
Expand All @@ -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)))

0 comments on commit 088d4f4

Please sign in to comment.