ユーザー名とパスワードをPythonに安全に保存する必要がありますが、どのようなオプションがありますか?


96

ユーザー名とパスワードの組み合わせを使用してサードパーティのサービスから定期的に情報を取得する小さなPythonスクリプトを書いています。100%防弾のようなものを作成する必要はありませんが(100%も存在しますか?)、十分なセキュリティ対策を講じたいので、少なくとも誰かがそれを壊すのに長い時間がかかります。

このスクリプトにはGUIがなく、によって定期的に実行されるcronため、実行するたびにパスワードを入力して復号化することは実際には機能せず、ユーザー名とパスワードを暗号化されたファイルまたは暗号化されたファイルに保存する必要がありますSQLiteデータベースでは、とにかくSQLiteを使用するので、これが望ましいでしょう。ある時点でパスワードを編集する必要があるかもしれません。さらに、現時点ではWindows専用であるため、プログラム全体をEXEでラップすることになります。

cronジョブを介して定期的に使用するユーザー名とパスワードの組み合わせを安全に保存するにはどうすればよいですか?


回答:


19

ssh-agentに似た戦略をお勧めします。ssh-agentを直接使用できない場合は、そのようなものを実装して、パスワードがRAMにのみ保持されるようにすることができます。cronジョブは、実行するたびにエージェントから実際のパスワードを取得し、それを1回使用し、delステートメントを使用してすぐに参照を解除するように資格情報を構成できます。

管理者は、起動時などにssh-agentを起動するためにパスワードを入力する必要がありますが、これは、プレーンテキストのパスワードがディスクのどこにでも保存されないようにするための妥当な妥協案です。


2
+1、それは非常に理にかなっています。起動時に基本的にユーザーにパスワードを要求するUIをいつでも構築できます。そうすれば、ディスクに保存されることはなく、詮索好きな目から安全になります。
Naftuli Kay 2011

54

Pythonは、ライブラリキーリングと統合しCryptProtectData、ユーザーのログオン資格情報を使用してデータを暗号化(関連するAPIのMacやLinux上と一緒に)Windows上のAPI。

簡単な使用法:

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

キーリングにユーザー名を保存する場合の使用法:

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)

後でキーリングから情報を取得する

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

アイテムはユーザーのオペレーティングシステムの資格情報で暗号化されるため、ユーザーアカウントで実行されている他のアプリケーションがパスワードにアクセスできます。

その脆弱性を少し隠すために、キーリングに保存する前に、何らかの方法でパスワードを暗号化/難読化することができます。もちろん、スクリプトをターゲットにしている人なら誰でも、ソースを見てパスワードの暗号化を解除/解読する方法を理解することができますが、少なくとも一部のアプリケーションがボールト内のすべてのパスワードをバキュームして自分のパスワードを取得するのを防ぐことができます。


ユーザー名はどのように保存する必要がありますか?んkeyringサポートは、ユーザー名とパスワードの両方を取得しますか?
Stevoisiak 2018

1
@DustinWyattget_passwordユーザー名の巧妙な使用。ただし、答えは、元の簡略化された例keyring.set_password()keyring.get_password()
Stevoisiak 2018

keyringPython標準ライブラリの一部ではありません
Ciasto piekarz

@Ciastopiekarz答えについて何かがあなたにそれが標準ライブラリの一部であるとあなたに信じさせましたか?
ダスティンワイアット

keyring安全にログとメモリあとがきからパスワードをスクラブ?
ケブマン

26

これと関連する質問への回答を調べた後、秘密データを暗号化して隠すためのいくつかの提案された方法を使用して、いくつかのコードをまとめました。このコードは、特にユーザーの介入なしにスクリプトを実行する必要がある場合に使用します(ユーザーが手動でスクリプトを開始する場合は、パスワードを入力して、この質問への回答が示すようにメモリにのみ保持することをお勧めします)。この方法は非常に安全ではありません。基本的に、スクリプトは秘密情報にアクセスできるため、システムに完全にアクセスできる人は誰でもスクリプトとそれに関連するファイルを持っており、それらにアクセスできます。これが行うことは、カジュアルな検査からデータを覆い隠し、データファイルを個別に検査する場合、またはスクリプトなしで一緒に検査する場合、データファイル自体を安全なままにします。

これに対する私の動機は、トランザクションを監視するために銀行口座の一部をポーリングするプロジェクトです。1、2分ごとにパスワードを再入力せずに、バックグラウンドで実行する必要があります。

このコードをスクリプトの先頭に貼り付け、saltSeedを変更してから、必要に応じてコードでstore()retrieve()とrequire()を使用します。

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

このメソッドのセキュリティは、スクリプト自体がそれらを読み取ることのみを許可するようにOS権限がシークレットファイルに設定されている場合、およびスクリプト自体がコンパイルされて実行可能ファイルのみ(読み取り不可)としてマークされている場合に大幅に向上します。その一部は自動化できますが、私は気にしませんでした。おそらく、スクリプトのユーザーを設定し、そのユーザーとしてスクリプトを実行する(そして、スクリプトのファイルの所有権をそのユーザーに設定する)必要があります。

