首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >生成密码的Python命令行程序

生成密码的Python命令行程序
EN

Code Review用户
提问于 2023-05-18 17:35:49
回答 2查看 1.5K关注 0票数 9

我从这个答案中提取了我的代码,我发现它很有用,所以我进一步开发了它,结果如下。

此脚本生成密码安全的密码。密码使用的字符是从不是空白字符或控制字符的94个可打印的ASCII字符中选择的。

这些字符分为4类:26个小写字母,26个大写字母,10个数字和32个标点符号。每个密码必须包含大写字母、小写字母和数字,并带有可选的标点符号。

如果允许特殊字符,密码的长度为8到94,如果不允许,则为8至62。密码可以只包含唯一字符和重复字符,密码是否包含重复字符由可选参数控制。密码中每个类别的字符数大致相同。

代码语言:javascript
复制
import random
import secrets
from functools import reduce
from operator import iconcat
from string import ascii_lowercase, ascii_uppercase, digits, punctuation
from typing import Generator, List

CHARSETS = (
    (ascii_lowercase, ascii_uppercase, digits),
    (ascii_lowercase, ascii_uppercase, punctuation, digits)
)

COUNTS = (
    ((3, 36), (2, 10)),
    ((4, 68), (3, 42), (2, 10))
)


def choose_charset(charset: str, number: int, unique: bool = False) -> List[str]:
    if not unique:
        return [secrets.choice(charset) for _ in range(number)]
    charset = list(charset)
    choices = []
    for _ in range(number):
        choice = secrets.choice(charset)
        choices.append(choice)
        charset.remove(choice)

    return choices


def split(number: int, symbol: bool = False) -> Generator[int, None, None]:
    for a, b in COUNTS[symbol]:
        n = max(
            2 + secrets.randbelow(
                number // a - 1
            ), number - b
        )
        number -= n
        yield n
    if number > 10:
        raise ValueError('remaining number is too big')
    yield number


def generate_password(length: int, unique: bool = False, symbol: bool = True) -> str:
    if not 8 <= length <= (62, 94)[symbol]:
        raise ValueError(
            'argument `length` should be an `int` between 8 and 94, or between 8 and 62 if symbols are not allowed')

    password = reduce(
        iconcat,
        map(
            choose_charset, CHARSETS[symbol],
            split(length, symbol), [unique]*4
        ),
        []
    )
    random.shuffle(password)
    return ''.join(password)


if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser(
        prog='Password_Generator',
        description='This program generates cryptographically secure passwords.'
    )
    parser.add_argument(
        'length', type=int,
        help='length of the passwords to be generated'
    )
    unique_parser = parser.add_mutually_exclusive_group(required=False)
    unique_parser.add_argument(
        '-U', '--unique', dest='unique', action='store_true',
        help='specifies passwords should contain unique characters only'
    )
    unique_parser.add_argument(
        '-NU', '--no-unique', dest='unique', action='store_false',
        help='specifies passwords should not contain unique characters only'
    )
    parser.set_defaults(unique=False)
    symbol_parser = parser.add_mutually_exclusive_group(required=False)
    symbol_parser.add_argument(
        '-S', '--symbol', dest='symbol', action='store_true',
        help='specifies passwords should contain special characters'
    )
    symbol_parser.add_argument(
        '-NS', '--no-symbol', dest='symbol', action='store_false',
        help='specifies passwords should not contain special characters'
    )
    parser.set_defaults(symbol=True)
    parser.add_argument(
        '-C', '--count', type=int, default=1,
        help='specifies the number of passwords to generate, default 1'
    )
    namespace = parser.parse_args()
    length, unique, symbol, count = [
        getattr(namespace, name) for name in ('length', 'unique', 'symbol', 'count')]

    for _ in range(count):
        print(generate_password(length, unique, symbol))

帮助消息

代码语言:javascript
复制
PS C:\Users\Xeni> D:\MyScript\password_generator.py -h
usage: Password_Generator [-h] [-U | -NU] [-S | -NS] [-C COUNT] length

This program generates cryptographically secure passwords.

positional arguments:
  length                length of the passwords to be generated

options:
  -h, --help            show this help message and exit
  -U, --unique          specifies passwords should contain unique characters only
  -NU, --no-unique      specifies passwords should not contain unique characters only
  -S, --symbol          specifies passwords should contain special characters
  -NS, --no-symbol      specifies passwords should not contain special characters
  -C COUNT, --count COUNT
                        specifies the number of passwords to generate, default 1

示例用法

