ساخت رمز عبور با پایتون؛ پسورد ساز قوی و امن

محسن جباره‌اصل · · ۹ دقیقه مطالعه
ساخت رمز عبور با پایتون و ماژول secrets

در این آموزش قدم‌به‌قدم سراغ ساخت رمز عبور با پایتون می‌رویم و یک پسورد ساز واقعی می‌نویسیم که رمزهای قوی، تصادفی و غیرقابل‌حدس تولید می‌کند. برنامه از شما طول رمز، تعداد رمزها و نوع کاراکترها را می‌پرسد و در پایان می‌گوید رمز ساخته‌شده چقدر قوی است.

در این سال‌ها که برای مشتری‌ها سایت و پنل مدیریت راه انداخته‌ام، بارها دیده‌ام که رمز پنل یا دیتابیس چیزی شبیه نام شرکت به‌اضافهٔ سال تأسیس بوده است. چنین رمزی را هر کسی با کمی حوصله حدس می‌زند. یک ابزار کوچک که در چند ثانیه رمز امن بسازد، هم برای خودتان کاربردی است و هم یک تمرین عالی برای یادگیری پایتون؛ چون در همین پروژهٔ کوچک با ماژول‌ها، تابع‌ها، حلقه‌ها، گرفتن ورودی و کمی ریاضی کار می‌کنید.

اگر تازه شروع کرده‌اید و هنوز با مفاهیم پایه راحت نیستید، پیشنهاد می‌کنم اول مقالهٔ یادگیری برنامه‌نویسی از صفر را بخوانید و بعد برگردید سراغ این پروژه.

پیش‌نیازها

برای این پروژه فقط به پایتون ۳ (نسخهٔ ۳٫۸ یا جدیدتر) نیاز دارید. هیچ کتابخانهٔ جانبی لازم نیست و با pip چیزی نصب نمی‌کنیم؛ همه‌چیز در کتابخانهٔ استاندارد پایتون هست. نسخهٔ پایتون خود را این‌طور بررسی کنید:

python --version

یک ویرایشگر ساده مثل VS Code یا حتی IDLE خود پایتون کافی است. یک فایل به اسم password_generator.py بسازید تا کد را مرحله‌به‌مرحله در آن بنویسیم.

چرا ماژول secrets و نه random؟

اولین سؤالی که شاگردهایم می‌پرسند این است که چرا از random استفاده نکنیم. جواب ساده است: ماژول random برای شبیه‌سازی و بازی ساخته شده و اعدادش از یک الگوریتم قابل‌پیش‌بینی می‌آیند. مستندات رسمی پایتون صریحاً می‌گوید برای کارهای امنیتی باید از ماژول secrets استفاده کرد، چون از منبع تصادفی امن سیستم‌عامل کمک می‌گیرد.

ماژول string هم مجموعه‌های آمادهٔ کاراکتر را در اختیارمان می‌گذارد تا لازم نباشد حروف الفبا را دستی تایپ کنیم:

import secrets
import string

print(string.ascii_lowercase)   # abcdefghijklmnopqrstuvwxyz
print(string.ascii_uppercase)   # ABCDEFGHIJKLMNOPQRSTUVWXYZ
print(string.digits)            # 0123456789

# pick one secure random character
print(secrets.choice(string.ascii_letters + string.digits))

تابع secrets.choice از یک رشته یا لیست، یک عضو را به‌صورت امن و تصادفی انتخاب می‌کند. کل پسورد ساز ما روی همین یک تابع بنا می‌شود.

قدم اول: ساده‌ترین تولید رمز

ساده‌ترین نسخه این است که به تعداد طول رمز، از الفبای مجاز یک کاراکتر برداریم و کنار هم بگذاریم:

import secrets
import string


def simple_password(length=12):
    alphabet = string.ascii_letters + string.digits
    return "".join(secrets.choice(alphabet) for _ in range(length))


print(simple_password())
print(simple_password(20))

اینجا دو مفهوم تازه داریم: عبارت مولد (generator expression) که داخل join نوشته شده و حلقه را در یک خط خلاصه می‌کند، و متغیر _ که یعنی «شمارندهٔ حلقه برایم مهم نیست». متد "".join هم کاراکترها را بدون فاصله به هم می‌چسباند.

