Skip to content

Commit

Permalink
Add basic utilities for manipulating pam_tdb data
Browse files Browse the repository at this point in the history
  • Loading branch information
anodos325 committed Sep 10, 2024
1 parent d31a4e0 commit 2c25f66
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 0 deletions.
15 changes: 15 additions & 0 deletions src/middlewared/middlewared/utils/crypto.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from base64 import b64encode
from hashlib import pbkdf2_hmac
from secrets import choice, compare_digest, token_urlsafe, token_hex
from string import ascii_letters, digits, punctuation

Expand Down Expand Up @@ -63,3 +65,16 @@ def generate_nt_hash(passwd):
"""
md4_hash_bytes = md4_hash_blob(passwd.encode('utf-16le'))
return md4_hash_bytes.hex().upper()


def generate_pbkdf2_512(passwd):
"""
Generate a pbkdf2_sha512 hash for password. This is used for
verification of API keys.
"""
prefix = 'pbkdf2-sha512'
rounds = 500000
salt_length = 16
salt = generate_string(string_size=salt_length, extra_chars='./').encode()
hash = pbkdf2_hmac('sha512', passwd.encode(), salt, rounds)
return f'${prefix}${rounds}${b64encode(salt).decode()}${b64encode(hash).decode()}'
6 changes: 6 additions & 0 deletions src/middlewared/middlewared/utils/tdb.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ class TDBHandle:
opath_fd = FD_CLOSED
keys_null_terminated = False

def __enter__(self):
return self

def __exit__(self, tp, val, traceback):
self.close()

def close(self):
""" Close the TDB handle and O_PATH open for the file """
if self.opath_fd == FD_CLOSED and self.hdl is None:
Expand Down
112 changes: 112 additions & 0 deletions src/middlewared/middlewared/utils/user_api_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import os

from base64 import b64encode
from dataclasses import dataclass
from struct import pack
from uuid import uuid4
from .tdb import (
TDBDataType,
TDBHandle,
TDBOptions,
TDBPathType,
)


PAM_TDB_DIR = '/var/run/pam_tdb'
PAM_TDB_FILE = os.path.join(PAM_TDB_DIR, 'pam_tdb.tdb')
PAM_TDB_DIR_MODE = 0o700
PAM_TDB_VERSION = 1
PAM_TDB_MAX_KEYS = 10 # Max number of keys per user

PAM_TDB_OPTIONS = TDBOptions(TDBPathType.CUSTOM, TDBDataType.BYTES)


@dataclass(frozen=True)
class UserApiKey:
expiry: int
userhash: str


@dataclass(frozen=True)
class PamTdbEntry:
keys: list[UserApiKey]
username: str


def _setup_pam_tdb_dir() -> None:
os.makedirs(PAM_TDB_DIR, mode=PAM_TDB_DIR_MODE, exist_ok=True)
os.chmod(PAM_TDB_DIR, PAM_TDB_DIR_MODE)


def _pack_user_auth_key(api_key: UserApiKey) -> bytes:
"""
Convert UserAuthToken to bytes for TDB insertion.
This is packed struct with expiry converted into signed 64 bit
integer, and the userhash converted into a pascal string.
"""
if not isinstance(api_key, UserApiKey):
raise TypeError(f'{type(api_key)}: not a UserAuthToken')

userhash = api_key.userhash.encode() + b'\x00'
return pack(f'<q{len(userhash)}p', api_key.expiry, userhash)


def write_entry(hdl: TDBHandle, entry: PamTdbEntry) -> None:
"""
Convert PamTdbEntry object into a packed struct and insert
into tdb file.
key: username
value: uint32_t (version) + uint32_t (cnt of keys)
"""
if not isinstance(entry, PamTdbEntry):
raise TypeError(f'{type(entry)}: expected UserAuthToken')

key_cnt = len(entry.keys)
if key_cnt > PAM_TDB_MAX_KEYS:
raise ValueError(f'{key_cnt}: count of entries exceeds maximum')

entry_bytes = pack('<II', PAM_TDB_VERSION, len(entry.keys))
parsed_cnt = 0
for key in entry.keys:
entry_bytes += _pack_user_auth_key(key)
parsed_cnt += 1

# since we've already packed struct with array length
# we need to rigidly ensure we don't exceed it.
assert parsed_cnt == key_cnt
hdl.store(entry.username, b64encode(entry_bytes))


def flush_user_api_keys(pam_entries: list[PamTdbEntry]) -> None:
"""
Write a PamTdbEntry object to the pam_tdb file for user
authentication. This method first writes to temporary file
and then renames over pam_tdb file to ensure flush is atomic
and reduce risk of lock contention while under a transaction
lock.
raises:
TypeError - not PamTdbEntry
AssertionError - count of entries changed while generating
tdb payload
RuntimeError - TDB library error
"""
_setup_pam_tdb_dir()

if not isinstance(pam_entries, list):
raise TypeError('Expected list of PamTdbEntry objects')

tmp_path = os.path.join(PAM_TDB_DIR, f'tmp_{uuid4()}.tdb')

with TDBHandle(tmp_path, PAM_TDB_OPTIONS) as hdl:
hdl.keys_null_terminated = False

try:
for entry in pam_entries:
write_entry(hdl, entry)
except Exception:
os.remove(tmp_path)
raise

os.rename(tmp_path, PAM_TDB_FILE)

0 comments on commit 2c25f66

Please sign in to comment.