2
\$\begingroup\$

Following suggestions from answers from my previous question, I have completely rewrote my script, to make it more concise and Pythonic, truly cryptographically secure, and added more functionality.

This program generates random passwords, each password has a minimum length of 8, will always contain UPPERCASE, lowercase and digits, with special characters being optional. And the passwords will guarantee to contain at least 2 characters from each category.

This version uses secrets throughout the entire script, shuffling is done by secrets.SystemRandom, therefore it is genuinely cryptographically secure.

This version doesn't impose upper length constraint if duplicates are allowed, and only limits the password length if characters must be unique, and the limit is 94 characters if special characters are allowed, or 62 if they aren't.

My rational to add the option to eliminate duplicates, is that if all characters are unique, then every character is different, thus there would be less patterns. But of course this is optional.

I have also added an option to make the passwords uniformly sample characters from each category, if the option is chosen, then the passwords will contain approximately equal number of characters from each category. This behavior is optional.

Finally I have added color support so that the characters will have a different color according to its category, an option to analyze the output, and I put the code inside functions and classes and completely rewrote the code, so that it is much more readable.


Code

import json
import secrets
import typer
from password_strength import PasswordStats
from pathlib import Path
from rich import print
from rich.console import Console
from rich.table import Table
from string import ascii_lowercase, ascii_uppercase, digits, punctuation
from typing import Any, List, Optional, Tuple
from typing_extensions import Annotated

RNG = secrets.SystemRandom()
CATEGORIES = (ascii_lowercase, ascii_uppercase, digits, punctuation)
CATEGORY_NAMES = ()
STYLES = tuple(
    "[{0}]{{}}[/{0}]".format(color) for color in ("magenta", "red", "cyan", "green")
)
CHARACTER_STYLES = {
    c: style.format(c) for style, category in zip(STYLES, CATEGORIES) for c in category
}
CHARACTER_STYLES["\\"] = "[green]\\\[/green]"
HELP_MSGS = (
    "length of the passwords to be generated",
    "specifies the number of passwords to be generated",
    "determines if the passwords should contain unique characters only",
    "determines if the passwords should contain special characters",
    "determines if the characters in the passwords should be uniformly sampled",
    "if True, will only output the summary of password statistical information",
    "if True, the passwords will be analyzed and additional information will be shown",
    "if True, password summary will be more extensive",
    "filepath to redirect the output to, if given, will save the output as a JSON file located at the path given, instead of printing to console",
)
COLUMNS = (
    "Password",
    "lower",
    "UPPER",
    "Digit",
    "Special",
    "Length",
    "Unique",
    "Repetition",
    "Entropy",
    "Strength",
)
ATTRIBUTES = (
    "letters_lowercase",
    "letters_uppercase",
    "numbers",
    "special_characters",
    "length",
    "alphabet_cardinality",
    "repeated_patterns_length",
)


def annotate(arg_type: type, optional: bool, help_msg: str) -> Annotated:
    if not optional:
        return Annotated[arg_type, typer.Argument(help=help_msg)]

    return Annotated[Optional[arg_type], typer.Option(help=help_msg)]


LENGTH = annotate(int, False, HELP_MSGS[0])
COUNT = annotate(int, True, HELP_MSGS[1])
BOOLEANS = tuple(annotate(bool, True, help_msg) for help_msg in HELP_MSGS[2:8])
REDIRECT = annotate(str, True, HELP_MSGS[8])


class Cyclic_List(list):
    def __getitem__(self, index: int) -> Any:
        return list.__getitem__(self, index % len(self))


class CharacterSet:
    def __init__(self, chars: str | list, unique: bool = False) -> None:
        self.unique = unique
        self.remaining = list(chars)
        if unique:
            RNG.shuffle(self.remaining)

    def next_char(self) -> str:
        return self.remaining.pop() if self.unique else secrets.choice(self.remaining)

    def get_chars(self, length: int) -> List[str]:
        if self.unique:
            if length > len(self.remaining):
                raise ValueError("length is too big, not enough characters remain")

            return [self.remaining.pop() for _ in range(length)]

        return RNG.choices(self.remaining, k=length)

    @property
    def is_empty(self) -> bool:
        return self.unique and not self.remaining