قدم دوم: تضمین وجود همهٔ انواع کاراکتر

نسخهٔ ساده یک ایراد دارد: ممکن است اتفاقی هیچ عددی در رمز نیاید و فرم ثبت‌نام سایت آن را رد کند. راه‌حل این است که کاراکترها را به چند «گروه» تقسیم کنیم، از هر گروه حداقل یکی برداریم، بقیه را از کل الفبا پر کنیم و در پایان ترتیب را به‌هم بریزیم:

import secrets
import string

SYMBOLS = "!@#$%^&*()-_=+[]{};:,.?"


def build_pools(use_upper, use_digits, use_symbols):
    pools = [string.ascii_lowercase]
    if use_upper:
        pools.append(string.ascii_uppercase)
    if use_digits:
        pools.append(string.digits)
    if use_symbols:
        pools.append(SYMBOLS)
    return pools


def generate_password(length, pools):
    if length < len(pools):
        raise ValueError(f"Length must be at least {len(pools)}")
    chars = [secrets.choice(pool) for pool in pools]
    alphabet = "".join(pools)
    chars += [secrets.choice(alphabet) for _ in range(length - len(chars))]
    secrets.SystemRandom().shuffle(chars)
    return "".join(chars)


print(generate_password(16, build_pools(True, True, True)))

چند نکته در این کد هست. build_pools لیستی از گروه‌های انتخاب‌شده برمی‌گرداند. در generate_password اول با یک list comprehension از هر گروه یک کاراکتر برمی‌داریم، بعد باقی طول را پر می‌کنیم. چون کاراکترهای تضمینی همیشه اول لیست هستند، با secrets.SystemRandom().shuffle لیست را به‌صورت امن به‌هم می‌ریزیم تا الگوی قابل‌حدسی باقی نماند. دستور raise ValueError هم جلوی درخواست‌های بی‌معنی مثل رمز ۲ کاراکتری با ۴ گروه را می‌گیرد.

قدم سوم: سنجش قدرت رمز با آنتروپی

رمز «قوی» یعنی چه؟ معیار رایج آنتروپی است: اگر رمز از الفبایی با N کاراکتر و طول L ساخته شود، تقریباً L × log2(N) بیت تصادفی دارد. هر بیت بیشتر، تعداد حالت‌هایی را که مهاجم باید امتحان کند دو برابر می‌کند.

import math


def entropy_bits(length, alphabet_size):
    return length * math.log2(alphabet_size)


def strength_label(bits):
    if bits < 50:
        return "Weak"
    if bits < 80:
        return "Good"
    return "Strong"


bits = entropy_bits(16, 85)
print(f"{bits:.0f} bits -> {strength_label(bits)}")

تابع math.log2 لگاریتم پایهٔ ۲ را حساب می‌کند. مرزهای ۵۰ و ۸۰ بیت که اینجا گذاشته‌ام یک قرارداد ساده برای آموزش است، نه استاندارد رسمی؛ می‌توانید آن‌ها را به سلیقهٔ خودتان تغییر دهید. این عدد هم تقریبی است، چون تضمین وجود هر گروه کمی از آزادی انتخاب کم می‌کند.

قدم چهارم: گرفتن ورودی بدون خطا

کاربر ممکن است به‌جای عدد، حرف تایپ کند یا فقط Enter بزند. دو تابع کمکی می‌نویسیم که ورودی را بررسی می‌کنند و اگر خالی بود، مقدار پیش‌فرض را برمی‌گردانند:

def ask_int(prompt, default, low, high):
    while True:
        raw = input(f"{prompt} [{default}]: ").strip()
        if not raw:
            return default
        if raw.isdigit() and low <= int(raw) <= high:
            return int(raw)
        print(f"Please enter a number between {low} and {high}.")


def ask_yes_no(prompt, default=True):
    hint = "Y/n" if default else "y/N"
    raw = input(f"{prompt} ({hint}): ").strip().lower()
    if not raw:
        return default
    return raw in ("y", "yes")