代码语言:javascript
复制
PS C:\Users\Xeni> D:\MyScript\password_generator.py 16 -C 8
1o71NV8{rt*.3l4W
L533*8X1m$9`!w6R
9#0<W4~ZuK#"51Vd
V9y93Y^2B34Go71/
50]9o1]E1&ap6HU#
~42pL"46T'7633zd
6Zw1a9z"F16H7~3Z
2Ab7S<r82o7_KN49
PS C:\Users\Xeni> D:\MyScript\password_generator.py 16 -U -C 8
xt7)kG48O9KW\^0]
Pz02ac51>8_UY43g
6[kYF?0H98Oq3'21
Fp50N>P47n+369A1
9v743R0%V5\2s186
Dbr1%\0a}486TQF;
K?l1Cz;92Wn7|Z54
1a@7]4yBsCx52Y0G
PS C:\Users\Xeni> D:\MyScript\password_generator.py 16 -U -NS -C 8
c0YgQ9C54k86Ff3m
Dwd3W412059ujm8H
17pWJvhyC9b82460
Qs52409Az673R18Z
89R2Pk6z401nN73H
7gDcO30yxG54d86K
i62s1kQC89IuBb45
iw02Zn96Ez1xQY8N

随着程序变得足够复杂,我希望对其进行审查,特别是这是我第一次使用argparse。我希望我的代码更加简洁和高效。

EN

回答 2

Code Review用户

回答已采纳

发布于 2023-05-19 01:46:27

将所有代码放入函数中。函数中的代码可以比浮动在顶层的代码更容易测试、试验和重组。浮动代码引入了依赖全局变量的诱惑,即使在程序大小适中的情况下,这也会造成相当大的麻烦。将所有内容放入函数中,就会迫使您清楚地考虑行为和数据流是如何组织的。这是一个经过时间考验的练习,值得一试。

在代码清晰性和可读性方面投入更多精力。我开始尝试这段代码,希望能写一篇典型的评论,建议改进各种细节,但我很快就被代码的复杂性和不透明性所淹没。举几个例子。(1)什么是COUNTS?常量名称是泛型的,没有注释可以提供线索,数据结构几乎(但不是完全)与CHARSETS并行,而且数字本身也不是很清楚。(2) split()函数的目的和行为是什么?再说一遍,这个名字是通用的,有点令人费解(什么是被分割的?)没有解释意图的文档字符串或注释。它的逻辑是由COUNTS中的神秘数字驱动的,使用了完全抽象的ab变量名。在对代码进行了实验之后,我感觉到split()的工作是确定我们将选择多少个字符(下、上、位、点)来构建密码,但代码中的任何内容都无法帮助我达到这个目的。我必须通过实验、打印和仔细阅读才能弄清楚这一点。(3)什么是choose_charset()?我们真的选择了一个字符集,这意味着在CHARSETS数据中进行选择吗?看上去不像那样。这个函数中的numbersplit()中的number相同吗?这些通用概念与密码length相比又如何呢?命名方案既通用又看似不一致。(4) generate_password()中的算法是一个划痕算法。我敢打赌,每1000名经验丰富的Python程序员中,就有不到1人使用过iconcat --这并不一定会使它成为一件坏事,但它确实传达了您正在行走的地形。经过一段时间的学习,我终于弄明白了它在做什么,但你真的让你的读者努力工作!考虑下面的比较。即使没有改进其他代码的命名和可读性,我们也可以通过执行正常的Python操作而不是做一些非常聪明的事情来澄清密码的生成。

代码语言:javascript
复制
# Original code: reduce, iconcat?!?, map, choose_charset -- huh?
password = reduce(
    iconcat,
    map(
        choose_charset, CHARSETS[symbol],
        split(length, symbol), [unique]*4
    ),
    []
)

# Revised code: ok, there is a tuple of "splits" (not sure what that means,
# but it's the same size as CHARSETS, so I sort of see the connection). Oh,
# and then we just build the password from the characters coming out of
# choose_charset(). I sort of get it now.
splits = tuple(split(length, symbol))
password = [
    char
    for charset, s in zip(CHARSETS[symbol], splits)
    for char in choose_charset(charset, s, unique)
]

代码正常工作吗?当我看到这种复杂性时,我的怀疑就会增加。我怎么知道代码在做正确的事情?我决定快速分析一下。由于您决定使用命令行参数解析器,我很快添加了一个由--analysis触发的新行为。它需要一堆密码,并计算每一类字符被观察的频率。这是一个新的函数,以及我如何将它编织到现有代码中的草图。

代码语言:javascript
复制
import sys
from collections import Counter

def main(args):
    parser = argparse.ArgumentParser(...)
    ...
    opts = parser.parse_args(args)
    ...
    if opts.analysis:
        passwords = [
            generate_password(opts.length, opts.unique, opts.symbol)
            for _ in range(count)
        ]
        analyze_passwords(opts, passwords)
    else:
        ...

def analyze_passwords(opts, passwords):
    csets = dict(
        lower = ascii_lowercase,
        upper = ascii_uppercase,
        punct = punctuation,
        digit = digits,
    )
    c = Counter(
        next(name for name, chars in csets.items() if c in chars)
        for p in passwords
        for c in p
    )
    for k in csets:
        print(k, round(c[k] / opts.count, 3))

if __name__ == '__main__':
    main(sys.argv[1:])

我分析了不同的密码长度(8,16,35,70),有和没有--unique。结果表明,您可能需要重新考虑您的方法。随着密码长度的增加,数字,特别是标点符号的代表过多,而小写字母的代表不足.而且,即使用户没有在命令行中指定该选项,也似乎总是在唯一模式下选择数字(或者以某种方式将数字限制在10种外观)。

代码语言:javascript
复制
Category | 8   | 16    | 35     | 70        # Non-unique.
----------------------------------------
lower    | 2.0 | 3.0   | 5.006  | 9.548
upper    | 2.0 | 2.983 | 5.869  | 18.794
punct    | 2.0 | 3.386 | 14.127 | 31.658
digit    | 2.0 | 6.632 | 9.998  | 10.0

Category | 8   | 16    | 35     | 70        # Unique.
-----------------------------------------
lower    | 2.0 | 2.997  | 5.007  | 9.559  
upper    | 2.0 | 3.0    | 5.857  | 18.77
punct    | 2.0 | 3.398  | 14.138 | 31.672
digit    | 2.0 | 6.606  | 9.998  | 10.0   

对抗复杂性的关键是:重点突出的数据对象。在考虑了你的程序一段时间后,我决定放弃任何逐行甚至逐功能建议的努力。我建议你重新考虑一下。例如,我建议您找出一种根本不需要它的方法,而不是改进split()函数。首先,一种方法是创建一个数据对象来表示CharacterSet以及生成密码时可以绑定到它的行为。这样的对象应该知道它的字符范围(下,上,数字,标点符号)。它应该知道如何随机地发出另一个字符,但要受任何唯一的约束。而且它应该知道下一个获得另一个字符的请求是否会因为该约束而失败。

代码语言:javascript
复制
class CharacterSet:

    def __init__(self, chars, unique = False):
        self.chars = chars
        self.unique = bool(unique)
        self.remaining = list(chars)
        random.shuffle(self.remaining)

    def next_char(self):
        if self.unique:
            # Because we shuffled, we can just pop from the end.
            return self.remaining.pop()
        else:
            return secrets.choice(self.chars) 

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

有了这个非常简单的数据对象之后,以尊重唯一约束的方式生成单个密码的代码(尽可能多地)从不同的字符类别中均匀选择的代码可能如下所示:

代码语言:javascript
复制
from itertools import cycle

def create_password(length, unique = False, include_punct = False):
    # Create the CharacterSet instances.
    categories = (ascii_lowercase, ascii_uppercase, digits, punctuation)
    csets = [
        CharacterSet(chars, unique)
        for chars in categories
    ]
    if not include_punct:
        csets.pop()
    random.shuffle(csets)

    # Build a password by just cycling though the shuffled
    # CharacterSet instances, getting their next character.
    password = []
    csets = cycle(csets)
    for _ in range(length):
        while True:
            cset = next(csets)
            if not cset.is_empty:
                break
        password.append(cset.next_char())
    random.shuffle(password)
    return ''.join(password)

下一步:重新考虑发行版。当上面的代码生成的密码通过analyze_passwords()函数运行时,密码在字符类别之间的分布非常均匀(同样,受唯一约束)。但你可能需要重新考虑是否有如此均匀的分布。我的建议是从每个类别的最低计数开始(例如,每个类别中的2个字符)。您可以按照上面所示的方式构建该密码。然后,要获得实现完整密码长度所需的其余字符,请从使用类别特定的CharacterSet实例中的所有剩余字符创建的通用CharacterSet中提取。

票数 16
EN

Code Review用户

发布于 2023-05-19 08:45:49

我觉得你想出了一个过度设计的解决方案,最终未能满足人们的期望。

生成密码是一项具有定义良好的用例的任务,我们几乎每天都会遇到这个任务。

大多数要求来自安全考虑:

  1. 使用密码安全的随机性源
  2. 消除生成密码中的所有可识别模式
  3. 不要对密码长度施加限制
  4. 尽可能广泛地使用字母/字符集

您的算法几乎在所有帐户上都失败了。

在从不同的字符集中提取字符后,用random.shuffle对这些字符进行洗牌,这并不是加密级的随机性,违反了第一项要求。

该算法过多地表示了数字和标点符号,正如FMc的伟大答案所指出的,它违反了第2号要求。

-U选项还针对需求2引入了模式。

如果选择了-NU选项,则密码的长度受字符集大小的限制,没有明显的原因。您可能认为长度足够好,但实际上,我无法生成一个密码与我的去设置(没有特殊的字符,64长度,足够好的熵,不需要处理每个网站对特殊字符的不同限制)。要求3失败。

这些选项允许包含所有可打印的特殊字符,或者不包含任何特殊字符。实际上,在现实世界中,您通常被限制在一些特殊字符的子集上,但是您不能在解决方案中使用这个子集。要求4失败。

现在,这组要求实际上并不难满足。实际上,从secrets模块文档的食谱和最佳做法中获得灵感,想出一个简单的解决方案非常简单:

代码语言:javascript
复制
import secrets


def generate_password(length, alphabet):
    return ''.join(secrets.choice(alphabet) for _ in range(length))

这允许满足所有四项要求。现在,在顶部添加一些设计良好的带有合理默认值的命令行开关,我们实际上有了一个可用的密码生成器。

票数 14
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/285045

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档