Public Access
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
74 lines
2.1 KiB
Python
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)
|