Muszę bezpiecznie przechowywać nazwę użytkownika i hasło w Pythonie, jakie mam opcje? [Zamknięte]

95

Piszę mały skrypt w Pythonie, który będzie okresowo pobierał informacje z usługi innej firmy, używając kombinacji nazwy użytkownika i hasła. Nie muszę tworzyć czegoś, co jest w 100% kuloodporne (czy 100% w ogóle istnieje?), Ale chciałbym zapewnić dobre zabezpieczenie, więc przynajmniej zajęłoby to dużo czasu, zanim ktoś to złamie.

Ten skrypt nie będzie miał GUI i będzie uruchamiany okresowo przez cron, więc wprowadzanie hasła za każdym razem, gdy jest uruchamiany w celu odszyfrowania rzeczy, tak naprawdę nie zadziała i będę musiał przechowywać nazwę użytkownika i hasło w zaszyfrowanym pliku lub zaszyfrowanym w bazie danych SQLite, co byłoby lepsze, ponieważ i tak będę używać SQLite, a może być konieczne zmodyfikowanie hasła w pewnym momencie. Ponadto prawdopodobnie zapakuję cały program w plik EXE, ponieważ w tym momencie jest on przeznaczony wyłącznie dla systemu Windows.

Jak mogę bezpiecznie przechowywać kombinację nazwy użytkownika i hasła, które będą używane okresowo w ramach cronzadania?

Naftuli Kay
źródło
Zobacz też : stackoverflow.com/questions/157938
dreftymac

Odpowiedzi:

19

Polecam strategię podobną do ssh-agent . Jeśli nie możesz bezpośrednio używać ssh-agent, możesz zaimplementować coś takiego, aby twoje hasło było przechowywane tylko w pamięci RAM. Zadanie cron mogło skonfigurować poświadczenia, aby uzyskać rzeczywiste hasło od agenta przy każdym uruchomieniu, użyć go raz i natychmiast usunąć odwołanie za pomocą delinstrukcji.

Administrator nadal musi wprowadzić hasło, aby uruchomić ssh-agent, przy starcie lub czymkolwiek, ale jest to rozsądny kompromis, który pozwala uniknąć przechowywania hasła w postaci zwykłego tekstu w dowolnym miejscu na dysku.

jagoda
źródło
2
+1, to ma sens. Zawsze mógłbym zbudować dla niego interfejs użytkownika, który zasadniczo prosi użytkownika o jego hasło podczas rozruchu, w ten sposób nigdy nie jest przechowywany na dysku i jest bezpieczny przed wścibskimi oczami.
Naftuli Kay
55

Pyton brelok biblioteki integruje się z CryptProtectDataAPI w systemie Windows (wraz z odpowiednimi API na Mac i Linux), który szyfruje dane z poświadczeniami logowania użytkownika.

Proste użycie:

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

Sposób użycia, jeśli chcesz przechowywać nazwę użytkownika na breloku:

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)

Później, aby uzyskać informacje z breloczka

# 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)  

Elementy są szyfrowane przy użyciu poświadczeń systemu operacyjnego użytkownika, dzięki czemu inne aplikacje działające na koncie użytkownika będą miały dostęp do hasła.

Aby nieco ukryć tę lukę, możesz zaszyfrować / zaciemnić hasło w jakiś sposób przed zapisaniem go w bazie kluczy. Oczywiście każdy, kto celował w twój skrypt, mógłby po prostu spojrzeć na źródło i dowiedzieć się, jak odszyfrować / odszyfrować hasło, ale przynajmniej zapobiegniesz, aby niektóre aplikacje odkurzyły wszystkie hasła w skarbcu i również twoje .

Dustin Wyatt
źródło
Jak powinna być przechowywana nazwa użytkownika? Czy keyringobsługuje odzyskiwanie zarówno nazwy użytkownika, jak i hasła?
Stevoisiak
1
@DustinWyatt Sprytne użycie get_passwordjako nazwy użytkownika. Chociaż myślę, że powinieneś rozpocząć odpowiedź od oryginalnego uproszczonego przykładu keyring.set_password()ikeyring.get_password()
Stevoisiak
keyringnie jest częścią standardowej biblioteki Pythona
Ciasto piekarz
@Ciastopiekarz Czy coś w tej odpowiedzi sprawiło, że uwierzyłeś, że jest częścią standardowej biblioteki?
Dustin Wyatt
Czy keyringbezpiecznie usuwa hasło z dzienników i pamięci po słowach?
Kebman
26

