Tôi cần lưu trữ an toàn tên người dùng và mật khẩu bằng Python, tôi có những lựa chọn nào?


96

Tôi đang viết một tập lệnh Python nhỏ sẽ định kỳ lấy thông tin từ dịch vụ của bên thứ 3 bằng cách sử dụng kết hợp tên người dùng và mật khẩu. Tôi không cần phải tạo ra thứ gì đó có khả năng chống đạn 100% (thậm chí 100% có tồn tại không?), Nhưng tôi muốn liên quan đến một biện pháp an ninh tốt để ít nhất sẽ mất nhiều thời gian để ai đó phá vỡ nó.

Tập lệnh này sẽ không có GUI và sẽ được chạy theo định kỳ cron, vì vậy việc nhập mật khẩu mỗi khi nó chạy để giải mã mọi thứ sẽ không thực sự hoạt động và tôi sẽ phải lưu trữ tên người dùng và mật khẩu trong một tệp được mã hóa hoặc được mã hóa trong cơ sở dữ liệu SQLite, sẽ tốt hơn vì dù sao thì tôi cũng sẽ sử dụng SQLite và tôi có thể cần phải chỉnh sửa mật khẩu vào một lúc nào đó. Ngoài ra, có lẽ tôi sẽ gói toàn bộ chương trình trong một EXE, vì nó dành riêng cho Windows tại thời điểm này.

Làm cách nào tôi có thể lưu trữ an toàn tổ hợp tên người dùng và mật khẩu để sử dụng định kỳ trong croncông việc?


Câu trả lời:


19

Tôi đề xuất một chiến lược tương tự như ssh-agent . Nếu bạn không thể sử dụng trực tiếp ssh-agent, bạn có thể triển khai một cái gì đó tương tự như vậy, để mật khẩu của bạn chỉ được lưu trong RAM. Công việc cron có thể đã cấu hình thông tin đăng nhập để lấy mật khẩu thực từ tác nhân mỗi lần nó chạy, sử dụng nó một lần và hủy tham chiếu nó ngay lập tức bằng delcâu lệnh.

Quản trị viên vẫn phải nhập mật khẩu để khởi động ssh-agent, tại thời điểm khởi động hoặc bất cứ điều gì, nhưng đây là một thỏa hiệp hợp lý để tránh việc lưu trữ mật khẩu văn bản thuần túy ở bất kỳ đâu trên đĩa.


2
+1, rất có ý nghĩa. Tôi luôn có thể xây dựng một giao diện người dùng cho nó về cơ bản yêu cầu người dùng nhập mật khẩu của anh ấy khi khởi động, theo cách đó, nó không bao giờ được lưu trữ trên đĩa và an toàn trước những con mắt tò mò.
Naftuli Kay

54

Các python keyring thư viện tích hợp với các CryptProtectDataAPI trên Windows (cùng với của API có liên quan trên máy Mac và Linux) để mã hóa dữ liệu với thông tin đăng nhập của người dùng.

Cách sử dụng đơn giản:

import keyring

# the service is just a namespace for your app
service_id = 'IM_YOUR_APP!'

keyring.set_password(service_id, 'dustin', 'my secret password')
password = keyring.get_password(service_id, 'dustin') # retrieve password

Cách sử dụng nếu bạn muốn lưu tên người dùng trên keyring:

import keyring

MAGIC_USERNAME_KEY = 'im_the_magic_username_key'

# the service is just a namespace for your app
service_id = 'IM_YOUR_APP!'  

username = 'dustin'

# save password
keyring.set_password(service_id, username, "password")

# optionally, abuse `set_password` to save username onto keyring
# we're just using some known magic string in the username field
keyring.set_password(service_id, MAGIC_USERNAME_KEY, username)

Sau đó để lấy thông tin của bạn từ keyring

# again, abusing `get_password` to get the username.
# after all, the keyring is just a key-value store
username = keyring.get_password(service_id, MAGIC_USERNAME_KEY)
password = keyring.get_password(service_id, username)  

Các mục được mã hóa bằng thông tin đăng nhập hệ điều hành của người dùng, do đó các ứng dụng khác đang chạy trong tài khoản người dùng của bạn sẽ có thể truy cập mật khẩu.

