Files
2025-09-06 14:11:05 +02:00

209 lines
8.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# generate_passphrase.py
# Generate Passphrase
# Deux modes: random_chars (cryptographique) et diceware (wordlist externe)
#
# Entrées (random_chars):
# length:int, use_lowercase:bool, use_uppercase:bool, use_digits:bool, use_symbols:bool,
# exclude_ambiguous:bool, extra_digits_suffix:int (0..6), deterministic_seed:int (=-1 pour off)
#
# Entrées (diceware):
# diceware_wordlist_path:str, num_words:int, separator:str, extra_digits_suffix:int,
# deterministic_seed:int (=-1 pour off)
#
# Sorties:
# passphrase: STRING
# entropy_bits: FLOAT
# recipe: STRING
#
# Notes:
# - RNG par defaut: secrets (cryptographiquement sûr)
# - Si deterministic_seed >= 0: bascule sur random.Random(seed) (DEV UNIQUEMENT)
import os
import math
import string
import random
from typing import List, Tuple
try:
import secrets
_HAS_SECRETS = True
except Exception:
_HAS_SECRETS = False
CATEGORY = "EncryptMaster"
AMBIGUOUS = set("Il1O0oB8S5Z2")
SYMBOLS_DEFAULT = "!@#$%^&*()_-+=[]{};:,.?/\\|~`"
def _safe_choice(seq, rng_secrets: bool, rnd: random.Random | None):
if rng_secrets and _HAS_SECRETS:
return secrets.choice(seq)
return rnd.choice(seq) # type: ignore
def _safe_randint(a: int, b: int, rng_secrets: bool, rnd: random.Random | None) -> int:
if rng_secrets and _HAS_SECRETS:
# secrets.randbelow pour bornes inclusives
return a + secrets.randbelow(b - a + 1)
return rnd.randint(a, b) # type: ignore
def _build_charset(use_lowercase: bool, use_uppercase: bool, use_digits: bool, use_symbols: bool, exclude_ambiguous: bool) -> List[str]:
pool = ""
if use_lowercase:
pool += string.ascii_lowercase
if use_uppercase:
pool += string.ascii_uppercase
if use_digits:
pool += string.digits
if use_symbols:
pool += SYMBOLS_DEFAULT
if exclude_ambiguous:
pool = "".join([c for c in pool if c not in AMBIGUOUS])
# sécurité: éviter pool vide
if not pool:
# minimal fallback : minuscules
pool = string.ascii_lowercase
return list(pool)
def _entropy_random_chars(length: int, alphabet_size: int, extra_digits_suffix: int) -> float:
# H = length*log2(S) + extra_digits_suffix*log2(10)
return length * math.log2(max(2, alphabet_size)) + extra_digits_suffix * math.log2(10)
def _entropy_diceware(num_words: int, vocab_size: int, extra_digits_suffix: int) -> float:
# H = num_words*log2(vocab_size) + extra_digits_suffix*log2(10)
return num_words * math.log2(max(2, vocab_size)) + extra_digits_suffix * math.log2(10)
def _load_wordlist(path: str) -> List[str]:
if not path or not os.path.exists(path):
raise FileNotFoundError(f"Diceware wordlist not found: {path}")
words = []
with open(path, "r", encoding="utf-8", errors="ignore") as f:
for line in f:
w = line.strip()
if w:
words.append(w)
# éviter doublons vides
words = [w for w in words if w]
if len(words) < 1000:
# 1000+ recommandé pour une entropie correcte
raise ValueError(f"Wordlist too small ({len(words)} entries). Provide a larger list (e.g., 2048–7776+).")
return words
def _gen_random_chars(length: int, charset: List[str], extra_digits_suffix: int, rng_secrets: bool, rnd: random.Random | None) -> str:
# assure au moins 1 caractère de chaque classe activée ? Non: on laisse aléatoire pur (entropie max)
out = "".join(_safe_choice(charset, rng_secrets, rnd) for _ in range(max(1, length)))
if extra_digits_suffix > 0:
digits = "".join(str(_safe_randint(0, 9, rng_secrets, rnd)) for _ in range(extra_digits_suffix))
out = out + digits
return out
def _gen_diceware(num_words: int, words: List[str], separator: str, extra_digits_suffix: int, rng_secrets: bool, rnd: random.Random | None) -> str:
picks = []
for _ in range(max(1, num_words)):
if rng_secrets and _HAS_SECRETS:
idx = secrets.randbelow(len(words))
else:
idx = rnd.randrange(len(words)) # type: ignore
picks.append(words[idx])
out = separator.join(picks)
if extra_digits_suffix > 0:
digits = "".join(str(_safe_randint(0, 9, rng_secrets, rnd)) for _ in range(extra_digits_suffix))
out = out + digits
return out
class GeneratePassphrase:
"""
Générateur de passphrase sécurisé.
- random_chars : jeu de caractères configurable
- diceware : mots aléatoires depuis un fichier wordlist (un mot par ligne)
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"mode": (["random_chars", "diceware"], {"default": "random_chars"}),
},
"optional": {
# random_chars
"length": ("INT", {"default": 20, "min": 4, "max": 256}),
"use_lowercase": ("BOOLEAN", {"default": True}),
"use_uppercase": ("BOOLEAN", {"default": True}),
"use_digits": ("BOOLEAN", {"default": True}),
"use_symbols": ("BOOLEAN", {"default": True}),
"exclude_ambiguous": ("BOOLEAN", {"default": True}),
# diceware
"diceware_wordlist_path": ("STRING", {"default": "", "multiline": False}),
"num_words": ("INT", {"default": 8, "min": 2, "max": 32}),
"separator": ("STRING", {"default": "-", "multiline": False}),
# commun
"extra_digits_suffix": ("INT", {"default": 0, "min": 0, "max": 10}),
"deterministic_seed": ("INT", {"default": -1, "min": -1, "max": 2**31-1}),
}
}
RETURN_TYPES = ("STRING", "FLOAT", "STRING")
RETURN_NAMES = ("passphrase", "entropy_bits", "recipe")
FUNCTION = "run"
CATEGORY = CATEGORY
def run(
self,
mode="random_chars",
length=20,
use_lowercase=True,
use_uppercase=True,
use_digits=True,
use_symbols=True,
exclude_ambiguous=True,
diceware_wordlist_path="",
num_words=8,
separator="-",
extra_digits_suffix=0,
deterministic_seed=-1,
):
# Sélection RNG
rng_secrets = deterministic_seed < 0
rnd = None
if not rng_secrets:
rnd = random.Random(deterministic_seed)
if mode == "diceware":
# Charge wordlist
try:
words = _load_wordlist(diceware_wordlist_path)
except Exception as e:
return (f"[GeneratePassphrase ERROR] {type(e).__name__}: {e}", 0.0, "load_wordlist_failed")
passphrase = _gen_diceware(num_words, words, separator, extra_digits_suffix, rng_secrets, rnd)
entropy = _entropy_diceware(num_words, len(words), extra_digits_suffix)
recipe = f"mode=diceware; words={num_words}; vocab={len(words)}; sep={repr(separator)}; digits+={extra_digits_suffix}; rng={'secrets' if rng_secrets else 'deterministic'}"
if not rng_secrets:
recipe += " [WARNING: deterministic RNG — dev/test only]"
return (passphrase, float(entropy), recipe)
# random_chars
charset = _build_charset(use_lowercase, use_uppercase, use_digits, use_symbols, exclude_ambiguous)
passphrase = _gen_random_chars(length, charset, extra_digits_suffix, rng_secrets, rnd)
entropy = _entropy_random_chars(length, len(charset), extra_digits_suffix)
recipe = (
f"mode=random_chars; length={length}; alphabet={len(charset)};"
f" lower={use_lowercase}; upper={use_uppercase}; digits={use_digits}; symbols={use_symbols};"
f" exclude_ambiguous={exclude_ambiguous}; digits+={extra_digits_suffix};"
f" rng={'secrets' if rng_secrets else 'deterministic'}"
)
if not rng_secrets:
recipe += " [WARNING: deterministic RNG — dev/test only]"
return (passphrase, float(entropy), recipe)
NODE_CLASS_MAPPINGS = {
"GeneratePassphrase": GeneratePassphrase,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"GeneratePassphrase": "Generate Passphrase",
}