사용자 이름과 비밀번호를 Python에 안전하게 저장해야합니다. 옵션은 무엇입니까?


96

사용자 이름과 암호 콤보를 사용하여 타사 서비스에서 주기적으로 정보를 가져 오는 작은 Python 스크립트를 작성 중입니다. 100 % 방탄이되는 무언가를 만들 필요는 없지만 (100 %도 존재합니까?) 보안 조치를 취하고 싶으므로 적어도 누군가가 그것을 깨는 데 오랜 시간이 걸립니다.

이 스크립트는 GUI가 없으며에서 주기적으로 실행 cron되므로 암호를 해독하기 위해 실행할 때마다 암호를 입력하는 것은 실제로 작동하지 않으며 사용자 이름과 암호를 암호화 된 파일 또는 암호화 된 파일에 저장해야합니다. 어쨌든 SQLite를 사용할 것이므로 SQLite 데이터베이스에서 선호되며 어느 시점에서 암호를 편집해야 할 수도 있습니다 . 또한이 시점에서 Windows 전용이므로 전체 프로그램을 EXE로 래핑 할 것입니다.

cron작업을 통해 주기적으로 사용할 사용자 이름 및 비밀번호 콤보를 안전하게 저장하려면 어떻게 해야합니까?


답변:


19

ssh-agent 와 유사한 전략을 권장합니다 . ssh-agent를 직접 사용할 수없는 경우 이와 유사한 것을 구현하여 암호가 RAM에만 유지되도록 할 수 있습니다. 크론 작업은 에이전트가 실행될 때마다 에이전트에서 실제 암호를 가져오고 한 번 사용하고 del명령문을 사용하여 즉시 참조 해제하도록 자격 증명을 구성 할 수 있습니다 .

관리자는 부팅시 또는 무엇이든간에 ssh-agent를 시작하기 위해 여전히 암호를 입력해야하지만 이는 디스크에 일반 텍스트 암호가 저장되지 않도록하는 합리적인 절충안입니다.


2
+1, 그것은 많은 의미가 있습니다. 저는 항상 사용자에게 부팅시 암호를 묻는 UI를 만들 수 있습니다. 이렇게하면 디스크에 저장 되지 않고 눈을 훔쳐도 안전합니다.
Naftuli Kay

54

라이브러리 열쇠 고리 파이썬 와 통합하고 CryptProtectDataWindows의 API를 사용자의 로그온 자격 증명을 사용하여 데이터를 암호화 (Mac 및 Linux에 관련 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

1
@DustinWyatt get_password사용자 이름 에 대한 영리한 사용 . 하지만, 난 당신의 원래 간단한 예와 답을 시작해야한다고 생각 keyring.set_password()하고keyring.get_password()
Stevoisiak

keyring파이썬 표준 라이브러리의 일부가 아닙니다
Ciasto piekarz

@Ciastopiekarz 답변에 대해 뭔가 표준 라이브러리의 일부라고 믿게 되었습니까?
Dustin Wyatt

keyring로그와 메모리에서 암호를 안전하게 제거 합니까 ?
Kebman

26

이 질문과 관련 질문에 대한 답변을 살펴본 후 비밀 데이터를 암호화하고 숨기는 몇 가지 제안 된 방법을 사용하여 몇 가지 코드를 작성했습니다. 이 코드는 특히 사용자 개입없이 스크립트를 실행해야하는 경우를위한 것입니다 (사용자가 수동으로 시작하는 경우 암호를 입력하고이 질문에 대한 답변에서 알 수 있듯이 메모리에만 유지하는 것이 가장 좋습니다). 이 방법은 매우 안전하지 않습니다. 기본적으로 스크립트는 비밀 정보에 액세스 할 수 있으므로 전체 시스템 액세스 권한이있는 사람은 누구나 스크립트 및 관련 파일을 갖고 액세스 할 수 있습니다. 이것이 수행하는 작업 id는 일상적인 검사에서 데이터를 모호하게하고 개별적으로 또는 스크립트없이 함께 검사하는 경우 데이터 파일 자체를 안전하게 유지합니다.

이것에 대한 동기는 거래를 모니터링하기 위해 은행 계좌 중 일부를 조사하는 프로젝트입니다. 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 권한이 설정되어 있고 스크립트 자체가 컴파일되고 실행 가능 (읽을 수 없음)으로 표시된 경우이 방법의 보안이 크게 향상됩니다. 그중 일부는 자동화 될 수 있지만 신경 쓰지 않았습니다. 스크립트에 대한 사용자를 설정하고 해당 사용자로 스크립트를 실행해야합니다 (그리고 스크립트 파일의 소유권을 해당 사용자에게 설정).

누구나 생각할 수있는 제안, 비판 또는 기타 취약한 점을 좋아합니다. 나는 암호화 코드를 작성하는 데 꽤 익숙하기 때문에 내가 한 일이 거의 확실히 향상 될 수 있습니다.


25

Python 프로그램이 사용해야하는 암호 및 기타 비밀을 저장하는 몇 가지 옵션이 있습니다. 특히 사용자에게 암호를 입력하도록 요청할 수없는 백그라운드에서 실행해야하는 프로그램이 있습니다.

피해야 할 문제 :

  1. 다른 개발자 또는 일반인이 볼 수있는 소스 제어에 암호를 확인합니다.
  2. 동일한 서버의 다른 사용자가 구성 파일 또는 소스 코드에서 암호를 읽습니다.
  3. 편집하는 동안 다른 사람이 어깨 너머로 볼 수있는 소스 파일에 암호가 있습니다.

옵션 1 : SSH

이것이 항상 옵션은 아니지만 아마도 최선일 것입니다. 개인 키는 네트워크를 통해 전송되지 않으며 SSH는 올바른 키가 있음을 증명하기 위해 수학적 계산을 실행합니다.

작동하려면 다음이 필요합니다.

  • 데이터베이스 또는 액세스중인 모든 항목은 SSH로 액세스 할 수 있어야합니다. "SSH"와 액세스중인 서비스를 검색해보십시오. 예 : "ssh postgresql" . 이것이 데이터베이스의 기능이 아닌 경우 다음 옵션으로 이동하십시오.
  • 데이터베이스를 호출 할 서비스를 실행할 계정을 만들고 SSH 키를 생성합니다 .
  • 호출 할 서비스에 공개 키를 추가하거나 해당 서버에 로컬 계정을 만들고 여기에 공개 키를 설치합니다.

옵션 2 : 환경 변수

이것은 가장 간단하므로 시작하기에 좋은 곳일 수 있습니다. Twelve Factor App에 잘 설명되어 있습니다 . 기본 아이디어는 소스 코드가 환경 변수에서 암호 또는 기타 비밀을 가져온 다음 프로그램을 실행하는 각 시스템에서 해당 환경 변수를 구성하는 것입니다. 대부분의 개발자에게 작동하는 기본값을 사용하면 좋은 터치가 될 수도 있습니다. 소프트웨어를 "기본적으로 보안"하는 것과 균형을 맞춰야합니다.

다음은 환경 변수에서 서버, 사용자 이름 및 비밀번호를 가져 오는 예입니다.

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)