def make_character_pool(
    unique: bool = False, symbol: bool = True, shuffle: bool = False
) -> Cyclic_List:
    pool = Cyclic_List(
        CharacterSet(category, unique) for category in CATEGORIES[: 3 + symbol]
    )
    if shuffle:
        for charset in pool:
            RNG.shuffle(charset.remaining)

    RNG.shuffle(pool)
    return pool


class CharacterPool:
    default_pool = make_character_pool(shuffle=True)
    default_pool_alnum = make_character_pool(symbol=False, shuffle=True)
    unified_charset = CharacterSet("".join(CATEGORIES))
    RNG.shuffle(unified_charset.remaining)
    unified_charset_alnum = CharacterSet("".join(CATEGORIES[:3]))
    RNG.shuffle(unified_charset_alnum.remaining)

    def __init__(self, unique: bool = False, symbol: bool = True) -> None:
        self.unique = unique
        self.symbol = symbol
        if not unique:
            self.pool = self.default_pool if symbol else self.default_pool_alnum
        else:
            self.pool = make_character_pool(True, symbol)

    def uniform_sample(self, length: int) -> List[str]:
        samples = []
        for i in range(length):
            while (charset := self.pool[i]).is_empty:
                self.pool.remove(charset)
                if not self.pool and length - i > 1:
                    raise ValueError("pool is empty")

            samples.append(charset.next_char())

        return samples

    def random_sample(self, length: int) -> List[str]:
        if not self.unique:
            charset = (
                self.unified_charset if self.symbol else self.unified_charset_alnum
            )
            return charset.get_chars(length)

        remaining = [char for charset in self.pool for char in charset.remaining]
        if len(remaining) < length:
            raise ValueError("not enough characters remaining")

        return CharacterSet(remaining, True).get_chars(length)


def generate_password(
    length: int, unique: bool = False, symbol: bool = True, uniform: bool = False
) -> str:
    if length < 8 or unique and length > (94 if symbol else 62):
        raise ValueError(
            "argument `length` should be an `int` greater than or equal to 8, "
            "and if duplicates aren't allowed, no greater than 94 if symbols are allowed, or 62 if they aren't"
        )

    first_part = length if uniform else 8
    pool = CharacterPool(unique, symbol)
    password = pool.uniform_sample(first_part)
    if length > first_part:
        password += pool.random_sample(length - first_part)

    RNG.shuffle(password)
    return "".join(password)


def flatten_password_stats(stats: PasswordStats, simple: bool = False) -> tuple:
    fields = tuple(getattr(stats, name) for name in ATTRIBUTES[: 7 - 3 * simple])
    if simple:
        return fields

    return fields + (
        round(stats.entropy_bits * stats.entropy_density, 3),
        round(stats.strength(), 3),
    )


def analyze_password(password: str) -> tuple:
    return flatten_password_stats(PasswordStats(password))


def tally_passwords(passwords: List[str], extensive: bool) -> tuple:
    count = len(passwords)
    if not extensive:
        stats = PasswordStats("".join(passwords))
        return tuple(
            zip(
                COLUMNS[1:5],
                (round(n / count, 3) for n in flatten_password_stats(stats, True)),
            )
        )

    fields = zip(*(analyze_password(password) for password in passwords))
    fields = (round(sum(field) / count, 3) for field in fields)
    return tuple(zip(COLUMNS[1:], fields))


def print_table(columns: Tuple[str], rows: List[list]) -> None:
    console = Console()
    table = Table(*columns)
    for row in rows:
        table.add_row(*map(str, row))

    console.print(table)


def output_tally(
    passwords: List[str], extensive: bool = False, redirect: str = None
) -> None:
    stats = tally_passwords(passwords, extensive)
    if not redirect:
        print_table(("Category", "Value"), stats)
    else:
        Path(redirect).write_text(json.dumps(dict(stats), indent=4))


def stylize_password(password: str) -> str:
    return "".join(CHARACTER_STYLES[c] for c in password)


def print_passwords(passwords: List[str]) -> None:
    for password in passwords:
        print(stylize_password(password))


def print_password_analytics(passwords: List[str]) -> None:
    rows = (
        (stylize_password(password), *analyze_password(password))
        for password in passwords
    )
    rows = sorted(rows, key=lambda x: (-x[-1], x[-3]))
    print_table(COLUMNS, rows)