Để che lấp lỗ hổng đó một chút, bạn có thể mã hóa / làm xáo trộn mật khẩu theo một số cách trước khi lưu trữ trên keyring. Tất nhiên, bất kỳ ai nhắm mục tiêu đến tập lệnh của bạn sẽ chỉ có thể xem nguồn và tìm ra cách giải mã / bỏ xáo trộn mật khẩu, nhưng ít nhất bạn sẽ ngăn được một số ứng dụng hút sạch tất cả mật khẩu trong vault và lấy mật khẩu của bạn. .


Tên người dùng nên được lưu trữ như thế nào? Có keyringhỗ trợ lấy cả tên người dùng và mật khẩu không?
Stevoisiak

1
@DustinWyatt Sử dụng thông minh get_passwordcho tên người dùng. Mặc dù, tôi nghĩ rằng bạn nên bắt đầu câu trả lời với các ví dụ đơn giản ban đầu của keyring.set_password()keyring.get_password()
Stevoisiak

keyringkhông phải là một phần của thư viện tiêu chuẩn python
Ciasto piekarz

@Ciastopiekarz Có điều gì về câu trả lời khiến bạn tin rằng nó là một phần của thư viện chuẩn không?
Dustin Wyatt

keyringan toàn xóa mật khẩu khỏi nhật ký và mật khẩu bộ nhớ không?
Kebman

26

Sau khi xem xét câu trả lời cho điều này và các câu hỏi liên quan, tôi đã tập hợp một số mã bằng cách sử dụng một số phương pháp được đề xuất để mã hóa và che giấu dữ liệu bí mật. Mã này dành riêng cho thời điểm tập lệnh phải chạy mà không có sự can thiệp của người dùng (nếu người dùng khởi động nó theo cách thủ công, tốt nhất nên để họ nhập mật khẩu và chỉ lưu nó trong bộ nhớ như câu trả lời cho câu hỏi này). Phương pháp này không siêu an toàn; Về cơ bản, tập lệnh có thể truy cập thông tin bí mật để bất kỳ ai có toàn quyền truy cập hệ thống đều có tập lệnh và các tệp liên quan của nó và có thể truy cập chúng. Điều này thực hiện id che giấu dữ liệu khỏi việc kiểm tra thông thường và giữ an toàn cho các tệp dữ liệu nếu chúng được kiểm tra riêng lẻ hoặc cùng nhau mà không cần tập lệnh.

Động lực của tôi cho điều này là một dự án thăm dò một số tài khoản ngân hàng của tôi để theo dõi các giao dịch - tôi cần nó chạy trong nền mà không cần nhập lại mật khẩu mỗi phút hoặc hai phút.

Chỉ cần dán mã này vào đầu tập lệnh của bạn, thay đổi SaltSeed và sau đó sử dụng store () truy xuất () và yêu cầu () trong mã của bạn nếu cần:

from getpass import getpass
from pbkdf2 import PBKDF2
from Crypto.Cipher import AES
import os
import base64
import pickle


### Settings ###

saltSeed = 'mkhgts465wef4fwtdd' # MAKE THIS YOUR OWN RANDOM STRING

PASSPHRASE_FILE = './secret.p'
SECRETSDB_FILE = './secrets'
PASSPHRASE_SIZE = 64 # 512-bit passphrase
KEY_SIZE = 32 # 256-bit key
BLOCK_SIZE = 16  # 16-bit blocks
IV_SIZE = 16 # 128-bits to initialise
SALT_SIZE = 8 # 64-bits of salt


### System Functions ###

def getSaltForKey(key):
    return PBKDF2(key, saltSeed).read(SALT_SIZE) # Salt is generated as the hash of the key with it's own salt acting like a seed value

def encrypt(plaintext, salt):
    ''' Pad plaintext, then encrypt it with a new, randomly initialised cipher. Will not preserve trailing whitespace in plaintext!'''

    # Initialise Cipher Randomly
    initVector = os.urandom(IV_SIZE)

    # Prepare cipher key:
    key = PBKDF2(passphrase, salt).read(KEY_SIZE)

    cipher = AES.new(key, AES.MODE_CBC, initVector) # Create cipher

    return initVector + cipher.encrypt(plaintext + ' '*(BLOCK_SIZE - (len(plaintext) % BLOCK_SIZE))) # Pad and encrypt