운영 체제에서 환경 변수를 설정하는 방법을 찾고 자체 계정으로 서비스를 실행하는 것을 고려하십시오. 이렇게하면 자신의 계정에서 프로그램을 실행할 때 환경 변수에 민감한 데이터가 없습니다. 이러한 환경 변수를 설정할 때 다른 사용자가 읽을 수 없도록 각별히주의하십시오. 예를 들어 파일 권한을 확인하십시오. 물론 루트 권한이있는 모든 사용자는 읽을 수 있지만 어쩔 수 없습니다.

옵션 3 : 구성 파일

이것은 환경 변수와 매우 유사하지만 텍스트 파일에서 비밀을 읽습니다. 여전히 배포 도구 및 지속적 통합 서버와 같은 항목에 대해 환경 변수가 더 유연하다는 것을 알았습니다. 구성 파일을 사용하기로 결정한 경우 Python은 표준 라이브러리에서 JSON , INI , netrcXML 과 같은 여러 형식을 지원합니다 . PyYAMLTOML 과 같은 외부 패키지를 찾을 수도 있습니다 . 개인적으로 JSON과 YAML이 가장 사용하기 쉽고 YAML은 주석을 허용합니다.

구성 파일에 대해 고려해야 할 세 가지 사항 :

  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)

이 기술을 사용하는 한 프로젝트는 Django 입니다. 사용자가 복사하고 수정할 수있는 settings.py라는 파일을 커밋하고 싶을 수 있지만 소스 제어에 커밋해서는 안됩니다 settings_template.py.

이 기술에 몇 가지 문제가 있습니다.

  1. 개발자는 실수로 파일을 소스 제어에 커밋 할 수 있습니다. .gitignore그 위험 을 줄이기 위해 그것을 추가하십시오 .
  2. 일부 코드는 소스 제어를받지 않습니다. 훈련을 받고 여기에 문자열과 숫자 만 넣으면 문제가되지 않습니다. 여기서 로깅 필터 클래스를 작성하기 시작하면 중지하십시오!

프로젝트에서 이미이 기술을 사용하는 경우 환경 변수로 쉽게 전환 할 수 있습니다. 모든 설정 값을 환경 변수로 이동하고 Python 모듈을 해당 환경 변수에서 읽도록 변경하십시오.


여보세요. 프로젝트에서 이미이 기술을 사용하는 경우 환경 변수로 쉽게 전환 할 수 있습니다. Windows 10에서 환경 변수를 수동으로 설정하는 방법을 알고 있지만 os.getenv(). 코드가 공유되는 경우 어떻게해야합니까? 다른 개발자가 코드를 다운로드 한 경우 환경 변수가 이미 설정되어 있는지 어떻게 확인해야합니까?
a_sid

