├── pwdsphinx ├── __init__.py ├── converters │ ├── __init__.py │ ├── raw.py │ ├── totp.py │ ├── minisig.py │ ├── ssh-ed25519.py │ └── sphage.py ├── utils.py ├── consts.py ├── config.py ├── ext.py ├── converter.py ├── bin2pass.py ├── ostore.py ├── v1sphinx.py └── websphinx.py ├── tests ├── __init__.py ├── data │ └── masterkey ├── editor.py ├── key.pem ├── start-servers.sh ├── cert.pem ├── ws-test.py ├── sphinx.cfg ├── test_conv.py ├── test_rules.py ├── opaque-store.cfg ├── tests.sh └── test_pass2bin.py ├── ext ├── .gitignore ├── icon.png ├── _locales │ └── en │ │ └── messages.json ├── build.sh ├── manifest_ff.json ├── manifest_chrome.json ├── popup.css ├── content_script.js ├── popup.html ├── webauthn.js ├── background.js ├── inject.js └── popup.js ├── contrib ├── sphinx-scripts │ ├── otp.sphinx │ ├── pass.sphinx │ ├── user-pass.sphinx │ ├── getacc-user-pass.sphinx │ └── user-pass-otp.sphinx ├── type-pwd ├── getpwd ├── dmenu-sphinx ├── pipe2tmpfile ├── sphage-test.sh ├── exec-on-click ├── Makefile ├── sphinx-x11 └── README.md ├── MANIFEST.in ├── configs ├── firefox │ └── websphinx.json └── chrome │ └── websphinx.json ├── man ├── websphinx.md ├── sphage.md ├── exec-on-click.md ├── getpwd.md ├── type-pwd.md ├── dmenu-sphinx.md ├── makefile ├── bin2pass.md ├── pipe2tmpfile.md ├── oracle.md └── sphinx-x11.md ├── .github └── workflows │ └── hcodeql-analysis.yml ├── setup.py ├── GettingStarted.md ├── sphinx.cfg_sample ├── README.md └── LICENSES └── CC-BY-SA-4.0.txt /pwdsphinx/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ext/.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /pwdsphinx/converters/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ext/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stef/pwdsphinx/HEAD/ext/icon.png -------------------------------------------------------------------------------- /tests/data/masterkey: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stef/pwdsphinx/HEAD/tests/data/masterkey -------------------------------------------------------------------------------- /contrib/sphinx-scripts/otp.sphinx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sphinx-x11 2 | 3 | wait-for-click 4 | otp 5 | enter 6 | -------------------------------------------------------------------------------- /contrib/sphinx-scripts/pass.sphinx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sphinx-x11 2 | 3 | wait-for-click 4 | pwd 5 | tab 6 | enter 7 | -------------------------------------------------------------------------------- /contrib/sphinx-scripts/user-pass.sphinx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sphinx-x11 2 | 3 | wait-for-click 4 | user 5 | tab 6 | pwd 7 | tab 8 | enter 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include *.py 3 | include pwdsphinx/converters/*.py 4 | include man/*.1 5 | include GettingStarted.md 6 | -------------------------------------------------------------------------------- /contrib/type-pwd: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | getpwd "$1@$2" | sphinx get "$1" "$2" | exec-on-click xdotool type --clearmodifiers -- '$(cat)' 4 | -------------------------------------------------------------------------------- /contrib/sphinx-scripts/getacc-user-pass.sphinx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sphinx-x11 2 | 3 | gethost 4 | getuser 5 | wait-for-click 6 | user 7 | tab 8 | pwd 9 | tab 10 | enter 11 | -------------------------------------------------------------------------------- /pwdsphinx/utils.py: -------------------------------------------------------------------------------- 1 | from itertools import zip_longest # for Python 3.x 2 | def split_by_n(iterable, n): 3 | return list(zip_longest(*[iter(iterable)]*n, fillvalue='')) 4 | -------------------------------------------------------------------------------- /contrib/sphinx-scripts/user-pass-otp.sphinx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sphinx-x11 2 | 3 | wait-for-click 4 | user 5 | tab 6 | pwd 7 | tab 8 | enter 9 | wait-for-click 10 | otp 11 | enter 12 | -------------------------------------------------------------------------------- /contrib/getpwd: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | prompt=${1:-sphinx} 4 | echo -en "SETTITLE sphinx password prompt\nSETPROMPT ${prompt} password\nGETPIN\n" | pinentry | grep '^D' | cut -c3- | tr -d '\n' 5 | -------------------------------------------------------------------------------- /tests/editor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | 5 | with open(sys.argv[1], 'r') as fd: 6 | data = fd.read() 7 | 8 | with open(sys.argv[1], 'w') as fd: 9 | fd.write('\n'.join(sorted(data.split('\n')))) 10 | -------------------------------------------------------------------------------- /configs/firefox/websphinx.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websphinx", 3 | "description": "Host for communicating with pwdphinx", 4 | "path": "/usr/bin/websphinx", 5 | "type": "stdio", 6 | "allowed_extensions": [ 7 | "sphinx@ctrlc.hu" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tests/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIBvDA1MfRSB+jbflO/Db0XkbHWoxHceapqqwdww/nXiHoAoGCCqGSM49 3 | AwEHoUQDQgAEz5dNStDbsZuMGWmM0uAL2l8uKSBvemZOVwtaNE5pHZzT25IElJun 4 | cdTlb8d7vJFRkR2H4AuwuCONeFtr+NR2+A== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /ext/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extDescription": { 3 | "message": "Web extension for PITCHFORKed Sphinx" 4 | }, 5 | "sizePlaceholder": { 6 | "message": "Size" 7 | }, 8 | "searchPlaceholder": { 9 | "message": "User name " 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /configs/chrome/websphinx.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websphinx", 3 | "description": "Host for communicating with Sphinx", 4 | "path": "/usr/bin/websphinx", 5 | "type": "stdio", 6 | "allowed_origins": [ 7 | "chrome-extension://ojbhlhidchjkmjmpeonendekpoacahni/" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /contrib/dmenu-sphinx: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | host=$(cat ~/.sphinx-hosts 2>/dev/null | dmenu -p hostname) 4 | tmp=$(mktemp) 5 | { echo $host; cat ~/.sphinx-hosts 2>/dev/null ; } | sort -u >$tmp && mv $tmp ~/.sphinx-hosts 6 | users=$(sphinx list $host) 7 | [[ "$(echo "$users" | wc -l)" -gt 1 ]] && user=$(echo $users | dmenu -p username) || user=$users 8 | type-pwd "$user" "$host" 9 | 10 | -------------------------------------------------------------------------------- /pwdsphinx/converters/raw.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | def convert(rwd, user, host, op, *opts): 4 | size = opts[1] 5 | # rwd[:] does not copy the underlying data, and thus 6 | # a clearmem() not only wipes the original, but also the copy... 7 | return rwd[:1] + rwd[1:size] 8 | 9 | schema = {"raw": convert} 10 | 11 | def main(): 12 | for rwd in sys.stdin: 13 | print(convert(rwd.strip())) 14 | 15 | if __name__ == '__main__': 16 | main() 17 | -------------------------------------------------------------------------------- /contrib/pipe2tmpfile: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | keyroot=/run/user/$(id -u) 4 | keyfile=$(mktemp -p ${keyroot}) 5 | 6 | cleanup() { 7 | rm -f "${keyfile}" 8 | } 9 | 10 | cat >$keyfile 11 | trap "cleanup" INT TERM QUIT EXIT 12 | 13 | replace() { 14 | for i do 15 | arg="$i" 16 | if [ "x$arg" == "x@@keyfile@@" ]; then 17 | arg="$keyfile" 18 | fi 19 | printf %s\\n "$arg" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" 20 | done 21 | echo " " 22 | } 23 | 24 | newargs=$(replace "$@") 25 | eval "set -- $newargs" 26 | "${@}" 27 | -------------------------------------------------------------------------------- /tests/start-servers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ORACLE=${ORACLE:-../../../pwdsphinx/oracle.py} 4 | PIDS="" 5 | 6 | cleanup() { 7 | echo killing oracles ${PIDS} 8 | kill ${PIDS} 9 | exit 10 | } 11 | 12 | start_server() { 13 | printf "starting %s %s\n" "$ORACLE" "$1" 14 | cd "servers/$1" 15 | "$ORACLE" >log 2>&1 & 16 | PIDS="$PIDS $!" 17 | sleep 0.1 18 | cd - >/dev/null 19 | } 20 | 21 | start_server 0 22 | start_server 1 23 | start_server 2 24 | start_server 3 25 | start_server 4 26 | 27 | trap "cleanup" INT 28 | while true; do sleep 1 ;done 29 | -------------------------------------------------------------------------------- /contrib/sphage-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | # simulate output from sphinx 6 | rwd=$(echo "asdf" | sha256sum | cut -d' ' -f 1 | rax2 -s) 7 | 8 | # convert rwd to age "identity" (privkey) 9 | privkey=$(mktemp) 10 | echo "$rwd" | python3 sphage privkey >"$privkey" 11 | 12 | # convert rwd to age "recipient" (pubkey) 13 | pubkey=$(echo -n "$rwd" | python3 sphage pubkey) 14 | 15 | # encrypt and decrypt hello world using the above key pair derived from rwd 16 | echo "hello world" | age -r $pubkey | age --decrypt -i "$privkey" 17 | 18 | # clean up 19 | rm -rf "$privkey" 20 | -------------------------------------------------------------------------------- /pwdsphinx/consts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | CREATE =b'\x00' # sphinx 4 | READ =b'\x33' # blob 5 | UNDO =b'\x55' # change sphinx 6 | GET =b'\x66' # sphinx 7 | V1GET =b'\x69' # v1 sphinx 8 | COMMIT =b'\x99' # change sphinx 9 | CHANGE_DKG =b'\xa0' # sphinx/dkg 10 | CHANGE =b'\xaa' # sphinx 11 | CREATE_DKG =b'\xf0' # sphinx/dkg 12 | V1DELETE =b'\xf9' # v1 sphinx+blobs 13 | DELETE =b'\xff' # sphinx+blobs 14 | 15 | CHALLENGE_CREATE = b'\x5a' 16 | CHALLENGE_VERIFY = b'\xa5' 17 | 18 | VERSION = b'\x01' 19 | 20 | V1RULE_SIZE = 79 21 | RULE_SIZE = V1RULE_SIZE+32 22 | -------------------------------------------------------------------------------- /contrib/exec-on-click: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # depends on xinput 4 | 5 | MOUSEID=$(xinput --list --short | fgrep "Virtual core pointer" | sed 's/.*id=\([0-9]*\).*/\1/') 6 | THIS=$$ 7 | # wait until left mouse click 8 | exec 2>/dev/null 9 | xinput --test-xi2 --root $MOUSEID | while true; do 10 | read -t 1 line 11 | case "$line" in 12 | EVENT\ type\ 16\ \(RawButtonRelease\)) 13 | read -t 1 line 14 | read -t 1 details; 15 | case "$details" in 16 | detail:\ 1) pkill -P $THIS xinput ; exit ;; 17 | esac 18 | ;; 19 | esac 20 | done 21 | eval "${@}" ; 22 | -------------------------------------------------------------------------------- /tests/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIBhTCCASugAwIBAgIURt1h20rXWGwyV5nuLDp2NBaXsgkwCgYIKoZIzj0EAwIw 3 | GDEWMBQGA1UEAwwNc3BoaW54IG9yYWNsZTAeFw0yMDA5MjkyMTI5MDBaFw0yMTA5 4 | MjQyMTI5MDBaMBgxFjAUBgNVBAMMDXNwaGlueCBvcmFjbGUwWTATBgcqhkjOPQIB 5 | BggqhkjOPQMBBwNCAATPl01K0Nuxm4wZaYzS4AvaXy4pIG96Zk5XC1o0TmkdnNPb 6 | kgSUm6dx1OVvx3u8kVGRHYfgC7C4I414W2v41Hb4o1MwUTAdBgNVHQ4EFgQUtpha 7 | TRgMR7SeM7gYPKoq8L874tcwHwYDVR0jBBgwFoAUtphaTRgMR7SeM7gYPKoq8L87 8 | 4tcwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiEAnN1Y9WDfVW6f 9 | slgOnPs8eQdyoqA7S/rFf9wE/ZxR4tECICfCYMKpIRMYPEk2C+kqoJueB/JVdGKh 10 | pYxdMvjx8bsj 11 | -----END CERTIFICATE----- 12 | -------------------------------------------------------------------------------- /pwdsphinx/converters/totp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from base64 import b32decode 4 | import hmac 5 | from struct import pack, unpack 6 | import sys 7 | from time import time 8 | 9 | def totp(key, user, host, *opts, time_step=30, digits=6, digest='sha1'): 10 | if isinstance(key, bytes): key=key.decode('utf8') 11 | key = b32decode(key.upper() + '=' * ((8 - len(key)) % 8)) 12 | ts = pack('>Q', int(time() / time_step)) 13 | mac = hmac.new(key, ts, digest).digest() 14 | offset = mac[-1] & 0x0f 15 | binary = unpack('>L', mac[offset:offset+4])[0] & 0x7fffffff 16 | return str(binary)[-digits:].zfill(digits) 17 | 18 | schema = {"otp": totp} 19 | 20 | def main(): 21 | args = [int(x) if x.isdigit() else x for x in sys.argv[1:]] 22 | for key in sys.stdin: 23 | print(totp(key.strip(), [], *args)) 24 | 25 | if __name__ == '__main__': 26 | main() 27 | -------------------------------------------------------------------------------- /ext/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SCRIPT=$(realpath "$0") 4 | BASEDIR=$(dirname "$SCRIPT") 5 | BUILDDIR="$BASEDIR/build" 6 | FFBUILDDIR="$BUILDDIR/ff" 7 | FFEXT="$BUILDDIR/ext_ff.zip" 8 | CHROMEBUILDDIR="$BUILDDIR/chrome" 9 | CHROMEEXT="$BUILDDIR/ext_chrome.zip" 10 | 11 | files="$BASEDIR/*js $BASEDIR/*html $BASEDIR/*css $BASEDIR/*png $BASEDIR/_locales/" 12 | 13 | [ ! -d "$BUILDDIR" ] && mkdir "$BUILDDIR" 14 | 15 | [ -d "$FFBUILDDIR" ] && rm -r "$FFBUILDDIR" 16 | [ -d "$CHROMEBUILDDIR" ] && rm -r "$CHROMEBUILDDIR" 17 | [ -f "$FFEXT" ] && rm "$FFEXT" 18 | [ -f "$CHROMEEXT" ] && rm "$CHROMEEXT" 19 | 20 | mkdir "$FFBUILDDIR" 21 | cp -r $files "$FFBUILDDIR" 22 | cp "$BASEDIR/manifest_ff.json" "$FFBUILDDIR/manifest.json" 23 | cd "$FFBUILDDIR" 24 | zip "$FFEXT" ./* 25 | cd - 26 | 27 | 28 | mkdir "$CHROMEBUILDDIR" 29 | cp -r $files "$CHROMEBUILDDIR" 30 | cp "$BASEDIR/manifest_chrome.json" "$CHROMEBUILDDIR/manifest.json" 31 | cd "$CHROMEBUILDDIR" 32 | zip "$CHROMEEXT" ./* 33 | cd - 34 | -------------------------------------------------------------------------------- /pwdsphinx/config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # SPDX-FileCopyrightText: 2023, Marsiske Stefan 3 | # SPDX-License-Identifier: GPL-3.0-or-later 4 | 5 | import os, tomllib 6 | from pwdsphinx.utils import split_by_n 7 | 8 | def getcfg(name): 9 | paths=[ 10 | # read global cfg 11 | f'/etc/{name}/config', 12 | # update with per-user configs 13 | os.path.expanduser(f"~/.{name}rc"), 14 | # over-ride with local directory config 15 | os.path.expanduser(f"~/.config/{name}/config"), 16 | os.path.expanduser(f"{name}.cfg") 17 | ] 18 | config = dict() 19 | for path in paths: 20 | try: 21 | with open(path, "rb") as f: 22 | data = tomllib.load(f) 23 | except FileNotFoundError: 24 | continue 25 | except tomllib.TOMLDecodeError as ex: 26 | print(f"error in {path} at {ex}") 27 | continue 28 | config.update(data) 29 | return config 30 | 31 | 32 | if __name__ == '__main__': 33 | import sys 34 | getcfg('sphinx').write(sys.stdout) 35 | -------------------------------------------------------------------------------- /ext/manifest_ff.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "WebSphinx", 5 | "description": "__MSG_extDescription__", 6 | "version": "0.1.1", 7 | "default_locale": "en", 8 | "applications": { 9 | "gecko": { 10 | "id": "sphinx@ctrlc.hu", 11 | "strict_min_version": "57.0" 12 | } 13 | }, 14 | 15 | "background": { 16 | "scripts": ["background.js"], 17 | "persistent": true 18 | }, 19 | "browser_action": { 20 | "default_icon": "icon.png", 21 | "default_popup": "popup.html" 22 | }, 23 | "commands": { 24 | "_execute_browser_action": { 25 | "suggested_key": { 26 | "default": "Ctrl+Shift+L" 27 | } 28 | } 29 | }, 30 | "content_scripts": [ 31 | { 32 | "matches": ["http://*/*", "https://*/*"], 33 | "js": ["content_script.js"], 34 | "run_at":"document_start" 35 | } 36 | ], 37 | "web_accessible_resources": ["webauthn.js"], 38 | "permissions": [ 39 | "scripting", 40 | "activeTab", 41 | "nativeMessaging", 42 | "" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /man/websphinx.md: -------------------------------------------------------------------------------- 1 | % websphinx(1) | native-messaging backend for SPHINX browser webextensions 2 | 3 | # NAME 4 | 5 | websphinx - native messaging backend for SPHINX browser webextensions 6 | 7 | # SYNOPSIS 8 | 9 | `websphinx` is not meant to be run by a user. 10 | 11 | # DESCRIPTION 12 | 13 | `websphinx` serves as the native messaging backend for web extensions 14 | that expose the SPHINX password storage protocol in a browser. When you 15 | install a SPHINX browser extension, the browser automatically launches 16 | this backend to handle password operations. 17 | 18 | # REPORTING BUGS 19 | 20 | https://github.com/stef/pwdsphinx/issues/ 21 | 22 | # AUTHOR 23 | 24 | Written by Stefan Marsiske. 25 | 26 | # COPYRIGHT 27 | 28 | Copyright © 2023 Stefan Marsiske. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. 29 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 30 | 31 | # SEE ALSO 32 | 33 | https://github.com/stef/websphinx-chrom/ 34 | 35 | https://github.com/stef/websphinx-firefox/ 36 | 37 | `sphinx(1)` 38 | -------------------------------------------------------------------------------- /tests/ws-test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import struct, json, sys, subprocess 4 | 5 | if sys.argv[1]=="get": 6 | msg = {'cmd': "login"} 7 | else: 8 | msg = {'cmd': sys.argv[1]} 9 | 10 | if sys.argv[1] in ('create', 'get', 'change', 'commit', 'undo', 'delete'): 11 | msg['name']= sys.argv[2] 12 | msg['site']= sys.argv[3] 13 | 14 | if sys.argv[1] in {'create', 'change'}: 15 | msg['rules']= sys.argv[4] 16 | msg['size']= sys.argv[5] 17 | 18 | if sys.argv[1] == 'list': 19 | msg['site']= sys.argv[2] 20 | 21 | msg['mode'] = 'ws-test' 22 | 23 | if sys.argv[1] == 'json': 24 | msg = sys.argv[2].replace("'", '"') 25 | else: 26 | msg = json.dumps(msg) 27 | 28 | print("cmd:", msg) 29 | cmd = struct.pack('i', len(msg))+msg.encode("utf-8") 30 | proc=subprocess.Popen(["../pwdsphinx/websphinx.py"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 31 | out, err = proc.communicate(input=cmd) 32 | print("ret", proc.returncode) 33 | print("stdout") 34 | for line in out.split(b'\n'): 35 | print(line) 36 | print("stderr") 37 | for line in err.split(b'\n'): 38 | print(line) 39 | -------------------------------------------------------------------------------- /man/sphage.md: -------------------------------------------------------------------------------- 1 | % sphage(1) | converts 32 bytes binary data into an age(1) keypair 2 | 3 | # NAME 4 | 5 | sphage - converts 32 bytes binary data into an age(1) keypair 6 | 7 | # SYNOPSIS 8 | 9 | ``` 10 | echo "32 byte high-entropy string....." | sphage privkey >/tmp/privatekey 11 | echo "32 byte high-entropy string....." | sphage pubkey >/tmp/pubkey 12 | ``` 13 | 14 | # DESCRIPTION 15 | 16 | `sphage` converts the raw output of `sphinx(1)` into a cryptographic key pair compatible with [age](https://age-encryption.org). It can also convert an age secret key into its corresponding age public key. This enables integration between SPHINX and age-based encryption workflows for sophisticated secrets management setups. 17 | 18 | # REPORTING BUGS 19 | 20 | https://github.com/stef/pwdsphinx/issues/ 21 | 22 | # AUTHOR 23 | 24 | Written by Stefan Marsiske. 25 | 26 | # COPYRIGHT 27 | 28 | Copyright © 2023 Stefan Marsiske. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. 29 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 30 | 31 | # SEE ALSO 32 | 33 | `sphinx(1)` 34 | -------------------------------------------------------------------------------- /man/exec-on-click.md: -------------------------------------------------------------------------------- 1 | % exec-on-click(1) | simple tool that executes command when a left mouse-click is detected 2 | 3 | # NAME 4 | 5 | exec-on-click - simple tool that executes command when a left mouse-click is detected 6 | 7 | # SYNOPSIS 8 | 9 | ``` 10 | exec-on-click 11 | ``` 12 | 13 | # DESCRIPTION 14 | 15 | `exec-on-click` is a simple tool that waits for a left mouse click and then executes whatever parameters the script has been called with. 16 | 17 | # EXAMPLE 18 | 19 | ``` 20 | echo -n "hello world" | exec-on-click xdotool type --clearmodifiers '$(cat)' 21 | ``` 22 | 23 | Types `hello world` into the current window using [xdotool](https://github.com/jordansissel/xdotool), a program that lets you simulate keyboard input and mouse activity. 24 | 25 | # REPORTING BUGS 26 | 27 | https://github.com/stef/pwdsphinx/issues/ 28 | 29 | # AUTHOR 30 | 31 | Written by Stefan Marsiske. 32 | 33 | # COPYRIGHT 34 | 35 | Copyright © 2023 Stefan Marsiske. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. 36 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 37 | 38 | # SEE ALSO 39 | 40 | `sphinx(1)` 41 | -------------------------------------------------------------------------------- /tests/sphinx.cfg: -------------------------------------------------------------------------------- 1 | [client] 2 | verbose = false 3 | datadir = "data/" 4 | timeout = 37 5 | rwd_keys = true 6 | threshold = 2 7 | validate_password = false 8 | # v1 server 9 | address="localhost" 10 | port=10000 11 | ssl_cert = "cert.pem" 12 | delete_upgraded=true 13 | 14 | [servers] 15 | [servers.zero] 16 | host="localhost" 17 | port=10000 18 | ssl_cert = "cert.pem" 19 | ltsigkey_path = "data/zero.pub" 20 | timeout = 37 21 | #ssl_cert = "servers/0/cert.pem" 22 | #ltsigkey = "servers/0/ltsig.key.pub" 23 | 24 | [servers.one] 25 | host="localhost" 26 | port=10001 27 | ssl_cert = "cert.pem" 28 | ltsigkey_path = "data/one.pub" 29 | timeout = 37 30 | #ssl_cert = "servers/1/cert.pem" 31 | #ltsigkey = "servers/0/ltsig.key.pub" 32 | 33 | [servers.two] 34 | host="localhost" 35 | port=10002 36 | ssl_cert = "cert.pem" 37 | ltsigkey_path = "data/two.pub" 38 | timeout = 37 39 | #ssl_cert = "servers/2/cert.pem" 40 | #ltsigkey = "servers/0/ltsig.key.pub" 41 | 42 | #[servers.drei] 43 | #host="localhost" 44 | #port=10003 45 | #ssl_cert = "cert.pem" 46 | #ltsigkey = "data/drei.pub" 47 | # 48 | #[servers.eris] 49 | #host="localhost" 50 | #port=10004 51 | #ssl_cert = "cert.pem" 52 | #ltsigkey = "data/eris.pub" 53 | 54 | [websphinx] 55 | pinentry="/usr/bin/pinentry" 56 | -------------------------------------------------------------------------------- /man/getpwd.md: -------------------------------------------------------------------------------- 1 | % getpwd(1) | simple tool that queries a password from a user and writes it to standard output 2 | 3 | # NAME 4 | 5 | getpwd - simple tool that queries a password from a user and writes it to standard output 6 | 7 | # SYNOPSIS 8 | 9 | ``` 10 | getpwd ["prompt"] | sphinx get username hostname 11 | ``` 12 | 13 | # DESCRIPTION 14 | 15 | `getpwd` securely prompts for a password using `pinentry` from the GnuPG project and outputs it to standard output. This approach is safer than echoing passwords directly into commands, as it prevents passwords from appearing in process lists or command history. 16 | 17 | The tool supports various `pinentry` interfaces including curses, GTK, and Qt variants, allowing you to choose the interface that best fits your desktop environment. 18 | 19 | The parameter, `prompt`, specifies the prompt text displayed when asking for the password. 20 | 21 | # REPORTING BUGS 22 | 23 | https://github.com/stef/pwdsphinx/issues/ 24 | 25 | # AUTHOR 26 | 27 | Written by Stefan Marsiske. 28 | 29 | # COPYRIGHT 30 | 31 | Copyright © 2023 Stefan Marsiske. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. 32 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 33 | 34 | # SEE ALSO 35 | 36 | `sphinx(1)` 37 | -------------------------------------------------------------------------------- /man/type-pwd.md: -------------------------------------------------------------------------------- 1 | % type-pwd(1) | tool which wraps sphinx(1) to get and type a password to an X11 application 2 | 3 | # NAME 4 | 5 | type-pwd - tool which wraps sphinx(1) to get and type a password to an X11 application 6 | 7 | # SYNOPSIS 8 | 9 | ``` 10 | type-pwd username hostname 11 | ``` 12 | 13 | # DESCRIPTION 14 | 15 | `type-pwd` combines `getpwd(1)`, `exec-on-click(1)`, and `sphinx(1)` to create a secure password entry workflow. It first prompts for your master password, then waits for you to click on a password field before typing the password as keystrokes. 16 | 17 | This approach ensures that your password never appears in the clipboard where malware could steal it. It also works on websites that disable copy and paste functionality in password fields. 18 | 19 | When prompted to click, make sure you click directly in the password entry field where you want the password entered. 20 | 21 | # REPORTING BUGS 22 | 23 | https://github.com/stef/pwdsphinx/issues/ 24 | 25 | # AUTHOR 26 | 27 | Written by Stefan Marsiske. 28 | 29 | # COPYRIGHT 30 | 31 | Copyright © 2023 Stefan Marsiske. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. 32 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 33 | 34 | # SEE ALSO 35 | 36 | `sphinx(1)`, `exec-on-click(1)`, `getpwd(1)` 37 | -------------------------------------------------------------------------------- /ext/manifest_chrome.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiIpLogUg/aqfPydJjvRyxwUYME3LdoG6tHItPEYPPtquG60U1mFopkN2epr9HoyDEX334AsBkyxY6qxbQZZZVY+8xSPnJZhE/g7Cpo/eLvbX68avgavWp2/RCXjaW4BW06v4IcCKlI09jdWR7Oo33RGcsZer/FJTLiunoUl+6W5ap3KAry1JrLg5FwHYicaghwNrxM9zCDUbr0n7g7C7p/oHC/iCSmJgMZo5qA6sXfxxZgy2lTtB0M5y/NihbgeTvoF+GgJ9iFwfwIP4nyK6JPThRqCFguTQCMcvaqhMey9MjUC5aIZ2fRbuEV4XuyV+48jK5Dun/pDgXlzhmzjowwIDAQAB", 3 | "manifest_version": 3, 4 | 5 | "name": "WebSphinx", 6 | "description": "__MSG_extDescription__", 7 | "version": "0.1", 8 | "default_locale": "en", 9 | 10 | "background": { 11 | "service_worker": "background.js", 12 | "type": "module" 13 | }, 14 | "action": { 15 | "default_icon": "icon.png", 16 | "default_popup": "popup.html" 17 | }, 18 | "commands": { 19 | "_execute_action": { 20 | "suggested_key": { 21 | "default": "Ctrl+Shift+L" 22 | } 23 | } 24 | }, 25 | "host_permissions": [ 26 | "*://*/*" 27 | ], 28 | "content_scripts": [ 29 | { 30 | "matches": ["http://*/*", "https://*/*"], 31 | "js": ["content_script.js"], 32 | "run_at":"document_start" 33 | } 34 | ], 35 | "web_accessible_resources": [ 36 | { 37 | "resources": ["webauthn.js"], 38 | "matches": ["http://*/*", "https://*/*"] 39 | } 40 | ], 41 | "permissions": [ 42 | "activeTab", 43 | "nativeMessaging" 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /man/dmenu-sphinx.md: -------------------------------------------------------------------------------- 1 | % dmenu-sphinx(1) | dmenu-frontend for retrieving and inserting passwords from sphinx(1) into X11 applications 2 | 3 | # NAME 4 | 5 | dmenu-sphinx - dmenu-frontend for retrieving and inserting passwords from sphinx(1) into X11 applications 6 | 7 | # SYNOPSIS 8 | 9 | ``` 10 | dmenu-sphinx [username] [hostname] 11 | ``` 12 | 13 | # DESCRIPTION 14 | 15 | `dmenu-sphinx` provides an interactive interface for retrieving SPHINX passwords and automatically typing them into X11 applications. It uses `dmenu(1)` to present hostname selection menus and builds on `type-pwd(1)` for password entry. 16 | 17 | The tool first displays cached hostnames from previous usage. If multiple usernames exist for the selected hostname, it presents a username selection menu. Otherwise, it proceeds directly to password generation. Finally, it invokes `type-pwd(1)` to type the password into the focused application. 18 | 19 | The hostname history is cached in the file `~/.sphinx-hosts`. 20 | 21 | # REPORTING BUGS 22 | 23 | https://github.com/stef/pwdsphinx/issues/ 24 | 25 | # AUTHOR 26 | 27 | Written by Stefan Marsiske. 28 | 29 | # COPYRIGHT 30 | 31 | Copyright © 2023 Stefan Marsiske. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. 32 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 33 | 34 | # SEE ALSO 35 | 36 | `sphinx(1)`, `type-pwd(1)`, `exec-on-click(1)`, `getpwd(1)` 37 | -------------------------------------------------------------------------------- /pwdsphinx/converters/minisig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys, base64, pysodium, binascii 4 | from pwdsphinx.consts import * 5 | 6 | # usage 7 | # getpwd | env/bin/sphinx create minisig://test asdf | pipe2tmpfile minisign -R -s @@keyfile@@ -p /tmp/minisig.pub 8 | # getpwd | env/bin/sphinx get minisig://test asdf | pipe2tmpfile minisign -S -s @@keyfile@@ -m filetosign 9 | 10 | """format is 11 | untrusted comment: sphinx generated minisign key\n 12 | base64( 13 | 4564 0000 4232 <\x00 * 48> 14 | 15 | 16 | 17 | <\x00 * 32> 18 | ) 19 | 20 | use crypto_sign_ed25519_sk_to_pk(unsigned char *pk, const unsigned char *sk); 21 | to derive pubkey from secret key 22 | """ 23 | 24 | def privkey(sk, kid): 25 | raw = (binascii.unhexlify("456400004232") + 26 | b'\x00' * 48 + 27 | kid + 28 | sk + 29 | b'\x00' * 32) 30 | return f"untrusted comment: minisign secret key\n{base64.b64encode(raw).decode('utf8')}" 31 | 32 | def pubkey(pk, kid): 33 | raw = (b'Ed' + kid + pk) 34 | return f"untrusted comment: minisign public key\n{base64.b64encode(raw).decode('utf8')}" 35 | 36 | def convert(rwd, user, host, op, *opts): 37 | seed=rwd[:32] 38 | kid=rwd[32:40] 39 | pk,sk=pysodium.crypto_sign_seed_keypair(seed) 40 | if op in {CREATE, CHANGE}: 41 | return pubkey(pk, kid) 42 | return privkey(sk, kid) 43 | 44 | schema = {'minisig': convert} 45 | -------------------------------------------------------------------------------- /tests/test_conv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | from pwdsphinx.converter import convert 5 | from pwdsphinx.consts import * 6 | from pwdsphinx import bin2pass 7 | 8 | char_classes = 'uld' 9 | symbols = bin2pass.symbols 10 | size = 0 11 | 12 | class TestConverters(unittest.TestCase): 13 | def test_bin2pass(self): 14 | rwd = b'\xaa' * 32 15 | pwd = convert(rwd, "asdf", "host", GET, char_classes, size, symbols) 16 | self.assertEqual(pwd, '2UH@/%XoTb+T-RT*tipUqT+b\'lYQ*kUiPOdq@sK') 17 | 18 | def test_bin2pass_8char(self): 19 | rwd = b'\xaa' * 32 20 | pwd = convert(rwd, "asdf", "host", GET, char_classes, 8, symbols) 21 | self.assertEqual(pwd, 'iPOdq@sK') 22 | 23 | def test_raw(self): 24 | rwd = b'\xaa' * 32 25 | pwd = convert(rwd, "raw://asdf", "host", GET, char_classes, len(rwd), symbols) 26 | self.assertEqual(pwd, rwd) 27 | 28 | def test_otp(self): 29 | pwd = 'A' * 16 30 | rwd, classes, symbols = bin2pass.pass2bin(pwd) 31 | pwd = convert(rwd, "otp://asdf", "host", GET, classes, len(pwd), symbols) 32 | self.assertIsInstance(pwd, str) 33 | self.assertEqual(len(pwd), 6) 34 | 35 | def test_age(self): 36 | rwd = b'\x55' * 32 37 | pwd = convert(rwd, "age://asdf", "host", GET, char_classes, size, symbols) 38 | self.assertEqual(pwd, 'AGE-SECRET-KEY-1242424242424242424242424242424242424242424242424242S6JN5PD') 39 | from pwdsphinx.converters.sphage import decode 40 | self.assertEqual(rwd, bytes(decode('age-secret-key-', pwd))) 41 | -------------------------------------------------------------------------------- /ext/popup.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | min-width: 217px; 8 | } 9 | 10 | label, input[type="checkbox"] { 11 | margin-left: 10px; 12 | } 13 | 14 | input[type='text']#pwdlen { 15 | width: auto; 16 | padding: 10px; 17 | border: 0; 18 | box-sizing: border-box; 19 | } 20 | 21 | #search, input[type='text'] { 22 | width: 100%; 23 | padding: 10px; 24 | border: 0; 25 | box-sizing: border-box; 26 | } 27 | 28 | #search:focus, input[type='text']:focus { 29 | outline: 0; 30 | } 31 | 32 | #results { 33 | list-style-type: none; 34 | margin: 0; 35 | padding: 0; 36 | } 37 | 38 | #results:empty { 39 | display: none; 40 | } 41 | 42 | button.selected, button#close { 43 | width: auto; 44 | background-color: #fff; 45 | border-left: 1px solid black; 46 | border-right: 1px solid black; 47 | border-top: 1px solid black; 48 | box-shadow: none; 49 | text-align: left; 50 | padding: 10px; 51 | } 52 | 53 | button.inactive { 54 | width: auto; 55 | background-color: #aaa; 56 | border: 1px solid black; 57 | box-shadow: none; 58 | text-align: left; 59 | padding: 10px; 60 | } 61 | 62 | button.insert { 63 | width: 100%; 64 | color: blue; 65 | text-align: center; 66 | } 67 | 68 | button { 69 | width: 100%; 70 | background-color: #fff; 71 | border: 0; 72 | box-shadow: none; 73 | text-align: left; 74 | padding: 10px; 75 | } 76 | 77 | button:hover, button:focus, .focus button { 78 | outline: 0; 79 | background-color: #eee; 80 | } 81 | 82 | .hidden { 83 | display: none; 84 | } 85 | -------------------------------------------------------------------------------- /man/makefile: -------------------------------------------------------------------------------- 1 | all: sphinx.1 oracle.1 bin2pass.1 websphinx.1 getpwd.1 exec-on-click.1 type-pwd.1 dmenu-sphinx.1 sphinx-x11.1 sphage.1 \ 2 | otp.sphinx.1 pass.sphinx.1 user-pass-otp.sphinx.1 user-pass.sphinx.1 getacc-user-pass.sphinx.1 pipe2tmpfile.1 3 | 4 | html: sphinx.html oracle.html bin2pass.html websphinx.html getpwd.html exec-on-click.html type-pwd.html dmenu-sphinx.html \ 5 | sphinx-x11.html sphage.html pipe2tmpfile.html 6 | 7 | install: $(DESTDIR)$(PREFIX)/share/man/man1/sphinx.1 $(DESTDIR)$(PREFIX)/share/man/man1/oracle.1 $(DESTDIR)$(PREFIX)/share/man/man1/bin2pass.1 \ 8 | $(DESTDIR)$(PREFIX)/share/man/man1/getpwd.1 $(DESTDIR)$(PREFIX)/share/man/man1/exec-on-click.1 $(DESTDIR)$(PREFIX)/share/man/man1/type-pwd.1 \ 9 | $(DESTDIR)$(PREFIX)/share/man/man1/dmenu-sphinx.1 $(DESTDIR)$(PREFIX)/share/man/man1/sphinx-x11.1 $(DESTDIR)$(PREFIX)/share/man/man1/sphage.1 \ 10 | $(DESTDIR)$(PREFIX)/share/man/man1/websphinx.1 $(DESTDIR)$(PREFIX)/share/man/man1/otp.sphinx.1 $(DESTDIR)$(PREFIX)/share/man/man1/pass.sphinx.1 \ 11 | $(DESTDIR)$(PREFIX)/share/man/man1/user-pass-otp.sphinx.1 $(DESTDIR)$(PREFIX)/share/man/man1/user-pass.sphinx.1 $(DESTDIR)$(PREFIX)/share/man/man1/pipe2tmpfile.1 12 | 13 | clean: 14 | rm -f *.1 15 | rm -f *.html 16 | 17 | otp.sphinx.1: sphinx-x11.1 18 | ln -s $< $@ 19 | pass.sphinx.1: sphinx-x11.1 20 | ln -s $< $@ 21 | user-pass-otp.sphinx.1: sphinx-x11.1 22 | ln -s $< $@ 23 | user-pass.sphinx.1: sphinx-x11.1 24 | ln -s $< $@ 25 | getacc-user-pass.sphinx.1: sphinx-x11.1 26 | ln -s $< $@ 27 | 28 | %.1: %.md 29 | pandoc -s -o $@ $< 30 | 31 | %.html: %.md 32 | pandoc -s -o $@ $< 33 | -------------------------------------------------------------------------------- /pwdsphinx/ext.py: -------------------------------------------------------------------------------- 1 | from os import mkdir, getenv 2 | from os.path import exists, split 3 | from pathlib import Path 4 | from sys import executable 5 | 6 | EXT_NM_TPL = '''{{ 7 | "name": "websphinx", 8 | "description": "Host for communicating with PITCHFORKed Sphinx", 9 | "path": "{cmd}", 10 | "type": "stdio", 11 | ''' 12 | 13 | CHR_NM_TPL = EXT_NM_TPL + ''' "allowed_origins": [ 14 | "chrome-extension://ojbhlhidchjkmjmpeonendekpoacahni/" 15 | ] 16 | }} 17 | ''' 18 | 19 | FF_NM_TPL = EXT_NM_TPL + ''' "allowed_extensions": [ 20 | "sphinx@ctrlc.hu" 21 | ] 22 | }} 23 | ''' 24 | 25 | 26 | def get_executable(): 27 | venv = getenv('VIRTUAL_ENV') 28 | if venv: 29 | return f'{venv}/bin/websphinx' 30 | d = split(Path(__file__).absolute())[0] 31 | return d + '/websphinx.py' 32 | 33 | 34 | def init_browser_ext(): 35 | cmd = get_executable() 36 | print(cmd) 37 | # init ff 38 | if exists(f'{Path.home()}/.mozilla/'): 39 | nm_dir = f'{Path.home()}/.mozilla/native-messaging-hosts/' 40 | if not exists(nm_dir): 41 | mkdir(nm_dir) 42 | with open(nm_dir+'websphinx.json', 'w') as outfile: 43 | ff_nm = FF_NM_TPL.format(cmd=cmd) 44 | outfile.write(ff_nm) 45 | # init chrome 46 | if exists(f'{Path.home()}/.config/chromium'): 47 | nm_dir = f'{Path.home()}/.config/chromium/NativeMessagingHosts/' 48 | if not exists(nm_dir): 49 | mkdir(nm_dir) 50 | with open(nm_dir+'websphinx.json', 'w') as outfile: 51 | chr_nm = CHR_NM_TPL.format(cmd=cmd) 52 | outfile.write(chr_nm) 53 | -------------------------------------------------------------------------------- /contrib/Makefile: -------------------------------------------------------------------------------- 1 | clean: 2 | 3 | install: $(DESTDIR)$(prefix)/usr/bin/dmenu-sphinx $(DESTDIR)$(prefix)/usr/bin/exec-on-click \ 4 | $(DESTDIR)$(prefix)/usr/bin/getpwd $(DESTDIR)$(prefix)/usr/bin/type-pwd \ 5 | $(DESTDIR)$(prefix)/usr/bin/otp.sphinx $(DESTDIR)$(prefix)/usr/bin/pass.sphinx \ 6 | $(DESTDIR)$(prefix)/usr/bin/user-pass-otp.sphinx $(DESTDIR)$(prefix)/usr/bin/user-pass.sphinx \ 7 | $(DESTDIR)$(prefix)/usr/bin/getacc-user-pass.sphinx \ 8 | $(DESTDIR)$(prefix)/usr/bin/sphinx-x11 $(DESTDIR)$(prefix)/usr/bin/pipe2tmpfile \ 9 | $(DESTDIR)$(prefix)/usr/share/doc/pwdsphinx-tools/README.x11.md 10 | 11 | $(DESTDIR)$(prefix)/usr/bin/dmenu-sphinx: dmenu-sphinx 12 | install -D -m 0755 $< $@ 13 | 14 | $(DESTDIR)$(prefix)/usr/bin/exec-on-click: exec-on-click 15 | install -D -m 0755 $< $@ 16 | 17 | $(DESTDIR)$(prefix)/usr/bin/getpwd: getpwd 18 | install -D -m 0755 $< $@ 19 | 20 | $(DESTDIR)$(prefix)/usr/bin/type-pwd: type-pwd 21 | install -D -m 0755 $< $@ 22 | 23 | $(DESTDIR)$(prefix)/usr/bin/sphinx-x11: sphinx-x11 24 | install -D -m 0755 $< $@ 25 | 26 | $(DESTDIR)$(prefix)/usr/bin/pipe2tmpfile: pipe2tmpfile 27 | install -D -m 0755 $< $@ 28 | 29 | $(DESTDIR)$(prefix)/usr/bin/otp.sphinx: sphinx-scripts/otp.sphinx 30 | install -D -m 0755 $< $@ 31 | 32 | $(DESTDIR)$(prefix)/usr/bin/pass.sphinx: sphinx-scripts/pass.sphinx 33 | install -D -m 0755 $< $@ 34 | 35 | $(DESTDIR)$(prefix)/usr/bin/user-pass-otp.sphinx: sphinx-scripts/user-pass-otp.sphinx 36 | install -D -m 0755 $< $@ 37 | 38 | $(DESTDIR)$(prefix)/usr/bin/user-pass.sphinx: sphinx-scripts/user-pass.sphinx 39 | install -D -m 0755 $< $@ 40 | 41 | $(DESTDIR)$(prefix)/usr/bin/getacc-user-pass.sphinx: sphinx-scripts/getacc-user-pass.sphinx 42 | install -D -m 0755 $< $@ 43 | 44 | $(DESTDIR)$(prefix)/usr/share/doc/pwdsphinx-tools/README.x11.md: README.md 45 | install -D -m 0644 $< $@ 46 | 47 | .PHONY: clean 48 | -------------------------------------------------------------------------------- /pwdsphinx/converter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from pwdsphinx import bin2pass 4 | import traceback, os, sys 5 | from importlib.machinery import SourceFileLoader 6 | from pathlib import Path 7 | 8 | converters = {} 9 | 10 | def load_converters(): 11 | global converters 12 | p = Path(__file__).parent.absolute() 13 | for converter_fname in os.listdir(f'{p}/converters/'): 14 | if converter_fname.startswith('_') or not converter_fname.endswith('.py'): 15 | continue 16 | try: 17 | name = converter_fname[:-3] 18 | import_path = 'converters.'+name 19 | if import_path in sys.modules: 20 | del sys.modules[import_path] 21 | s = SourceFileLoader(import_path, 22 | f'{p}/converters/' + converter_fname).load_module() 23 | except: 24 | print("failed to load converter", converter_fname) 25 | traceback.print_exc() 26 | continue 27 | for schema, converter in s.schema.items(): 28 | if schema in converters: 29 | raise ValueError(f"{schema} is a already in loaded converters") 30 | converters[schema]=converter 31 | 32 | load_converters() 33 | 34 | def convert(rwd, user, host, op, *opts): 35 | 36 | if '://' not in user: 37 | return bin2pass.derive(rwd, *opts) 38 | elif user.startswith('otp://'): 39 | # need to recover the predefined base32 otp key 40 | rwd = bin2pass.derive(rwd, *opts) 41 | 42 | schema, name = user.split("://",1) 43 | return converters[schema](rwd, name, host, op, *opts) 44 | 45 | def convertedBy(user): 46 | for k in converters.keys(): 47 | if user.startswith(f"{k}://"): return k 48 | return None 49 | 50 | if __name__ == "__main__": 51 | convert(b'\xaa' * 32, 'asdf', 'uld', 0, '') 52 | import sys 53 | args = [int(x) if x.isdigit() else x for x in sys.argv[1:]] 54 | rwd = sys.stdin.buffer.read(32) 55 | print(convert(rwd, *args)) 56 | -------------------------------------------------------------------------------- /tests/test_rules.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from os import listdir 3 | from shutil import rmtree 4 | from unittest.mock import Mock 5 | from io import BytesIO 6 | import sys, pysodium 7 | 8 | from pwdsphinx import sphinx, bin2pass 9 | 10 | # to get coverage, run 11 | # PYTHONPATH=.. coverage run ../tests/rules.py 12 | # coverage report -m 13 | # to just run the tests do 14 | # python3 -m unittest discover --start-directory ../tests 15 | 16 | def equ(classes, syms, size, check, xor): 17 | unpacked = sphinx.unpack_rule(sphinx.pack_rule(classes, syms, size, check, xor)) 18 | assert set(classes) == unpacked[0] 19 | assert list(syms) == unpacked[1] 20 | assert size == unpacked[2] 21 | if sphinx.validate_password: 22 | assert check == unpacked[3] 23 | else: 24 | assert 0 == unpacked[3] 25 | assert xor == unpacked[4] 26 | 27 | from itertools import chain, combinations 28 | def powerset(iterable): 29 | "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)" 30 | s = list(iterable) 31 | return chain.from_iterable(combinations(s, r) for r in range(len(s)+1)) 32 | 33 | class TestRules(unittest.TestCase): 34 | def test_rules(self): 35 | for cls in powerset('uld'): 36 | equ(''.join(cls), bin2pass.symbols, 64, 31, b'\x00'*64) 37 | if cls!=tuple(): 38 | equ(''.join(cls), '', 64, 31, b'\x00'*64) 39 | 40 | equ('uld', bin2pass.symbols[:16], 64, 31, b'\x00'*64) 41 | equ('uld', bin2pass.symbols[16:], 64, 31, b'\x00'*64) 42 | 43 | equ('uld', bin2pass.symbols, 64, 31, b'\xff'*64) 44 | equ('uld', bin2pass.symbols, 64, 31, b'\xaa'*64) 45 | 46 | for i in range(128): 47 | equ('uld', bin2pass.symbols, i, 31, b'\xaa'*64) 48 | for i in range(32): 49 | equ('uld', bin2pass.symbols, 64, i, b'\xaa'*64) 50 | 51 | if __name__ == '__main__': 52 | unittest.main() 53 | -------------------------------------------------------------------------------- /man/bin2pass.md: -------------------------------------------------------------------------------- 1 | % bin2pass(1) | converts binary input to passwords 2 | 3 | # NAME 4 | 5 | bin2pass - converts binary input to passwords 6 | 7 | # SYNOPSIS 8 | 9 | ``` 10 | bin2pass [d|u|l] [] [] ?@[\]^_`{}~ 25 | ``` 26 | 27 | Note that spaces are allowed in the symbol set. Be careful to properly quote special characters that your shell might interpret, such as `"`, `!`, and `\`. 28 | 29 | # EXAMPLES 30 | 31 | Generate the longest possible random password from `/dev/random`, with the resulting password having characters from digits, lowercase and uppercase letters, and the `space` and `*` symbols. 32 | 33 | ``` 34 | dd if=/dev/random bs=1 count=32 | ./pwdsphinx/bin2pass.py " *" 35 | ``` 36 | 37 | # REPORTING BUGS 38 | 39 | https://github.com/stef/pwdsphinx/issues/ 40 | 41 | # AUTHOR 42 | 43 | Written by Stefan Marsiske. 44 | 45 | # COPYRIGHT 46 | 47 | Copyright © 2023 Stefan Marsiske. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. 48 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 49 | 50 | # SEE ALSO 51 | 52 | `sphinx(1)` 53 | -------------------------------------------------------------------------------- /ext/content_script.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | const br = chrome || browser; 3 | const s = document.createElement("script"); 4 | const src = br.runtime.getURL("webauthn.js"); 5 | //const bg = br.runtime.connect({'name': 'content-script'}); 6 | const site = window.location.hostname; 7 | let pagePorts = {}; 8 | 9 | s.setAttribute('src', src); 10 | s.setAttribute('id', "sphinx-webauthn-page-script"); 11 | (document.head || document.documentElement).appendChild(s); 12 | 13 | window.addEventListener('message', webauthnEventHandler); 14 | 15 | br.runtime.onMessage.addListener((m) => { 16 | let port = pagePorts[m.results.id]; 17 | port.postMessage(m.results); 18 | delete pagePorts[m.results.id]; 19 | }); 20 | 21 | function genId() { 22 | return Math.random().toString(32).slice(2) + Math.random().toString(32).slice(2); 23 | } 24 | 25 | function webauthnEventHandler(msg) { 26 | let options = msg.data; 27 | if(!options.type || options.type != "sphinxWebauthnEvent") { 28 | return; 29 | } 30 | if(msg.origin != window.origin) { 31 | console.log("invalid webauthnEvent sender"); 32 | return; 33 | } 34 | const site = window.location.hostname; 35 | // TODO 36 | let bgMsg = { 37 | "site": site, 38 | "action": options.action, 39 | "params": options.params, 40 | "id": genId(), 41 | }; 42 | pagePorts[bgMsg.id] = msg.ports[0]; 43 | br.runtime.sendMessage(bgMsg); 44 | //br.runtime.sendMessage(bgMsg).then( 45 | // function(response) { 46 | // console.log('response received from bg', response); 47 | // pagePort.postMessage(response); 48 | // }, 49 | // function(error) { 50 | // console.log('error received from bg', error); 51 | // pagePort.postMessage(error); 52 | // } 53 | //); 54 | } 55 | })(); 56 | 57 | -------------------------------------------------------------------------------- /tests/opaque-store.cfg: -------------------------------------------------------------------------------- 1 | [client] 2 | # you must change this value, it ensures that your record ids are 3 | # unique you must also make sure to not lose this value, if you do, 4 | # you lose access to your records. 5 | id_salt="Please_MUST-be_changed! and backed up to something difficult to guess" 6 | # the number of servers successfully participating in an 7 | # operation. must be less than 129, but lower 1 digit number are 8 | # probable the most robust. 9 | threshold=2 10 | # the time in seconds a distributed keygen (DKG) protocol message is 11 | # considered fresh. anything older than this is considered invalid and 12 | # aborts a DKG. Higher values help with laggy links, lower values can 13 | # be fine if you have high-speed connections to all servers. 14 | ts_epsilon=1200 15 | 16 | # the list of servers, must be 1 item, if threshold is 1, or one more 17 | # than threshold. 18 | [servers] 19 | [servers.zero] 20 | # address of server 21 | host="127.0.0.1" 22 | # port where server is running 23 | port=23000 24 | timeout=30 25 | # self-signed public key of the server 26 | # - not needed for proper Lets Encrypt certs 27 | #ssl_cert = "../../../opaque-store/.arch/test-2of3-setup/0/cert.pem" 28 | #ltsigkey="../../../opaque-store/.arch/test-2of3-setup/zero.pub" 29 | ssl_cert = "cert.pem" 30 | ltsigkey = "data/os_zero.pub" 31 | 32 | [servers.eins] 33 | # address of server 34 | host="127.0.0.1" 35 | # port where server is running 36 | port=23001 37 | timeout=30 38 | # public key of the server 39 | #ssl_cert = "../../../opaque-store/.arch/test-2of3-setup/1/cert.pem" 40 | #ltsigkey="../../../opaque-store/.arch/test-2of3-setup/eins.pub" 41 | ssl_cert = "cert.pem" 42 | ltsigkey = "data/os_one.pub" 43 | 44 | [servers.zwei] 45 | # address of server 46 | host="127.0.0.1" 47 | # port where server is running 48 | port=23002 49 | timeout=30 50 | # public key of the server 51 | #ssl_cert = "../../../opaque-store/.arch/test-2of3-setup/2/cert.pem" 52 | #ltsigkey="../../../opaque-store/.arch/test-2of3-setup/zwei.pub" 53 | ssl_cert = "cert.pem" 54 | ltsigkey = "data/os_two.pub" 55 | -------------------------------------------------------------------------------- /.github/workflows/hcodeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 22 * * 2' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | # Initializes the CodeQL tools for scanning. 22 | - name: Initialize CodeQL 23 | uses: github/codeql-action/init@v3 24 | # Override language selection by uncommenting this and choosing your languages 25 | # with: 26 | # languages: go, javascript, csharp, python, cpp, java 27 | 28 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 29 | # If this step fails, then you should remove it and run the build manually (see below) 30 | #- name: Autobuild 31 | # uses: github/codeql-action/autobuild@v3 32 | 33 | # ℹ️ Command-line programs to run using the OS shell. 34 | # 📚 https://git.io/JvXDl 35 | 36 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 37 | # and modify them (or add more) to build your code if your project 38 | # uses a compiled language 39 | 40 | - run: | 41 | sudo apt update 42 | sudo apt install -y libsodium-dev pkgconf # build-essential git python3-pip 43 | # liboprf 44 | git clone https://github.com/stef/liboprf/ 45 | cd liboprf/src 46 | sudo mkdir -p /usr/include/oprf/ 47 | sudo PREFIX=/usr make install 48 | pip3 install ../python/ 49 | cd ../.. 50 | git clone https://github.com/stef/equihash 51 | cd equihash 52 | sudo PREFIX=/usr make install 53 | pip3 install python/ 54 | cd .. 55 | sudo ldconfig 56 | pip3 install . 57 | cd tests 58 | python3 -m unittest discover -fcb -v . 59 | 60 | - name: Perform CodeQL Analysis 61 | uses: github/codeql-action/analyze@v3 62 | -------------------------------------------------------------------------------- /ext/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 13 | 21 | 30 | 31 |
32 |
33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | hash() { 4 | md5sum | { read md5 rest; echo $md5; } 5 | } 6 | 7 | [[ -d data ]] || { 8 | echo no data directory found 9 | echo please start ../pwdsphinx/oracle.py 10 | exit 1 11 | } 12 | echo "create user1" 13 | rwd0="$(echo -n 'asdf' | ../pwdsphinx/sphinx.py create user1 example.com ulsd)" 14 | echo "get user1 rwd" 15 | rwd="$(echo -n 'asdf' | ../pwdsphinx/sphinx.py get user1 example.com)" 16 | [[ "$rwd" == "$rwd0" ]] || false 17 | echo "change user1 rwd" 18 | rwd1="$(echo -ne 'asdf\nasdf' | ../pwdsphinx/sphinx.py change user1 example.com)" 19 | echo "get user1 rwd" 20 | rwd="$(echo -n 'asdf' | ../pwdsphinx/sphinx.py get user1 example.com)" 21 | [[ "$rwd" == "$rwd0" ]] || false 22 | echo "commit user1 changed rwd" 23 | echo -n 'asdf' | ../pwdsphinx/sphinx.py commit user1 example.com 24 | rwd="$(echo -n 'asdf' | ../pwdsphinx/sphinx.py get user1 example.com)" 25 | [[ "$rwd" == "$rwd1" ]] || false 26 | echo "undo user1" 27 | echo -n 'asdf' | ../pwdsphinx/sphinx.py undo user1 example.com 28 | rwd="$(echo -n 'asdf' | ../pwdsphinx/sphinx.py get user1 example.com)" 29 | [[ "$rwd" == "$rwd0" ]] || false 30 | echo "commit again user1 changed rwd" 31 | echo -n 'asdf' | ../pwdsphinx/sphinx.py commit user1 example.com 32 | rwd="$(echo -n 'asdf' | ../pwdsphinx/sphinx.py get user1 example.com)" 33 | [[ "$rwd" == "$rwd1" ]] || false 34 | rwd0="$rwd1" 35 | echo "commit user1 changed rwd again - fail" 36 | echo -n 'asdf' | ../pwdsphinx/sphinx.py commit user1 example.com || true 37 | echo "get user1 rwd" 38 | rwd="$(echo -n 'asdf' | ../pwdsphinx/sphinx.py get user1 example.com)" 39 | [[ "$rwd" == "$rwd0" ]] || false 40 | 41 | 42 | echo "create user2 rwd" 43 | rwds0="$(echo -n 'asdf' | ../pwdsphinx/sphinx.py create user2 example.com ulsd)" 44 | echo "get user2 rwd" 45 | rwds="$(echo -n 'asdf' | ../pwdsphinx/sphinx.py get user2 example.com)" 46 | [[ "$rwds" == "$rwds0" ]] || false 47 | echo "list users rwd" 48 | md5="$(echo -n 'asdf' | ../pwdsphinx/sphinx.py list example.com | hash)" 49 | [[ "$md5" == "57c246efc4d56f6210462408b5f8ef2e" ]] 50 | echo "delete user2 rwd" 51 | echo -n 'asdf' | ../pwdsphinx/sphinx.py delete user2 example.com 52 | echo "list users rwd" 53 | md5="$(echo -n 'asdf' | ../pwdsphinx/sphinx.py list example.com | hash)" 54 | [[ "$md5" == "a609316768619f154ef58db4d847b75e" ]] 55 | echo "get user2 rwd - fail" 56 | echo -n 'asdf' | ../pwdsphinx/sphinx.py get user2 example.com || true 57 | 58 | echo "all tests passed" 59 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # SPDX-FileCopyrightText: 2018, Marsiske Stefan 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | import os 7 | #from distutils.core import setup, Extension 8 | from setuptools import setup 9 | 10 | 11 | # Utility function to read the README file. 12 | # Used for the long_description. It's nice, because now 1) we have a top level 13 | # README file and 2) it's easier to type in the README file than to put a raw 14 | # string in below ... 15 | def read(fname): 16 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 17 | 18 | from setuptools.command.sdist import sdist as SetuptoolsSdist 19 | class BuildMakefilesSdist(SetuptoolsSdist): 20 | def run(self): 21 | os.chdir('man') 22 | os.system('make') 23 | os.chdir('..') 24 | SetuptoolsSdist.run(self) 25 | from setuptools.command.build import build as SetuptoolsBuild 26 | class BuildMakefilesBuild(SetuptoolsBuild): 27 | def run(self): 28 | os.chdir('man') 29 | os.system('make') 30 | os.chdir('..') 31 | SetuptoolsBuild.run(self) 32 | 33 | setup(name = 'pwdsphinx', 34 | version = '2.0.3', 35 | description = 'SPHINX password protocol', 36 | license = "GPLv3", 37 | author = 'Stefan Marsiske', 38 | author_email = 'sphinx@ctrlc.hu', 39 | url = 'https://github.com/stef/pwdsphinx', 40 | long_description=read('README.md'), 41 | long_description_content_type="text/markdown", 42 | packages = ['pwdsphinx', 'pwdsphinx.converters'], 43 | install_requires = ("pysodium", "SecureString", 44 | "qrcodegen","zxcvbn-python", 'pyequihash', 'pyoprf >= 0.6.0'), 45 | classifiers = ["Development Status :: 4 - Beta", 46 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 47 | "Topic :: Security :: Cryptography", 48 | "Topic :: Security", 49 | ], 50 | entry_points = { 51 | 'console_scripts': [ 52 | 'oracle = pwdsphinx.oracle:parse_params', 53 | 'sphinx = pwdsphinx.sphinx:main', 54 | 'websphinx = pwdsphinx.websphinx:main', 55 | 'bin2pass = pwdsphinx.bin2pass:main', 56 | 'sphage = pwdsphinx.converters.sphage:main', 57 | ], 58 | }, 59 | cmdclass={'sdist': BuildMakefilesSdist, 60 | 'build': BuildMakefilesBuild}, 61 | #ext_modules = [libsphinx], 62 | ) 63 | -------------------------------------------------------------------------------- /man/pipe2tmpfile.md: -------------------------------------------------------------------------------- 1 | % pipe2tmpfile(1) | simple tool that allows commands that expect files to work with data coming through a pipe 2 | 3 | # NAME 4 | 5 | pipe2tmpfile - simple tool that allows commands that expect files to work with data coming through a pipe 6 | 7 | # SYNOPSIS 8 | 9 | ``` 10 | echo input | pipe2tmpfile @@keyfile@@ [additional args...] 11 | ``` 12 | 13 | The `command` will run with a temporary file containing the piped content, where `@@keyfile@@` is replaced with the temporary file path. 14 | 15 | # DESCRIPTION 16 | 17 | `pipe2tmpfile` is a utility that bridges the gap between commands that output data to stdout and commands that require file input. It reads data from standard input, writes it to a secure temporary file, and then executes a specified command with the temporary file path substituted for the `@@keyfile@@` token. 18 | 19 | This tool is particularly useful when working with sensitive data like cryptographic keys or passwords that should not be written to permanent storage. The temporary file is automatically deleted after the command finishes running, ensuring no sensitive data remains on disk. 20 | 21 | 22 | # EXAMPLE 23 | 24 | Sign a file using a minisign key stored in SPHINX: 25 | 26 | ```sh 27 | getpwd | sphinx get minisig://user1 minisign-test-key | pipe2tmpfile minisign -S -s @@keyfile@@ -m filetosign 28 | ``` 29 | 30 | This command: 31 | 32 | 1. Prompts for your master password via `getpwd` 33 | 2. Retrieves the `minisign` private key from SPHINX 34 | 3. Writes the key to a secure temporary file 35 | 4. Runs `minisign` to sign `filetosign` using the temporary key file 36 | 5. Automatically deletes the temporary key file 37 | 38 | # SECURITY CONSIDERATIONS 39 | 40 | Since the output of SPHINX is generally sensitive, it is advised not to write it 41 | to permanent storage. Thus, `pipe2tmpfile` tries to store it in a temporary 42 | file storage. By default, this is stored under `/run/user/$(id -u)`. 43 | However, users can provide an alternative path to store these files 44 | by setting the environment variable `keyroot`. 45 | 46 | The temporary files are deleted after the execution of the command. 47 | 48 | # REPORTING BUGS 49 | 50 | https://github.com/stef/pwdsphinx/issues/ 51 | 52 | # AUTHOR 53 | 54 | Written by Stefan Marsiske. 55 | 56 | # COPYRIGHT 57 | 58 | Copyright © 2024 Stefan Marsiske. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. 59 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 60 | 61 | # SEE ALSO 62 | 63 | `sphinx(1)` 64 | -------------------------------------------------------------------------------- /tests/test_pass2bin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import unittest, random, math 4 | from pwdsphinx import bin2pass 5 | 6 | # to get coverage, run 7 | # PYTHONPATH=.. coverage run ../tests/pass2bin.py 8 | # coverage report -m 9 | # to just run the tests do 10 | # python3 -m unittest discover --start-directory ../tests 11 | 12 | class TestRules(unittest.TestCase): 13 | def test_invert_simple(self): 14 | target = "this is a pass2bin test string" 15 | rwd, classes, symbols = bin2pass.pass2bin(target) 16 | self.assertEqual(bin2pass.derive(rwd, classes, len(target), symbols), target) 17 | 18 | def test_invert_too_long(self): 19 | target = "this is a pass2bin test stringthis is a pass2bin test stringthis is a pass2bin test stringthis is a pass2bin test stringthis is a pass2bin test string" 20 | self.assertRaises(OverflowError, bin2pass.pass2bin, target) 21 | 22 | def test_invert_iter(self): 23 | chars = bin2pass.allchars 24 | for i in range(1,len(chars)): 25 | target=''.join(chars[:i]) 26 | try: 27 | rwd, classes, symbols = bin2pass.pass2bin(target) 28 | except OverflowError: 29 | break 30 | self.assertEqual(bin2pass.derive(rwd, classes, len(target), symbols), target) 31 | 32 | def test_invert_reviter(self): 33 | chars = bin2pass.allchars 34 | for i in range(1,len(chars)): 35 | target=''.join(chars[-i:]) 36 | try: 37 | rwd,classes, symbols = bin2pass.pass2bin(target) 38 | except OverflowError: 39 | break 40 | self.assertEqual(bin2pass.derive(rwd, classes, len(target), symbols), target) 41 | 42 | def test_invert_random(self): 43 | chars = bin2pass.allchars 44 | for _ in range(1000): 45 | target=''.join(random.choices(chars,k=random.randrange(1,39))) 46 | rwd,classes, symbols = bin2pass.pass2bin(target) 47 | self.assertEqual(bin2pass.derive(rwd, classes, len(target), symbols), target) 48 | 49 | def test_all_zeroes(self): 50 | logbase = int(math.log(1<<256, len(bin2pass.allchars))) 51 | target = bin2pass.allchars[0] * (logbase-1) + bin2pass.allchars[1] 52 | for _ in range(logbase): 53 | rwd,classes, symbols = bin2pass.pass2bin(target) 54 | self.assertEqual(bin2pass.derive(rwd, classes, len(target), symbols), target) 55 | target = target[1:]+bin2pass.allchars[0] 56 | 57 | def test_short_zeroes(self): 58 | logbase = int(math.log(1<<256, len(bin2pass.allchars))) 59 | target = bin2pass.allchars[0] * (logbase//2) + bin2pass.allchars[1] 60 | for _ in range(len(target)): 61 | ctarget = ''.join(target) 62 | (rwd,classes,symbols) = bin2pass.pass2bin(ctarget) 63 | self.assertEqual(bin2pass.derive(rwd, classes, len(ctarget), symbols), ctarget) 64 | target = target[1:]+bin2pass.allchars[0] 65 | 66 | if __name__ == '__main__': 67 | unittest.main() 68 | -------------------------------------------------------------------------------- /pwdsphinx/bin2pass.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: 2018, 2021, Marsiske Stefan 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | import sys, math, random 7 | 8 | #from itertools import chain 9 | #tuple(bytes([x]) for x in chain(range(32,48),range(58,65),range(91,97),range(123,127))) 10 | symbols = ' !"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' 11 | 12 | sets = { 13 | # symbols 14 | # digits 15 | 'd': tuple(bytes([x]) for x in range(48,58)), 16 | # upper-case 17 | 'u': tuple(bytes([x]) for x in range(65,91)), 18 | # lower-case 19 | 'l': tuple(bytes([x]) for x in range(97,123))} 20 | 21 | allchars = ''.join(tuple(c.decode('utf8') for x in (sets[c] for c in ('u','l','d') if c in 'uld') for c in x) + tuple(symbols)) 22 | 23 | def bin2pass(raw, chars, size): 24 | v = int.from_bytes(raw, 'big') 25 | result = '' 26 | while (size > 0 and len(result) < size) or (size == 0 and v > 0): 27 | idx = v % len(chars) 28 | v //= len(chars) 29 | result = chars[idx] + result 30 | return result 31 | 32 | def pass2bin(string, chars = allchars): 33 | classes = {'u','l','d'} 34 | sym = symbols 35 | # reduce char classes to necessary minimum to 36 | # accomodate longer passwords 37 | if chars == None: 38 | chars = [] 39 | for c in ('u','l','d'): 40 | s = set(x.decode('utf8') for x in sets[c]) 41 | if s & set(string): 42 | chars.append(''.join(sorted(s))) 43 | else: 44 | classes.remove(c) 45 | if set(string) & set(symbols): 46 | chars+=symbols 47 | else: 48 | sym = '' 49 | chars=''.join(chars) 50 | 51 | le_str = string[::-1] 52 | logbase = int(math.log(1<<512, len(chars))) 53 | r = sum(chars.find(le_str[i]) * len(chars)**i for i in range(len(le_str))) 54 | # add padding 55 | r += sum(chars.find(random.choice(chars)) * len(chars)**i for i in range(len(le_str), logbase)) 56 | return int.to_bytes(r, 64, 'big'), ''.join(classes), sym 57 | 58 | def derive(rwd, rule, size, syms=symbols): 59 | chars = tuple(c.decode('utf8') for x in (sets[c] for c in ('u','l','d') if c in rule) for c in x) + tuple(x for x in symbols if x in set(syms)) 60 | password = bin2pass(rwd,chars, size) 61 | if size>0: password=password[:size] 62 | return password 63 | 64 | def usage(): 65 | print("usage: %s [d|u|l] [] \" !\"#$%%&'()*+,-./:;<=>?@[\\]^_`{|}~\" {default: uld}" % sys.argv[0]) 66 | sys.exit(0) 67 | 68 | def main(): 69 | if len(sys.argv)>4 or 'h' in sys.argv or '--help' in sys.argv: 70 | usage() 71 | 72 | if len(sys.argv)==2: 73 | if sys.argv[1]=='s': 74 | print("all symbols:", symbols) 75 | return 76 | 77 | size = 0 78 | raw = sys.stdin.buffer.read(64) 79 | syms = symbols 80 | rule = '' 81 | 82 | for arg in sys.argv[1:]: 83 | try: 84 | size = int(arg) 85 | continue 86 | except ValueError: pass 87 | # a symbol set specification? 88 | if set(arg) - set(symbols) == set(): 89 | syms = set(arg) 90 | elif set(arg) - set("uld") == set(): 91 | rule = arg 92 | elif len(arg) == 0: 93 | syms = '' 94 | else: 95 | usage() 96 | 97 | if size<0: 98 | print("error size must be < 0") 99 | usage() 100 | 101 | print(derive(raw,rule,size,syms)) 102 | 103 | if __name__ == '__main__': 104 | main() 105 | -------------------------------------------------------------------------------- /contrib/sphinx-x11: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | #set -x 4 | 5 | script="$1" 6 | user="$2" 7 | host="$3" 8 | 9 | MOUSEID=$(xinput --list --short | grep -F "Virtual core pointer" | sed 's/.*id=\([0-9]*\).*/\1/') 10 | 11 | x11_type() { 12 | xdotool type --clearmodifiers -- "$1" 13 | } 14 | 15 | wfc() { 16 | # wait until left mouse click 17 | THIS=$(exec sh -c 'echo "$PPID"') 18 | xinput --test-xi2 --root "$MOUSEID" | while true; do 19 | read -t 1 line || continue 20 | echo "$line" | /bin/grep -qs '^EVENT type 16 (RawButtonRelease)$' && { 21 | read -t 1 line 22 | read -t 1 line 23 | read -t 1 details; 24 | echo "$details" | /bin/grep -qs '^\s*detail: 1$' && { 25 | pkill -9 -e -P "$THIS" xinput >/dev/null 26 | break 27 | } 28 | } 29 | done 2>/dev/null 30 | } 31 | 32 | wait_for_click() { 33 | # wrapping wfc so that when xinput is killed the message "KILLED" is suppressed 34 | wfc >/dev/null 2>&1 35 | } 36 | 37 | getpwd() { 38 | prompt=${1:-sphinx} 39 | printf "SETTITLE sphinx password prompt\nSETPROMPT %s password\nGETPIN\n" "${prompt}" | pinentry | grep '^D' | cut -c3- | tr -d '\n' 40 | } 41 | 42 | pwd() { 43 | getpwd "$user@$host" | { sphinx get "$user" "$host" || return ; } | xdotool type --clearmodifiers "$(head -1)" 44 | } 45 | 46 | otp() { 47 | getpwd "$user@$host" | { sphinx get "otp://$user" "$host" || return ; } | xdotool type --clearmodifiers "$(head -1)" 48 | } 49 | 50 | tab() { 51 | xdotool key --clearmodifiers Tab 52 | } 53 | 54 | enter() { 55 | xdotool key --clearmodifiers enter 56 | } 57 | 58 | xdoget() { 59 | title="$1" 60 | shift 1 61 | printf '' | /usr/bin/xclip -i 62 | sleep 0.2 63 | /usr/bin/xdotool key --window "$windowid" "$@" 64 | retries=0 65 | while [ $retries -lt 3 ]; do 66 | sleep 0.2 67 | x=$(/usr/bin/xclip -o) 68 | printf "%s" "$x" | /bin/grep -Eqs '^https?:.*' && { 69 | echo "$x" | cut -d'/' -f3 70 | break 71 | } 72 | retries=$((retries+1)) 73 | done 74 | #[ $retries -ge 3 ] && { echo "failed to get host" >&2 ; false; } 75 | } 76 | 77 | gethost() { 78 | wait_for_click 79 | windowid=$(/usr/bin/xdotool getactivewindow) 80 | title=$(/usr/bin/xdotool getwindowname "$windowid" | /bin/sed -e 's/^ *//g;s/ *$//g') 81 | case "$title" in 82 | #*Pentadactyl|*Vimperator) host="$(xdoget "$title" Escape y)";; 83 | *Iceweasel|*Firefox) host="$(xdoget "$title" Escape ctrl+l ctrl+a ctrl+c Escape Tab)";; 84 | *Chromium) host="$(xdoget "$title" Escape ctrl+l ctrl+a ctrl+c Escape Tab)";; 85 | #*Uzbl\ browser*) host="$(xdoget "title" Escape y u)";; 86 | #luakit*) host="$(xdoget "title" shift+o Home ctrl+Right Right ctrl+shift+End ctrl+c Escape)";; 87 | esac 88 | #echo "$host" 89 | } 90 | 91 | getuser() { 92 | [ -z "$host" ] && { echo "no host" >&2; false; } 93 | users=$(sphinx list "$host") 94 | [ "$(echo "$users" | wc -l)" -gt 1 ] && user=$(echo $users | dmenu -p username) || user=$users 95 | #echo "$user" 96 | } 97 | 98 | cat "$script" | while read -r line; do 99 | case "$line" in 100 | type\ *) x11_type "${line##type }";; 101 | wait-for-click) wait_for_click;; 102 | user) x11_type "$user";; 103 | host) x11_type "$host";; 104 | pwd) pwd;; 105 | otp) otp;; 106 | tab) tab;; 107 | enter) enter;; 108 | gethost) gethost;; 109 | getuser) getuser;; 110 | esac 111 | done 112 | -------------------------------------------------------------------------------- /GettingStarted.md: -------------------------------------------------------------------------------- 1 | # Getting Started with SPHINX 2 | 3 | So you want to start using SPHINX for handling your passwords. Great, welcome! 4 | 5 | SPHINX is a cryptographic password storage protocol that uses client-server architecture. Unlike traditional password managers that encrypt a database file, SPHINX servers store only cryptographic blobs that are mathematically useless without your master password. Even if compromised, your actual passwords remain secure with information-theoretic security guarantees. 6 | 7 | **Note:** To use SPHINX, your client needs to be able to connect to the oracle. 8 | 9 | ## Quick Start 10 | 11 | ### 1. Install SPHINX Client 12 | 13 | **Debian/Ubuntu and derivatives:** 14 | 15 | ```bash 16 | sudo apt install pwdsphinx 17 | # Optional: for browser extensions and X11 integration 18 | sudo apt install pinentry-gtk2 xdotool xinput 19 | ``` 20 | 21 | **Other systems:** See the **[installation guide](https://sphinx.pm/client_install.html)** for building from source and dependencies. 22 | 23 | ### 2. Initialize Your Client 24 | 25 | ```bash 26 | sphinx init 27 | ``` 28 | 29 | This creates your master key (`~/.sphinx/masterkey`) - **BACK THIS UP!** It also automatically sets up browser extension hosts if `~/.mozilla` or `~/.config/chromium` directories are found. 30 | 31 | For Android devices, export your config using `sphinx qr key`. This creates a QR code that can be read by the [androsphinx](https://github.com/dnet/androsphinx) Android app, allowing the app to use the same configuration as you have set up above. 32 | 33 | ### 3. Configure a Server 34 | 35 | Edit `~/.sphinxrc`: 36 | 37 | ```ini 38 | [servers] 39 | [server.server-name] 40 | address=your.sphinx-server.tld 41 | port=443 42 | ``` 43 | 44 | **Need a server?** 45 | 46 | - **Use existing server:** See **[public servers](https://sphinx.pm/servers.html)** 47 | - **Host your own:** Follow the **[server setup guide](https://sphinx.pm/server_install.html)** 48 | 49 | ### 4. Test Your Setup 50 | 51 | ```bash 52 | # Create a test password 53 | echo -n "testpassword" | sphinx create testuser example.com 54 | 55 | # Retrieve it (use getpwd for security) 56 | getpwd | sphinx get testuser example.com 57 | 58 | # Clean up 59 | sphinx delete testuser example.com 60 | 61 | # Verify it's gone 62 | echo -n "testpassword" | sphinx get testuser example.com # Should error 63 | ``` 64 | 65 | ## What's Next? 66 | 67 | - **[Complete Usage Guide](man/sphinx.md)**: All operations and options 68 | - **[Browser Extensions](#browser-extensions)**: Seamless web login 69 | - **[X11 Integration](https://sphinx.pm/x11-integration.html)**: Desktop automation 70 | - **[Server Hosting](https://sphinx.pm/server_install.html)**: Run your own oracle 71 | 72 | ## Browser Extensions 73 | 74 | SPHINX provides browser extensions for Firefox and Chrome/Chromium 75 | that enable seamless password filling on websites. 76 | 77 | websphinx consists of two parts: the frontend (which is the add-on to install from 78 | the browser extension store) and the backend (which handles everything). 79 | The backend is actually a native messaging host that communicates with the browser extension. 80 | The native messaging host is auto-configured by `sphinx init`. 81 | 82 | ### Prerequisites 83 | 84 | Install pinentry for secure password input: 85 | 86 | ```bash 87 | # Choose one appropriate for your desktop environment: 88 | sudo apt install pinentry-gtk2 # GTK/GNOME 89 | sudo apt install pinentry-qt # KDE/Qt 90 | sudo apt install pinentry-gnome3 # Modern GNOME 91 | sudo apt-get install pinentry-fltk # Lightweight option 92 | ``` 93 | 94 | ### Firefox Extension 95 | 96 | 1. **Install from [Firefox Add-ons Store](https://github.com/stef/pwdsphinx/releases/tag/v2.0.0)** 97 | 2. **Configure pinentry** (if not using default `/usr/bin/pinentry`): 98 | 99 | Add to `~/.sphinxrc`: 100 | 101 | ```ini 102 | [websphinx] 103 | pinentry=/usr/bin/pinentry-gtk-2 104 | ``` 105 | 106 | 3. **Restart Firefox** and enjoy! 107 | 108 | ### Chrome/Chromium Extension 109 | 110 | 1. **Download** from [websphinx-chrom repository](https://github.com/stef/websphinx-chrom) 111 | 2. **Install in Developer Mode:** 112 | - Open `chrome://extensions` 113 | - Enable "Developer mode" 114 | - Click "Load unpacked extension" 115 | - Select the downloaded directory 116 | 3. **Configure pinentry** (same as Firefox above) 117 | 4. **Restart browser** and enjoy! 118 | -------------------------------------------------------------------------------- /pwdsphinx/converters/ssh-ed25519.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys, base64, pysodium, binascii, struct 4 | from pwdsphinx.consts import * 5 | 6 | # usage 7 | # create key and save pubkey 8 | # getpwd | env/bin/sphinx create ssh-ed25519://test asdf | pipe2tmpfile ssh-keygen -e -f @@keyfile@@ >pubkey 9 | # sign file 10 | # getpwd | env/bin/sphinx get ssh-ed25519://test asdf | pipe2tmpfile ssh-keygen -Y sign -n file -f @@keyfile@@ content.txt > content.txt.sig 11 | # verify file with pubkey 12 | # ssh-keygen -Y check-novalidate -n file -f /tmp/ssh-ed.pubkey -s /tmp/content.txt.sig I", len(comment)) + comment 24 | 25 | secrethalf = ( 26 | binascii.unhexlify("a2224bbaa2224bba" # iv/salt? (Not sure about these 8 bytes) 27 | # Here's a repeat of the public key (part of the private key pair) 28 | "0000000b" # int length = 11 29 | "7373682d65643235353139" # string key type = ssh-ed25519 30 | "00000020") + # int length = 32 31 | # public key payload 32 bytes 32 | # probably encoding a point on the ed25519 curve 33 | pk + 34 | binascii.unhexlify("00000040") + # int length = 64 35 | # 32 bytes private key payload 1 36 | sk + # really sk[32] + pk[32] 37 | comment) # int length + comment as string 38 | # padding 3 bytes incrementing integers, pads to blocksize 8, starts with "remaining payload" 39 | padding = bytes(i+1 for i in range(-len(secrethalf)%8)) 40 | secrethalf = secrethalf + padding 41 | assert len(secrethalf) % 8 == 0 42 | 43 | raw = (binascii.unhexlify("6f70656e7373682d6b65792d763100" # ASCII magic "openssh-key-v1" plus null byte 44 | "00000004" # int length = 4 45 | "6e6f6e65" #string cipher = none 46 | "00000004" # int length = 4 47 | "6e6f6e65" # string kdfname = none 48 | "00000000" # int length = 0 49 | # zero-length kdfoptions placeholder here 50 | "00000001" # int number of public keys = 1 51 | "00000033" # int length first public key = 51 (4 + 11 + 4 + 32) 52 | "0000000b" # int length = 11 53 | "7373682d65643235353139" # string key type = ssh-ed25519 54 | "00000020") + # int length = 32 55 | # public key payload 32 bytes 56 | # probably encoding a point on the ed25519 curve 57 | pk + 58 | struct.pack(">I", len(secrethalf)) + # int length = 144 size of remaining payload 59 | # 8 + 4 + 11 + 4 + 32 + 4 + 64 + 4 + {10 + 3} 60 | secrethalf) 61 | 62 | return ("-----BEGIN OPENSSH PRIVATE KEY-----\n" + 63 | '\n'.join(''.join(l) for l in split_by_n(base64.b64encode(raw).decode('utf8'), 70)) + 64 | "\n-----END OPENSSH PRIVATE KEY-----") 65 | 66 | def pubkey(rwd, user, host): 67 | seed=rwd[:32] 68 | pk,sk=pysodium.crypto_sign_seed_keypair(seed) 69 | 70 | raw = (binascii.unhexlify("0000000b" # int length = 11 71 | "7373682d65643235353139" # string key type = ssh-ed25519 72 | "00000020") + # int length = 32 73 | pk) 74 | 75 | return f"ssh-ed25519 {base64.b64encode(raw).decode('utf8')} {user}@{host}" 76 | 77 | def convert(rwd, user, host, op, *opts): 78 | if op in {CREATE, CHANGE}: 79 | return pubkey(rwd, user, host) 80 | return privkey(rwd,user, host) 81 | 82 | schema = {'ssh-ed25519': convert} 83 | -------------------------------------------------------------------------------- /contrib/README.md: -------------------------------------------------------------------------------- 1 | # SPHINX X11 Integration Tools 2 | 3 | This directory contains tools that can be used on their own, or in concert to interact with pwdsphinx on X11 desktops. SPHINX is not your average legacy consumer-grade password manager - its CLI is powerful, but its X11 integration is what makes it truly efficient for daily use on Linux desktops and laptops. 4 | 5 | The tools are designed with security in mind, ensuring that passwords never appear in process lists, command history, or the clipboard where malware could intercept them. Instead, they use secure input methods and direct keyboard injection via `xdotool` and `xinput` to automate password entry and multi-factor authentication seamlessly. 6 | 7 | These tools build on SPHINX's core operations (`create`, `get`, `change`, `commit`, `undo`, `list`, `delete`) and alternate converters (`otp://`, `age://`, `minisig://`, `ssh-ed25519://`, `raw://`) to provide a complete desktop integration experience. 8 | 9 | For detailed technical documentation of the scripting language and advanced usage, see the [X11 integration documentation](https://sphinx.pm/x11-integration.html). For core SPHINX CLI operations used by these tools, see [sphinx(1)](../man/sphinx.md). 10 | 11 | ## Tools Reference 12 | 13 | ### getpwd (depends on pinentry) 14 | 15 | This is a simple script that uses `pinentry` from the GnuPG project to securely query a password and write it to standard output. This should be safer than echoing a password into pwdsphinx, since your password will not show up in your process list nor your command line history. 16 | 17 | See [`getpwd(1)`](../man/getpwd.md) for usage details. 18 | 19 | ### exec-on-click (depends on xinput) 20 | 21 | This is a simple shell script that depends on `xinput`, which waits for a left mouse click and then executes the specified command. 22 | 23 | See [`exec-on-click(1)`](../man/exec-on-click.md) for usage details. 24 | 25 | ### type-pwd (depends on xdotool, exec-on-click and getpwd) 26 | 27 | This script combines `getpwd`, `exec-on-click`, and the pwdsphinx client to create a secure password entry workflow. It prompts for your master password, waits for you to click on a password field, then types the password as keystrokes. This approach ensures passwords never appear in the clipboard where malware could intercept them. 28 | 29 | See [`type-pwd(1)`](../man/type-pwd.md) for usage details. 30 | 31 | ### dmenu-sphinx (depends on dmenu, type-pwd) 32 | 33 | This tool provides an interactive interface for retrieving SPHINX passwords using `dmenu` to present hostname and username selection menus. It builds on `type-pwd` for secure password entry with hostname history caching. 34 | 35 | See [`dmenu-sphinx(1)`](../man/dmenu-sphinx.md) for usage details. 36 | 37 | ### pipe2tmpfile 38 | 39 | This tool bridges commands that output to stdout with commands requiring file input. It reads data from stdin, writes it to a secure temporary file, executes a command with `@@keyfile@@` replaced by the temp file path, then automatically cleans up. 40 | 41 | See [`pipe2tmpfile(1)`](../man/pipe2tmpfile.md) for usage details and security considerations. 42 | 43 | ### sphinx-x11 44 | 45 | This script language interpreter integrates the SPHINX CLI with X11 using a domain-specific language (DSL) for automating password entry and multi-factor authentication. It includes example scripts for various login workflows including 2FA support. 46 | 47 | See [`sphinx-x11(1)`](../man/sphinx-x11.md) for script language vocabulary, examples, and usage details. 48 | 49 | ## Customization and Usage 50 | 51 | You can create your own scripts for different sites and workflows using the scripting language, and bind them to keyboard shortcuts for even faster access. This approach keeps your passwords out of the clipboard and leverages X11 automation for secure, efficient logins. 52 | 53 | If you prefer browser integration, SPHINX also [provides web extensions for Firefox and Chrome-based browsers](../GettingStarted.md#setting-up-browser-extensions). However, always be cautious with browser extensions and review their security implications. 54 | 55 | ## See Also 56 | 57 | **Manual Pages:** [`getpwd(1)`](../man/getpwd.md), [`exec-on-click(1)`](../man/exec-on-click.md), [`type-pwd(1)`](../man/type-pwd.md), [`dmenu-sphinx(1)`](../man/dmenu-sphinx.md), [`pipe2tmpfile(1)`](../man/pipe2tmpfile.md), [`sphinx-x11(1)`](../man/sphinx-x11.md), [`websphinx(1)`](../man/websphinx.md) 58 | 59 | **[Technical Reference](https://sphinx.pm/x11-integration.html):** Complete scripting language vocabulary, advanced usage patterns, and detailed script analysis 60 | 61 | **[OPAQUE-Store Integration](https://sphinx.pm/opaque-store_integration.html):** Encrypted storage for keys and secrets beyond password generation 62 | -------------------------------------------------------------------------------- /sphinx.cfg_sample: -------------------------------------------------------------------------------- 1 | # the client section is only needed if you use the client functionality 2 | [client] 3 | # whether to produce some output on the console 4 | #verbose = False 5 | 6 | # the directory where the client stores its master secret - you might want to 7 | # back this up 8 | #datadir = ~/.config/sphinx 9 | 10 | # master password optional for authentication, if it is False it protects 11 | # against offline master pwd bruteforce attacks. The drawback is that for known 12 | # (host,username) tuples the seeds/blobs can be changed/deleted by an attacker 13 | # if the client masterkey is known 14 | #rwd_keys=False 15 | 16 | # stores a check digit of 5 bits in the rule blob, this helps to notice most 17 | # typos of the master password, while decreasing security slightly 18 | #validate_password=True 19 | 20 | # userlist enables the maintenance of an encrypted blob of all records 21 | # belonging to the same sphinx user (defined by their masterkey) and 22 | # hostname. This enables the usage of the `list` command to the client. 23 | # if you disable this, you have to remember your usernames in other ways. 24 | #userlist=True 25 | 26 | # if you have still v1 passwords on the server, they get automatically upgraded 27 | # to v2 records. If you don't have any clients that can only do v1, then it is 28 | # safe and nice to delete the old v1 passwords automatically. If you use for 29 | # example androsphinx android client, it only supports v1, so you don't want to 30 | # delete the v1 records. Default is false. 31 | # delete_upgraded = false 32 | 33 | # the threshold - must specify at least this many servers in the 34 | # [servers] section 35 | # threshold = 3 36 | 37 | # if you still need to consult a v1 server 38 | # address = "127.0.0.1" 39 | # port = 2355 40 | 41 | # the servers used by the client 42 | [servers] 43 | # you need at least one server. the name is freely chosen (in this case it is 44 | # "first", but should not change, unless you want to lose access to your 45 | # existing passwords. 46 | [servers.first] 47 | # the ip address of the server 48 | host="127.0.0.1" 49 | # the port where the server is running, 443 is nice to punch through firewalls. 50 | port=443 51 | # the long term signature key of the server. 52 | ltsigkey="32byteBase64EncodedValue==" 53 | # or alternatively if you want to store the raw binary public key in a file 54 | # ltsigkey_path = "path/to/ltsigkey.pub" 55 | 56 | # in case you want to use a threshold version of SPHINX you need at least 3 57 | # servers (and the threshold is then 2) 58 | #[servers.2nd] 59 | #host="127.0.0.1" 60 | #port=2355 61 | #ltsigkey="2nd.pub" 62 | # 63 | #[servers.3rd] 64 | #host="127.0.0.1" 65 | #port=5523 66 | #ltsigkey="3rd.pub" 67 | 68 | # the server section is only needed if you run the oracle yourself. 69 | [server] 70 | # the ipv4 address the server is listening on 71 | #address="127.0.0.1" 72 | 73 | # the port on which the server is listening, use 443 if available, so that 74 | # the oracle can be accessed from behind tight firewalls 75 | #port=2355 76 | 77 | # ssl key - no default must be specified 78 | ssl_key="key.pem" 79 | 80 | # ssl cert - no default must be specified 81 | ssl_cert="cert.pem" 82 | 83 | # tcp connection timeouts, increase in case you have bad networks, with the 84 | # caveat that this might lead to easier resource exhaustion - blocking all 85 | # workers. 86 | #timeout=3 87 | 88 | # how many worker processes can run in parallel 89 | # max_kids=5 90 | 91 | # the root directory where all data is stored 92 | #datadir= "/var/lib/sphinx" 93 | 94 | # whether to produce some output on the console 95 | #verbose=false 96 | 97 | # decay ratelimit after rl_decay seconds 98 | #rl_decay= 1800 99 | 100 | # increase hardness after rl_threshold attempts if not decaying 101 | #rl_threshold= 1 102 | 103 | # when checking freshness of puzzle solution, allow this extra 104 | # gracetime in addition to the hardness max solution time 105 | #rl_gracetime=10 106 | 107 | # a path pointing at a long-term signing key. If this file doesn't exist, 108 | # you can generate it by running `oracle init`, it will also generate a public 109 | # key, which all your clients need to put in their ltsigkey configuration 110 | # variable. 111 | ltsigkey="ltsig.key" 112 | 113 | # the websphinx section is only needed if you use the browser webextensions 114 | [websphinx] 115 | # the path of your pinentry program 116 | pinentry=/usr/bin/pinentry 117 | 118 | # a file where websphinx logs, this is only for dev/debug purposes 119 | log= 120 | 121 | # in case you use webauthn, we need to store mappings between webauthn user ids 122 | # and webauthn public keys. it's lame, but no way around it. back up this 123 | # directory and sync it to other hosts where you want to use the webextension 124 | # with the same webauthn accounts. 125 | webauthn_data_dir = "path/to/webauthn/data/dir" 126 | -------------------------------------------------------------------------------- /man/oracle.md: -------------------------------------------------------------------------------- 1 | % oracle(1) | server for the SPHINX password manager 2 | 3 | # NAME 4 | 5 | oracle - server for the SPHINX password manager 6 | 7 | # SYNOPSIS 8 | 9 | `oracle [init]` 10 | 11 | # DESCRIPTION 12 | 13 | The SPHINX protocol only makes sense if the server (called *oracle*) is located somewhere other than where you type your password. pwdsphinx comes with a server implemented in Python 3, which you can host off-site from your usual desktop or smartphone. For production deployments, [**zphinx-zerver**](https://github.com/stef/zphinx-zerver) is recommended over pwdsphinx. zphinx-zerver is a production-grade server implementation written in Zig that offers better reliability for hosting SPHINX services. 14 | 15 | The server can be started simply by running `oracle`. It does not take any parameters. 16 | 17 | # CONFIGURATION 18 | 19 | The server can be configured using any of the following files: 20 | 21 | - `/etc/sphinx/config` 22 | - `~/.sphinxrc` 23 | - `~/.config/sphinx/config` 24 | - `./sphinx.cfg` 25 | 26 | Files are parsed in the order listed above, so global settings can be overridden by per-user and per-directory settings. 27 | 28 | Configuration is done by editing variables in the `[server]` section of the configuration file. 29 | 30 | - `address`: Determines on what address the server is listening. The default is `localhost`: you might want to change that to a specific IP address. 31 | - `port`: Sets the port the server is listening on. The default is `2355`. Another recommended port value is `443`, which is allowed by most firewalls, while `2355` is not. 32 | - `ssl_key`, `ssl_cert`: Required. Have no defaults, and must be set to point at a traditional TLS certificate and secret key file. It is recommended to not use self-signed certs, but CA-signed certs that are recognized widely by browsers and other TLS clients when possible. 33 | - `datadir`: The data directory where all the device "secrets" are stored. This defaults to `data/` in the current directory. Backup this directory regularly and securely, since the loss of this directory means users lose access to their passwords. 34 | - `verbose`: Enables logging to standard output. 35 | - `timeout`: Sets the TCP connection timeout. Increase for slow networks, with the caveat that this might lead to easier resource exhaustion, by blocking all workers. 36 | - `max_kids`: Sets the maximum number of requests handled in parallel. The `timeout` config variable makes sure that all handlers are recycled in predictable time. 37 | - `rl_decay`: Specifies the number of seconds after which a rate-limit level decays to an easier difficulty. Together with `rl_threshold` and `rl_gracetime`, these params are used to configure rate limiting. 38 | - `rl_threshold`: Configures the number of failed attempts before increasing the difficulty level 39 | - `rl_gracetime`: Sets the number of additional seconds allowed - beyond the max solution time fixed for a certain difficulty - before a rate-limiting puzzle expires. 40 | - `ltsigkey`: Sets the path to the long-term signature private key. You can generate one by running `oracle init`. This will also create a public key and its Base64 encoded variant, which should be published to all potential users so that they can use your oracle in a threshold setup. 41 | 42 | # INITIALIZING AN ORACLE 43 | 44 | Given a configuration, the oracle can generate its own long-term signature key by running: 45 | 46 | ``` 47 | oracle init 48 | ``` 49 | 50 | This stores the private key at the location specified by `ltsigkey` and outputs the corresponding public key at the same location, with a `.pub` extension. The public key is also displayed as a Base64-encoded string on standard output. 51 | 52 | # SECURITY CONSIDERATIONS 53 | 54 | The `max_kids` and `timeout` settings can be used to control how many requests are served in parallel and how long each request can run. Without careful tuning, an attacker could launch a denial-of-service attack by keeping all `max_kids` connections busy. 55 | 56 | Since the server only knows about failed authorizations for management operations (not incorrect master passwords for `get` requests), brute-force attempts can only be mitigated via rate limiting. Adjusting `rl_*` parameters allows you to make puzzles more difficult. On devices with less than 1GB RAM, you can increase the difficulty enough that they cannot solve the puzzles. 57 | 58 | Rate limiting in general should not be noticeable, unless dozens of `get` requests are made to the same record. At the highest difficulty level, solving should take around 20–40 seconds, depending on CPU performance. 59 | 60 | # REPORTING BUGS 61 | 62 | https://github.com/stef/pwdsphinx/issues/ 63 | 64 | # AUTHOR 65 | 66 | Written by Stefan Marsiske. 67 | 68 | # COPYRIGHT 69 | 70 | Copyright © 2024 Stefan Marsiske. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. 71 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 72 | 73 | # SEE ALSO 74 | 75 | `sphinx(1)`, `getpwd(1)` 76 | -------------------------------------------------------------------------------- /man/sphinx-x11.md: -------------------------------------------------------------------------------- 1 | % sphinx-x11(1) | simple script interpreter for integrating password managers with X11 2 | 3 | # NAME 4 | 5 | sphinx-x11 — simple script interpreter for integrating password managers with X11 6 | 7 | # DESCRIPTION 8 | 9 | `sphinx-x11(1)` is a simple "script" language interpreter that 10 | integrates the SPHINX CLI with X11. 11 | 12 | # SPHINX-SCRIPT PARAMETERS 13 | 14 | All `sphinx-x11(1)` scripts expect a username and a hostname as the 15 | first and second parameter respectively. 16 | 17 | # VOCABULARY 18 | 19 | - `type "text..."`: Types the given text into the currently focused X11 window 20 | - `wait-for-click`: Waits until the user clicks anywhere 21 | - `user`: Types the username (usually given as the first parameter to the sphinx script) into the currently focused X11 window 22 | - `host`: Types the hostname (usually given as the second script parameter) into the currently focused X11 window 23 | - `pwd`: Gets a password via `getpwd(1)` and `sphinx(1)`, then types it into the currently focused X11 window 24 | - `otp`: Calculates the current Time-based One-Time Password (TOTP) pin code using an OTP secret stored in `sphinx(1)` using `getpwd(1)`, then types it into the currently focused X11 window. 25 | - `tab`: Types a tab character into the currently focused X11 window, often moving between form fields. 26 | - `enter`: Sends an Enter key press into the focused X11 window, usually submitting a form 27 | - `gethost`: Waits for a left mouse click on a browser window, copies the URL from the address bar, extracts the hostname, and stores it in the internal `$host` variable for use with `host` or `pwd` defined above. 28 | - `getuser`: Runs `sphinx list $host`. If multiple users are found, it presents them in a dmenu widget. If/when one user is found/selected, it is set as an internal `$user` variable which can then be used with `user` or `pwd` defined above. 29 | 30 | Any lines not consisting of these tokens are simply ignored. 31 | 32 | # OTP SUPPORT 33 | 34 | In this implementation, a TOTP value is stored with a username prefixed by `otp://` so that a regular username can co-exist with its TOTP secret in SPHINX. 35 | 36 | For example, in a common two-factor authentication (2FA) login, the first `pwd` operation might use `joe` as the username, and the TOTP value would be retrieved with `otp://joe` as the username, which allows for seamless 2FA login. 37 | 38 | # DEFAULT SCRIPTS 39 | 40 | `sphinx-x11(1)` ships with five default scripts. On Debian-based systems, these use a `sx11-` prefix instead of the `.sphinx` extension. 41 | 42 | - **pass.sphinx **: Gets a password using `sphinx(1)`, types it, and submits it. 43 | - **user-pass.sphinx **: Gets a password using `sphinx(1)`, types the username, and then submits it. 44 | - **user-pass-otp.sphinx **: Gets a password, and a TOTP pin code using `sphinx(1)`, types the username, the password, then submits the form, and finally enters the TOTP pin and submits again. 45 | - **otp.sphinx **: Gets a TOTP pin using `sphinx(1)` and types and submits it. 46 | - **getacc-user-pass.sphinx**: Waits for a click on a browser window, from which it gets the target `host`. It uses this together with `sphinx list` to lists users associated with the host. Then, it waits for another click in the username input field of a login form, gets a password using `sphinx(1)`, types the username, password, and submits. This script is convenient but carries phishing risks if a malicious site manipulates the clipboard. Use this script very carefully. At the moment, this security problem is not fixed since there is no simple way to get the current tab's URL from a browser securely via a web extension. 47 | 48 | All of these scripts wait for user interaction before retrieving 49 | passwords (and/or TOTP tokens) and entering them, navigating with 50 | `tab` and `enter`. You are welcome to contribute adapted sphinx 51 | scripts for websites that have other login semantics. 52 | 53 | # EXAMPLE 54 | 55 | The following example demonstrates the `user-pass-otp.sphinx` script: 56 | 57 | ``` 58 | #!sphinx-x11 59 | 60 | wait-for-click 61 | user 62 | tab 63 | pwd 64 | tab 65 | enter 66 | wait-for-click 67 | otp 68 | enter 69 | ``` 70 | 71 | **Explanation:** 72 | 73 | - Line 1: Specifies `sphinx-x11(1)` as the script interpreter. 74 | - Line 3: Waits for the user to click. 75 | - Line 4: Types the username (first script parameter). 76 | - Line 5: Sends a `tab` key to move to the next form field. 77 | - Line 6: Retrieves and types the password for the specified `user` and `host` (second script parameter). 78 | - Line 7: Sends another `tab` key to move focus. 79 | - Line 8: Presses `enter` to submit the form. 80 | - Line 9: Waits for the user to click in the TOTP input field of the next form. 81 | - Line 10: Retrieves the TOTP value via `pwdsphinx/getpwd` and types it. 82 | - Line 11: Presses `enter` to submit the TOTP form. 83 | 84 | # REPORTING BUGS 85 | 86 | https://github.com/stef/pwdsphinx/issues/ 87 | 88 | # AUTHOR 89 | 90 | Written by Stefan Marsiske. 91 | 92 | # COPYRIGHT 93 | 94 | Copyright © 2023 Stefan Marsiske. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. 95 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 96 | 97 | # SEE ALSO 98 | 99 | `sphinx(1)`, `type-pwd(1)`, `exec-on-click(1)`, `getpwd(1)` 100 | -------------------------------------------------------------------------------- /pwdsphinx/ostore.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os, subprocess 4 | from tempfile import mkstemp 5 | try: 6 | from opaquestore import client 7 | available = True 8 | except: 9 | client = None 10 | available = False 11 | 12 | if available: 13 | client.config = client.processcfg(client.getcfg('opaque-store')) 14 | 15 | def usage(params): 16 | print("\nOPAQUE Store style blobs") 17 | print(" echo -n 'password' | %s store file-to-store" % params[0]) 18 | print(" echo -n 'password' | %s read " % params[0]) 19 | print(" echo -n 'password' | %s replace [force] file-to-store" % params[0]) 20 | print(" echo -n 'password' | %s edit [force] " % params[0]) 21 | print(" echo -n 'password' | %s changepwd [force] " % params[0]) 22 | print(" echo -n 'password' | %s erase [force] " % params[0]) 23 | print(" echo -n 'password' | %s recovery-tokens " % params[0]) 24 | print(" echo -n 'password' | %s unlock " % params[0]) 25 | 26 | def is_cmd(params): 27 | if params[1] not in cmds: return False 28 | return True 29 | 30 | def connect(): 31 | s = client.Multiplexer(client.config['servers']) 32 | s.connect() 33 | return s 34 | 35 | def store(pwd, keyid, path): 36 | # create & recovery-tokens 37 | with open(path,'r') as fd: 38 | data = fd.read() 39 | with connect() as s: 40 | client.create(s, pwd, keyid.encode('utf8'), data.encode('utf8')) 41 | with connect() as s: 42 | token = client.get_recovery_tokens(s, pwd, keyid.encode('utf8')) 43 | print("successfully created opaque store record. Store the following recovery token, in case this record is locked") 44 | print(token) 45 | 46 | def read(pwd, keyid): 47 | with connect() as s: 48 | print(client.get(s, pwd, keyid.encode('utf8'))) 49 | 50 | def replace(pwd, keyid, path, force=False): 51 | with open(path,'r') as fd: 52 | data = fd.read() 53 | with connect() as s: 54 | client.update(s, pwd, keyid.encode('utf8'), data.encode('utf8'), force) 55 | 56 | def erase(pwd, keyid, ctx, force=False): 57 | with connect() as s: 58 | client.delete(s, pwd, keyid.encode('utf8'), force) 59 | # also handle delete sphinx record in case ostore.erase 60 | m = ctx['m'](ctx['servers']) 61 | m.connect() 62 | try: 63 | ctx['delete'](m, ctx['pwd'], ctx['user'], ctx['host']) 64 | finally: 65 | m.close() 66 | 67 | def changepwd(pwd, keyid, ctx, force=False): 68 | m = ctx['m'](ctx['servers']) 69 | m.connect() 70 | try: 71 | newpwd = ctx['change'](m, ctx['pwd'], ctx['newpwd'], ctx['user'], ctx['host']) 72 | finally: 73 | m.close() 74 | 75 | with connect() as s: 76 | data = client.get(s, pwd, keyid.encode('utf8')) 77 | 78 | with connect() as s: 79 | client.delete(s, pwd, keyid.encode('utf8'), force) 80 | 81 | with connect() as s: 82 | client.create(s, newpwd, keyid.encode('utf8'), data.encode('utf8')) 83 | 84 | with connect() as s: 85 | token = client.get_recovery_tokens(s, newpwd, keyid.encode('utf8')) 86 | 87 | m = ctx['m'](ctx['servers']) 88 | m.connect() 89 | try: 90 | ctx['commit'](m, ctx['pwd'], ctx['user'], ctx['host']) 91 | finally: 92 | m.close() 93 | print("Store the following recovery token, in case this record is locked") 94 | print(token) 95 | 96 | def edit(pwd, keyid, force=False): 97 | if not os.path.exists('/dev/tty'): 98 | print("can only edit on systems that have /dev/tty, sorry") 99 | return False 100 | with connect() as s: 101 | data = client.get(s, pwd, keyid.encode('utf8')) 102 | fd, fname = mkstemp() 103 | fd = os.fdopen(fd,'w') 104 | fd.write(data) 105 | fd.close() 106 | tty = os.open("/dev/tty", os.O_RDWR|os.O_LARGEFILE) 107 | subprocess.run([os.environ.get("EDITOR"), fname], stdin=tty, stdout=tty, stderr=tty) 108 | with open(fname,"r") as fd: 109 | data = fd.read() 110 | os.unlink(fname) 111 | with connect() as s: 112 | client.update(s, pwd, keyid.encode('utf8'), data.encode('utf8'), force) 113 | 114 | def recoverytoken(pwd, keyid): 115 | with connect() as s: 116 | token = client.get_recovery_tokens(s, pwd, keyid.encode('utf8')) 117 | print("Store the following recovery token, in case this record is locked") 118 | print(token) 119 | 120 | def unlock(pwd, keyid, token): 121 | # unlock + get 122 | with connect() as s: 123 | client.unlock(s, token, keyid.encode('utf8')) 124 | with connect() as s: 125 | print(client.get(s, pwd, keyid.encode('utf8'))) 126 | 127 | cmds = {'store': store, 128 | 'read': read, 129 | 'replace': replace, 130 | 'edit': edit, 131 | 'changepwd': changepwd, 132 | 'erase': erase, 133 | 'recovery-tokens': recoverytoken, 134 | 'unlock': unlock} 135 | 136 | def parse(params): 137 | if params[1] not in cmds: return False 138 | 139 | op = cmds[params[1]] 140 | args = [] 141 | 142 | if params[1] in {'replace', 'edit', 'erase', 'changepwd'} and params[2]=='force': 143 | del params[2] 144 | args.append(True) 145 | 146 | keyid=params[2] 147 | 148 | if params[1] in {'store', 'replace'}: 149 | if not os.path.isfile(params[3]): 150 | raise ValueError(f'opaque store parameter "{params[3]}" is not a file, how would i store it?') 151 | args.insert(0, params[3]) 152 | 153 | if params[1] == 'unlock': 154 | args.insert(0,params[3]) 155 | 156 | return op, keyid, args 157 | -------------------------------------------------------------------------------- /pwdsphinx/converters/sphage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: 2018-2021, Marsiske Stefan 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | # This file contains a copy of the reference implementation for 7 | # Bech32/Bech32m, which has this (c) notice: 8 | # 9 | # Copyright (c) 2017, 2020 Pieter Wuille 10 | # 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documentation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furnished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in 19 | # all copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | # THE SOFTWARE. 28 | 29 | import sys, base64, pysodium 30 | from enum import Enum 31 | from pwdsphinx.consts import * 32 | 33 | class Encoding(Enum): 34 | """Enumeration type to list the various supported encodings.""" 35 | BECH32 = 1 36 | BECH32M = 2 37 | 38 | CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" 39 | BECH32M_CONST = 0x2bc830a3 40 | 41 | def bech32_polymod(values): 42 | """Internal function that computes the Bech32 checksum.""" 43 | generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] 44 | chk = 1 45 | for value in values: 46 | top = chk >> 25 47 | chk = (chk & 0x1ffffff) << 5 ^ value 48 | for i in range(5): 49 | chk ^= generator[i] if ((top >> i) & 1) else 0 50 | return chk 51 | 52 | 53 | def bech32_hrp_expand(hrp): 54 | """Expand the HRP into values for checksum computation.""" 55 | return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] 56 | 57 | 58 | def bech32_verify_checksum(hrp, data): 59 | """Verify a checksum given HRP and converted data characters.""" 60 | const = bech32_polymod(bech32_hrp_expand(hrp) + data) 61 | if const == 1: 62 | return Encoding.BECH32 63 | if const == BECH32M_CONST: 64 | return Encoding.BECH32M 65 | return None 66 | 67 | def bech32_create_checksum(hrp, data, spec): 68 | """Compute the checksum values given HRP and data.""" 69 | values = bech32_hrp_expand(hrp) + data 70 | const = BECH32M_CONST if spec == Encoding.BECH32M else 1 71 | polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ const 72 | return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] 73 | 74 | 75 | def bech32_encode(hrp, data, spec=2): 76 | """Compute a Bech32 string given HRP and data values.""" 77 | combined = data + bech32_create_checksum(hrp, data, spec) 78 | return hrp + '1' + ''.join([CHARSET[d] for d in combined]) 79 | 80 | def bech32_decode(bech): 81 | """Validate a Bech32/Bech32m string, and determine HRP and data.""" 82 | if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or 83 | (bech.lower() != bech and bech.upper() != bech)): 84 | return (None, None, None) 85 | bech = bech.lower() 86 | pos = bech.rfind('1') 87 | if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: 88 | return (None, None, None) 89 | if not all(x in CHARSET for x in bech[pos+1:]): 90 | return (None, None, None) 91 | hrp = bech[:pos] 92 | data = [CHARSET.find(x) for x in bech[pos+1:]] 93 | spec = bech32_verify_checksum(hrp, data) 94 | if spec is None: 95 | return (None, None, None) 96 | return (hrp, data[:-6], spec) 97 | 98 | def convertbits(data, frombits, tobits, pad=True): 99 | """General power-of-2 base conversion.""" 100 | acc = 0 101 | bits = 0 102 | ret = [] 103 | maxv = (1 << tobits) - 1 104 | max_acc = (1 << (frombits + tobits - 1)) - 1 105 | for value in data: 106 | if value < 0 or (value >> frombits): 107 | return None 108 | acc = ((acc << frombits) | value) & max_acc 109 | bits += frombits 110 | while bits >= tobits: 111 | bits -= tobits 112 | ret.append((acc >> bits) & maxv) 113 | if pad: 114 | if bits: 115 | ret.append((acc << (tobits - bits)) & maxv) 116 | elif bits >= frombits or ((acc << (tobits - bits)) & maxv): 117 | return None 118 | return ret 119 | 120 | def decode(hrp, addr): 121 | hrpgot, data, spec = bech32_decode(addr) 122 | if hrpgot != hrp: 123 | return None 124 | decoded = convertbits(data, 5, 8, False) 125 | if decoded is None or len(decoded) < 2 or len(decoded) > 40: 126 | return None 127 | return bytes(decoded) 128 | 129 | def encode(hrp, data): 130 | ret = bech32_encode(hrp, convertbits(data, 8, 5)) 131 | if decode(hrp, ret) == None: 132 | return None 133 | return ret 134 | 135 | def convert(rwd, user, host, op, *opts): 136 | if op in {CREATE, CHANGE}: 137 | pk = pysodium.crypto_scalarmult_base(rwd[:32]) 138 | return encode("age", pk) 139 | return encode('age-secret-key-', rwd[:32]).upper() 140 | 141 | schema = {'age': convert} 142 | 143 | def usage(help=False): 144 | print("usage: sphage.py [] <[32 bytes of cryptographic entropy] >key") 145 | if help: sys.exit(0) 146 | sys.exit(1) 147 | 148 | def main(): 149 | if len(sys.argv)!=2: 150 | usage() 151 | op = sys.argv[1] 152 | if op in ('help', '-h', '--help'): 153 | usage(True) 154 | if op not in ('pubkey', 'privkey'): 155 | usage() 156 | 157 | rwd = sys.stdin.buffer.readline().rstrip(b'\n') 158 | if op == "pubkey": 159 | if rwd.startswith(b'AGE-SECRET-KEY-'): 160 | rwd = decode('age-secret-key-', rwd.decode('utf8')) 161 | pk = pysodium.crypto_scalarmult_base(rwd) 162 | ret = encode("age", pk) 163 | elif op == "privkey": 164 | ret = encode('age-secret-key-', rwd).upper() 165 | print(ret) 166 | 167 | if __name__ == '__main__': 168 | main() 169 | -------------------------------------------------------------------------------- /ext/webauthn.js: -------------------------------------------------------------------------------- 1 | // save original credential functions 2 | const browserCredentials = { 3 | create: navigator.credentials.create.bind(navigator.credentials), 4 | get: navigator.credentials.get.bind(navigator.credentials), 5 | }; 6 | 7 | 8 | // override credentials.create 9 | navigator.credentials.create = async function(args) { 10 | let options = args.publicKey; 11 | if(!options || !options.pubKeyCredParams) { 12 | // not webauthn call 13 | // TODO throw popop warning 14 | return await browserCredentials.create(options); 15 | } 16 | //algos: 17 | // -8: Ed25519 <- only supported 18 | // -7: ES256 19 | // -257: RS256 20 | let hasSupportedAlgo = false; 21 | for(let a of options.pubKeyCredParams) { 22 | if(a.alg == -8) { 23 | hasSupportedAlgo = true; 24 | break; 25 | } 26 | } 27 | if(!hasSupportedAlgo) { 28 | return await browserCredentials.create(options); 29 | } 30 | const host = window.location.hostname; 31 | options.challenge = arrayBufferToBase64(options.challenge); 32 | options["type"] = "webauthn.create"; 33 | options["origin"] = window.location.origin; 34 | // Required response fields 35 | const params = { 36 | 'challenge': options.challenge, 37 | 'username': options.user.name, 38 | 'userid': arrayBufferToBase64(options.user.id), 39 | 'clientDataJSON': JSON.stringify(options), 40 | }; 41 | let response = await createEvent("webauthn-create", params); 42 | response.clientDataJSON = JSON.stringify(options); 43 | let createObj = createCreateCredentialsResponse(response); 44 | return createObj; 45 | }; 46 | 47 | // override credentials.get 48 | navigator.credentials.get = async function(options) { 49 | if(!options && !options.publicKey) { 50 | // not webauthn call 51 | return await browserCredentials.get(options); 52 | } 53 | const pubKeyObj = options.publicKey; 54 | if(pubKeyObj['allowCredentials'].length == 0) { 55 | return await browserCredentials.get(options); 56 | } 57 | const key = pubKeyObj['allowCredentials'][0].id; 58 | options["type"] = "webauthn.get"; 59 | options["origin"] = window.location.origin; 60 | options["challenge"] = arrayBufferToBase64(pubKeyObj.challenge); 61 | // Required response fields 62 | const params = { 63 | 'pk': arrayBufferToBase64(key), 64 | 'challenge': arrayBufferToBase64(pubKeyObj.challenge), 65 | 'clientDataJSON': JSON.stringify(options), 66 | }; 67 | const response = await createEvent("webauthn-get", params); 68 | response.challenge = pubKeyObj.challenge; 69 | response.clientDataJSON = JSON.stringify(options); 70 | let getObj = createGetCredentialsResponse(response); 71 | return getObj; 72 | }; 73 | 74 | // initiate messaging with the backend 75 | async function createEvent(action, params) { 76 | let msg = { 77 | "type": "sphinxWebauthnEvent", 78 | "action": action, 79 | "params": params, 80 | }; 81 | const { port1: localPort, port2: remotePort } = new MessageChannel(); 82 | const promise = new Promise((resolve) => { 83 | localPort.onmessage = (event) => { 84 | resolve(event.data); 85 | } 86 | }); 87 | try { 88 | window.postMessage(msg, '*', [remotePort]); 89 | } catch(err) { 90 | console.log("Failed to send message to content script:", err); 91 | } 92 | let resp = await promise; 93 | return resp; 94 | } 95 | 96 | // create the response object of credentials.create 97 | function createCreateCredentialsResponse(res) { 98 | if(res.error) { 99 | return; 100 | } 101 | res.pk = res.pk.replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); 102 | const credential = { 103 | id: res.pk, 104 | rawId: stringToBuffer(res.pk, true), 105 | type: "public-key", 106 | authenticatorAttachment: "platform", 107 | response: { 108 | // TODO attestationObject https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAttestationResponse/attestationObject 109 | attestationObject: stringToBuffer(res.attestationObject, true), 110 | clientDataJSON: stringToBuffer(res.clientDataJSON), // A JSON string in an ArrayBuffer, representing the client data that was passed to CredentialsContainer.create() 111 | publicKeyAlgorithm: -8, 112 | transports: ["internal"], 113 | //getPublicKey: () => stringToBuffer(res.pk, true), 114 | //getPublicKeyAlgorithm: () => -8, 115 | //getTransport: () => "", 116 | //getAuthenticatorData: () => "", 117 | }, 118 | key: stringToBuffer(res.pk, true), 119 | getClientExtensionResults: () => {}, 120 | }; 121 | Object.setPrototypeOf(credential.response, AuthenticatorAttestationResponse.prototype); 122 | Object.setPrototypeOf(credential, PublicKeyCredential.prototype); 123 | credential.response.getTransports = () => credential.response.transports; 124 | credential.response.getPublicKey = () => credential.rawId; 125 | credential.response.getPublicKeyAlgorithm = () => -8; 126 | credential.response.getAuthenticatorData = () => stringToBuffer(res.authData, true); 127 | return credential; 128 | } 129 | 130 | // create the response object of credentials.get 131 | function createGetCredentialsResponse(res) { 132 | if(res.error) { 133 | return; 134 | } 135 | res.pk = res.pk.replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); 136 | const credential = { 137 | id: res.pk, 138 | rawId: stringToBuffer(res.pk, true), 139 | challenge: res.challenge, 140 | response: { 141 | clientDataJSON: stringToBuffer(res.clientDataJSON), // A JSON string in an ArrayBuffer, representing the client data that was passed to CredentialsContainer.create() 142 | authenticatorData: stringToBuffer(res.authData, true), 143 | signature: stringToBuffer(res.sig, true), 144 | userHandle: res.userid, 145 | }, 146 | type: "public-key", 147 | authenticatorAttachment: null, 148 | } 149 | credential.getClientExtensionResults = () => {}; 150 | return credential; 151 | } 152 | 153 | function stringToBuffer(s, isB64) { 154 | if(!s) { 155 | return new Uint8Array(0); 156 | } 157 | if(isB64) { 158 | s = atob(s.replaceAll('-', '+').replaceAll('_', '/')); 159 | } 160 | const arr = Uint8Array.from(s, c => c.charCodeAt(0)); 161 | return arr.buffer; 162 | } 163 | 164 | function arrayBufferToBase64(buffer) { 165 | let binary = ''; 166 | const bytes = new Uint8Array( buffer ); 167 | for (let i = 0; i < bytes.byteLength; i++) { 168 | binary += String.fromCharCode(bytes[i]); 169 | } 170 | return toBase64(binary); 171 | } 172 | 173 | function toBase64(s) { 174 | return window.btoa(s).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); 175 | } 176 | -------------------------------------------------------------------------------- /ext/background.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of WebSphinx. 3 | * Copyright (C) 2018 pitchfork@ctrlc.hu 4 | * 5 | * WebSphinx is free software; you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation; either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * WebSphinx is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License along 16 | * with this program; if not, write to the Free Software Foundation, Inc., 17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 18 | */ 19 | 20 | "use strict"; 21 | 22 | const APP_NAME = "websphinx"; 23 | 24 | var browser = browser || chrome; 25 | var ports = {}; 26 | var nativeport = browser.runtime.connectNative(APP_NAME); 27 | var changeData = true; 28 | 29 | nativeport.onMessage.addListener((response) => { 30 | // internal error handling 31 | if (browser.runtime.lastError) { 32 | var error = browser.runtime.lastError.message; 33 | console.error(error); 34 | ports['popup'].postMessage({ status: "ERROR", error: error }); 35 | return; 36 | } 37 | 38 | // client error handling 39 | if(response.results == 'fail') { 40 | if(response.cmd) { 41 | response.results = {'error': true, 'id': response.id, 'cmd': response.cmd}; 42 | const tabId = Number(response.tabId); 43 | browser.tabs.sendMessage(tabId, response); 44 | } 45 | console.log('websphinx failed'); 46 | return; 47 | } 48 | 49 | // handle manual inserts 50 | if(response.results.mode == "manual") { 51 | //console.log("manual"); 52 | // its a manual mode response so we just pass it to the popup 53 | ports['popup'].postMessage(response); 54 | return; 55 | } 56 | 57 | // handle get current pwd 58 | if(response.results.cmd == 'login') { 59 | // 1st step in an automatic change pwd 60 | if(changeData==true) { 61 | // we got the old password 62 | changeData={password: response.results.password}; 63 | // now change the password 64 | response.results.cmd="change"; 65 | delete response.results['password']; // don't send the password back 66 | nativeport.postMessage(response.results); 67 | return; 68 | } 69 | let login = { 70 | username: response.results.name, 71 | password: response.results.password 72 | }; 73 | browser.tabs.executeScript({code: 'document.websphinx.login(' + JSON.stringify(login) + ');'}); 74 | return; 75 | } 76 | 77 | // handle webauthn create 78 | if(response.results.cmd == 'webauthn-create') { 79 | const tabId = Number(response.results.tabId); 80 | browser.tabs.sendMessage(tabId, response); 81 | return; 82 | } 83 | 84 | // handle webauthn get 85 | if(response.results.cmd == 'webauthn-get') { 86 | const tabId = Number(response.results.tabId); 87 | browser.tabs.sendMessage(tabId, response); 88 | return; 89 | } 90 | 91 | // handle list users 92 | if(response.results.cmd == 'list') { 93 | ports['popup'].postMessage(response); 94 | return; 95 | } 96 | 97 | // handle create password 98 | if(response.results.cmd == 'create') { 99 | let account = { 100 | username: response.results.name, 101 | password: response.results.password 102 | }; 103 | browser.tabs.executeScript({code: 'document.websphinx.create(' + JSON.stringify(account) + ');'}); 104 | return; 105 | } 106 | 107 | // handle change password 108 | if(response.results.cmd == 'change') { 109 | let change = { 110 | 'old': changeData, 111 | 'new': response.results 112 | } 113 | browser.tabs.executeScript({code: 'document.websphinx.change(' + JSON.stringify(change) + ');'}); 114 | changeData = false; 115 | return; 116 | } 117 | 118 | // handle commit result 119 | if(response.results.cmd == 'commit') { 120 | ports['popup'].postMessage(response); 121 | return; 122 | } 123 | console.log("unhandled native port response"); 124 | console.log(response); 125 | }); 126 | 127 | browser.runtime.onConnect.addListener(function(p) { 128 | ports[p.name] = p; 129 | 130 | if(p.name == 'popup') { 131 | // proxy from CS to native backend 132 | p.onMessage.addListener(function(request, sender, sendResponse) { 133 | // prepare message to native backend 134 | let msg = { 135 | cmd: request.action, 136 | mode: request.mode, 137 | site: request.site 138 | }; 139 | 140 | if(request.action!="list") msg.name=request.name; 141 | if (request.action == "login") changeData=false; 142 | if (request.action == "create") { 143 | msg.rules= request.rules; 144 | msg.size= request.size; 145 | } 146 | if (request.action == "change") { 147 | if(request.mode != "manual") { 148 | // first get old password 149 | // but this will trigger the login inject in the nativport onmessage cb 150 | changeData = true; 151 | msg.cmd= "login"; 152 | } 153 | } 154 | 155 | if(request.action!="login" && 156 | request.action!="list" && 157 | request.action!="create" && 158 | request.action!="change" && 159 | request.action!="commit") { 160 | console.log("unhandled popup request"); 161 | console.log(request); 162 | return; 163 | } 164 | 165 | // send request to native backend 166 | nativeport.postMessage(msg); 167 | }); 168 | } 169 | if(p.name == 'content-script') { 170 | p.onMessage.addListener(function(request, sender, sendResponse) { 171 | let msg = { 172 | cmd: request.action, 173 | mode: request.mode, 174 | site: request.site, 175 | challenge: request.params.challenge, 176 | name: request.params.username, 177 | id: request.id, 178 | }; 179 | nativeport.postMessage(msg); 180 | }); 181 | } 182 | }); 183 | 184 | // handle "synchronous" calls for webauthn 185 | chrome.runtime.onMessage.addListener( 186 | function(request, sender, sendResponse) { 187 | if(!request.action || !request.action.startsWith('webauthn')) { 188 | return; 189 | } 190 | let msg = {} 191 | const tabId = sender.tab.id; 192 | if(request.action == "webauthn-create") { 193 | msg = { 194 | cmd: request.action, 195 | mode: request.mode, 196 | site: request.site, 197 | clientDataJSON: request.params.clientDataJSON, 198 | challenge: request.params.challenge, 199 | name: request.params.username, 200 | userid: request.params.userid, 201 | id: request.id, 202 | tabId: tabId, 203 | }; 204 | } 205 | if(request.action == "webauthn-get") { 206 | msg = { 207 | cmd: request.action, 208 | mode: request.mode, 209 | site: request.site, 210 | clientDataJSON: request.params.clientDataJSON, 211 | challenge: request.params.challenge, 212 | pk: request.params.pk, 213 | name: request.params.username, 214 | userid: request.params.userid, 215 | id: request.id, 216 | tabId: tabId, 217 | }; 218 | } 219 | nativeport.postMessage(msg); 220 | } 221 | ); 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # SPHINX: A Password **S**tore that **P**erfectly **H**ides from **I**tself (**N**o **X**aggeration) 8 | 9 | SPHINX is a cryptographic password storage protocol that provides information-theoretic security. pwdsphinx is a Python wrapper around [liboprf](https://github.com/stef/liboprf) - a cryptographic password storage 10 | as described in https://eprint.iacr.org/2015/1099. 11 | 12 | Unlike traditional password managers, SPHINX only stores random numbers unrelated to your actual passwords, ensuring the server learns nothing about them. 13 | 14 | **Key Features:** 15 | 16 | - 🔒 **Information-theoretic security**: Mathematically proven protection 17 | - 🌐 **Zero-trust architecture**: Server knows nothing about your passwords 18 | - 🚫 **Offline bruteforce resistance**: Your passwords are safe even if the server is compromised 19 | - 🏠 **Self-hostable**: Run your own server or use someone else's 20 | - 📱 **Cross-platform**: CLI, browser extensions, Android app, and X11 integration 21 | - 🔑 **Password generation**: Creates strong passwords according to site requirements 22 | 23 | ## Also on Radicle 24 | 25 | To clone this repo on [Radicle](https://radicle.xyz): 26 | 27 | ```bash 28 | rad clone rad:z3rjK2hk7ckb1thexdsuyaM7e4FwS 29 | ``` 30 | 31 | ## Dependencies 32 | 33 | ### Required Dependencies 34 | 35 | - **[liboprf](https://github.com/stef/liboprf)**: An Oblivious Pseudo-Random Function (OPRF) is a cryptographic protocol where a client can evaluate a pseudo-random function on their input using a key held by a server, without the server learning the client's input or the function's output. liboprf implements OPRFs, which is used to enables SPHINX's zero-knowledge password storage. 36 | - **[libequihash](https://github.com/stef/equihash/)**: This provides rate limiting proof-of-work with fast verification 37 | - **pysodium** and **pyoprf**: Python cryptographic bindings. Both can be installed using either 38 | your OS package manager or pip. 39 | 40 | ### Optional Dependencies 41 | 42 | **For browser extensions:** 43 | If you also want to use the websphinx browser extension, you 44 | also need to install an X11 variant of pinentry from the GnuPG project. 45 | 46 | ```bash 47 | # Install any one of these, using the equivalent of `apt-get` on your operating system: 48 | apt-get install pinentry-qt # For KDE/Qt environments 49 | apt-get install pinentry-gtk2 # For older GNOME/GTK environments 50 | apt-get install pinentry-gnome3 # For modern GNOME environments 51 | apt-get install pinentry-fltk # Lightweight option 52 | ``` 53 | 54 | **For X11 integration:** 55 | 56 | - **xdotool**: Keyboard/mouse automation 57 | - **xinput**: Input device control 58 | - **dmenu**: Interactive menus 59 | 60 | **For extended storage:** 61 | If you want to store other "secrets" that are longer than just 77 chars, you 62 | can install OPAQUE-Store: 63 | 64 | - **[opaque-store](https://github.com/stef/opaque-store/)**: Encrypted file storage: `pip3 install opaquestore` 65 | - **[OPAQUE-Store](https://github.com/stef/libopaque)**: OPAQUE protocol implementation, a dependency for OPAQUE-Store above. 66 | 67 | ## Installation 68 | 69 | ```bash 70 | pip3 install pwdsphinx 71 | ``` 72 | 73 | On Debian-based systems, you can also do: 74 | 75 | ```bash 76 | sudo apt install pwdsphinx 77 | ``` 78 | 79 | ## Architecture 80 | 81 | SPHINX uses a client-server architecture where: 82 | 83 | ### Server (Oracle) 84 | 85 | The server stores only cryptographic blobs that are useless without your master password. Even if compromised, your actual passwords remain secure. 86 | 87 | **Host your own server:** See [`oracle(1)`](man/oracle.md) or the [Server Installation Guide](GettingStarted.md#hosting-your-own-oracle) for how to configure your server. 88 | 89 | ### Client 90 | 91 | The client combines your master password with the server's response to regenerate your actual passwords deterministically. 92 | 93 | **Supported platforms:** 94 | 95 | - **Command Line:** Full-featured CLI client (see [`sphinx(1)`](man/sphinx.md) for how to configure a client) 96 | - **Browser:** [Firefox and Chromium extensions](./GettingStarted.md#setting-up-browser-extensions) with native messaging 97 | - **Desktop:** [X11 integration scripts](./contrib/README.md) for form filling 98 | - **Mobile:** Android app ([androsphinx](https://github.com/dnet/androsphinx)) 99 | 100 | ## Usage 101 | 102 | SPHINX provides a complete lifecycle for password management: 103 | 104 | ### Core Operations 105 | 106 | - **`create`**: Generate new password for a site 107 | - **`get`**: Retrieve existing password 108 | - **`change`**: Update password (two-phase commit) 109 | - **`commit`**: Activate changed password 110 | - **`undo`**: Revert to previous password 111 | - **`delete`**: Remove password record 112 | - **`list`**: Show usernames for a site 113 | 114 | ### Management Operations 115 | 116 | - **`init`**: Initialize client with new master key. It also sets up browser extensions if `~/.mozilla` or `~/.config/chromium` directories are found. 117 | - **`healthcheck`**: Test server connectivity 118 | - **`qr`**: Export configuration as QR code 119 | 120 | See [`sphinx(1)`](man/sphinx.md) for detailed command syntax and examples. 121 | 122 | ## OPAQUE-Store Client Integration 123 | 124 | If you have OPAQUE-Store installed and configured correctly, you get a number of 125 | additional operations, which allow you to store traditionally encrypted blobs 126 | of information. For a gentle introduction on how this works using the OPAQUE protocol, have a look at this post: https://www.ctrlc.hu/~stef/blog/posts/How_to_recover_static_secrets_using_OPAQUE.html 127 | 128 | The following operations will be available if OPAQUE-Store is setup correctly: 129 | 130 | ```sh 131 | echo -n 'password' | sphinx store file-to-store 132 | echo -n 'password' | sphinx read 133 | echo -n 'password' | sphinx replace [force] file-to-store 134 | echo -n 'password' | sphinx edit [force] 135 | echo -n 'password' | sphinx changepwd [force] 136 | echo -n 'password' | sphinx erase [force] 137 | echo -n 'password' | sphinx recovery-tokens 138 | echo -n 'password' | sphinx unlock 139 | ``` 140 | 141 | See the [OPAQUE-Store X11 integration](https://sphinx.pm/opaque-store_integration.html) documentation for more details on these operations and how the integration 142 | works with SPHINX. 143 | 144 | ## Browser Integration 145 | 146 | There is WebSphinx, our browser extension that provides 147 | seamless password filling. See the [browser extension instructions](./GettingStarted.md#Setting-Up-Browser-Extensions) on how to set it up 148 | on Firefox and Chrome/Chromium browsers 149 | 150 | ## X11 Desktop Integration 151 | 152 | SPHINX includes shell scripts for X11 desktop integration using `dmenu`, `xdotool`, `xinput`, and `pinentry`. 153 | 154 | The main script [`dmenu-sphinx.sh`](./contrib/dmenu-sphinx) provides interactive password filling with a dmenu interface. It stores hostname history in `~/.sphinx-hosts` (link to `/dev/null` if you consider this sensitive). 155 | 156 | The integration enables automatic form filling in X11 applications through keyboard automation for password entry. It works with pinentry for secure password input. 157 | 158 | See [`contrib/README.md`](contrib/README.md) for setup examples and script combinations. 159 | 160 | ## More documentation 161 | 162 | ### For Users 163 | 164 | - **[Getting Started Guide](GettingStarted.md)**: Complete setup and usage tutorial 165 | - **[Manual Pages](man/)**: Detailed command reference 166 | - [`sphinx(1)`](man/sphinx.md): Main client commands 167 | - [`oracle(1)`](man/oracle.md): Server configuration and management 168 | - [`getpwd(1)`](man/getpwd.md): Secure password input utility 169 | 170 | ### For Developers & Advanced Users 171 | 172 | - **[X11 Integration](https://sphinx.pm/x11-integration.html)**: Desktop automation scripts 173 | - **[OPAQUE-Store Integration](https://sphinx.pm/opaque-store_integration.html)**: Encrypted file storage 174 | - **[Contributing Scripts](contrib/README.md)**: Helper utilities and examples 175 | 176 | ## Credits 177 | 178 | This project was funded through the NGI0 PET Fund, a fund established 179 | by NLnet with financial support from the European Commission's Next 180 | Generation Internet programme, under the aegis of DG Communications 181 | Networks, Content and Technology under grant agreement No 825310. 182 | 183 | This project was funded through the e-Commons Fund, a fund established by NLnet 184 | with financial support from the Netherlands Ministry of the Interior and 185 | Kingdom Relations. 186 | 187 | Everlasting gratuity to asciimoo, dnet, jonathan and hugo for their 188 | contributions, patience, and support. 189 | -------------------------------------------------------------------------------- /pwdsphinx/v1sphinx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-FileCopyrightText: 2018-2024, Marsiske Stefan 4 | # SPDX-License-Identifier: GPL-3.0-or-later 5 | 6 | import sys, os, socket, ssl, struct, time 7 | from SecureString import clearmem 8 | from equihash import solve 9 | import pysodium 10 | try: 11 | from pwdsphinx import bin2pass 12 | from pwdsphinx.config import getcfg 13 | from pwdsphinx.consts import * 14 | from pwdsphinx.converter import convert 15 | except ImportError: 16 | import bin2pass 17 | from config import getcfg 18 | from consts import * 19 | from converter import convert 20 | 21 | # override consts from consts.py 22 | VERSION = b'\x00' 23 | RULE_SIZE = 79 24 | 25 | #### config #### 26 | 27 | cfg = getcfg('sphinx') 28 | enabled=False 29 | verbose = cfg.get('client',{}).get('verbose', False) 30 | hostname = cfg.get('client',{}).get('address') 31 | timeout = int(cfg.get('client',{}).get('timeout', '5')) 32 | if hostname is not None: 33 | enabled = True 34 | address = socket.gethostbyname(hostname) 35 | port = int(cfg.get('client',{}).get('port',2355)) 36 | try: 37 | ssl_cert = os.path.expanduser(cfg.get('client',{}).get('ssl_cert')) # only for dev, production system should use proper certs! 38 | except TypeError: # ignore exception in case ssl_cert is not set, thus None is attempted to expand. 39 | ssl_cert = None 40 | 41 | datadir = os.path.expanduser(cfg.get('client',{}).get('datadir','~/.config/sphinx')) 42 | # make RWD optional in (sign|seal)key, if it is b'' then this protects against 43 | # offline master pwd bruteforce attacks, drawback that for known (host,username) tuples 44 | # the seeds/blobs can be controlled by an attacker if the masterkey is known 45 | rwd_keys = cfg.get('client',{}).get('rwd_keys', False) 46 | validate_password = cfg.get('client',{}).get('validate_password',True) 47 | userlist = cfg.get('client',{}).get('userlist', True) 48 | 49 | if verbose and enabled: 50 | print("v1 hostname:", hostname, file=sys.stderr) 51 | print("v1 address:", address, file=sys.stderr) 52 | print("v1 port:", port, file=sys.stderr) 53 | print("v1 timeout:", timeout, file=sys.stderr) 54 | print("v1 ssl_cert:", ssl_cert, file=sys.stderr) 55 | 56 | #### consts #### 57 | 58 | ENC_CTX = b"sphinx encryption key" 59 | SIGN_CTX = b"sphinx signing key" 60 | SALT_CTX = b"sphinx host salt" 61 | PASS_CTX = b"sphinx password context" 62 | CHECK_CTX = b"sphinx check digit context" 63 | 64 | #### Helper fns #### 65 | 66 | def get_masterkey(): 67 | try: 68 | with open(os.path.join(datadir,'masterkey'), 'rb') as fd: 69 | mk = fd.read() 70 | return mk 71 | except FileNotFoundError: 72 | raise ValueError("ERROR: Could not find masterkey!\nIf sphinx was working previously it is now broken.\nIf this is a fresh install all is good, you just need to run `%s init`." % sys.argv[0]) 73 | 74 | def connect(): 75 | ctx = ssl.create_default_context() 76 | if(ssl_cert): 77 | ctx.load_verify_locations(ssl_cert) # only for dev, production system should use proper certs! 78 | ctx.check_hostname=False # only for dev, production system should use proper certs! 79 | ctx.verify_mode=ssl.CERT_NONE # only for dev, production system should use proper certs! 80 | else: 81 | ctx.load_default_certs() 82 | ctx.verify_mode = ssl.CERT_REQUIRED 83 | ctx.check_hostname = True 84 | 85 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 86 | s.settimeout(timeout) 87 | s = ctx.wrap_socket(s, server_hostname=hostname) 88 | s.connect((address, port)) 89 | return s 90 | 91 | def get_signkey(id, rwd): 92 | mk = get_masterkey() 93 | seed = pysodium.crypto_generichash(SIGN_CTX, mk) 94 | clearmem(mk) 95 | # rehash with rwd so the user always contributes his pwd and the sphinx server it's seed 96 | seed = pysodium.crypto_generichash(seed, id) 97 | if rwd_keys: 98 | seed = pysodium.crypto_generichash(seed, rwd) 99 | pk, sk = pysodium.crypto_sign_seed_keypair(seed) 100 | clearmem(seed) 101 | return sk, pk 102 | 103 | def get_sealkey(): 104 | mk = get_masterkey() 105 | sk = pysodium.crypto_generichash(ENC_CTX, mk) 106 | clearmem(mk) 107 | return sk 108 | 109 | def encrypt_blob(blob): 110 | # todo implement padding to hide length information 111 | sk = get_sealkey() 112 | nonce = pysodium.randombytes(pysodium.crypto_secretbox_NONCEBYTES) 113 | ct = pysodium.crypto_aead_xchacha20poly1305_ietf_encrypt(blob,VERSION,nonce,sk) 114 | clearmem(sk) 115 | return VERSION+nonce+ct 116 | 117 | def decrypt_blob(blob): 118 | # todo implement padding to hide length information 119 | sk = get_sealkey() 120 | version = blob[:1] 121 | if version > VERSION: 122 | raise ValueError("Your client is too old to handle this response. Please update your client.") 123 | blob = blob[1:] 124 | nonce = blob[:pysodium.crypto_secretbox_NONCEBYTES] 125 | blob = blob[pysodium.crypto_secretbox_NONCEBYTES:] 126 | res = pysodium.crypto_aead_xchacha20poly1305_ietf_decrypt(blob,version,nonce,sk) 127 | clearmem(sk) 128 | return version, res 129 | 130 | def sign_blob(blob, id, rwd): 131 | sk, pk = get_signkey(id, rwd) 132 | res = pysodium.crypto_sign_detached(blob,sk) 133 | clearmem(sk) 134 | return b''.join((blob,res)) 135 | 136 | def getid(host, user): 137 | mk = get_masterkey() 138 | salt = pysodium.crypto_generichash(SALT_CTX, mk) 139 | clearmem(mk) 140 | return pysodium.crypto_generichash(b'|'.join((user.encode(),host.encode())), salt, 32) 141 | 142 | def unpack_rule(ct): 143 | version, packed = decrypt_blob(ct) 144 | xor_mask = packed[-32:] 145 | v = int.from_bytes(packed[:-32], "big") 146 | 147 | size = v & ((1<<7) - 1) 148 | rule = {c for i,c in enumerate(('u','l','d')) if (v >> 7) & (1 << i)} 149 | symbols = [c for i,c in enumerate(bin2pass.symbols) if (v>>(7+3) & (1<>(7+3+33)) 152 | else: 153 | check_digit = 0 154 | 155 | return rule, symbols, size, check_digit, xor_mask 156 | 157 | def xor(x,y): 158 | return bytes(a ^ b for (a, b) in zip(x, y)) 159 | 160 | def ratelimit(s,req): 161 | pkt0 = b''.join([CHALLENGE_CREATE, req]) 162 | s.send(pkt0) 163 | challenge = s.recv(1+1+8+32) # n,k,ts,sig 164 | if len(challenge)!= 1+1+8+32: 165 | if verbose: print("challengelen incorrect: %s %s" %(len(challenge), repr(challenge)), file=sys.stderr) 166 | raise ValueError("ERROR: failed to get ratelimit challenge") 167 | s.close() 168 | n = challenge[0] 169 | k = challenge[1] 170 | 171 | try: 172 | os.write(3,f"{n} {k}\n".encode('utf8')) 173 | except OSError: pass 174 | 175 | if k==4: 176 | if n < 90: 177 | if verbose: print("got an easy puzzle: %d" % n, file=sys.stderr) 178 | elif n > 100: 179 | if verbose: print("got a hard puzzle: %d" % n, file=sys.stderr) 180 | else: 181 | if verbose: print("got a moderate puzzle: %d" % n, file=sys.stderr) 182 | seed = challenge + req 183 | 184 | delta = time.time() 185 | solution = solve(n, k, seed) 186 | delta = time.time() - delta 187 | try: 188 | os.write(3,f"{delta}".encode('utf8')) 189 | except OSError: pass 190 | 191 | s = connect() 192 | pkt1 = b''.join([CHALLENGE_VERIFY, challenge]) 193 | s.send(pkt1) 194 | s.send(req) 195 | s.send(solution) 196 | return s 197 | 198 | def auth(s,op,id,alpha=None,pwd=None,r=None): 199 | if r is None: 200 | nonce = s.recv(32) 201 | if len(nonce)!=32: 202 | return False 203 | rwd = b'' 204 | else: 205 | msg = s.recv(64) 206 | if len(msg)!=64: 207 | return False 208 | beta = msg[:32] 209 | nonce = msg[32:] 210 | rwd = finish(pwd, r, alpha, beta, id) 211 | 212 | sk, pk = get_signkey(id, rwd) 213 | clearmem(rwd) 214 | sig = pysodium.crypto_sign_detached(op+nonce,sk) 215 | clearmem(sk) 216 | s.send(sig) 217 | 218 | resp = s.recv(6) 219 | if resp==b'\x00\x04auth': return True 220 | return False 221 | 222 | def challenge(pwd): 223 | h0 = pysodium.crypto_generichash(pwd, outlen=pysodium.crypto_core_ristretto255_HASHBYTES); 224 | H0 = pysodium.crypto_core_ristretto255_from_hash(h0) 225 | clearmem(h0) 226 | r = pysodium.crypto_core_ristretto255_scalar_random() 227 | alpha = pysodium.crypto_scalarmult_ristretto255(r, H0) 228 | clearmem(H0) 229 | return r, alpha 230 | 231 | def finish(pwd, r, alpha, beta, salt): 232 | if(alpha==beta): raise ValueError("alpha == beta") 233 | if not pysodium.crypto_core_ristretto255_is_valid_point(alpha): raise ValueError("invalid beta") 234 | 235 | rinv = pysodium.crypto_core_ristretto255_scalar_invert(r) 236 | H0_k = pysodium.crypto_scalarmult_ristretto255(rinv, beta) 237 | clearmem(rinv) 238 | rwd0 = pysodium.crypto_generichash(pwd+H0_k, outlen=pysodium.crypto_core_ristretto255_BYTES); 239 | clearmem(H0_k) 240 | rwd = pysodium.crypto_pwhash(pysodium.crypto_core_ristretto255_BYTES, 241 | rwd0, salt[:pysodium.crypto_pwhash_SALTBYTES], 242 | pysodium.crypto_pwhash_OPSLIMIT_INTERACTIVE, 243 | pysodium.crypto_pwhash_MEMLIMIT_INTERACTIVE) 244 | clearmem(rwd0) 245 | return rwd 246 | 247 | #### OPs #### 248 | 249 | def get(pwd, user, host): 250 | if isinstance(pwd, str): pwd = pwd.encode() 251 | s = connect() 252 | id = getid(host, user) 253 | r, alpha = challenge(pwd) 254 | msg = b''.join([V1GET, id, alpha]) 255 | s = ratelimit(s, msg) 256 | 257 | resp = s.recv(32+RULE_SIZE) # beta + sealed rules 258 | if resp == b'\x00\x04fail' or len(resp)!=32+RULE_SIZE: 259 | s.close() 260 | raise ValueError("ERROR: Either the record does not exist, or the request to server was corrupted during transport.") 261 | beta = resp[:32] 262 | rules = resp[32:] 263 | rwd = finish(pwd, r, alpha, beta, id) 264 | 265 | try: 266 | classes, symbols, size, checkdigit, xormask = unpack_rule(rules) 267 | except ValueError: 268 | s.close() 269 | raise ValueError("ERROR: failed to unpack password rules from server") 270 | s.close() 271 | 272 | if validate_password and (checkdigit != (pysodium.crypto_generichash(CHECK_CTX, rwd, 1)[0] & ((1<<5)-1))): 273 | raise ValueError("ERROR: bad checkdigit") 274 | 275 | rwd = xor(pysodium.crypto_generichash(PASS_CTX, rwd),xormask) 276 | return rwd, classes, size, symbols 277 | 278 | def delete(pwd, user, host): 279 | s = connect() 280 | # run sphinx to recover rwd for authentication 281 | id = getid(host, user) 282 | r, alpha = challenge(pwd) 283 | msg = b''.join([V1DELETE, id, alpha]) 284 | s = ratelimit(s, msg) 285 | 286 | if isinstance(pwd, str): pwd = pwd.encode() 287 | if not auth(s,msg,id,alpha,pwd,r): 288 | s.close() 289 | raise ValueError("ERROR: Failed to authenticate to server while deleting password on server or record doesn't exist") 290 | 291 | if not userlist: 292 | s.send(b"\0"*96) 293 | else: 294 | # delete user from user list for this host 295 | # a malicous server could correlate all accounts on this services to this users here 296 | # first query user record for this host 297 | id = getid(host, '') 298 | signed_id = sign_blob(id, id, b'') 299 | s.send(signed_id) 300 | # wait for user blob 301 | bsize = s.recv(2) 302 | bsize = struct.unpack('!H', bsize)[0] 303 | if bsize == 0: 304 | # this should not happen, it means something is corrupt 305 | s.close() 306 | raise ValueError("ERROR: server has no associated user record for this host", file=sys.stderr) 307 | 308 | blob = s.recv(bsize) 309 | if blob == b'fail': 310 | s.close() 311 | raise ValueError("ERROR: invalid signature on list of users") 312 | version, blob = decrypt_blob(blob) 313 | users = set(blob.decode().split('\x00')) 314 | if user not in users: 315 | # this should not happen, but maybe it's a sign of corruption? 316 | s.close() 317 | raise ValueError(f'warning "{user}" is not in user record', file=sys.stderr) 318 | users.remove(user) 319 | blob = ('\x00'.join(sorted(users))).encode() 320 | # notice we do not add rwd to encryption of user blobs 321 | blob = encrypt_blob(blob) 322 | bsize = len(blob) 323 | if bsize >= 2**16: 324 | s.close() 325 | raise ValueError("ERROR: blob is bigger than 64KB.") 326 | blob = struct.pack("!H", bsize) + blob 327 | blob = sign_blob(blob, id, b'') 328 | 329 | s.send(blob) 330 | 331 | if b'ok' != s.recv(2): 332 | s.close() 333 | raise ValueError("ERROR: server failed to save updated list of user names for host: %s." % host) 334 | 335 | s.close() 336 | return True 337 | 338 | def read_blob(s, id): 339 | msg = b''.join([READ, id]) 340 | s = ratelimit(s, msg) 341 | if auth(s,msg,id) is False: 342 | s.close() 343 | return 344 | bsize = s.recv(2) 345 | bsize = struct.unpack('!H', bsize)[0] 346 | blob = s.recv(bsize) 347 | s.close() 348 | if blob == b'fail': 349 | return 350 | return decrypt_blob(blob) 351 | 352 | def users(host): 353 | s = connect() 354 | res = read_blob(s, getid(host, '')) 355 | if not res: return set() 356 | version, res = res 357 | users = set(res.decode().split('\x00')) 358 | return users 359 | 360 | #print(get(connect(), b'asdf','asdf','test')) 361 | #print(delete(connect(), b'asdf','asdf','test')) 362 | -------------------------------------------------------------------------------- /ext/inject.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of WebSphinx. 3 | * Copyright (c) 2016 Danny van Kooten 4 | * Copyright (C) 2017 Iwan Timmer 5 | * Copyright (C) 2018 pitchfork@ctrlc.hu 6 | * 7 | * WebSphinx is free software; you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation; either version 2 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * WebSphinx is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License along 18 | * with this program; if not, write to the Free Software Foundation, Inc., 19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | */ 21 | 22 | "use strict"; 23 | 24 | var browser = browser || chrome; 25 | 26 | (function(doc) { 27 | const FORM_MARKERS = [ 28 | "login", 29 | "log-in", 30 | "log_in", 31 | "signin", 32 | "sign-in", 33 | "sign_in" 34 | ]; 35 | const USERNAME_FIELDS = { 36 | selectors: [ 37 | "input[name*=user i]", 38 | "input[name*=login i]", 39 | "input[name*=email i]", 40 | "input[id*=user i]", 41 | "input[id*=login i]", 42 | "input[id*=email i]", 43 | "input[class*=user i]", 44 | "input[class*=login i]", 45 | "input[class*=email i]", 46 | "input[type=email i]", 47 | "input[type=text i]", 48 | "input[type=tel i]" 49 | ], 50 | types: ["email", "text", "tel"] 51 | }; 52 | const PASSWORD_FIELDS = { 53 | selectors: ["input[type=password i]"] 54 | }; 55 | const INPUT_FIELDS = { 56 | selectors: PASSWORD_FIELDS.selectors.concat(USERNAME_FIELDS.selectors) 57 | }; 58 | const SUBMIT_FIELDS = { 59 | selectors: [ 60 | "[type=submit i]", 61 | "button[name*=login i]", 62 | "button[name*=log-in i]", 63 | "button[name*=log_in i]", 64 | "button[name*=signin i]", 65 | "button[name*=sign-in i]", 66 | "button[name*=sign_in i]", 67 | "button[id*=login i]", 68 | "button[id*=log-in i]", 69 | "button[id*=log_in i]", 70 | "button[id*=signin i]", 71 | "button[id*=sign-in i]", 72 | "button[id*=sign_in i]", 73 | "button[class*=login i]", 74 | "button[class*=log-in i]", 75 | "button[class*=log_in i]", 76 | "button[class*=signin i]", 77 | "button[class*=sign-in i]", 78 | "button[class*=sign_in i]", 79 | "input[type=button i][name*=login i]", 80 | "input[type=button i][name*=log-in i]", 81 | "input[type=button i][name*=log_in i]", 82 | "input[type=button i][name*=signin i]", 83 | "input[type=button i][name*=sign-in i]", 84 | "input[type=button i][name*=sign_in i]", 85 | "input[type=button i][id*=login i]", 86 | "input[type=button i][id*=log-in i]", 87 | "input[type=button i][id*=log_in i]", 88 | "input[type=button i][id*=signin i]", 89 | "input[type=button i][id*=sign-in i]", 90 | "input[type=button i][id*=sign_in i]", 91 | "input[type=button i][class*=login i]", 92 | "input[type=button i][class*=log-in i]", 93 | "input[type=button i][class*=log_in i]", 94 | "input[type=button i][class*=signin i]", 95 | "input[type=button i][class*=sign-in i]", 96 | "input[type=button i][class*=sign_in i]" 97 | ] 98 | }; 99 | 100 | function queryAllVisible(parent, field, form) { 101 | var result = []; 102 | for (var i = 0; i < field.selectors.length; i++) { 103 | var elems = parent.querySelectorAll(field.selectors[i]); 104 | for (var j = 0; j < elems.length; j++) { 105 | var elem = elems[j]; 106 | // Select only elements from specified form 107 | if (form && form != elem.form) { 108 | continue; 109 | } 110 | // Ignore disabled fields 111 | if (elem.disabled) { 112 | continue; 113 | } 114 | // Elem or its parent has a style 'display: none', 115 | // or it is just too narrow to be a real field (a trap for spammers?). 116 | if (elem.offsetWidth < 30 || elem.offsetHeight < 10) { 117 | continue; 118 | } 119 | // We may have a whitelist of acceptable field types. If so, skip elements of a different type. 120 | if (field.types && field.types.indexOf(elem.type.toLowerCase()) < 0) { 121 | continue; 122 | } 123 | // Elem takes space on the screen, but it or its parent is hidden with a visibility style. 124 | var style = window.getComputedStyle(elem); 125 | if (style.visibility == "hidden") { 126 | continue; 127 | } 128 | // Elem is outside of the boundaries of the visible viewport. 129 | var rect = elem.getBoundingClientRect(); 130 | if ( 131 | rect.x + rect.width < 0 || 132 | rect.y + rect.height < 0 || 133 | (rect.x > window.innerWidth || rect.y > window.innerHeight) 134 | ) { 135 | continue; 136 | } 137 | // This element is visible, will use it. 138 | result.push(elem); 139 | } 140 | } 141 | return result; 142 | } 143 | 144 | function queryFirstVisible(parent, field, form) { 145 | var elems = queryAllVisible(parent, field, form); 146 | return elems.length > 0 ? elems[0] : undefined; 147 | } 148 | 149 | function form() { 150 | var elems = queryAllVisible(document, INPUT_FIELDS, undefined); 151 | var forms = []; 152 | for (var i = 0; i < elems.length; i++) { 153 | var form = elems[i].form; 154 | if (form && forms.indexOf(form) < 0) { 155 | forms.push(form); 156 | } 157 | } 158 | if (forms.length == 0) { 159 | return undefined; 160 | } 161 | if (forms.length == 1) { 162 | return forms[0]; 163 | } 164 | 165 | // If there are multiple forms, try to detect which one is a login form 166 | var formProps = []; 167 | for (var i = 0; i < forms.length; i++) { 168 | var form = forms[i]; 169 | var props = [form.id, form.name, form.className]; 170 | formProps.push(props); 171 | for (var j = 0; j < FORM_MARKERS.length; j++) { 172 | var marker = FORM_MARKERS[j]; 173 | for (var k = 0; k < props.length; k++) { 174 | var prop = props[k]; 175 | if (prop.toLowerCase().indexOf(marker) > -1) { 176 | return form; 177 | } 178 | } 179 | } 180 | } 181 | 182 | console.error( 183 | "Unable to detect which of the multiple available forms is the login form. Please submit an issue for browserpass on github, and provide the following list in the details: " + 184 | JSON.stringify(formProps) 185 | ); 186 | return forms[0]; 187 | } 188 | 189 | function find(field) { 190 | return queryFirstVisible(document, field, form()); 191 | } 192 | 193 | function update(field, value) { 194 | if (!value.length) { 195 | return false; 196 | } 197 | 198 | // Focus the input element first 199 | var el = find(field); 200 | if (!el) { 201 | return false; 202 | } 203 | var eventNames = ["click", "focus"]; 204 | eventNames.forEach(function(eventName) { 205 | el.dispatchEvent(new Event(eventName, { bubbles: true })); 206 | }); 207 | 208 | // Focus may have triggered unvealing a true input, find it again 209 | el = find(field); 210 | if (!el) { 211 | return false; 212 | } 213 | 214 | // Now set the value and unfocus 215 | el.setAttribute("value", value); 216 | el.value = value; 217 | eventNames = [ 218 | "keypress", 219 | "keydown", 220 | "keyup", 221 | "input", 222 | "blur", 223 | "change" 224 | ]; 225 | eventNames.forEach(function(eventName) { 226 | el.dispatchEvent(new Event(eventName, { bubbles: true })); 227 | }); 228 | return true; 229 | } 230 | 231 | function update_all(field, value) { 232 | if (!value.length) { 233 | return false; 234 | } 235 | 236 | let password_inputs = queryAllVisible(document, PASSWORD_FIELDS, form()); 237 | password_inputs.forEach(function(el) { 238 | let eventNames = ["click", "focus"]; 239 | eventNames.forEach(function(eventName) { 240 | el.dispatchEvent(new Event(eventName, { bubbles: true })); 241 | }); 242 | 243 | // Focus may have triggered unvealing a true input, find it again 244 | let pwd_inputs = queryAllVisible(document, PASSWORD_FIELDS, form()); 245 | pwd_inputs.forEach(function(el2) { 246 | // Now set the value and unfocus 247 | el2.setAttribute("value", value); 248 | el2.value = value; 249 | let eventNames = [ 250 | "keypress", 251 | "keydown", 252 | "keyup", 253 | "input", 254 | "blur", 255 | "change" 256 | ]; 257 | eventNames.forEach(function(eventName) { 258 | el2.dispatchEvent(new Event(eventName, { bubbles: true })); 259 | }); 260 | }); 261 | }); 262 | return true; 263 | } 264 | 265 | function set_pwd(el, value) { 266 | let eventNames = ["click", "focus"]; 267 | eventNames.forEach(function(eventName) { 268 | el.dispatchEvent(new Event(eventName, { bubbles: true })); 269 | }); 270 | 271 | // Focus may have triggered unvealing a true input, find it again 272 | //let pwd_inputs = queryAllVisible(document, PASSWORD_FIELDS, form()); 273 | // we ignore this for now 274 | //pwd_inputs.forEach(function(el2) { 275 | 276 | // Now set the value and unfocus 277 | el.setAttribute("value", value); 278 | el.value = value; 279 | eventNames = [ 280 | "keypress", 281 | "keydown", 282 | "keyup", 283 | "input", 284 | "blur", 285 | "change" 286 | ]; 287 | eventNames.forEach(function(eventName) { 288 | el.dispatchEvent(new Event(eventName, { bubbles: true })); 289 | }); 290 | //}); 291 | } 292 | class WebSphinx { 293 | 294 | recon() { 295 | var username = ''; 296 | var el = find(USERNAME_FIELDS); 297 | if(el) { 298 | username=el.value; 299 | } 300 | var password_inputs = queryAllVisible(document, PASSWORD_FIELDS, form()); 301 | browser.runtime.sendMessage({"username": username, "password_fields": password_inputs.length}); 302 | }; 303 | 304 | login(login) { 305 | update(USERNAME_FIELDS, login.username); 306 | update(PASSWORD_FIELDS, login.password); 307 | 308 | var password_inputs = queryAllVisible(document, PASSWORD_FIELDS, form()); 309 | if (password_inputs.length > 1) { 310 | // There is likely a field asking for OTP code, so do not submit form just yet 311 | password_inputs[1].select(); 312 | } else { 313 | window.requestAnimationFrame(function() { 314 | // Try to submit the form, or focus on the submit button (based on user settings) 315 | var submit = find(SUBMIT_FIELDS); 316 | if (submit) { 317 | submit.focus(); 318 | } else { 319 | // There is no submit button. We need to keep focus somewhere within the form, so that Enter hopefully submits the form. 320 | var password = find(PASSWORD_FIELDS); 321 | if (password) { 322 | password.focus(); 323 | } else { 324 | var username = find(USERNAME_FIELDS); 325 | if (username) { 326 | username.focus(); 327 | } 328 | } 329 | } 330 | }); 331 | } 332 | }; 333 | 334 | create(account) { 335 | update(USERNAME_FIELDS, account.username); 336 | update_all(PASSWORD_FIELDS, account.password); 337 | 338 | window.requestAnimationFrame(function() { 339 | // Try to submit the form, or focus on the submit button (based on user settings) 340 | var submit = find(SUBMIT_FIELDS); 341 | if (submit) { 342 | submit.focus(); 343 | } else { 344 | // There is no submit button. We need to keep focus somewhere within the form, so that Enter hopefully submits the form. 345 | var password = find(PASSWORD_FIELDS); 346 | if (password) { 347 | password.focus(); 348 | } 349 | } 350 | }); 351 | }; 352 | 353 | change(changed) { 354 | var pwd_inputs = queryAllVisible(document, PASSWORD_FIELDS, form()); 355 | if(pwd_inputs.length!=3) { 356 | console.log("wtf"); 357 | console.log(pwd_inputs); 358 | return; 359 | } 360 | set_pwd(pwd_inputs[0],changed.old.password); 361 | set_pwd(pwd_inputs[1],changed.new.password); 362 | set_pwd(pwd_inputs[2],changed.new.password); 363 | 364 | window.requestAnimationFrame(function() { 365 | // Try to submit the form, or focus on the submit button (based on user settings) 366 | var submit = find(SUBMIT_FIELDS); 367 | if (submit) { 368 | submit.focus(); 369 | } else { 370 | // There is no submit button. We need to keep focus somewhere within the form, so that Enter hopefully submits the form. 371 | var password = find(PASSWORD_FIELDS); 372 | if (password) { 373 | password.focus(); 374 | } 375 | } 376 | }); 377 | }; 378 | 379 | inject(pwd) { 380 | let el = document.activeElement; 381 | if(el.type=="password") { 382 | set_pwd(el, pwd); 383 | } 384 | } 385 | } 386 | doc.websphinx = new WebSphinx(); 387 | })(document); 388 | -------------------------------------------------------------------------------- /pwdsphinx/websphinx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # This file is part of WebSphinx. 4 | # 5 | # SPDX-FileCopyrightText: 2018, Marsiske Stefan 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | # 8 | # WebSphinx is free software; you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # WebSphinx is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License version 3 for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License along 19 | # with this program; if not, If not, see . 20 | 21 | 22 | import subprocess 23 | import sys, struct, json, pysodium 24 | import cbor2 25 | from zxcvbn import zxcvbn 26 | from SecureString import clearmem 27 | from pyoprf.multiplexer import Multiplexer 28 | from binascii import b2a_base64 29 | from base64 import urlsafe_b64decode 30 | try: 31 | from pwdsphinx import sphinx, bin2pass 32 | from pwdsphinx.config import getcfg 33 | except ImportError: 34 | import sphinx 35 | from config import getcfg 36 | 37 | cfg = getcfg('sphinx') 38 | pinentry = cfg['websphinx']['pinentry'] 39 | log = cfg['websphinx'].get('log') 40 | 41 | if 'webauthn_data_dir' not in cfg['websphinx']: 42 | raise Exception("Cannot find webauthn user directory (webauthn_data_dir). Add it to your config file") 43 | 44 | webauthn_data_dir = cfg['websphinx']['webauthn_data_dir'] 45 | 46 | def handler(cb, cmd, *args): 47 | m = Multiplexer(sphinx.servers) 48 | m.connect() 49 | cb(cmd(m, *args)) 50 | m.close() 51 | 52 | def getpwd(title): 53 | proc=subprocess.Popen([pinentry, '-g'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 54 | out, err = proc.communicate(input=('SETTITLE sphinx password prompt\nSETDESC %s\nSETPROMPT master password\ngetpin\n' % (title)).encode()) 55 | if proc.returncode == 0: 56 | for line in out.split(b'\n'): 57 | if line.startswith(b"D "): return line[2:] 58 | 59 | def fetchOK(proc, cmd): 60 | proc.stdin.write(f"{cmd}\n".encode("utf8")) 61 | proc.stdin.flush() 62 | if((line:=proc.stdout.readline())!=b"OK\n"): 63 | raise ValueError(f"fail \"{cmd}\": {line}") 64 | 65 | def pwdq(pwd): 66 | q = zxcvbn(pwd.decode('utf8')) 67 | q['guesses'] 68 | q['score'] 69 | q['crack_times_display'] 70 | q['feedback'] 71 | 72 | proc=subprocess.Popen([pinentry, '-g'], 73 | stdin=subprocess.PIPE, 74 | stdout=subprocess.PIPE, 75 | stderr=subprocess.PIPE) 76 | 77 | if not (resp:=proc.stdout.readline()).startswith(b'OK Pleased to meet you'): 78 | raise ValueError(f"strange greeting \"{resp}\"") 79 | fetchOK(proc ,"SETTITLE Password Quality Check") 80 | fetchOK(proc ,"SETOK use this") 81 | fetchOK(proc ,"SETCANCEL try another") 82 | fetchOK(proc ,"SETDESC your %s%s (%s/4) master password:%%0a - can be online recovered in %s,%%0a - offline in %s,%%0a - trying ~%s guesses%%0a%%0aAre you sure you want to use this password?" % 83 | ("★" * q['score'], 84 | "☆" * (4-q['score']), 85 | q['score'], 86 | q['crack_times_display']['online_throttling_100_per_hour'], 87 | q['crack_times_display']['offline_slow_hashing_1e4_per_second'], 88 | q['guesses'])) 89 | try: 90 | fetchOK(proc ,"CONFIRM") 91 | except ValueError: 92 | return False 93 | return True 94 | 95 | # Send message using Native messaging protocol 96 | def send_message(data): 97 | msg = json.dumps(data).encode('utf-8') 98 | if log: 99 | log.write(msg) 100 | log.write(b'\n') 101 | log.flush() 102 | length = struct.pack('@I', len(msg)) 103 | sys.stdout.buffer.write(length) 104 | sys.stdout.buffer.write(msg) 105 | sys.stdout.buffer.flush() 106 | 107 | def users(data): 108 | def callback(users): 109 | res = {'names': [i for i in users.split("\n")], 110 | 'cmd': 'list', "mode": data['mode'], 'site': data['site']} 111 | send_message({ 'results': res }) 112 | 113 | try: 114 | handler(callback, sphinx.users, data['site']) 115 | except: 116 | send_message({ 'results': 'fail' }) 117 | 118 | def get(data): 119 | def callback(arg): 120 | res = { 'password': arg, 'name': data['name'], 'site': data['site'], 'cmd': 'login', "mode": data['mode']} 121 | send_message({ 'results': res }) 122 | try: 123 | pwd=getpwd("get password for user \"%s\" at host \"%s\"" % (data['name'], data['site'])) 124 | handler(callback, sphinx.get, pwd, data['name'], data['site']) 125 | except: 126 | send_message({ 'results': 'fail' }) 127 | 128 | def create(data): 129 | def callback(arg): 130 | res = { 'password': arg, 'name': data['name'], 'site': data['site'], 'cmd': 'create', "mode": data['mode']} 131 | send_message({ 'results': res }) 132 | try: 133 | pwd=None 134 | while not pwd: 135 | pwd=getpwd("create password for user \"%s\" at host \"%s\"%%0a" % (data['name'], data['site'])) 136 | pwd2=getpwd("REPEAT: create for user \"%s\" at host \"%s\"%%0a" % (data['name'], data['site'])) 137 | if pwd != pwd2: 138 | send_message({ 'results': 'fail' }) 139 | return 140 | if not pwdq(pwd): pwd=None 141 | 142 | symbols = '' 143 | if 's' in data['rules']: 144 | symbols = bin2pass.symbols 145 | data['rules'] = ''.join(set(data['rules']) - set(['s'])) 146 | handler(callback, sphinx.create, pwd, data['name'], data['site'], data['rules'], symbols, int(data['size']), None) 147 | except: 148 | send_message({ 'results': 'fail' }) 149 | 150 | def change(data): 151 | def callback(arg): 152 | res = { 'password': arg, 'name': data['name'], 'site': data['site'], 'cmd': 'change', "mode": data['mode']} 153 | send_message({ 'results': res }) 154 | try: 155 | oldpwd="" 156 | if cfg['client'].get('rwd_keys'): 157 | oldpwd=getpwd("current password for \"%s\" at host: \"%s\"%%0a" % (data['name'], data['site'])) 158 | pwd=None 159 | while not pwd: 160 | pwd=getpwd("new password for user \"%s\" at host \"%s\"%%0a" % (data['name'], data['site'])) 161 | pwd2=getpwd("REPEAT: new for user \"%s\" at host \"%s\"%%0a" % (data['name'], data['site'])) 162 | if pwd != pwd2: 163 | send_message({ 'results': 'fail' }) 164 | return 165 | if not pwdq(pwd): pwd=None 166 | 167 | symbols = '' 168 | if 's' in data['rules']: 169 | symbols = bin2pass.symbols 170 | data['rules'] = ''.join(set(data['rules']) - set(['s'])) 171 | handler(callback, sphinx.change, oldpwd, pwd, data['name'], data['site'], data['rules'], symbols, int(data['size']), None) 172 | except: 173 | send_message({ 'results': 'fail' }) 174 | 175 | def commit(data): 176 | def callback(arg): 177 | res = { 'result': arg, 'name': data['name'], 'site': data['site'], 'cmd': 'commit', "mode": data['mode']} 178 | send_message({ 'results': res }) 179 | try: 180 | pwd="" 181 | if cfg['client'].get('rwd_keys'): 182 | pwd=getpwd("commit password for \"%s\" at host: \"%s\"%%0a" % (data['name'], data['site'])) 183 | handler(callback, sphinx.commit, pwd, data['name'], data['site']) 184 | except: 185 | send_message({ 'results': 'fail' }) 186 | 187 | def undo(data): 188 | def callback(arg): 189 | res = { 'result': arg, 'name': data['name'], 'site': data['site'], 'cmd': 'undo', "mode": data['mode']} 190 | send_message({ 'results': res }) 191 | try: 192 | pwd="" 193 | if cfg['client'].get('rwd_keys'): 194 | pwd=getpwd("undo password for \"%s\" at host: \"%s\"%%0a" % (data['name'], data['site'])) 195 | handler(callback, sphinx.undo, pwd, data['name'], data['site']) 196 | except: 197 | send_message({ 'results': 'fail' }) 198 | 199 | def qrcode(data): 200 | try: 201 | sphinx.qrcode("svg", True) 202 | res = { 'result': arg, 'cmd': 'qrcode', "mode": data['mode']} 203 | send_message({ 'results': res }) 204 | except: 205 | send_message({ 'results': 'fail' }) 206 | 207 | 208 | def echo(data): 209 | send_message(data) 210 | 211 | def webauthn_create(data): 212 | try: 213 | pwd=None 214 | while not pwd: 215 | pwd=getpwd("create password for user \"%s\" at host \"%s\"%%0a" % (data['name'], data['site'])) 216 | pwd2=getpwd("REPEAT: create for user \"%s\" at host \"%s\"%%0a" % (data['name'], data['site'])) 217 | if pwd != pwd2: 218 | send_message({ 'results': 'fail', 'id': data['id'], 'tabId': data['tabId'], 'cmd': res['cmd']}) 219 | return 220 | if not pwdq(pwd): pwd=None 221 | # TODO use webauthn:// instead of raw:// - don't forget to rewrite it in handle() 222 | rand_bytes = pysodium.randombytes(32) 223 | pk, sk = pysodium.crypto_sign_seed_keypair(rand_bytes) 224 | # signed_challenge = pysodium.crypto_sign(data['challenge'], sk) 225 | auth_data = pysodium.crypto_hash_sha256(data['site'].encode('utf8')) + ( # rpIdHash 226 | b'\x4d' + # flags https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Authenticator_data 227 | b'\x00' * 4 + # signCount 228 | b'\x00' * 16 + # attestedCredentialData->AAGUID 229 | struct.pack('>H', len(pk)) + # attestedCredentialData->pk length 230 | pk + # attestedCredentialData->credentialId (pk) 231 | cbor2.dumps({ # cose encoded public key https://datatracker.ietf.org/doc/html/rfc8152#section-13 232 | 1: 1, # key type => okp 233 | 3: -8, # algorithm => Ed25519 (-8) 234 | -1: 6, # curve => ed25519 235 | -2: pk, # x => pubkey 236 | # no y required (y is -3) 237 | })) # attestedCredentialData->credentialPublicKey 238 | 239 | # TODO use opaque 240 | with open(webauthn_data_dir + "/" + pk.hex(), "wb") as wf: 241 | wf.write(urlsafe_b64decode(data['userid']+"==")) 242 | 243 | att_sig = pysodium.crypto_sign_detached(auth_data + pysodium.crypto_hash_sha256(data['clientDataJSON'].encode('utf-8')), sk) 244 | 245 | attestation = cbor2.dumps({ 246 | 'fmt': 'packed', 247 | 'attStmt': { 248 | 'alg': -8, 249 | 'sig': att_sig, 250 | }, 251 | 'authData': auth_data 252 | }) 253 | #log.write((data['clientDataJSON']+"\n").encode('utf-8')) 254 | #log.write((auth_data.hex() + "\n").encode('utf-8')) 255 | 256 | res = { 257 | 'pk': b2a_base64(pk).decode('utf8').strip(), 258 | 'attestationObject': b2a_base64(attestation).decode('utf-8').strip(), 259 | 'authData': b2a_base64(auth_data).decode('utf-8').strip(), 260 | 'name': data['name'], 261 | 'site': data['site'], 262 | 'cmd': 'webauthn-create', 263 | 'id': data['id'], 264 | 'tabId': data['tabId'] 265 | } 266 | 267 | # create new user with pk 268 | m = Multiplexer(sphinx.servers) 269 | m.connect() 270 | # TODO add optional argument to create() to skip extending the userlist 271 | orig_userlist = sphinx.userlist 272 | sphinx.userlist = False 273 | sphinx.create(m, pwd, 'raw://'+pk.hex(), data['site'], '', '', target = rand_bytes) 274 | sphinx.userlist = orig_userlist 275 | m.close() 276 | clearmem(sk) 277 | clearmem(rand_bytes) 278 | send_message({'results': res}) 279 | except Exception as e: 280 | send_message({ 'results': 'fail', 'id': data.get('id', ''), 'tabId': data.get('tabId', -1), 'cmd': 'webauthn-create'}) 281 | #send_message({ 'results': 'fail', 'id': data.get('id', ''), 'tabId': data.get('tabId', -1), 'cmd': 'webauthn-create', 'exception': str(e)}) 282 | 283 | def webauthn_get(data): 284 | def callback(rand_bytes): 285 | pk, sk = pysodium.crypto_sign_seed_keypair(rand_bytes) 286 | clearmem(rand_bytes) 287 | 288 | # TODO use opaque 289 | with open(webauthn_data_dir + "/" + pk.hex(), "rb") as wf: 290 | userid = wf.read() 291 | 292 | auth_data = pysodium.crypto_hash_sha256(data['site'].encode('utf8')) + ( # rpIdHash 293 | b'\x4d' + # flags https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/Authenticator_data 294 | b'\x00' * 4 + # signCount 295 | b'\x00' * 16 + # attestedCredentialData->AAGUID 296 | struct.pack('>H', len(pk)) + # attestedCredentialData->pk length 297 | pk + # attestedCredentialData->credentialId (pk) 298 | cbor2.dumps({ # cose encoded public key https://datatracker.ietf.org/doc/html/rfc8152#section-13 299 | 1: 1, # key type => okp 300 | 3: -8, # algorithm => Ed25519 (-8) 301 | -1: 6, # curve => ed25519 302 | -2: pk, # x => pubkey 303 | # no y required (y is -3) 304 | })) # attestedCredentialData->credentialPublicKey 305 | sig = pysodium.crypto_sign_detached(auth_data + pysodium.crypto_hash_sha256(data['clientDataJSON'].encode('utf-8')), sk) 306 | clearmem(sk) 307 | 308 | res = { 309 | 'userid': b2a_base64(userid).decode('utf-8').strip(), 310 | 'sig': b2a_base64(sig).decode('utf8').strip(), 311 | 'authData': b2a_base64(auth_data).decode('utf-8').strip(), 312 | 'site': data['site'], 313 | 'cmd': 'webauthn-get', 314 | 'id': data['id'], 315 | 'tabId': data['tabId'], 316 | 'pk': b2a_base64(pk).decode('utf-8').strip(), 317 | } 318 | send_message({'results': res}) 319 | try: 320 | pwd=getpwd("get webauthn password at host \"%s\"" % data['site']) 321 | pk = urlsafe_b64decode(data['pk']+"==") 322 | handler(callback, sphinx.get, pwd, 'raw://'+pk.hex(), data['site']) 323 | except Exception as e: 324 | send_message({ 'results': 'fail', 'id': data.get('id', ''), 'tabId': data.get('tabId', -1), 'cmd': 'webauthn-get'}) 325 | #send_message({ 'results': 'fail', 'id': data.get('id', ''), 'tabId': data.get('tabId', -1), 'cmd': 'webauthn-get', 'exception': str(e)}) 326 | 327 | 328 | func_map = { 329 | 'login': get, 330 | 'list': users, 331 | 'create': create, 332 | 'change': change, 333 | 'commit': commit, 334 | 'undo': undo, 335 | 'qrcode': qrcode, 336 | 'echo': echo, 337 | 'webauthn-create': webauthn_create, 338 | 'webauthn-get': webauthn_get, 339 | } 340 | 341 | 342 | def main(): 343 | global log 344 | if log: log = open(log,'ab') 345 | while True: 346 | # Read message using Native messaging protocol 347 | length_bytes = sys.stdin.buffer.read(4) 348 | if len(length_bytes) == 0: 349 | return 350 | 351 | length = struct.unpack('i', length_bytes)[0] 352 | data = json.loads(sys.stdin.buffer.read(length).decode('utf-8')) 353 | 354 | if log: 355 | log.write(repr(data).encode()) 356 | log.write(b'\n') 357 | log.flush() 358 | 359 | if data['cmd'] in func_map: 360 | func_map[data['cmd']](data) 361 | else: 362 | send_message({ 'results': 'fail' }) 363 | 364 | if __name__ == '__main__': 365 | main() 366 | -------------------------------------------------------------------------------- /ext/popup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of WebSphinx. 3 | * Copyright (C) 2017 Iwan Timmer 4 | * Copyright (C) 2018 pitchfork@ctrlc.hu 5 | * 6 | * WebSphinx is free software; you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation; either version 2 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * WebSphinx is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License along 17 | * with this program; if not, write to the Free Software Foundation, Inc., 18 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | */ 20 | 21 | "use strict"; 22 | 23 | const APP_NAME = "websphinx"; 24 | 25 | var browser = browser || chrome; 26 | 27 | var self = null; 28 | 29 | class Sphinx { 30 | constructor() { 31 | this.selectionIndex = -1; 32 | this.site = ''; 33 | this.user = ''; 34 | this.users = []; 35 | this.mode = ''; 36 | this.inputs = 0; 37 | this.background = browser.runtime.connect({'name': 'popup'}); 38 | 39 | browser.tabs.query({ currentWindow: true, active: true }, this.onTabs.bind(this)); 40 | 41 | // tabs + close 42 | document.getElementById("login_tab").addEventListener("click",this.switchTab.bind(this)); 43 | document.getElementById("create_tab").addEventListener("click",this.switchTab.bind(this)); 44 | document.getElementById("change_tab").addEventListener("click",this.switchTab.bind(this)); 45 | document.getElementById("close").addEventListener("click",function() {window.close();}); 46 | 47 | // manual get/insert buttons 48 | document.getElementById("login_pwd").addEventListener("click",this.getpwd); 49 | document.getElementById("old_pwd").addEventListener("click",this.getpwd); 50 | document.getElementById("new_pwd").addEventListener("click",this.newpwd); 51 | document.getElementById("create_pwd").addEventListener("click",this.createpwd); 52 | 53 | document.getElementById('save_pwd').addEventListener("click", this.onClickCommit.bind(this)); 54 | 55 | let size_wdgt = document.getElementById("pwdlen"); 56 | size_wdgt.setAttribute("placeholder", browser.i18n.getMessage("sizePlaceholder")); 57 | size_wdgt.addEventListener("keydown", this.onKeyDownCreate); 58 | 59 | this.search = document.getElementById("search"); 60 | this.search.setAttribute("placeholder", browser.i18n.getMessage("searchPlaceholder")); 61 | this.search.addEventListener("blur", this.onBlur.bind(this)); 62 | 63 | self = this; 64 | } 65 | 66 | userList() { 67 | setTimeout(() => { 68 | this.search.focus(); 69 | }, 100); 70 | 71 | let ul = document.getElementById("results"); 72 | if(ul.firstChild == null && this.users.length>0) { 73 | for (let result of this.users) { 74 | let domain = result.split('/').reverse()[0]; 75 | 76 | let item = document.createElement("li"); 77 | let button = document.createElement("button"); 78 | //let favicon = document.createElement("img"); 79 | let label = document.createElement("span"); 80 | 81 | label.textContent = result; 82 | button.addEventListener("click", this.onClick.bind(this)); 83 | 84 | button.appendChild(label); 85 | item.appendChild(button); 86 | ul.appendChild(item); 87 | } 88 | results.className = null; 89 | } 90 | } 91 | 92 | commit_ui() { 93 | this.select_tab('change'); 94 | //document.getElementById("change_phase1").className = "hidden"; 95 | document.getElementById("change_phase2").className = null; 96 | document.getElementById("autofill").className = "hidden"; 97 | document.getElementById("results").className = "hidden"; 98 | } 99 | 100 | decide() { 101 | if(this.inputs == 1) { // one password field -> probably login 102 | // only one user in our db - use that to auto login 103 | this.select_tab('login'); 104 | 105 | if(this.users.length == 1) { 106 | this.background.postMessage({ "action": "login", "site": this.site, "name": this.users[0], "mode": "insert" }); 107 | //window.close(); 108 | return; 109 | } 110 | // user set in the forms user field, auto select that user 111 | for(let user of this.users) { 112 | if(user == this.user && user != '') { 113 | this.background.postMessage({ "action": "login", "site": this.site, "name": user, "mode": "insert" }); 114 | //window.close(); 115 | return; 116 | } 117 | } 118 | // can't decide let user select which username to use for login 119 | } else if(this.inputs == 2) { 120 | // 2 password fields -> either create user, or login with OTP field. 121 | if(this.users.length == 0) { // no users associated wit this site, should be a register form 122 | this.select_tab('create'); 123 | return; 124 | } 125 | // if there is already a user specified in the username field 126 | // and that is a registered user with us, then we assume it's a 127 | // login form 128 | for(let user of this.users) { 129 | if(user == this.user && user != '') { 130 | this.background.postMessage({ "action": "login", "site": this.site, "name": user, "mode": "insert" }); 131 | this.select_tab('login'); 132 | return; 133 | } 134 | } 135 | // unsure: could be a login form with an OTP field, but could also be a registration form. 136 | this.select_tab('create'); 137 | return; 138 | } else if(this.inputs == 3) { // probably change password field 139 | if(this.users.length == 0) { // no users associated wit this site, can't be a change password form 140 | // todo handle this case, but how? 141 | this.select_tab('create'); 142 | return; 143 | } 144 | if(this.users.length == 1) { // we have only one registered user with this site, so it's easy 145 | this.commit_ui(); 146 | this.background.postMessage({ "action": "change", "site": this.site, "name": this.users[0], "mode": "insert" }); 147 | //window.close(); 148 | return; 149 | } 150 | // choose user to change password for 151 | this.select_tab('change'); 152 | } else { 153 | console.log("are you kidding me?"); 154 | } 155 | } 156 | 157 | recon_cb(response) { 158 | browser.runtime.onMessage.removeListener(self.recon_cb); 159 | //console.log(response); 160 | self.user = response.username; 161 | self.inputs = response.password_fields; 162 | self.decide(); 163 | } 164 | 165 | get_users_cb(response) { 166 | self.background.onMessage.removeListener(self.get_users_cb); 167 | //console.log(response); 168 | if(response.results) { 169 | self.users = response.results.names; 170 | } 171 | 172 | // now also figure out what this page is about 173 | browser.tabs.executeScript({ file: '/inject.js', allFrames: true }, function() { 174 | browser.tabs.executeScript({code: 'document.websphinx.recon();'}); 175 | }); 176 | } 177 | 178 | onTabs(tabs) { 179 | // clear user list 180 | let results = document.getElementById("results"); 181 | while (results.firstChild) 182 | results.removeChild(results.firstChild); 183 | 184 | if (tabs[0] && tabs[0].url) { 185 | this.site = new URL(tabs[0].url).hostname; 186 | } 187 | 188 | browser.runtime.onMessage.addListener(this.recon_cb); 189 | this.background.onMessage.addListener(this.get_users_cb); 190 | 191 | // first get users associated with this site 192 | this.background.postMessage({ 193 | action: "list", 194 | mode: "init", 195 | site: this.site 196 | }); 197 | } 198 | 199 | onBlur(event) { 200 | let results = document.getElementById("results"); 201 | if (results.children[this.selectionIndex]) 202 | results.children[this.selectionIndex].className = ''; 203 | 204 | this.selectionIndex = -1; 205 | } 206 | 207 | commit_cb(response) { 208 | self.background.onMessage.removeListener(self.commit_cb); 209 | console.log(response); 210 | // todo better handling 211 | } 212 | 213 | onClickCommit(event) { 214 | this.background.onMessage.addListener(this.commit_cb); 215 | this.background.postMessage({ "action": "commit", "site": this.site, "name": this.user, "mode": "" }); 216 | window.close(); 217 | } 218 | 219 | onAutoClick(event) { 220 | self.background.postMessage({ "action": self.mode, "site": self.site, "name": self.search.value, "mode": "insert" }); 221 | } 222 | 223 | onClick(event) { 224 | this.background.postMessage({ "action": this.mode, "site": this.site, "name": event.target.textContent, "mode": "insert" }); 225 | if(this.mode != 'change') window.close(); 226 | else { 227 | this.user = event.target.textContent; 228 | this.commit_ui(); 229 | } 230 | } 231 | 232 | onKeyDown(event) { 233 | let results = document.getElementById("results"); 234 | if (event.keyCode == 0x0d) { 235 | if(self.search.value!='') { 236 | self.background.postMessage({ "action": self.mode, "site": self.site, "name": self.search.value, "mode": "insert" }); 237 | } else if(results.children[self.selectionIndex]) { 238 | self.background.postMessage({ "action": self.mode, "site": self.site, "name": results.children[self.selectionIndex].textContent, "mode": "insert" }); 239 | } 240 | if(self.mode != 'change') window.close(); 241 | else self.commit_ui(); 242 | } else if (event.keyCode == 0x26 && self.selectionIndex > 0) 243 | self.selectionIndex--; 244 | else if (event.keyCode == 0x28 && self.selectionIndex < results.childElementCount - 1) 245 | self.selectionIndex++; 246 | else 247 | return; 248 | 249 | for (let e of results.getElementsByClassName('focus')) 250 | e.className = ''; 251 | 252 | if(self.selectionIndex >= 0 && self.selectionIndex < results.childElementCount) { 253 | results.children[self.selectionIndex].className = "focus"; 254 | } 255 | event.preventDefault(); 256 | } 257 | 258 | flashError(el) { 259 | el.style="background: red;"; 260 | setTimeout(() => { 261 | el.focus(); 262 | }, 100); 263 | setTimeout(() => { 264 | el.style=''; 265 | }, 1000); 266 | } 267 | 268 | getpwdrules() { 269 | const rules = [{"title": "Upper", "value": 'u',}, 270 | {"title": "Lower", "value": 'l'}, 271 | {"title": "Symbols", "value": "s"}, 272 | {"title": "Digits", "value": "d"}]; 273 | 274 | // get character class rules 275 | let r = ""; 276 | for (let rule of rules) { 277 | let checkbox = document.getElementById(rule['title']); 278 | if(checkbox.checked) r+=rule['value']; 279 | } 280 | if(r=="") { 281 | for (let rule of rules) { 282 | let checkbox = document.getElementById(rule['title']); 283 | let label = checkbox.nextSibling; 284 | label.style="background: red;"; 285 | setTimeout(() => { 286 | checkbox.focus(); 287 | }, 100); 288 | setTimeout(() => { 289 | label.style=''; 290 | }, 1000); 291 | } 292 | return; 293 | } 294 | // get password size 295 | let size = 0; 296 | let input = document.getElementById('pwdlen'); 297 | if(input.value != '') { 298 | try { size = Number(input.value) } catch (e) { 299 | this.sizeError(input); 300 | return; 301 | } 302 | } 303 | if(isNaN(size)) { 304 | this.flashError(input); 305 | return; 306 | } 307 | return [r,size]; 308 | } 309 | 310 | submitCreate() { 311 | let r_ = self.getpwdrules(); 312 | if (r_ == null) return; 313 | let r=r_[0], size = r_[1]; 314 | 315 | if(self.user == '') { 316 | if(self.search.value == '') { 317 | self.flashError(self.search); 318 | return; 319 | } 320 | // we assume the value of the search field to be the username to be created 321 | self.user = self.search.value; 322 | } 323 | self.background.postMessage({ "action": "create", "site": self.site, "name": self.user, "rules": r, "size": size, "mode": "insert" }); 324 | window.close(); 325 | } 326 | 327 | onKeyDownCreate(event) { 328 | if (event.keyCode == 0x0d) { 329 | self.submitCreate(); 330 | event.preventDefault(); 331 | } 332 | } 333 | 334 | select_tab(tabid) { 335 | let tab = document.getElementById(tabid+'_tab'); 336 | this.switchTab({target: tab}); 337 | } 338 | 339 | switchTab(event) { 340 | if(this.mode==event.target.id.slice(0,-4)) return; 341 | 342 | // set mode 343 | this.mode = event.target.id.slice(0,-4); 344 | 345 | let autofill = document.getElementById("autofill"); 346 | let tabs = document.getElementById("tabs"); 347 | for (let selected of tabs.getElementsByClassName('selected')) { 348 | selected.className="inactive"; 349 | let tab = selected.id.slice(0,-4); 350 | document.getElementById(tab).className="hidden"; 351 | // remove event listeners 352 | if(tab == "create") { 353 | this.search.removeEventListener("keydown", this.onKeyDownCreate); 354 | autofill.removeEventListener("click", this.submitCreate); 355 | } else { 356 | this.search.removeEventListener("keydown", this.onKeyDown); 357 | autofill.removeEventListener("click", this.onAutoClick); 358 | } 359 | } 360 | event.target.className="selected"; 361 | 362 | let selected = document.getElementById(this.mode); 363 | selected.className=null; 364 | 365 | if(this.mode=="create") { 366 | document.getElementById("results").className = "hidden"; 367 | this.search.addEventListener("keydown", this.onKeyDownCreate); 368 | autofill.addEventListener("click", this.submitCreate); 369 | 370 | for(let u of this.users) { 371 | if(this.user == u) this.user = ''; 372 | if(this.search.value==u) this.search.value = ''; 373 | } 374 | 375 | if(this.user!='' && this.search.value=='') this.search.value=this.user; 376 | else if(this.user=='' && this.search.value!='') this.user=this.search.value; 377 | else if(this.user!='' && this.search.value!='' && this.user!=this.search.value) this.user=this.search.value; 378 | 379 | if(this.user == '') setTimeout(() => {this.search.focus();}, 100); 380 | else setTimeout(() => {document.getElementById("pwdlen").focus();}, 100); 381 | } else { 382 | this.search.addEventListener("keydown", this.onKeyDown); 383 | autofill.addEventListener("click", this.onAutoClick); 384 | this.userList(); 385 | if(this.user!='') this.search.value=this.user; 386 | 387 | let sf = false, uf = false; 388 | for(let u of this.users) { 389 | if(this.user == u) uf=true; 390 | if(this.search.value==u) sf=true; 391 | } 392 | if(sf && !uf) this.user=this.search.value; 393 | else if(!sf && uf) this.search.value=this.user; 394 | else if(sf && uf && this.user!=this.search.value) this.user=this.search.value; 395 | } 396 | 397 | if(this.user!='') { 398 | autofill.className = "insert"; 399 | } 400 | } 401 | 402 | fetchpwd(el, eh, mode, rules, size) { 403 | if(this.user == '') { 404 | if(this.search.value == '') { 405 | this.flashError(this.search); 406 | return; 407 | } 408 | // we assume the value of the search field to be the username to be created 409 | this.user = this.search.value; 410 | } 411 | 412 | // todo on change password when inserting new password also show save new password button 413 | let fetchpwd_cb = function(response) { 414 | self.background.onMessage.removeListener(self.fetchpwd_cb); 415 | el.getElementsByClassName('pwd_action')[0].textContent = "Insert"; 416 | el.removeEventListener("click", eh); 417 | el.addEventListener("click",function(e) { 418 | // inject response.password into currently focused element 419 | browser.tabs.executeScript({code: 'document.websphinx.inject(' + JSON.stringify(response.results.password) + ');'}); 420 | }); 421 | if(mode=="change") self.commit_ui(); 422 | } 423 | 424 | this.background.onMessage.addListener(fetchpwd_cb); 425 | this.background.postMessage({ "action": mode, 426 | "site": this.site, 427 | "name": this.user, 428 | "rules": rules, // optional only used with create 429 | "size": size, // optional only used with create 430 | "mode": "manual" }); // needed for background to trigger proper callback 431 | } 432 | 433 | getpwd(e) { 434 | self.fetchpwd(e.target, self.getpwd, "login", null, null); 435 | } 436 | 437 | newpwd(e) { 438 | self.fetchpwd(e.target, self.newpwd, "change", null, null); 439 | } 440 | 441 | createpwd(e) { 442 | let r_ = self.getpwdrules(); 443 | if (r_ == null) return; 444 | let r=r_[0], size = r_[1]; 445 | self.fetchpwd(e.target, self.createpwd, "create", r, size); 446 | } 447 | 448 | } 449 | 450 | document.addEventListener("DOMContentLoaded", function(event) { 451 | new Sphinx(); 452 | }); 453 | -------------------------------------------------------------------------------- /LICENSES/CC-BY-SA-4.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Creative Commons Attribution-ShareAlike 4.0 International 3 | 4 | Creative Commons Corporation ("Creative Commons") is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an "as-is" basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible. 5 | 6 | Using Creative Commons Public Licenses 7 | 8 | Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses. 9 | 10 | Considerations for licensors: Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors 11 | 12 | Considerations for the public: By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensor's permission is not necessary for any reason–for example, because of any applicable exception or limitation to copyright–then that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. 13 | 14 | Although not required by our licenses, you are encouraged to respect those requests where reasonable. More considerations for the public : wiki.creativecommons.org/Considerations_for_licensees 15 | 16 | Creative Commons Attribution-ShareAlike 4.0 International Public License 17 | 18 | By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions. 19 | 20 | Section 1 – Definitions. 21 | 22 | a. Adapted Material means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image. 23 | b. Adapter's License means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License. 24 | c. BY-SA Compatible License means a license listed at creativecommons.org/compatiblelicenses, approved by Creative Commons as essentially the equivalent of this Public License. 25 | d. Copyright and Similar Rights means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights. 26 | e. Effective Technological Measures means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements. 27 | f. Exceptions and Limitations means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material. 28 | g. License Elements means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike. 29 | h. Licensed Material means the artistic or literary work, database, or other material to which the Licensor applied this Public License. 30 | i. Licensed Rights means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license. 31 | j. Licensor means the individual(s) or entity(ies) granting rights under this Public License. 32 | k. Share means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them. 33 | l. Sui Generis Database Rights means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world. 34 | m. You means the individual or entity exercising the Licensed Rights under this Public License. Your has a corresponding meaning. 35 | 36 | Section 2 – Scope. 37 | 38 | a. License grant. 39 | 1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to: 40 | A. reproduce and Share the Licensed Material, in whole or in part; and 41 | B. produce, reproduce, and Share Adapted Material. 42 | 2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions. 43 | 3. Term. The term of this Public License is specified in Section 6(a). 44 | 4. Media and formats; technical modifications allowed. The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material. 45 | 5. Downstream recipients. 46 | A. Offer from the Licensor – Licensed Material. Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License. 47 | B. Additional offer from the Licensor – Adapted Material. Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapter's License You apply. 48 | C. No downstream restrictions. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material. 49 | 6. No endorsement. Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i). 50 | b. Other rights. 51 | 1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise. 52 | 2. Patent and trademark rights are not licensed under this Public License. 53 | 3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties. 54 | 55 | Section 3 – License Conditions. 56 | 57 | Your exercise of the Licensed Rights is expressly made subject to the following conditions. 58 | 59 | a. Attribution. 60 | 1. If You Share the Licensed Material (including in modified form), You must: 61 | A. retain the following if it is supplied by the Licensor with the Licensed Material: 62 | i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated); 63 | ii. a copyright notice; 64 | iii. a notice that refers to this Public License; 65 | iv. a notice that refers to the disclaimer of warranties; 66 | 67 | v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable; 68 | B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and 69 | C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License. 70 | 2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information. 71 | 3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable. 72 | b. ShareAlike.In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply. 73 | 1. The Adapter's License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License. 74 | 2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material. 75 | 3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply. 76 | 77 | Section 4 – Sui Generis Database Rights. 78 | 79 | Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material: 80 | 81 | a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database; 82 | b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and 83 | c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database. 84 | For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights. 85 | 86 | Section 5 – Disclaimer of Warranties and Limitation of Liability. 87 | 88 | a. Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You. 89 | b. To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You. 90 | c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability. 91 | 92 | Section 6 – Term and Termination. 93 | 94 | a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically. 95 | b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates: 96 | 1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or 97 | 2. upon express reinstatement by the Licensor. 98 | c. For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License. 99 | d. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License. 100 | e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License. 101 | 102 | Section 7 – Other Terms and Conditions. 103 | 104 | a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed. 105 | b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License. 106 | 107 | Section 8 – Interpretation. 108 | 109 | a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License. 110 | b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions. 111 | c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor. 112 | d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority. 113 | 114 | Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the "Licensor." The text of the Creative Commons public licenses is dedicated to the public domain under the CC0 Public Domain Dedication. Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at creativecommons.org/policies, Creative Commons does not authorize the use of the trademark "Creative Commons" or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses. 115 | 116 | Creative Commons may be contacted at creativecommons.org. 117 | 118 | --------------------------------------------------------------------------------