def decrypt(ciphertext, salt):
    ''' Reconstruct the cipher object and decrypt. Will not preserve trailing whitespace in the retrieved value!'''

    # Prepare cipher key:
    key = PBKDF2(passphrase, salt).read(KEY_SIZE)

    # Extract IV:
    initVector = ciphertext[:IV_SIZE]
    ciphertext = ciphertext[IV_SIZE:]

    cipher = AES.new(key, AES.MODE_CBC, initVector) # Reconstruct cipher (IV isn't needed for edecryption so is set to zeros)

    return cipher.decrypt(ciphertext).rstrip(' ') # Decrypt and depad


### User Functions ###

def store(key, value):
    ''' Sore key-value pair safely and save to disk.'''
    global db

    db[key] = encrypt(value, getSaltForKey(key))
    with open(SECRETSDB_FILE, 'w') as f:
        pickle.dump(db, f)

def retrieve(key):
    ''' Fetch key-value pair.'''
    return decrypt(db[key], getSaltForKey(key))

def require(key):
    ''' Test if key is stored, if not, prompt the user for it while hiding their input from shoulder-surfers.'''
    if not key in db: store(key, getpass('Please enter a value for "%s":' % key))


### Setup ###

# Aquire passphrase:
try:
    with open(PASSPHRASE_FILE) as f:
        passphrase = f.read()
    if len(passphrase) == 0: raise IOError
except IOError:
    with open(PASSPHRASE_FILE, 'w') as f:
        passphrase = os.urandom(PASSPHRASE_SIZE) # Random passphrase
        f.write(base64.b64encode(passphrase))

        try: os.remove(SECRETSDB_FILE) # If the passphrase has to be regenerated, then the old secrets file is irretrievable and should be removed
        except: pass
else:
    passphrase = base64.b64decode(passphrase) # Decode if loaded from already extant file

# Load or create secrets database:
try:
    with open(SECRETSDB_FILE) as f:
        db = pickle.load(f)
    if db == {}: raise IOError
except (IOError, EOFError):
    db = {}
    with open(SECRETSDB_FILE, 'w') as f:
        pickle.dump(db, f)

### Test (put your code here) ###
require('id')
require('password1')
require('password2')
print
print 'Stored Data:'
for key in db:
    print key, retrieve(key) # decode values on demand to avoid exposing the whole database in memory
    # DO STUFF

Tính bảo mật của phương pháp này sẽ được cải thiện đáng kể nếu quyền của hệ điều hành được đặt trên các tệp bí mật để chỉ cho phép bản thân tập lệnh đọc chúng và nếu bản thân tập lệnh được biên dịch và đánh dấu là chỉ thực thi (không thể đọc). Một số trong số đó có thể được tự động hóa, nhưng tôi không bận tâm. Nó có thể sẽ yêu cầu thiết lập một người dùng cho tập lệnh và chạy tập lệnh với tư cách là người dùng đó (và đặt quyền sở hữu các tệp của tập lệnh cho người dùng đó).

Tôi muốn có bất kỳ đề xuất, chỉ trích hoặc các điểm dễ bị tổn thương khác mà bất kỳ ai có thể nghĩ ra. Tôi còn khá mới với việc viết mã tiền điện tử nên những gì tôi đã làm gần như chắc chắn có thể được cải thiện.


26

Có một số tùy chọn để lưu trữ mật khẩu và các bí mật khác mà một chương trình Python cần sử dụng, đặc biệt là một chương trình cần chạy ở chế độ nền, nơi nó không thể chỉ yêu cầu người dùng nhập mật khẩu.

Các vấn đề cần tránh:

  1. Kiểm tra mật khẩu để kiểm soát nguồn nơi các nhà phát triển khác hoặc thậm chí công chúng có thể nhìn thấy nó.
  2. Những người dùng khác trên cùng một máy chủ đọc mật khẩu từ tệp cấu hình hoặc mã nguồn.
  3. Có mật khẩu trong một tệp nguồn mà người khác có thể nhìn thấy nó qua vai bạn khi bạn đang chỉnh sửa nó.

Tùy chọn 1: SSH