Po przejrzeniu odpowiedzi na to i pokrewne pytania stworzyłem kod, korzystając z kilku sugerowanych metod szyfrowania i ukrywania tajnych danych. Ten kod jest szczególnie przydatny, gdy skrypt ma działać bez interwencji użytkownika (jeśli użytkownik uruchamia go ręcznie, najlepiej jest wprowadzić hasło i zachować je w pamięci tylko zgodnie z odpowiedzią na to pytanie). Ta metoda nie jest super bezpieczna; zasadniczo skrypt może uzyskać dostęp do tajnych informacji, więc każdy, kto ma pełny dostęp do systemu, ma skrypt i powiązane z nim pliki oraz może uzyskać do nich dostęp. To, co robi, id zasłania dane przed przypadkową inspekcją i pozostawia same pliki danych, jeśli są one badane pojedynczo lub razem bez skryptu.

Moją motywacją do tego jest projekt, który sonduje niektóre z moich rachunków bankowych w celu monitorowania transakcji - potrzebuję, aby działał w tle bez ponownego wprowadzania haseł co minutę lub dwie.

Po prostu wklej ten kod na górze skryptu, zmień saltSeed, a następnie użyj store () retrieve () i require () w swoim kodzie w razie potrzeby:

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

Bezpieczeństwo tej metody zostałoby znacznie poprawione, gdyby uprawnienia systemu operacyjnego zostały ustawione na tajnych plikach, aby umożliwić tylko samemu skryptowi odczytanie ich, i gdyby sam skrypt został skompilowany i oznaczony jako tylko wykonywalny (nieczytelny). Niektóre z nich można by zautomatyzować, ale nie przejmowałem się. Prawdopodobnie wymagałoby to skonfigurowania użytkownika dla skryptu i uruchomienia skryptu jako ten użytkownik (i przypisania prawa własności do plików skryptu temu użytkownikowi).

Z przyjemnością przyjmuję wszelkie sugestie, krytykę lub inne słabe punkty, o których każdy może pomyśleć. Jestem całkiem nowy w pisaniu kodu kryptograficznego, więc to, co zrobiłem, prawie na pewno można ulepszyć.

drodgers
źródło
26

Istnieje kilka opcji przechowywania haseł i innych sekretów, których program w Pythonie musi używać, w szczególności program, który musi działać w tle, gdzie nie może po prostu poprosić użytkownika o wpisanie hasła.

Problemy, których należy unikać:

  1. Sprawdzanie hasła w kontroli źródła, gdzie inni programiści, a nawet opinia publiczna, mogą je zobaczyć.
  2. Inni użytkownicy na tym samym serwerze odczytują hasło z pliku konfiguracyjnego lub kodu źródłowego.
  3. Posiadanie hasła w pliku źródłowym, gdzie inni mogą je zobaczyć przez ramię podczas jego edycji.

Opcja 1: SSH

Nie zawsze jest to opcja, ale prawdopodobnie jest najlepsza. Twój klucz prywatny nigdy nie jest przesyłany przez sieć, SSH wykonuje tylko obliczenia matematyczne, aby udowodnić, że masz właściwy klucz.

Aby to działało, potrzebujesz:

  • Baza danych lub cokolwiek, do czego uzyskujesz dostęp, musi być dostępne przez SSH. Spróbuj wyszukać „SSH” oraz dowolną usługę, do której uzyskujesz dostęp. Na przykład „ssh postgresql” . Jeśli to nie jest funkcja w Twojej bazie danych, przejdź do następnej opcji.
  • Utwórz konto, aby uruchomić usługę, która będzie wykonywać wywołania bazy danych, i wygeneruj klucz SSH .
  • Albo dodaj klucz publiczny do usługi, do której będziesz dzwonić, albo utwórz konto lokalne na tym serwerze i zainstaluj tam klucz publiczny.

Opcja 2: zmienne środowiskowe

Ten jest najprostszy, więc może być dobrym miejscem do rozpoczęcia. Jest to dobrze opisane w aplikacji Twelve Factor . Podstawowym pomysłem jest to, że kod źródłowy po prostu pobiera hasło lub inne wpisy tajne ze zmiennych środowiskowych, a następnie konfiguruje te zmienne środowiskowe w każdym systemie, w którym uruchamiasz program. Może być również miłym akcentem, jeśli użyjesz wartości domyślnych, które będą działać dla większości programistów. Musisz zrównoważyć to z zapewnieniem „domyślnego bezpieczeństwa” oprogramowania.

Oto przykład, który pobiera serwer, nazwę użytkownika i hasło ze zmiennych środowiskowych.

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)