誰もが思いつくような提案、批判、その他の脆弱性のポイントがあればいいのですが。私は暗号コードを書くのはかなり新しいので、私がしたことはほぼ確実に改善される可能性があります。


26

Pythonプログラムが使用する必要のあるパスワードやその他のシークレットを保存するためのオプションがいくつかあります。特に、ユーザーにパスワードの入力を求めることができないバックグラウンドで実行する必要があるプログラムです。

避けるべき問題:

  1. パスワードをチェックインして、他の開発者や一般の人々が見ることができるソース管理にアクセスします。
  2. 同じサーバー上の他のユーザーが構成ファイルまたはソースコードからパスワードを読み取っています。
  3. あなたがそれを編集している間、他の人があなたの肩越しにそれを見ることができるソースファイルにパスワードを持っていること。

オプション1:SSH

これは常にオプションであるとは限りませんが、おそらく最良の方法です。秘密鍵がネットワークを介して送信されることはありません。SSHは数学的な計算を実行して、正しい鍵を持っていることを証明します。

それを機能させるには、次のものが必要です。

  • データベースまたはアクセスしているものはすべて、SSHでアクセスできる必要があります。「SSH」に加えて、アクセスしているサービスを検索してみてください。たとえば、「sshpostgresql」です。これがデータベースの機能でない場合は、次のオプションに進みます。
  • データベースを呼び出すサービスを実行するためのアカウントを作成し、SSHキー生成します
  • 呼び出すサービスに公開鍵を追加するか、そのサーバーにローカルアカウントを作成して、そこに公開鍵をインストールします。

オプション2:環境変数

これは最も単純なので、始めるのに良い場所かもしれません。これは、Twelve FactorAppで詳しく説明されています。基本的な考え方は、ソースコードが環境変数からパスワードまたはその他のシークレットを取得し、プログラムを実行する各システムでそれらの環境変数を構成することです。また、ほとんどの開発者に有効なデフォルト値を使用する場合にも便利です。ソフトウェアを「デフォルトで安全」にすることとバランスを取る必要があります。

これは、環境変数からサーバー、ユーザー名、およびパスワードを取得する例です。

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)

オペレーティングシステムで環境変数を設定する方法を調べ、独自のアカウントでサービスを実行することを検討してください。そうすれば、自分のアカウントでプログラムを実行するときに、環境変数に機密データが含まれなくなります。これらの環境変数を設定するときは、他のユーザーがそれらを読み取れないように特に注意してください。たとえば、ファイルのアクセス許可を確認してください。もちろん、root権限を持つすべてのユーザーがそれらを読むことができますが、それは仕方がありません。systemdを使用している場合は、サービスユニットを確認し、シークレットのEnvironmentFile代わりに使用するように注意してEnvironmentください。Environment値は、を使用してすべてのユーザーが表示できますsystemctl show

オプション3:構成ファイル

これは環境変数と非常に似ていますが、テキストファイルからシークレットを読み取ります。それでも、デプロイメントツールや継続的インテグレーションサーバーなどの環境変数はより柔軟であることがわかります。構成ファイルを使用することにした場合、Pythonは、JSONINInetrcXMLなど、標準ライブラリでいくつかの形式をサポートしますPyYAMLTOMLなどの外部パッケージもあります。個人的には、JSONとYAMLが最も使いやすく、YAMLではコメントが許可されています。

構成ファイルで考慮すべき3つのこと:

  1. ファイルはどこにありますか?のようなデフォルトの場所~/.my_appと、別の場所を使用するためのコマンドラインオプションがあります。
  2. 他のユーザーがファイルを読み取れないことを確認してください。
  3. 明らかに、構成ファイルをソースコードにコミットしないでください。ユーザーがホームディレクトリにコピーできるテンプレートをコミットすることをお勧めします。

オプション4:Pythonモジュール

一部のプロジェクトは、その秘密をPythonモジュールに直接入れています。

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

次に、そのモジュールをインポートして値を取得します。

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

db_connect(db_server, db_user, db_password)

この手法を使用するプロジェクトの1つがDjangoです。明らかに、ユーザーがコピーおよび変更できるsettings.pyというファイルをコミットしたい場合でも、ソース管理にコミットするべきではありませんsettings_template.py

この手法にはいくつか問題があります。

  1. 開発者は、誤ってファイルをソース管理にコミットする可能性があります。に追加すると.gitignore、そのリスクが軽減されます。
  2. 一部のコードはソース管理下にありません。あなたが訓練されていて、ここに文字列と数字だけを入れれば、それは問題にはなりません。ここでロギングフィルタークラスの作成を開始する場合は、停止してください。

プロジェクトですでにこの手法を使用している場合は、環境変数に簡単に移行できます。すべての設定値を環境変数に移動し、Pythonモジュールを変更してそれらの環境変数から読み取るだけです。