def get_password_analytics(passwords: List[str]) -> List[dict]:
    rows = (
        dict(zip(COLUMNS, (password, *analyze_password(password))))
        for password in passwords
    )
    rows = sorted(rows, key=lambda x: (-x["Strength"], x["Repetition"]))
    return rows


def process_passwords(
    passwords: List[str], analyze: bool, redirect: str = None
) -> list:
    if redirect:
        return get_password_analytics(passwords) if analyze else passwords
    if analyze:
        print_password_analytics(passwords)
    else:
        print_passwords(passwords)


def serialize_table(table: List[dict]) -> str:
    return "[\n" + ",\n".join("\t" + json.dumps(row) for row in table) + "\n]"


def serialize(obj: Any, custom: bool = False):
    return serialize_table(obj) if custom else json.dumps(obj, indent=4)


def output_passwords(passwords: List[str], analyze: bool, redirect: str = None) -> None:
    rows = process_passwords(passwords, analyze, redirect)
    if redirect:
        Path(redirect).write_text(serialize(rows, analyze))


def main(
    length: LENGTH,
    count: COUNT = 1,
    unique: BOOLEANS[0] = False,
    symbol: BOOLEANS[1] = True,
    uniform: BOOLEANS[2] = False,
    summary: BOOLEANS[3] = False,
    analyze: BOOLEANS[4] = False,
    extensive: BOOLEANS[5] = False,
    redirect: REDIRECT = None,
) -> None:
    """This program generates cryptographically secure passwords.
    The passwords will always contain UPPERCASE ASCII letters, lowercase letters, and digits.
    You can specify whether or not the passwords should contain special characters, by default they are included.
    The characters in the passwords are randomly chosen using cryptographically secure Python `secrets` standard library.
    The passwords generated will have a minimum length of 8, and will contain at least 2 characters from each selected character types.
    You can specify whether or not the passwords should contain unqiue characters only, by default duplicates are allowed.
    If duplicates aren't allowed, there will be password length constraint depending on whether or not special characters are included.
    There are no length constraints otherwise. You can also specify whether or not the characters from each category should be uniformly distributed in the passwords.
    """
    passwords = [
        generate_password(length, unique, symbol, uniform) for _ in range(count)
    ]

    if not summary:
        output_passwords(passwords, analyze, redirect)

    else:
        output_tally(passwords, extensive, redirect)


if __name__ == "__main__":
    typer.run(main)

Help message

PS C:\Users\Xeni> C:\Python310\python.exe D:\MyScript\password_generator.py --help

 Usage: password_generator.py [OPTIONS] LENGTH

 This program generates cryptographically secure passwords. The passwords will always contain UPPERCASE ASCII letters, lowercase letters, and digits. You can specify whether or not the passwords should
 contain special characters, by default they are included. The characters in the passwords are randomly chosen using cryptographically secure Python `secrets` standard library. The passwords generated will
 have a minimum length of 8, and will contain at least 2 characters from each selected character types. You can specify whether or not the passwords should contain unqiue characters only, by default
 duplicates are allowed. If duplicates aren't allowed, there will be password length constraint depending on whether or not special characters are included. There are no length constraints otherwise. You can
 also specify whether or not the characters from each category should be uniformly distributed in the passwords.

╭─ Arguments ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ *    length      INTEGER  length of the passwords to be generated [default: None] [required]                                                                                                                  │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─ Options ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ --count                          INTEGER  specifies the number of passwords to be generated [default: 1]                                                                                                      │
│ --unique       --no-unique                determines if the passwords should contain unique characters only [default: no-unique]                                                                              │
│ --symbol       --no-symbol                determines if the passwords should contain special characters [default: symbol]                                                                                     │
│ --uniform      --no-uniform               determines if the characters in the passwords should be uniformly sampled [default: no-uniform]                                                                     │
│ --summary      --no-summary               if True, will only output the summary of password statistical information [default: no-summary]                                                                     │
│ --analyze      --no-analyze               if True, the passwords will be analyzed and additional information will be shown [default: no-analyze]                                                              │
│ --extensive    --no-extensive             if True, password summary will be more extensive [default: no-extensive]                                                                                            │
│ --redirect                       TEXT     filepath to redirect the output to, if given, will save the output as a JSON file located at the path given, instead of printing to console [default: None]         │
│ --help                                    Show this message and exit.                                                                                                                                         │
╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯

