From 11de5948c87af6a1ed6a24fa2c392e241223c566 Mon Sep 17 00:00:00 2001 From: Balakrishnan Balasubramanian Date: Mon, 15 May 2023 22:41:21 -0400 Subject: [PATCH] WIP config refactor --- Pipfile.lock | 53 +++++++++++++++++++++++++++++++++++++++++ mail4one/config.py | 34 ++++++++++++++++++++++++++ mail4one/pwhash.py | 40 +++++++++++++++++++++++++++++++ mail4one/pwhash_test.py | 22 +++++++++++++++++ 4 files changed, 149 insertions(+) create mode 100644 mail4one/config.py create mode 100644 mail4one/pwhash.py create mode 100644 mail4one/pwhash_test.py diff --git a/Pipfile.lock b/Pipfile.lock index e69de29..72af456 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -0,0 +1,53 @@ +{ + "_meta": { + "hash": { + "sha256": "5c16e20a67c73101d465516d657e21c6c1d3f853ae16dcfe782b7f9e8ba139e5" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "aiosmtpd": { + "hashes": [ + "sha256:f821fe424b703b2ea391dc2df11d89d2afd728af27393e13cf1a3530f19fdc5e", + "sha256:f9243b7dfe00aaf567da8728d891752426b51392174a34d2cf5c18053b63dcbc" + ], + "index": "pypi", + "version": "==1.4.4.post2" + }, + "atpublic": { + "hashes": [ + "sha256:3098ee12d0107cc5009d61f4e80e5edcfac4cda2bdaa04644af75827cb121b18", + "sha256:37f714748e77b8a7b34d59b7b485fd452a0d5906be52cb1bd28d29a2bd84f295" + ], + "markers": "python_version >= '3.7'", + "version": "==3.1.1" + }, + "attrs": { + "hashes": [ + "sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04", + "sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1.0" + }, + "python-jata": { + "hashes": [ + "sha256:3c05e3008721ebf19b95b6c51495afa9df6017f88356e4d48853034a20011579", + "sha256:ff4cd7ca75c9a8306b69ef6e878c296a5602f3279c6f9a82b6105b8eba764760" + ], + "index": "pypi", + "version": "==1.2" + } + }, + "develop": {} +} diff --git a/mail4one/config.py b/mail4one/config.py new file mode 100644 index 0000000..8e5fa5e --- /dev/null +++ b/mail4one/config.py @@ -0,0 +1,34 @@ +import json +from jata import Jata, MutableDefault + + +class Match(Jata): + name: str + alias: list[str] = MutableDefault(lambda: []) + alias_regex: list[str] = MutableDefault(lambda: []) + +class Rule(Jata): + match_name: str + negate: bool = False + stop_check: bool = False + +class Mbox(Jata): + name: str + rules: list[str] + + +class User(Jata): + username: str + password_hash: str + mbox: str + + +class Config(Jata): + certfile: str + keyfile: str + mails_path: str + rules: list[Rule] + boxes: list[Mbox] + users: list[User] + + diff --git a/mail4one/pwhash.py b/mail4one/pwhash.py new file mode 100644 index 0000000..88bee54 --- /dev/null +++ b/mail4one/pwhash.py @@ -0,0 +1,40 @@ +import os +from hashlib import scrypt +from struct import pack, unpack +from base64 import b32encode, b32decode + +# Links +# https://pkg.go.dev/golang.org/x/crypto/scrypt#Key +# https://crypto.stackexchange.com/a/35434 + +SCRYPT_N = 16384 +SCRYPT_R = 8 +SCRYPT_P = 1 +VERSION = b'\x01' +SALT_LEN = 32 +PACK_FMT = f" str: + salt = os.urandom(SALT_LEN) + sh = scrypt(password.encode(), salt=salt, n=SCRYPT_N, r=SCRYPT_R, p=SCRYPT_P) + pack_bytes = pack(PACK_FMT, VERSION, salt, sh, SCRYPT_N, SCRYPT_R, SCRYPT_P) + return b32encode(pack_bytes).decode() + + +class PWInfo: + def __init__(self, salt, sh): + self.salt = salt + self.scrypt_hash = sh + +def parse_hash(pwhash: str) -> PWInfo: + decoded = b32decode(pwhash.encode()) + ver, salt, sh, n, r, p = unpack(PACK_FMT, decoded) + if not (ver, n, r, p, len(salt)) == (VERSION, SCRYPT_N, SCRYPT_R, SCRYPT_P, SALT_LEN): + raise Exception(f"Invalid hash: {ver=}, {n=}, {r=}, {p=}, f{len(salt)=} != {VERSION=}, {SCRYPT_N=}, {SCRYPT_R=}, {SCRYPT_P=}, {SALT_LEN=}") + return PWInfo(salt, sh) + +def check_pass(password: str, pwinfo: PWInfo) -> bool: + # No need for constant time compare for hashes. See https://security.stackexchange.com/a/46215 + return pwinfo.scrypt_hash == scrypt(password.encode(), salt=pwinfo.salt, n=SCRYPT_N, r=SCRYPT_R, p=SCRYPT_P) + diff --git a/mail4one/pwhash_test.py b/mail4one/pwhash_test.py new file mode 100644 index 0000000..778b69f --- /dev/null +++ b/mail4one/pwhash_test.py @@ -0,0 +1,22 @@ +from .pwhash import * + +def check_pass_from_hash(password: str, pwhash: str) -> bool: + try: + pwinfo = parse_hash(pwhash) + except: + return False + return check_pass(password, pwinfo) + +test_hash = "AFWMBONQ2XGHWBTKVECDBBJWYEMS4DFIXIJML4VP76JQT5VWVLALE3KVKFEBAGWG3DOY53DK3H2EACWOBHJFYAIHDA3OFDQN2UAXI5TLBFOW4O2GWXNBGQ5QFMOJ5Z27HGYNO73DS5WPX2INNE47EGI6Z5UAAQAAAAEAAAIA" + +def main(): + print(gen_pwhash("helloworld")) + print("------------") + print(check_pass_from_hash("hElloworld", test_hash)) + print(check_pass_from_hash("helloworld", "foobar")) + print("------------") + print(check_pass_from_hash("helloworld", test_hash)) + + +if __name__ == '__main__': + main()