こんにちは。プロジェクトですでにこの手法を使用している場合は、環境変数に簡単に移行できます。 Windows 10で環境変数を手動で設定する方法を知っていますが、を使用してPythonコードから環境変数にアクセスできますos.getenv()。コードが共有されている場合、これをどのように行う必要がありますか?コードが別の開発者によってダウンロードされた場合、その開発者は環境変数がすでに設定されていることをどのように確認する必要がありますか?
a_sid

妥当なデフォルト値をos.getenv()@a_sidに渡そうとしているので、少なくとも環境変数を設定していないユーザーに対してコードが実行されます。適切なデフォルト値がない場合は、を取得しNoneたときに明確なエラーを発生させます。それ以外は、設定ファイルに明確なコメントを入れてください。私が何かを誤解した場合は、別の質問をすることをお勧めします。
ドンカークビー

8

パスワードを暗号化しようとしてもあまり意味がありません。パスワードを隠そうとしている人はPythonスクリプトを持っており、Pythonスクリプトにはパスワードを復号化するためのコードが含まれています。パスワードを取得する最も速い方法は、サードパーティのサービスでパスワードを使用する直前に、Pythonスクリプトにprintステートメントを追加することです。

したがって、パスワードを文字列としてスクリプトに保存し、base64でエンコードして、ファイルを読み取るだけでは不十分になるようにしてから、1日呼び出します。


ユーザー名とパスワードを定期的に編集する必要があり、Windoze用のEXEですべてをラップします。これを反映するように投稿を編集しました。最終的に保存する場所に単純にbase64を設定する必要がありますか?
Naftuli Kay 2011

プレーンテキストのパスワードはとにかく自動化された方法で取得する必要があり、したがって保存されているものから取得できる必要があるため、パスワードの「暗号化」は役に立たないことに同意します。しかし、実行可能なアプローチがあります。
wberry 2011

私はあなたの名前を認識したと思いますが、あなたはTalkPythonの初心者と専門家のパネルにいました。初心者として、あなたのメッセージは本当に私に共感しました、ありがとう!
マナキン

7

私ができる最善のことは、スクリプトファイルとそれが実行されているシステムを保護することだと思います。

基本的に次のことを行います。

  • ファイルシステムのアクセス許可を使用する(chmod 400)
  • システム上の所有者のアカウントの強力なパスワード
  • システムが危険にさらされる可能性を減らします(ファイアウォール、不要なサービスの無効化など)
  • それを必要としない人のための管理/ root / sudo特権を削除します

残念ながら、これはWindowsであり、EXEでラップします。また、パスワードを頻繁に変更する必要があるため、ハードコーディングすることはできません。
Naftuli Kay 2011

1
Windowsにはまだファイルシステムのアクセス許可があります。パスワードを外部ファイルに保存し、自分以外のすべての人のアクセスを削除します。おそらく、管理者権限も削除する必要があります。
Corey D

ええ、パーミッションの使用はここで唯一の信頼できるセキュリティオプションです。明らかに、どの管理者も(少なくともWindows /通常のLinuxディストリビューションでは)データにアクセスできますが、それはすでに負けた戦いです。
Voo 2011

それは本当です。パスワードの復号化が自動化されている場合、それはプレーンテキストのパスワードを使用するのと同じくらい優れています。本当のセキュリティは、アクセス権を持つユーザーアカウントをロックダウンすることです。できる最善の方法は、そのユーザーアカウントにのみ読み取り専用のアクセス許可を与えることです。おそらく、そのサービス専用の特別なユーザーを作成します。
セペロ2014

1

オペレーティングシステムは、多くの場合、ユーザーのデータを保護するためのサポートを備えています。Windowsの場合は、http://msdn.microsoft.com/en-us/library/aa380261.aspxのように見えます

http://vermeulen.ca/python-win32api.htmlを使用して、Pythonからwin32apiを呼び出すことができます。

私が理解している限り、これはデータを保存するので、データを保存するために使用されたアカウントからのみアクセスできます。データを編集する場合は、値を抽出、変更、保存するコードを記述して編集できます。


これは私にとって最良の選択のように見えますが、実際の例が不足していることを考えると、この答えはそれを受け入れるにはあまりにも不完全であると感じています。
ArtOfWarfare 2015

1
ここにPythonでこれらの関数を使用するためのいくつかの例があります:stackoverflow.com/questions/463832/using-dpapi-with-python
ArtOfWarfare 2015

1

使った システムに他の一般的に言及されているライブラリをインストール(コンパイル)するのに問題があったため、暗号化。(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)

私のスクリプトは、物理的に安全なシステム/部屋で実行されています。「暗号化スクリプト」を使用して資格情報を構成ファイルに暗号化します。そして、それらを使用する必要があるときに復号化します。「暗号化スクリプト」は実際のシステムにはなく、暗号化された設定ファイルのみが存在します。コードを分析する人は、コードを分析することで暗号化を簡単に破ることができますが、必要に応じてそれをEXEにコンパイルすることもできます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.