Sprawdź, jak ustawić zmienne środowiskowe w systemie operacyjnym i rozważ uruchomienie usługi na własnym koncie. W ten sposób nie masz wrażliwych danych w zmiennych środowiskowych, gdy uruchamiasz programy na swoim własnym koncie. Po skonfigurowaniu tych zmiennych środowiskowych należy uważać, aby inni użytkownicy nie mogli ich odczytać. Sprawdź na przykład uprawnienia do plików. Oczywiście każdy użytkownik z uprawnieniami roota będzie mógł je przeczytać, ale nie można na to poradzić. Jeśli używasz systemd, spójrz na jednostkę usługową i uważaj, aby użyć EnvironmentFilezamiast Environmentjakichkolwiek sekretów. Environmentwartości mogą być przeglądane przez każdego użytkownika z systemctl show.

Opcja 3: Pliki konfiguracyjne

Jest to bardzo podobne do zmiennych środowiskowych, ale odczytujesz sekrety z pliku tekstowego. Nadal uważam, że zmienne środowiskowe są bardziej elastyczne w przypadku narzędzi do wdrażania i serwerów ciągłej integracji. Jeśli zdecydujesz się użyć pliku konfiguracyjnego, Python obsługuje kilka formatów w bibliotece standardowej, takich jak JSON , INI , netrc i XML . Możesz również znaleźć pakiety zewnętrzne, takie jak PyYAML i TOML . Osobiście uważam, że JSON i YAML są najprostsze w użyciu, a YAML pozwala na komentarze.

Trzy rzeczy do rozważenia w przypadku plików konfiguracyjnych:

  1. Gdzie jest plik? Może domyślna lokalizacja, jak ~/.my_appi opcja wiersza poleceń, aby użyć innej lokalizacji.
  2. Upewnij się, że inni użytkownicy nie mogą odczytać pliku.
  3. Oczywiście nie wysyłaj pliku konfiguracyjnego do kodu źródłowego. Możesz chcieć zatwierdzić szablon, który użytkownicy będą mogli skopiować do swojego katalogu domowego.

Opcja 4: moduł Python

Niektóre projekty po prostu umieszczają swoje sekrety bezpośrednio w module Pythona.

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

Następnie zaimportuj ten moduł, aby uzyskać wartości.

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

db_connect(db_server, db_user, db_password)

Jednym z projektów wykorzystujących tę technikę jest Django . Oczywiście nie powinieneś angażować settings.pysię w kontrolę źródła, chociaż możesz chcieć zatwierdzić plik o nazwie, settings_template.pyktóry użytkownicy mogą kopiować i modyfikować.

Widzę kilka problemów z tą techniką:

  1. Deweloperzy mogą przypadkowo przekazać plik do kontroli źródła. Dodanie go .gitignorezmniejsza to ryzyko.
  2. Część twojego kodu nie jest pod kontrolą źródła. Jeśli jesteś zdyscyplinowany i umieszczasz tutaj tylko ciągi i liczby, nie będzie to problem. Jeśli zaczniesz pisać tutaj klasy filtrów logowania, przestań!

Jeśli Twój projekt już korzysta z tej techniki, łatwo jest przejść do zmiennych środowiskowych. Po prostu przenieś wszystkie wartości ustawień do zmiennych środowiskowych i zmień moduł Pythona, aby czytał z tych zmiennych środowiskowych.

Don Kirkby
źródło
Witaj. Jeśli Twój projekt już korzysta z tej techniki, łatwo jest przejść do zmiennych środowiskowych. Wiem, jak ręcznie ustawić zmienne środowiskowe w systemie Windows 10, ale mogę uzyskać do nich dostęp z mojego kodu Pythona za pomocą os.getenv(). Jak powinniśmy to zrobić, jeśli kod jest udostępniany? Jeśli kod jest pobierany przez innego programistę, w jaki sposób powinien upewnić się, że zmienne środowiskowe są już dla niego ustawione?
a_sid
Próbuję przekazać rozsądną wartość domyślną do os.getenv()@a_sid, aby kod działał przynajmniej dla użytkownika, który nie ustawił zmiennych środowiskowych. Jeśli nie ma dobrej wartości domyślnej, zgłoś wyraźny błąd, gdy otrzymasz None. Poza tym umieść wyraźne komentarze w pliku ustawień. Jeśli coś źle zrozumiałem, proponuję zadać osobne pytanie.
Don Kirkby
8

Nie ma sensu próbować zaszyfrować hasła: osoba, przed którą próbujesz je ukryć, ma skrypt Pythona, który będzie miał kod do odszyfrowania. Najszybszym sposobem uzyskania hasła będzie dodanie instrukcji print do skryptu Pythona tuż przed użyciem hasła w usłudze innej firmy.