Đây không phải lúc nào cũng là một lựa chọn, nhưng nó có lẽ là tốt nhất. Khóa riêng tư của bạn không bao giờ được truyền qua mạng, SSH chỉ chạy các phép tính toán học để chứng minh rằng bạn có đúng khóa.

Để làm cho nó hoạt động, bạn cần những điều sau:

  • Cơ sở dữ liệu hoặc bất cứ thứ gì bạn đang truy cập cần phải được SSH truy cập. Thử tìm kiếm "SSH" cùng với bất kỳ dịch vụ nào bạn đang truy cập. Ví dụ: "ssh postgresql" . Nếu đây không phải là một tính năng trên cơ sở dữ liệu của bạn, hãy chuyển sang tùy chọn tiếp theo.
  • Tạo một tài khoản để chạy dịch vụ sẽ thực hiện các cuộc gọi đến cơ sở dữ liệu và tạo khóa SSH .
  • Thêm khóa công khai vào dịch vụ bạn sẽ gọi hoặc tạo tài khoản cục bộ trên máy chủ đó và cài đặt khóa công khai ở đó.

Tùy chọn 2: Biến môi trường

Đây là cách đơn giản nhất, vì vậy nó có thể là một nơi tốt để bắt đầu. Nó được mô tả tốt trong ứng dụng Twelve Factor . Ý tưởng cơ bản là mã nguồn của bạn chỉ lấy mật khẩu hoặc các bí mật khác từ các biến môi trường, sau đó bạn định cấu hình các biến môi trường đó trên mỗi hệ thống mà bạn chạy chương trình. Nó cũng có thể là một liên lạc tốt nếu bạn sử dụng các giá trị mặc định sẽ phù hợp với hầu hết các nhà phát triển. Bạn phải cân bằng điều đó với việc làm cho phần mềm của bạn "an toàn theo mặc định".

Dưới đây là một ví dụ lấy máy chủ, tên người dùng và mật khẩu từ các biến môi trường.

import os

server = os.getenv('MY_APP_DB_SERVER', 'localhost')
user = os.getenv('MY_APP_DB_USER', 'myapp')
password = os.getenv('MY_APP_DB_PASSWORD', '')

db_connect(server, user, password)

Tra cứu cách thiết lập các biến môi trường trong hệ điều hành của bạn và xem xét việc chạy dịch vụ theo tài khoản của chính nó. Bằng cách đó, bạn không có dữ liệu nhạy cảm trong các biến môi trường khi chạy chương trình trong tài khoản của riêng mình. Khi bạn thiết lập các biến môi trường đó, hãy cẩn thận để người dùng khác không thể đọc chúng. Kiểm tra quyền đối với tệp, chẳng hạn. Tất nhiên bất kỳ người dùng nào có quyền root sẽ có thể đọc chúng, nhưng điều đó không thể tránh được. Nếu bạn đang sử dụng systemd, hãy xem đơn vị dịch vụ và cẩn thận sử dụng EnvironmentFilethay vì sử dụng Environmentcho bất kỳ bí mật nào. Environmentbất kỳ người dùng nào có thể xem giá trị systemctl show.

Tùy chọn 3: Tệp cấu hình

Điều này rất giống với các biến môi trường, nhưng bạn đọc các bí mật từ một tệp văn bản. Tôi vẫn thấy các biến môi trường linh hoạt hơn cho những thứ như công cụ triển khai và máy chủ tích hợp liên tục. Nếu bạn quyết định sử dụng tệp cấu hình, Python hỗ trợ một số định dạng trong thư viện chuẩn, như JSON , INI , netrcXML . Bạn cũng có thể tìm thấy các gói bên ngoài như PyYAMLTOML . Cá nhân tôi thấy JSON và YAML là cách sử dụng đơn giản nhất và YAML cho phép nhận xét.

Ba điều cần xem xét với các tệp cấu hình:

  1. Tệp ở đâu? Có thể một vị trí mặc định như ~/.my_app, và một tùy chọn dòng lệnh để sử dụng một vị trí khác.
  2. Đảm bảo rằng những người dùng khác không thể đọc tệp.
  3. Rõ ràng, không cam kết tệp cấu hình thành mã nguồn. Bạn có thể muốn cam kết một mẫu mà người dùng có thể sao chép vào thư mục chính của họ.

