Files
devx/src/devx/utils/crypto.py
T
emil ef3b882e5b
Post-merge / detect-type (push) Successful in 13s
Post-merge / validate-commit-msg (push) Successful in 10s
Post-merge / configure-repo (push) Successful in 28s
Post-merge / vikunja (push) Successful in 44s
Post-merge / sync-wiki (push) Successful in 58s
Post-merge / release (push) Successful in 1m7s
Post-merge / publish (push) Successful in 44s
Post-merge / badges (push) Successful in 1m6s
DEVX-124: feat: extract shared utilities from infra and grm into devx
2026-07-09 11:51:50 +00:00

74 lines
2.1 KiB
Python

"""Cryptographic secret generation helpers.
Provides safe secret/password generators that avoid shell-option
interpretation issues (e.g. leading ``-`` being parsed as a flag by
``su -c`` in Docker entrypoints).
Usage::
from devx.utils.crypto import generate_secret, generate_password
api_key = generate_secret()
db_password = generate_password(length=32)
"""
from __future__ import annotations
import secrets
_SYMBOLS = "!@#$%^&*()-_=+[]{}|;:,.<>?"
_UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
_LOWER = "abcdefghijklmnopqrstuvwxyz"
_DIGITS = "0123456789"
def generate_secret() -> str:
"""Generate a URL-safe secret that never starts with ``-``.
A leading ``-`` causes passwords to be interpreted as command-line
options when passed through shell expansion chains (e.g. Nextcloud's
Docker entrypoint uses ``su -c`` which strips quoting).
Returns:
A 43-character URL-safe base64 secret.
"""
value = secrets.token_urlsafe(32)
while value.startswith("-"):
value = secrets.token_urlsafe(32)
return value
def generate_password(length: int = 32) -> str:
"""Generate a password guaranteed to contain upper, lower, digit, and symbol.
The first character is always alphanumeric to avoid being interpreted
as a command-line option when passed through shell expansion chains.
Args:
length: Desired password length (minimum 4).
Returns:
A password string with guaranteed character class coverage.
"""
pools = [_UPPER, _LOWER, _DIGITS, _SYMBOLS]
chars = [secrets.choice(p) for p in pools]
all_chars = "".join(pools)
chars += [secrets.choice(all_chars) for _ in range(length - len(pools))]
secrets.SystemRandom().shuffle(chars)
while chars[0] in _SYMBOLS:
secrets.SystemRandom().shuffle(chars)
return "".join(chars)
def generate_hex_secret(length: int = 32) -> str:
"""Generate a hexadecimal secret of the given length.
Args:
length: Desired number of hex characters (doubled internally
since ``token_hex`` produces pairs).
Returns:
A hexadecimal string.
"""
return secrets.token_hex(length // 2)