パスワードをハードコーディングしないためのTips
パスワードをハードコーディングすることはセキュリティーを考えるまでもなく、非常に危険です。
そこでパスワードを隠蔽する方法をいくつか考えてみましょう。
getpass を使う方法
起動時に getpass を使ってとキーボードからパスワードを受け取る方法です。 簡単ですがバッチジョブとして実行すること難しくなります。
code: python
import getpass
password = getpass.getpass(prompt='Password: ')
print(password)
環境変数を利用する方法
環境変数としてパスワードを設定しておき、プログラムからその値を読み取る方法です。
プログラムで必要になるパスワードを簡単に外部に移して隠蔽することができます。
code: test_prog.py
import os
password = os.environ('MAIL_PASSWORD', None)
print(password)
code: bash
ENVFILE=$HOME/.secret/test_prog.sh
source ${ENVFILE}
}
python test_prog.py
この例の場合、$HOME/.secret/test_prog.sh に 環境変数として MAIL_PASSWORD を設定しておきます。
code: bash
export MAIL_PASSWORD="Your_Secret_Password_Here"
bash などコマンドヒストリを持つようなシェルでは、コマンドラインに入力した文字列が記録されます。直接、コマンドラインでパスワードを環境変数にセットしないようにしましょう。
python-dotenv を使う方法
前述の例では環境変数をセットするシェルスクリプトが必要でした。python-dotenv を利用するとファイル.envを読み取って環境変数に設定してくれます。
拡張モジュールなのでインストールが必要です。
code: bash
$ pip install python-dotenv
前述の例のMAIL_PASSWORDを設定する .envファイルを用意します。
code: bash
$ echo 'MAIL_PASSWORD="Your_Secret_Password_Here"' > .env
Pythonからは次のようにすると読み込むことができます。
code: python
import os
from os.path import join, dirname
from dotenv import load_dotenv
load_dotenv(verbose=True)
dotenv_path = join(dirname(__file__), '.env')
load_dotenv(dotenv_path)
print(f'MAIL_PASSWORD=os.environ.get("MAIL_PASSWORD"))
気づいたかもしれませんが、ファイル名は指定することができるので、
.envしか受け入れないというわけではありません。
pydantic を使って設定する方法
構成ファイルを処理する場合、pydantic は型変換もしてくれるのでとても便利です。
また、pydantic は python-dotenv と組み合わせることもできるので、よりスマートになります。
まず pydantic をインストールしておきましょう。
code: bash
$ pip install pydantic
Python では次のように記述します。
code: baseconfig.py
import os
import pydantic
class BaseSettings(pydantic.BaseSettings):
class Config:
env_prefix = ""
env_file = "myapp.env"
env_file_encoding = "utf-8"
use_enum_values = True
env_file で 環境設定をするファイル名を指定できます。
code: config.py
from baseconfig import basedir, BaseSettings
from typing import Optional
class MAILSettings(BaseSettings):
MAIL_SERVER: str = "smtp.gmail.com"
MAIL_PORT: int = 587
MAIL_USE_TLS: bool = True
MAIL_USE_SSL: bool = False
MAIL_USERNAME: Optionalstr = None MAIL_PASSWORD: Optionalstr = None MAIL_DEFAULT_SENDER: Optionalstr = "iisaka51@gmail.com" # for debug
MAIL_DEBUG: bool = False
MAIL_SUPPRESS_SEND: bool = False
使うときは、次のようにします。
code: python
In 1: # %load pydantic_deom.py ...: from config import MAILSettings
...:
...: conf = MAILSettings()
...: print(conf.MAIL_PASSWORD)
Your_Secret_Password_Here
クラス変数として登録すると、あとは pydantic が env_file で与えたファイルから読み取って、タイプヒントで定義した型に自動的に変換してくれます。
keyring を使用する方法
keyring は、オペレーティングシステムの認証情報ストアにパスワードを保存できるようにするためのパッケージです。複数の異なるオペレーティングシステムで動作できるという点で優れています。 code: bash
$ pip install keyring
使用方法は keyring をインポートして、set_passwordメソッドを使用してユーザー名とパスワードの組み合わせを保存することができます。このメソッドには3つのパラメタがあります。
servicename: ユーザ名/パスワードが関連付けられたサービス名
username: ユーザ名
password: パスワード
パスワードの保存
code: python
keyring.set_password("test", "admin_user", "admin_secret_password")
パスワードの取得
code: python
keyring.get_password("test", "admin_user")
コードが簡潔に記述できて便利ですが、プラットフォームで keyring のバックエンドシステムが動作している必要があります。
passlib を使う方法
ハッシュを使ってパスワードを保護します。暗号化は双方向の方法として機能するため、暗号化されたパスワードはすべて復号化することができます。 しかし、ハッシュは、パスワードなどの値を事前にハッシュ化して保存しておきます。新しいく与えられた値をハッシュ化して、保存していた値と比較することでパスワード保護の機能を提供します。 ハッシュされたパスワードから元のパスワードを導く方法は理想的にはなくなります。
code: bash
$ pip install passlib
passlib の使用方法はCryptContextオブジェクトの設定します。
code: python
from passlib.context import CryptContext
これを行うには、使用するハッシュアルゴリズムを決定する必要があります。 passlib にはには複数のアルゴリズムをサポートしています。
argon2
bcrypt
pbkdf2_sha256
pbkdf2_sha512
sha256_crypt
sha512_crypt
これらのアルゴリズムのいくつかは、追加のパッケージをインストールする必要があります。それらの依存パッケージをインストールせずに使用すると、最初にパッケージをインストールするように求めるメッセージが表示されます。
code: python
from passlib.context import CryptContext
context = CryptContext(
default="sha256_crypt",
sha256_crypt__default_rounds=50000
)
hashed_password = context.hash("your_secret_password")
password = getpass.getpass('Password: ')
if context.verify(password, hashed_password):
print('Authentication OK')
else:
print('Authentication NG')
passlib を使ったハッシュ化したパスワード保護は、Flask や Django などの Webアプリケーションの認証情報など、多くのパスワードを保存する場合に使用することができます。
ただし、復号化を行うことが事実上できないことに注意が必要です。
暗号化によるパスワード保護
Python による暗号化を説明するときに必要になってくる用語について説明しておきましょう。
プレーンテキスト(PlainText)
ユーザーが簡単に理解できる標準的なテキストです。つまり、暗号化されていないテキストです。
暗号テキスト(Ciphertext)
プレーンテキストに暗号化を適用した後の出力テキストです。暗号テキストを理解して覚えることは困難になります。
暗号化(Encryption)
暗号化処理によりプレーンテキストを暗号化テキストに変換します。
復号化(Decryption)
暗号化の逆の処理です。ここでは、暗号化テキストをプレーンテキストに変換します。
cryptography を使って暗号化する方法
シークレットキーを使って暗号化したパスワードを環境変数として設定しておき、
アプリケーションでは環境変数から読み取った暗号化パスワードを復号化して使用する方法です。
シークレットキーがあれば簡単に復号できてしまうので注意が必要です。
この例では、 pydantic を使った設定ファイルを用意しています。
pydantic は python-dotenv がインストールされていれば、
env_file で与えた env ファイルから環境変数を設定してくれます。
code: config.py
import pydantic
from pydantic_choices import choice
class BaseSettings(pydantic.BaseSettings):
class Config:
env_prefix = ""
env_file = ".env"
env_file_encoding = "utf-8"
use_enum_values = True
class CryptSettings(BaseSettings):
CRYPT_SECRET_KEY: Optionalstr = None CRYPTED_PASSWORD: Optionalstr = None code: python
import os
from cryptography.fernet import Fernet
from config import CryptSettings
conf = CryptSettings()
crypt_secret_key = conf.CRYPT_SECRET_KEY
crypted_pass = conf.CRYPTED_PASSWORD
password = Fernet(crypt_secret_key).decrypt(crypted_pass.encode()).decode()
都度暗号化するのが面倒になるので次のようなスクリプトを準備すると便利です
code: python
import os, sys
import click
from cryptography.fernet import Fernet
from config import CryptSettings
@click.group()
def cmd():
pass
def _get_key():
conf = CryptSettings()
key = conf.CRYPT_SECRET_KEY
if key is None:
key = Fernet.generate_key()
return key
click.command(help="generate password and key")
@click.password_option()
def generate(password):
secret_key = _get_key()
token = Fernet(secret_key).encrypt(password.encode())
click.echo('\nFollows environment to set your application.\n')
click.echo(f"CRYPT_SECRET_KEY='{str(secret_key.decode())}'")
click.echo(f"CRYPTED_PASSWORD='{token.decode()}'")
@click.command(help="decrypt password ")
@click.argument('encrypted_password', nargs=1, required=True)
def decrypt(password):
crypt_secret_key = _get_key()
f = Fernet(crypt_secret_key)
pass_str = Fernet(crypt_secret_key).decrypt(password.encode())
click.echo(f'Password: {pass_str.decode()}')
cmd.add_command(generate)
cmd.add_command(decrypt)
cmd.add_command(getkey)
if __name__ == '__main__':
cmd()
GnuPG を使って暗号化する方法
前述の cryptography を使って暗号化する方法では、シークレットキーが復号化に必要になるため、セキュリティーが脆弱となってしまいます。
PKI(public key infrastructure)フレームワークと呼ばれる秘密鍵と公開鍵を使って暗号化/復号化を行うしくみがあります。これは、秘密鍵を使って暗号化されたものを、公開鍵を使って復号化することができるもので、安全性が高くなるものです。
GnuPG(Gnu Privacy Guard) はPKIフレームワークのひとつです。GnuPGは、RFC2440(日本語訳) として標準化された OpenPGP を実装したものでフリーの暗号化ソフトです。 Python で GnuPG を使用するためには python-gnupg が必要になります。
code: bash
$ pip install python-gnupg
キーの生成
はじめにいくつかのキーを生成することからはじめます。
code: python
import gnupg
gpg = gnupg.GPG()
input_data = gpg.gen_key_input(
name_email='testuser@example.com',
passphrase='Your_passphrase_here',
key_type="RSA",
key_length=1024)
key = gpg.gen_key(input_data)
gen_key()メソッドが返す値は、キーの fingerprint です。
既存のキーを確認するためには list_keys() メソッドを呼び出します。
code: python
import gnupg
from pprint import pprint
gpg = gnupg.GPG()
keys = gpg.list_keys()
pprint(keys)
code: bash
[{'algo': '1',
'cap': 'escaESCA',
'compliance': '',
'curve': '',
'date': '1600833550',
'dummy': '',
'expires': '',
'fingerprint': '91BF938290315FF0532484D66AD3C2C73808C29B',
'flag': '',
'hash': '',
'issuer': '',
'keyid': '6AD3C2C73808C29B',
'length': '1024',
'origin': '0',
'ownertrust': 'u',
'sig': '',
'sigs': [],
'subkeys': [],
'token': '',
'trust': 'u',
'type': 'pub',
'updated': ''},
]
公開鍵と秘密鍵のエクスポート
生成したキーから公開鍵と秘密鍵をエクポートします。
code: python
import gnupg
gpg = gnupg.GPG()
key = '91BF938290315FF0532484D66AD3C2C73808C29B'
ascii_armored_public_keys = gpg.export_keys(key,
passphrase='Your_passphrase_here')
ascii_armored_private_keys = gpg.export_keys(key, True,
passphrase='Your_passphrase_here')
with open('mykeyfile.asc', 'w') as f:
f.write(ascii_armored_public_keys)
f.write(ascii_armored_private_keys)
code: bash
-----BEGIN PGP PUBLIC KEY BLOCK-----
mI0EX2rIDgEEANwC62TDRnl0oiegRDpdvObZ1RdUMmaQZ8tdVhCzXsz5SIDHd4po
LRtHnD2XlKEF/fizXXOJc9Q37pd3GNaj3vRSkrm061enXKoRsTJHo1sOzz+U2K16
auEjcVLA3tZ/0Hv8Vq0+xVJ5Kg0OUILjrID4xPxoOVyXwrQCnDEe9+K1ABEBAAG0
KEF1dG9nZW5lcmF0ZWQgS2V5IDx0ZXN0dXNlckBleGFtcGxlLmNvbT6IzgQTAQgA
OBYhBJG/k4KQMV/wUySE1mrTwsc4CMKbBQJfasgOAhsvBQsJCAcCBhUKCQgLAgQW
AgMBAh4BAheAAAoJEGrTwsc4CMKbWjkEAKy7IJK+wV19R27daNCWmvMzgYKEC4ya
O/Z/ibDsBxRGg2NetRJZO2UhorFKEWe8HJTIvo50HB4u6F74KuIulh6HtK1oIp+X
VMptnjZxTGdm8P4/UmP6PWOowzR8EdNo6WHb7eUPGLADDwhpfYPp0ohwJta4YOJF
cL028uiHnz9R
=Ci+Q
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PRIVATE KEY BLOCK-----
lQIGBF9qyA4BBADcAutkw0Z5dKInoEQ6Xbzm2dUXVDJmkGfLXVYQs17M+UiAx3eK
aC0bR5w9l5ShBf34s11ziXPUN+6XdxjWo970UpK5tOtXp1yqEbEyR6NbDs8/lNit
emrhI3FSwN7Wf9B7/FatPsVSeSoNDlCC46yA+MT8aDlcl8K0ApwxHvfitQARAQAB
/gcDAiAd7pdYYewR5WoFZkEARDLW55ZtFgz5+6LYnbe2bhUZzL8aPulJm9lE1apj
lLTgCmItxxCnBn+6AsfDzNxbcog3Qn+/i50/PMqx39TNSDbbk34kQ54MYxrGUtFA
GglY2f4PWW6zy2pNXQURviA8eYJCnC8/Z1RPA6H4QHYxqJYn6AVOLErKEo995GEd
B5JH0eHaM70TNHJVineXOMBATcauDoCHvy1cq+xLNBBrFTSk3axczWt8wgjR5VDd
QSvMLw9Mt2Ey+beAmiP7sVNWgbqCr5s5/97tHG4dLREAlQ6VXF4cBt0RIfbxNVtj
d8ed6mOizRuUpKYbQawuWM/oAgRnWp7uD0hROrqoVaUCx+k5RRfyepGWBWBbTNQ2
lp2be0QAFeHVis+E4ccWvdd8NNK83UkSJvbd1W7SBe7tApIUCb+eoXS0UO5m813o
vYd7g1UJ/F5vwwXMRjFsB2HnyysONbM7HACNVGqLJO5sdLpiiPK8OZG0KEF1dG9n
ZW5lcmF0ZWQgS2V5IDx0ZXN0dXNlckBleGFtcGxlLmNvbT6IzgQTAQgAOBYhBJG/
k4KQMV/wUySE1mrTwsc4CMKbBQJfasgOAhsvBQsJCAcCBhUKCQgLAgQWAgMBAh4B
AheAAAoJEGrTwsc4CMKbWjkEAKy7IJK+wV19R27daNCWmvMzgYKEC4yaO/Z/ibDs
BxRGg2NetRJZO2UhorFKEWe8HJTIvo50HB4u6F74KuIulh6HtK1oIp+XVMptnjZx
TGdm8P4/UmP6PWOowzR8EdNo6WHb7eUPGLADDwhpfYPp0ohwJta4YOJFcL028uiH
nz9R
=HlE9
-----END PGP PRIVATE KEY BLOCK-----
キーの保存場所を指定
デフォルトでは実行したディレクトリにキーがエクスポートされます。
キーの保存場所を指定するときは、GPG() を次のようにして実行します。
code: python
import gnupg
gpg = gnupg.GPG(gnupghome='/home/testuser/gpghome')
公開鍵と秘密鍵のインポート
code: python
import gnupg
from pprint import pprint
gpg = gnupg.GPG()
key_data = open('mykeyfile.asc').read()
import_result = gpg.import_keys(key_data)
pprint(import_result.results)
code: bash
[{'fingerprint': '91BF938290315FF0532484D66AD3C2C73808C29B',
'ok': '0',
'text': 'Not actually changed\n'},
{'fingerprint': '91BF938290315FF0532484D66AD3C2C73808C29B',
'ok': '0',
'text': 'Not actually changed\n'},
{'fingerprint': '91BF938290315FF0532484D66AD3C2C73808C29B',
'ok': '16',
'text': 'Not actually changed\nContains private key\n'}]
テキストの暗号化/復号化
テキストや文字列を暗号化/復号化してみましょう。
暗号化にはキーを特定する fingerprint や メールアドレスが必要になります。
同様に復号化にはキーを生成するときに使用したパスフレーズが必要になります。
code: python
import gnupg
gpg = gnupg.GPG()
raw_password = 'Your_Secret_Password'
encrypted_data = gpg.encrypt(raw_password, 'testuser@example.com')
encrypted_string = str(encrypted_data)
decrypted_data = gpg.decrypt(encrypted_string,
passphrase='Your_passphrase_here')
print(f'result: {encrypted_data.ok}')
print(f'status: {encrypted_data.status}')
print(f'unencrypted_string: {raw_password}')
print(f'encrypted_string: {encrypted_string}')
print(f'decrypted_data: {decrypted_data}')
code: bash
result: True
status: encryption ok
unencrypted_string: Your_Secret_Password
encrypted_string: -----BEGIN PGP MESSAGE-----
hIwDCAwEeEXm1KEBA/9R7UOqne8fEQnfDl1Duc4EK3zvCyVWPqlK3mz7ZH8AzpHb
aR1IGIGZ4MZUyFucSpZb5ks0OetxlPU0G0295LbK6V4AJ0yODySlMcU/PsHJziJz
CJLvlpSmp9AHOhEQUFR6NY3kT4d9qZkQ8AEuT6ZKhAhQvE0OTgUCU7oHDYmfAdJP
AR3s/lr1spzOEewKANNpEvStdPURlLqk+jTudqHDCMpCVd4T3hSR0E+PGCnl16mz
QSxaklHJSQ9qlgioBCzLwT8hk4Mk1VbrPs+DyeWnDQ==
=r0LX
-----END PGP MESSAGE-----
decrypted_data: Your_Secret_Password
復号化には秘密鍵を使用しないため、アプリケーションが動作するプラットフォームにはGnuPG のキーもしくはエクスポートした公開鍵を保存するだけでよくなります。