WIP config refactor
This commit is contained in:
parent
0350f1dd80
commit
11de5948c8
53
Pipfile.lock
generated
53
Pipfile.lock
generated
@ -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": {}
|
||||||
|
}
|
34
mail4one/config.py
Normal file
34
mail4one/config.py
Normal file
@ -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]
|
||||||
|
|
||||||
|
|
40
mail4one/pwhash.py
Normal file
40
mail4one/pwhash.py
Normal file
@ -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"<c{SALT_LEN}s64slhh"
|
||||||
|
|
||||||
|
|
||||||
|
def gen_pwhash(password: str) -> 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)
|
||||||
|
|
22
mail4one/pwhash_test.py
Normal file
22
mail4one/pwhash_test.py
Normal file
@ -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()
|
Loading…
Reference in New Issue
Block a user