حلقهٔ while True آن‌قدر سؤال را تکرار می‌کند تا ورودی معتبر برسد. متد isdigit بررسی می‌کند رشته فقط رقم باشد، و مقایسهٔ زنجیره‌ای low <= x <= high یکی از ویژگی‌های زیبای پایتون است که در خیلی از زبان‌ها نیست.

کد کامل ساخت رمز عبور با پایتون

حالا همهٔ تکه‌ها را کنار هم می‌گذاریم و یک تابع main اضافه می‌کنیم که برنامه را اجرا کند. این کد را کامل در فایل password_generator.py بگذارید و با دستور python password_generator.py اجرا کنید:

import math
import secrets
import string

SYMBOLS = "!@#$%^&*()-_=+[]{};:,.?"


def build_pools(use_upper, use_digits, use_symbols):
    """Return the list of character groups the user selected."""
    pools = [string.ascii_lowercase]
    if use_upper:
        pools.append(string.ascii_uppercase)
    if use_digits:
        pools.append(string.digits)
    if use_symbols:
        pools.append(SYMBOLS)
    return pools


def generate_password(length, pools):
    """Build a password that has at least one character from every pool."""
    if length < len(pools):
        raise ValueError(f"Length must be at least {len(pools)}")
    chars = [secrets.choice(pool) for pool in pools]
    alphabet = "".join(pools)
    chars += [secrets.choice(alphabet) for _ in range(length - len(chars))]
    secrets.SystemRandom().shuffle(chars)
    return "".join(chars)


def entropy_bits(length, alphabet_size):
    """Approximate strength: bits of randomness in the password."""
    return length * math.log2(alphabet_size)


def strength_label(bits):
    if bits < 50:
        return "Weak"
    if bits < 80:
        return "Good"
    return "Strong"


def ask_int(prompt, default, low, high):
    """Ask for a whole number; Enter keeps the default."""
    while True:
        raw = input(f"{prompt} [{default}]: ").strip()
        if not raw:
            return default
        if raw.isdigit() and low <= int(raw) <= high:
            return int(raw)
        print(f"Please enter a number between {low} and {high}.")


def ask_yes_no(prompt, default=True):
    """Ask a yes/no question; Enter keeps the default."""
    hint = "Y/n" if default else "y/N"
    raw = input(f"{prompt} ({hint}): ").strip().lower()
    if not raw:
        return default
    return raw in ("y", "yes")


def main():
    print("=== Strong Password Generator ===")
    length = ask_int("Password length", 16, 8, 128)
    count = ask_int("How many passwords", 3, 1, 20)
    use_upper = ask_yes_no("Include uppercase letters?")
    use_digits = ask_yes_no("Include digits?")
    use_symbols = ask_yes_no("Include symbols?")

    pools = build_pools(use_upper, use_digits, use_symbols)
    alphabet_size = sum(len(pool) for pool in pools)
    bits = entropy_bits(length, alphabet_size)

    print()
    for number in range(1, count + 1):
        print(f"{number}. {generate_password(length, pools)}")
    print(f"\nAlphabet size: {alphabet_size} characters")
    print(f"Entropy: {bits:.0f} bits -> {strength_label(bits)}")


if __name__ == "__main__":
    main()

شرط if __name__ == "__main__" باعث می‌شود اگر بعداً این فایل را در برنامهٔ دیگری import کردید، سؤال‌ها خودبه‌خود پرسیده نشوند و فقط از تابع‌ها استفاده کنید.

خروجی نمونه

با زدن Enter برای همهٔ سؤال‌ها (یعنی مقدارهای پیش‌فرض)، خروجی چیزی شبیه این است. رمزها در هر اجرا فرق می‌کنند و قرار هم نیست تکرار شوند:

=== Strong Password Generator ===
Password length [16]:
How many passwords [3]:
Include uppercase letters? (Y/n):
Include digits? (Y/n):
Include symbols? (Y/n):