Tùy chọn 4: Mô-đun Python

Một số dự án chỉ đưa các bí mật của họ vào ngay một mô-đun Python.

# settings.py
db_server = 'dbhost1'
db_user = 'my_app'
db_password = 'correcthorsebatterystaple'

Sau đó nhập mô-đun đó để nhận các giá trị.

# my_app.py
from settings import db_server, db_user, db_password

db_connect(db_server, db_user, db_password)

Một dự án sử dụng kỹ thuật này là Django . Rõ ràng, bạn không nên cam kết settings.pykiểm soát nguồn, mặc dù bạn có thể muốn cam kết một tệp được gọi là settings_template.pyngười dùng có thể sao chép và sửa đổi.

Tôi thấy một số vấn đề với kỹ thuật này:

  1. Các nhà phát triển có thể vô tình chuyển tệp sang quyền kiểm soát nguồn. Thêm nó để .gitignoregiảm nguy cơ đó.
  2. Một số mã của bạn không được kiểm soát nguồn. Nếu bạn có kỷ luật và chỉ đặt chuỗi và số ở đây, điều đó sẽ không thành vấn đề. Nếu bạn bắt đầu viết các lớp lọc ghi nhật ký ở đây, hãy dừng lại!

Nếu dự án của bạn đã sử dụng kỹ thuật này, thật dễ dàng để chuyển đổi sang các biến môi trường. Chỉ cần di chuyển tất cả các giá trị cài đặt sang các biến môi trường và thay đổi mô-đun Python để đọc từ các biến môi trường đó.


Xin chào. Nếu dự án của bạn đã sử dụng kỹ thuật này, rất dễ dàng chuyển đổi sang các biến môi trường. Tôi biết cách đặt các biến môi trường trong Windows 10 theo cách thủ công nhưng có thể truy cập chúng từ mã python của mình bằng cách sử dụng os.getenv(). Làm thế nào chúng ta nên làm điều này nếu mã đang được chia sẻ? Nếu mã được tải xuống bởi một nhà phát triển khác, làm cách nào để đảm bảo rằng các biến môi trường đã được đặt cho anh ta?
a_sid

Tôi cố gắng chuyển một giá trị mặc định hợp lý đến os.getenv(), @a_sid, vì vậy mã ít nhất sẽ chạy cho người dùng chưa đặt các biến môi trường. Nếu không có giá trị mặc định tốt, hãy nêu ra lỗi rõ ràng khi bạn nhận được None. Ngoài ra, hãy ghi chú thích rõ ràng vào tệp cài đặt. Nếu tôi hiểu sai điều gì đó, tôi khuyên bạn nên đặt một câu hỏi riêng.
Don Kirkby

8

Không có nhiều điểm khi cố gắng mã hóa mật khẩu: người mà bạn đang cố giấu mật khẩu có tập lệnh Python, tập lệnh này sẽ có mã để giải mã. Cách nhanh nhất để lấy mật khẩu là thêm câu lệnh in vào tập lệnh Python ngay trước khi nó sử dụng mật khẩu với dịch vụ của bên thứ ba.

Vì vậy, hãy lưu trữ mật khẩu dưới dạng một chuỗi trong tập lệnh và mã hóa base64 để chỉ đọc tệp là không đủ, sau đó gọi nó là một ngày.


Tôi sẽ cần chỉnh sửa tên người dùng và mật khẩu định kỳ và tôi sẽ gói toàn bộ nội dung trong EXE cho Windoze; Tôi đã chỉnh sửa bài đăng để phản ánh điều này. Tôi có nên đơn giản là base64 nó ở bất cứ nơi nào tôi muốn lưu trữ nó không?
Naftuli Kay

Tôi đồng ý rằng "mã hóa" mật khẩu không giúp ích được gì, vì mật khẩu văn bản thuần túy dù sao cũng phải được lấy theo cách tự động và do đó phải có được từ bất cứ thứ gì được lưu trữ. Nhưng có những cách tiếp cận khả thi.
wberry

Tôi nghĩ rằng tôi đã nhận ra tên của bạn, bạn đã ở trong bảng điều khiển dành cho người mới bắt đầu và chuyên gia trên TalkPython, với tư cách là người mới bắt đầu, thông điệp của bạn thực sự gây được tiếng vang với tôi, cảm ơn!
Manakin