Więc zapisz hasło jako ciąg znaków w skrypcie i zakoduj base64, aby samo odczytanie pliku nie wystarczyło, a następnie nazwij to dzień.

Ned Batchelder
źródło
Będę musiał okresowo edytować nazwę użytkownika i hasło, a całość zapakuję w plik EXE dla Windoze; Zredagowałem post, aby to odzwierciedlić. Czy powinienem po prostu bazować na 64 miejscu, gdziekolwiek go przechowuję?
Naftuli Kay
Zgadzam się, że „zaszyfrowanie” hasła nie pomaga, ponieważ hasło w postaci zwykłego tekstu i tak musi być uzyskane w sposób zautomatyzowany, a zatem musi być dostępne z każdego przechowywanego. Ale są realne podejścia.
wberry
Myślałem, że rozpoznałem Twoje imię, byłeś na panelu dla początkujących i ekspertów w TalkPython, jako początkujący, Twoja wiadomość naprawdę przemówiła do mnie, dzięki!
Manakin
7

Myślę, że najlepsze, co możesz zrobić, to chronić plik skryptu i system, na którym działa.

Zasadniczo wykonaj następujące czynności:

  • Użyj uprawnień systemu plików (chmod 400)
  • Silne hasło do konta właściciela w systemie
  • Ograniczenie możliwości włamania do systemu (zapora ogniowa, wyłączenie niepotrzebnych usług itp.)
  • Usuń uprawnienia administracyjne / root / sudo dla tych, którzy ich nie potrzebują
Corey D.
źródło
Niestety to Windows, zapakuję go w plik EXE i będę musiał co jakiś czas zmieniać hasło, więc zakodowanie tego nie będzie możliwe.
Naftuli Kay
1
Windows nadal ma uprawnienia do systemu plików. Przechowuj hasło w pliku zewnętrznym i usuń dostęp wszystkich, z wyjątkiem własnego. Prawdopodobnie musisz także usunąć ich uprawnienia administracyjne.
Corey D
Tak, używanie uprawnień jest tutaj jedyną niezawodną opcją bezpieczeństwa. Oczywiście każdy administrator nadal będzie mógł uzyskać dostęp do danych (przynajmniej w systemach Windows / zwykłych dystrybucjach Linuksa), ale wtedy jest to już przegrana bitwa.
Voo
To prawda. Gdy odszyfrowanie hasła jest zautomatyzowane, jest to równie dobre, jak posiadanie hasła w postaci zwykłego tekstu. Prawdziwe bezpieczeństwo polega na zablokowaniu konta użytkownika z dostępem. Najlepsze, co można zrobić, to przyznać uprawnienia tylko do odczytu tylko temu kontu użytkownika. Ewentualnie stworzyć specjalnego użytkownika, specjalnie i tylko dla tej usługi.
Sepero
1

systemy operacyjne często obsługują zabezpieczenie danych użytkownika. w przypadku Windows wygląda to jak http://msdn.microsoft.com/en-us/library/aa380261.aspx

możesz wywołać apis win32 z Pythona za pomocą http://vermeulen.ca/python-win32api.html

o ile rozumiem, dane będą przechowywane tak, aby można było uzyskać do nich dostęp tylko z konta używanego do ich przechowywania. jeśli chcesz edytować dane, możesz to zrobić, pisząc kod, aby wyodrębnić, zmienić i zapisać wartość.

Andrew Cooke
źródło
Wydaje mi się, że to najlepszy wybór, ale uważam, że ta odpowiedź jest zbyt niekompletna, aby ją zaakceptować, biorąc pod uwagę, że brakuje jej rzeczywistych przykładów.
ArtOfWarfare
1
Oto kilka przykładów użycia tych funkcji w Pythonie: stackoverflow.com/questions/463832/using-dpapi-with-python
ArtOfWarfare
1

Użyłem kryptografii, ponieważ miałem problemy z instalacją (kompilacją) innych powszechnie wymienianych bibliotek w moim systemie. (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)

Mój skrypt działa w fizycznie bezpiecznym systemie / pomieszczeniu. Szyfruję dane uwierzytelniające „skryptem szyfrującym” do pliku konfiguracyjnego. A potem odszyfruj, kiedy muszę ich użyć. "Skryptu szyfrującego" nie ma w rzeczywistym systemie, jest tylko zaszyfrowany plik konfiguracyjny. Ktoś, kto analizuje kod, może łatwo złamać szyfrowanie, analizując kod, ale nadal możesz skompilować go do pliku EXE, jeśli to konieczne.

KRBA
źródło