enter image description here

Passwords without constraints:

PS C:\Users\Xeni> C:\Python310\python.exe D:\MyScript\password_generator.py 24 --count 8
%2.7vvuTjXNMz-Ic6|M.2lfq
DT|BYol?(!1ue'a$dk680!TW
!FDA70&#Dr]:ZvRD2zQ,HR$[
AIG4r(0XL>d@0-)zA5Iy/)}&
;f9+FWz@n51!OfzFLDtLl+}M
u%~<DhWM)_92>Id.w=zk<Y!,
rg7kYs?H>TI={:1t[3}o'MLf
Hq%7)h.4a0>]P{TMK8MPvr/R

enter image description here

Passwords with only unique characters:

PS C:\Users\Xeni> C:\Python310\python.exe D:\MyScript\password_generator.py 24 --count 8 --unique
:ar3c4k7G=Kb.M1]6pe;~>&2
nb1Nc{v}A.;7o?08I/4CimZt
5JS2xcys~ZDe>j#0p.T3FV;n
4d120/|XnNj}.g)l5OFZuY_C
sKcLVzUki3yp.ZY['N8SuB4}
>A+40q|']1[axJ~Hfog.=,$m
whF:".4)^lp7~X>RHfLN/Ysn
3OUF:Ab%v7J;`.c2+i}0g\&R

enter image description here

Passwords without duplicates and special characters:

PS C:\Users\Xeni> C:\Python310\python.exe D:\MyScript\password_generator.py 24 --count 8 --unique --no-symbol
GecxVfsPA4D2EpmFShHRL8Ya
GQ8UAbghWX2lT74c0eBoP6RM
y54DVnbuKk9v8YFzqNX6lBGA
BRiA9F78k2dyaj5ePWvwMoZE
2xY08JBMLj7dPRU1AOI5lmDF
z8dJP4j1TaIoNEKM6D7bwSsA
B1CHSImo8Zc3NwxMkJhl2r7X
5NY9yDQwntElJUzaZsd0jC1F

enter image description here

Debug information:

PS C:\Users\Xeni> C:\Python310\python.exe D:\MyScript\password_generator.py 64 --count 256 --debug
┏━━━━━━━━━━┳━━━━━━━━┓
┃ Category ┃ Count  ┃
┡━━━━━━━━━━╇━━━━━━━━┩
│ special  │ 20.895 │
│ upper    │ 17.812 │
│ lower    │ 17.477 │
│ digit    │ 7.816  │
└──────────┴────────┘

How can this script be improved?


Update

I have added a new option, now that the passwords can be analyzed:

PS C:\Users\Xeni> C:\Python310\python.exe D:\MyScript\password_generator.py 32 --count 16 --analyze
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━━┳━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┓
┃ Password                         ┃ lower ┃ UPPER ┃ Digit ┃ Special ┃ Length ┃ Unique ┃ Repetition ┃ Entropy ┃ Strength ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━━╇━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━┩
│ TBIm95o7V'kS^h\Gu$}t==jy}p<g)*HJ │ 10    │ 8     │ 3     │ 11      │ 32     │ 30     │ 2          │ 154.096 │ 0.983    │
│ |c9GXWJ-HHL:sR2)lJf}T@tIPY#FzR6> │ 6     │ 15    │ 3     │ 8       │ 32     │ 29     │ 2          │ 151.04  │ 0.982    │
│ \uG&Z@j<UHDIczi4QI4.h?@WP35LMJU# │ 6     │ 14    │ 4     │ 8       │ 32     │ 28     │ 0          │ 147.908 │ 0.981    │
│ ucqk-!P$$j[h?o2_(x[98B44rzqNTl&" │ 12    │ 4     │ 5     │ 11      │ 32     │ 28     │ 4          │ 147.908 │ 0.981    │
│ i?!zqD,B47.8ie4~MRr0a(l<^B6zceEb │ 12    │ 6     │ 6     │ 8       │ 32     │ 27     │ 4          │ 144.697 │ 0.98     │
│ )0O/`8t@RBZ]'_DlDt*^z_C~M}@TbLfB │ 6     │ 11    │ 2     │ 13      │ 32     │ 27     │ 0          │ 144.697 │ 0.98     │
│ =7,(q28yt^z3#}son04*VA=r~3=+4)U* │ 8     │ 3     │ 8     │ 13      │ 32     │ 27     │ 0          │ 144.697 │ 0.98     │
│ 0Kwl6:7`0#P?9;x<.}SWK}H,7;du-+=^ │ 5     │ 6     │ 6     │ 15      │ 32     │ 27     │ 0          │ 144.697 │ 0.98     │
│ !#!d[0/+WmyRlGJzsqTC05z,~s1'G8+P │ 9     │ 8     │ 5     │ 10      │ 32     │ 26     │ 0          │ 141.402 │ 0.979    │
│ oP}^I_B[+4Z@c19"aAP^.4Cf=8@#[ysB │ 6     │ 8     │ 5     │ 13      │ 32     │ 26     │ 2          │ 141.402 │ 0.979    │
│ v~55HM'5>,;EqcGBCOMV]6CARFv!Chx& │ 6     │ 14    │ 4     │ 8       │ 32     │ 26     │ 2          │ 141.402 │ 0.979    │
│ ?S:Hq8a*JH&Fx{XMH2*M7ecd)PH'U{d+ │ 7     │ 12    │ 3     │ 10      │ 32     │ 25     │ 0          │ 138.019 │ 0.978    │
│ \P!Zr]!?qhZs(ig$~Nnpi!$PI9@$75e6 │ 10    │ 6     │ 4     │ 12      │ 32     │ 25     │ 2          │ 138.019 │ 0.978    │
│ ?}9;j_#-9^jr6XG,zESSo!29i%$Tji$> │ 8     │ 6     │ 5     │ 13      │ 32     │ 25     │ 2          │ 138.019 │ 0.978    │
│ Y%QmtY_9u)yAq3318739uUVsTqYSaL3j │ 10    │ 10    │ 9     │ 3       │ 32     │ 24     │ 4          │ 134.54  │ 0.977    │
│ $w5E}<wKjwR3Ib=8R9cQ8~\L!8b}~2U{ │ 7     │ 8     │ 7     │ 10      │ 32     │ 24     │ 0          │ 134.54  │ 0.977    │
└──────────────────────────────────┴───────┴───────┴───────┴─────────┴────────┴────────┴────────────┴─────────┴──────────┘

enter image description here


Update

I have cleaned up the code a little bit, and I have added the option to redirect the output of the program to JSON files.

Summary

{
    "lower": 6.392,
    "UPPER": 6.464,
    "Digit": 3.693,
    "Special": 7.451
}

Extensive Summary

{
    "lower": 6.442,
    "UPPER": 6.394,
    "Digit": 3.735,
    "Special": 7.429,
    "Length": 24.0,
    "Unique": 21.387,
    "Repetition": 0.712,
    "Entropy": 102.105,
    "Strength": 0.925
}

Password Analytics

[
    {"Password": "hB1'GIUg0c;K5=.<kq`*29AP", "lower": 5, "UPPER": 7, "Digit": 5, "Special": 7, "Length": 24, "Unique": 24, "Repetition": 0, "Entropy": 110.039, "Strength": 0.933},
    {"Password": "ao,|omGwzH{lt\\7L]eN[6#8X", "lower": 9, "UPPER": 5, "Digit": 3, "Special": 7, "Length": 24, "Unique": 23, "Repetition": 0, "Entropy": 107.112, "Strength": 0.931},
    {"Password": "9VPl(:tk\"@,~c7YiI)B[Xoi>", "lower": 7, "UPPER": 6, "Digit": 2, "Special": 9, "Length": 24, "Unique": 23, "Repetition": 2, "Entropy": 107.112, "Strength": 0.931},
    {"Password": "G~p[q#skTXcra4Ao\\GNgbn2]", "lower": 11, "UPPER": 6, "Digit": 2, "Special": 5, "Length": 24, "Unique": 23, "Repetition": 0, "Entropy": 107.112, "Strength": 0.931},
    {"Password": "Ge\\z3la][_G5BOC/s!foF0'N", "lower": 7, "UPPER": 7, "Digit": 3, "Special": 7, "Length": 24, "Unique": 23, "Repetition": 0, "Entropy": 107.112, "Strength": 0.931},
    {"Password": ":.9&v|UAQ(Yl!\\J2@b'{9GhH", "lower": 4, "UPPER": 7, "Digit": 3, "Special": 10, "Length": 24, "Unique": 23, "Repetition": 2, "Entropy": 107.112, "Strength": 0.931},
    {"Password": "k`bQIb0?,z2~dU\"!#mTFLy67", "lower": 7, "UPPER": 6, "Digit": 4, "Special": 7, "Length": 24, "Unique": 23, "Repetition": 0, "Entropy": 107.112, "Strength": 0.931},
    {"Password": "Yo\"4t|LvA+SUM6w<%ry/5%sg", "lower": 8, "UPPER": 6, "Digit": 3, "Special": 7, "Length": 24, "Unique": 23, "Repetition": 0, "Entropy": 107.112, "Strength": 0.931},
    {"Password": "&JD1N]!|5-SQ84yi9@,t&lWf", "lower": 5, "UPPER": 6, "Digit": 5, "Special": 8, "Length": 24, "Unique": 23, "Repetition": 0, "Entropy": 107.112, "Strength": 0.931},
    {"Password": "mq%P^~Ak'7?uhC][YW&oy0Pf", "lower": 8, "UPPER": 6, "Digit": 2, "Special": 8, "Length": 24, "Unique": 23, "Repetition": 0, "Entropy": 107.112, "Strength": 0.931},
    {"Password": "z*../Vq!D@`9Kd88-N,imG7l", "lower": 6, "UPPER": 5, "Digit": 4, "Special": 9, "Length": 24, "Unique": 22, "Repetition": 4, "Entropy": 104.096, "Strength": 0.927},
    {"Password": "zG@fm-+T-eE2;kkw9QnB08KJ", "lower": 8, "UPPER": 7, "Digit": 4, "Special": 5, "Length": 24, "Unique": 22, "Repetition": 4, "Entropy": 104.096, "Strength": 0.927},
    {"Password": "H;4VnYD$7LA$&+syz2*6(+'h", "lower": 5, "UPPER": 6, "Digit": 4, "Special": 9, "Length": 24, "Unique": 22, "Repetition": 0, "Entropy": 104.096, "Strength": 0.927},
    {"Password": "@zh8nbkuw56/4&F=_a'J'upQ", "lower": 10, "UPPER": 3, "Digit": 4, "Special": 7, "Length": 24, "Unique": 22, "Repetition": 0, "Entropy": 104.096, "Strength": 0.927},
    {"Password": "2(D%<UnKx4)3R&iTP3.Y`A&q", "lower": 4, "UPPER": 8, "Digit": 4, "Special": 8, "Length": 24, "Unique": 22, "Repetition": 0, "Entropy": 104.096, "Strength": 0.927},
    {"Password": "K6ObUNq/<7c~z(56`Ss\\9(DR", "lower": 5, "UPPER": 7, "Digit": 5, "Special": 7, "Length": 24, "Unique": 22, "Repetition": 2, "Entropy": 104.096, "Strength": 0.927},
    {"Password": "lb+xc{:y8$qW@gp>8jJ%rJKA", "lower": 10, "UPPER": 5, "Digit": 2, "Special": 7, "Length": 24, "Unique": 22, "Repetition": 2, "Entropy": 104.096, "Strength": 0.927},
    {"Password": "1'4*Y\"#;onM20*G,&mMvC8{z", "lower": 5, "UPPER": 5, "Digit": 5, "Special": 9, "Length": 24, "Unique": 22, "Repetition": 2, "Entropy": 104.096, "Strength": 0.927},
    {"Password": "qXP8ICA-jhqKOinSj<[BwI6(", "lower": 8, "UPPER": 10, "Digit": 2, "Special": 4, "Length": 24, "Unique": 21, "Repetition": 0, "Entropy": 100.986, "Strength": 0.924},
    {"Password": "-B@1F?x@D=y\\\\g9TO-wL#j_E", "lower": 5, "UPPER": 7, "Digit": 2, "Special": 10, "Length": 24, "Unique": 21, "Repetition": 2, "Entropy": 100.986, "Strength": 0.924},
    {"Password": "vM[]9rX&n*Xr.c6H@PS*iQ\"#", "lower": 6, "UPPER": 7, "Digit": 2, "Special": 9, "Length": 24, "Unique": 21, "Repetition": 0, "Entropy": 100.986, "Strength": 0.924},
    {"Password": "9qCdUfh\"v2\\_a'\\1H0\\-i-]4", "lower": 7, "UPPER": 3, "Digit": 5, "Special": 9, "Length": 24, "Unique": 21, "Repetition": 0, "Entropy": 100.986, "Strength": 0.924},
    {"Password": "!vK`[$r%[5?*])k2eF`l\\a5n", "lower": 7, "UPPER": 2, "Digit": 3, "Special": 12, "Length": 24, "Unique": 21, "Repetition": 0, "Entropy": 100.986, "Strength": 0.924},
    {"Password": "lPJlA\\WFS[8i&4d5bi]M;;M'", "lower": 6, "UPPER": 8, "Digit": 3, "Special": 7, "Length": 24, "Unique": 20, "Repetition": 2, "Entropy": 97.776, "Strength": 0.92},
    {"Password": "o8XR_8]c8kys0/1$8T~CD&DA", "lower": 5, "UPPER": 7, "Digit": 6, "Special": 6, "Length": 24, "Unique": 20, "Repetition": 0, "Entropy": 97.776, "Strength": 0.92},
    {"Password": "itqkY88iQ]qhDUb;;%Z_cd3'", "lower": 10, "UPPER": 5, "Digit": 3, "Special": 6, "Length": 24, "Unique": 20, "Repetition": 4, "Entropy": 97.776, "Strength": 0.92},
    {"Password": "$R>HA=s0aq9+T3iRIaEnaAX)", "lower": 7, "UPPER": 9, "Digit": 3, "Special": 5, "Length": 24, "Unique": 20, "Repetition": 2, "Entropy": 97.776, "Strength": 0.92},
    {"Password": "av~Q=Tt3?kEd$Yd:DfQ/33jp", "lower": 9, "UPPER": 6, "Digit": 3, "Special": 6, "Length": 24, "Unique": 20, "Repetition": 4, "Entropy": 97.776, "Strength": 0.92},
    {"Password": "\\/~1}o_biH8/yC|s33N}*:Cb", "lower": 6, "UPPER": 4, "Digit": 4, "Special": 10, "Length": 24, "Unique": 19, "Repetition": 2, "Entropy": 94.456, "Strength": 0.916},
    {"Password": "gdmgh8P#d].qY@B`~Y!P6=5P", "lower": 7, "UPPER": 6, "Digit": 3, "Special": 8, "Length": 24, "Unique": 19, "Repetition": 0, "Entropy": 94.456, "Strength": 0.916},
    {"Password": "!uuHN\"7P\\y@UP@0PoL_wy*7X", "lower": 6, "UPPER": 8, "Digit": 3, "Special": 7, "Length": 24, "Unique": 18, "Repetition": 2, "Entropy": 91.019, "Strength": 0.911},
    {"Password": "Utg,S0W&7mK40wbK%KJ,WKmU", "lower": 6, "UPPER": 10, "Digit": 4, "Special": 4, "Length": 24, "Unique": 16, "Repetition": 0, "Entropy": 83.752, "Strength": 0.9}
]
\$\endgroup\$
4
  • 1
    \$\begingroup\$ I have installed Python 3.10 latest and Python 3.11 latest, and python.exe points to Python 3.11, but I mainly use Python 3.10. \$\endgroup\$ Commented May 19, 2023 at 16:27
  • 1
    \$\begingroup\$ According to this website, all the passwords generated by my program are crack-resistant, even the shortest ones. \$\endgroup\$ Commented May 19, 2023 at 17:30
  • 1
    \$\begingroup\$ After extensive testing I have determined that the passwords generated by my program are truly secure, so I have changed my passwords to ones it generated. \$\endgroup\$ Commented May 20, 2023 at 9:31
  • \$\begingroup\$ The word "secure" is pretty meaningless without any indication of the threat model (including your adversaries' capabilities). I don't think this program is at all secure against someone who has root on the machine where you run it, for example. \$\endgroup\$ Commented May 21, 2023 at 14:35

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.