7

Tôi nghĩ điều tốt nhất bạn có thể làm là bảo vệ tệp script và hệ thống mà nó đang chạy.

Về cơ bản làm như sau:

  • Sử dụng quyền đối với hệ thống tệp (chmod 400)
  • Mật khẩu mạnh cho tài khoản của chủ sở hữu trên hệ thống
  • Giảm khả năng hệ thống bị xâm phạm (tường lửa, tắt các dịch vụ không cần thiết, v.v.)
  • Loại bỏ các đặc quyền quản trị / root / sudo cho những người không cần nó

Thật không may, đó là Windows, tôi sẽ gói nó trong một EXE và tôi sẽ cần phải thay đổi mật khẩu thường xuyên, vì vậy mã hóa cứng sẽ không phải là một lựa chọn.
Naftuli Kay

1
Windows vẫn có quyền đối với hệ thống tệp. Lưu trữ mật khẩu trong một tệp bên ngoài và xóa quyền truy cập của mọi người ngoại trừ quyền truy cập của riêng bạn. Bạn cũng có thể phải xóa các đặc quyền quản trị của họ.
Corey D

Vâng, sử dụng quyền là tùy chọn bảo mật đáng tin cậy duy nhất ở đây. Rõ ràng là bất kỳ quản trị viên nào vẫn có thể truy cập vào dữ liệu (ít nhất là trên windows / các bản phân phối linux thông thường) nhưng sau đó thì đó là một trận chiến đã mất.
Voo

Đúng rồi. Khi quá trình giải mã mật khẩu được tự động hóa, thì điều đó cũng tốt như khi có một mật khẩu văn bản thuần túy. Bảo mật thực sự là khóa tài khoản người dùng có quyền truy cập. Điều tốt nhất có thể làm là cấp quyền chỉ đọc cho chỉ tài khoản người dùng đó. Có thể tạo một người dùng đặc biệt, cụ thể và chỉ cho dịch vụ đó.
Sepero

1

hệ điều hành thường có hỗ trợ để bảo mật dữ liệu cho người dùng. trong trường hợp cửa sổ, nó giống như http://msdn.microsoft.com/en-us/library/aa380261.aspx

bạn có thể gọi win32 apis từ python bằng cách sử dụng http://vermeulen.ca/python-win32api.html

Theo như tôi hiểu, điều này sẽ lưu trữ dữ liệu để nó chỉ có thể được truy cập từ tài khoản được sử dụng để lưu trữ. nếu bạn muốn chỉnh sửa dữ liệu, bạn có thể làm như vậy bằng cách viết mã để trích xuất, thay đổi và lưu giá trị.


Đây có vẻ là lựa chọn tốt nhất đối với tôi, nhưng tôi cảm thấy câu trả lời này quá không đầy đủ để chấp nhận nó, vì nó thiếu bất kỳ ví dụ thực tế nào.
ArtOfWarfare

1
Có một số ví dụ để sử dụng các hàm này trong Python ở đây: stackoverflow.com/questions/463832/using-dpapi-with-python
ArtOfWarfare

1

Tôi đã sử dụng Cryptography vì tôi gặp sự cố khi cài đặt (biên dịch) các thư viện thường được đề cập khác trên hệ thống của mình. (Win7 x64, Python 3.5)

from cryptography.fernet import Fernet
key = Fernet.generate_key()
cipher_suite = Fernet(key)
cipher_text = cipher_suite.encrypt(b"password = scarybunny")
plain_text = cipher_suite.decrypt(cipher_text)

Tập lệnh của tôi đang chạy trong hệ thống / phòng an toàn vật lý. Tôi mã hóa thông tin đăng nhập bằng "tập lệnh mã hóa" thành tệp cấu hình. Và sau đó giải mã khi tôi cần sử dụng chúng. "Tập lệnh mã hóa" không có trên hệ thống thực, chỉ có tệp cấu hình được mã hóa. Ai đó phân tích mã có thể dễ dàng phá vỡ mã hóa bằng cách phân tích mã, nhưng bạn vẫn có thể biên dịch nó thành EXE nếu cần.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.