적절한 기본값을 os.getenv()@a_sid 에 전달하려고 하므로 최소한 환경 변수를 설정하지 않은 사용자에 대해 코드가 실행됩니다. 좋은 기본값이 없으면 None. 그 외에는 설정 파일에 명확한 주석을 넣으십시오. 내가 뭔가를 잘못 이해했다면 별도의 질문을하시기 바랍니다.
Don Kirkby

8

암호를 암호화하려는 것은 별 의미가 없습니다. 암호를 숨기려는 사람은 암호를 해독하는 코드가있는 Python 스크립트를 가지고 있습니다. 암호를 얻는 가장 빠른 방법은 타사 서비스에서 암호를 사용하기 직전에 Python 스크립트에 print 문을 추가하는 것입니다.

따라서 암호를 스크립트에 문자열로 저장하고 파일을 읽는 것만으로는 충분하지 않도록 base64로 인코딩 한 다음 하루를 호출하십시오.


주기적으로 사용자 이름과 암호를 편집해야하며 Windoze 용 EXE로 모든 것을 래핑 할 것입니다. 이것을 반영하기 위해 게시물을 편집했습니다. 내가 저장하는 곳에서 단순히 base64로 만들어야합니까?
Naftuli Kay

암호를 "암호화"하는 것은 도움이되지 않는다는 데 동의합니다. 일반 텍스트 암호는 어쨌든 자동화 된 방식으로 얻어야하기 때문에 저장된 모든 것에서 얻을 수 있어야하기 때문입니다. 그러나 실행 가능한 접근 방식이 있습니다.
wberry

내가 당신의 이름을 알고 있다고 생각했는데, 당신은 TalkPython의 초보자 및 전문가 패널에 있었으며, 초보자로서 당신의 메시지는 저에게 정말 공감했습니다. 감사합니다!
마나 킨

7

당신이 할 수있는 최선은 스크립트 파일과 그것이 실행되는 시스템을 보호하는 것이라고 생각합니다.

기본적으로 다음을 수행하십시오.

  • 파일 시스템 권한 사용 (chmod 400)
  • 시스템의 소유자 계정에 대한 강력한 암호
  • 시스템 손상 가능성 감소 (방화벽, 불필요한 서비스 비활성화 등)
  • 필요하지 않은 사용자에 대한 관리 / 루트 / 수도 권한 제거

안타깝게도 Windows이고 EXE로 래핑 할 것입니다. 암호를 자주 변경해야하므로 하드 코딩 할 수 없습니다.
Naftuli Kay

1
Windows에는 여전히 파일 시스템 권한이 있습니다. 비밀번호를 외부 파일에 저장하고 자신을 제외한 모든 사람의 액세스를 제거합니다. 관리자 권한을 제거해야 할 수도 있습니다.
Corey D

예, 권한을 사용하는 것이 여기서 유일하게 신뢰할 수있는 보안 옵션입니다. 분명히 모든 관리자는 여전히 데이터에 액세스 할 수 있지만 (적어도 Windows / 일반적인 Linux 배포판에서는) 이미 패배 한 것입니다.
Voo

사실입니다. 암호 해독이 자동화되면 일반 텍스트 암호를 사용하는 것만 큼 좋습니다. 실제 보안은 액세스 권한이있는 사용자 계정을 잠그는 것입니다. 가장 좋은 방법은 해당 사용자 계정에만 읽기 전용 권한을 부여하는 것입니다. 특별히 해당 서비스에 대해서만 특별한 사용자를 생성 할 수 있습니다.
Sepero 2007

1

운영 체제는 종종 사용자를위한 데이터 보안을 지원합니다. Windows의 경우 http://msdn.microsoft.com/en-us/library/aa380261.aspx 처럼 보입니다 .

http://vermeulen.ca/python-win32api.html을 사용하여 파이썬에서 win32 api를 호출 할 수 있습니다.

내가 이해하는 한, 이것은 데이터를 저장하는 데 사용 된 계정에서만 액세스 할 수 있도록 데이터를 저장합니다. 데이터를 편집하려면 값을 추출, 변경 및 저장하는 코드를 작성하면됩니다.


이것은 나에게 최선의 선택처럼 보이지만 실제 예가 없기 때문에이 답변이 너무 불완전하다고 생각합니다.
ArtOfWarfare

1
여기 Python에서 이러한 함수를 사용하는 몇 가지 예가 있습니다. stackoverflow.com/questions/463832/using-dpapi-with-python
ArtOfWarfare

1

내 시스템에 일반적으로 언급되는 다른 라이브러리를 설치 (컴파일)하는 데 문제가 있었기 때문에 Cryptography를 사용했습니다 . (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.