1. &5l74Z{uj$KXG{:M
2. %-fOW4Exk+Wl-TUi
3. 6+u2(-XxOnu@@NWW

Alphabet size: 85 characters
Entropy: 103 bits -> Strong

همان‌طور که می‌بینید با الفبای ۸۵ کاراکتری و طول ۱۶، رمز حدود ۱۰۳ بیت آنتروپی دارد. اگر علامت‌ها را حذف کنید الفبا ۶۲ کاراکتری می‌شود و برای رسیدن به همان قدرت باید طول را بیشتر کنید؛ این دقیقاً همان نکته‌ای است که این تمرین می‌خواهد نشان دهد: طول رمز مهم‌تر از پیچیدگی ظاهری آن است.

ایده‌هایی برای توسعه

  • حذف کاراکترهای گیج‌کننده: گزینه‌ای اضافه کنید که حروفی مثل l، I، O و 0 را از الفبا حذف کند تا رمز راحت‌تر خوانده شود.
  • رمز عبارتی (passphrase): به‌جای کاراکتر، چند کلمه از یک لیست کلمات با secrets.choice انتخاب کنید؛ مثل river-candle-orbit-mango.
  • آرگومان خط فرمان: با ماژول argparse کاری کنید که بتوان نوشت python password_generator.py --length 24 --count 5.
  • کپی خودکار: رمز اول را در کلیپ‌بورد قرار دهید تا لازم نباشد دستی کپی شود.
  • رابط گرافیکی: با tkinter یک پنجرهٔ کوچک با دکمهٔ «ساخت رمز» طراحی کنید.

خطاهای رایج و راه‌حل

  • استفاده از random به‌جای secrets: برنامه کار می‌کند ولی رمزها از نظر امنیتی قابل اعتماد نیستند. برای هر چیزی که به رمز، توکن یا کلید مربوط است فقط از secrets استفاده کنید.
  • ValueError: invalid literal for int(): وقتی ورودی کاربر را مستقیم به int بدهید و او حرف تایپ کند این خطا رخ می‌دهد. تابع ask_int با بررسی isdigit جلوی آن را می‌گیرد.
  • نام‌گذاری فایل به اسم secrets.py: اگر فایل خود را secrets.py یا string.py بنامید، پایتون به‌جای ماژول استاندارد فایل شما را import می‌کند و خطای AttributeError می‌گیرید. اسم فایل را عوض کنید.
  • ذخیرهٔ رمزها در فایل متنی: رمز تولیدشده را در یک فایل ساده روی دسکتاپ نگه ندارید؛ آن را مستقیم در یک مدیر رمز عبور (Password Manager) ذخیره کنید.

سؤالات متداول

رمز عبور قوی حداقل چند کاراکتر باید باشد؟

عدد جادویی وجود ندارد، ولی هرچه طولانی‌تر بهتر. در این برنامه حداقل را ۸ و پیش‌فرض را ۱۶ گذاشتیم؛ برای حساب‌های مهم ۱۶ کاراکتر یا بیشتر انتخاب معقولی است.

آیا رمزهای ساخته‌شده جایی ذخیره یا ارسال می‌شوند؟

خیر. برنامه کاملاً آفلاین است، به اینترنت وصل نمی‌شود و رمزها فقط در خروجی ترمینال خودتان نمایش داده می‌شوند.

آیا می‌توانم این کد را در سایت یا پروژهٔ خودم استفاده کنم؟

بله. تابع generate_password مستقل است و می‌توانید آن را در یک اسکریپت دیگر یا بک‌اند یک وب‌سایت import کنید؛ مثلاً برای ساخت رمز موقت کاربران جدید.

جمع‌بندی

در این مقاله ساخت رمز عبور با پایتون را از یک تابع یک‌خطی شروع کردیم و به یک پسورد ساز کامل رسیدیم که همهٔ انواع کاراکتر را تضمین می‌کند، ورودی کاربر را کنترل می‌کند و قدرت رمز را با آنتروپی می‌سنجد. در مسیر با ماژول‌های secrets، string و math، list comprehension و حلقه‌های اعتبارسنجی هم آشنا شدید. برای پروژه‌های بیشتر به بخش وبلاگ سر بزنید یا نمونه‌کارهای کدنویسی من را در بخش کد ببینید.

اگر برای سایت، پنل مدیریت یا ابزار اختصاصی کسب‌وکارتان به برنامه‌نویس نیاز دارید، خوشحال می‌شوم کمکتان کنم؛ از طریق صفحهٔ تماس با من در ارتباط باشید.

مقاله‌های دیگر