Preciso armazenar com segurança um nome de usuário e senha em Python, quais são minhas opções?


94

Estou escrevendo um pequeno script Python que periodicamente extrairá informações de um serviço de terceiros usando uma combinação de nome de usuário e senha. Não preciso criar algo 100% à prova de balas (será que 100% existe?), Mas gostaria de envolver uma boa medida de segurança para que no mínimo demorasse muito para alguém quebrá-lo.

Este script não terá uma GUI e será executado periodicamente por cron, portanto, inserir uma senha toda vez que for executado para descriptografar as coisas não funcionará realmente e terei que armazenar o nome de usuário e a senha em um arquivo criptografado ou criptografado em um banco de dados SQLite, o que seria preferível, pois estarei usando SQLite de qualquer maneira e talvez precise editar a senha em algum momento. Além disso, provavelmente irei embrulhar todo o programa em um EXE, já que é exclusivamente para Windows neste momento.

Como posso armazenar com segurança a combinação de nome de usuário e senha para ser usada periodicamente por meio de um crontrabalho?


Respostas:


18

Eu recomendo uma estratégia semelhante ao ssh-agent . Se você não pode usar o ssh-agent diretamente, você pode implementar algo parecido, de forma que sua senha seja mantida apenas na RAM. O cron job pode ter configurado credenciais para obter a senha real do agente sempre que for executado, use-a uma vez e remova a referência imediatamente usando a delinstrução.

O administrador ainda precisa inserir a senha para iniciar o ssh-agent, no momento da inicialização ou o que for, mas este é um compromisso razoável que evita ter uma senha de texto simples armazenada em qualquer lugar do disco.


2
1, isso faz muito sentido. Eu sempre poderia construir uma IU para ele que essencialmente pedisse ao usuário sua senha na inicialização, dessa forma ela nunca seria armazenada no disco e estaria protegida de olhares indiscretos.
Naftuli Kay

53

A biblioteca do chaveiro python se integra com oCryptProtectData API no Windows (junto com APIs relevantes no Mac e Linux) que criptografa os dados com as credenciais de logon do usuário.

Uso simples:

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

Use se você quiser armazenar o nome de usuário no chaveiro:

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)

Mais tarde, para obter suas informações do chaveiro

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

Os itens são criptografados com as credenciais do sistema operacional do usuário, portanto, outros aplicativos em execução na sua conta de usuário seriam capazes de acessar a senha.

Para ocultar um pouco essa vulnerabilidade, você pode criptografar / ofuscar a senha de alguma maneira antes de armazená-la no chaveiro. Claro, qualquer pessoa que visasse seu script seria apenas capaz de olhar a fonte e descobrir como descriptografar / desofuscar a senha, mas você pelo menos evitaria que algum aplicativo limpasse todas as senhas no cofre e obtivesse a sua também .


Como o nome de usuário deve ser armazenado? O keyringsuporte para recuperação de nome de usuário e senha?
Stevoisiak de

1
@DustinWyatt Uso inteligente de get_passwordpara o nome de usuário. Embora, eu acho que você deve começar a resposta com o exemplo original simplificado de keyring.set_password()ekeyring.get_password()
Stevoisiak

keyringnão faz parte da biblioteca padrão do python
Ciasto piekarz

@Ciastopiekarz Algo sobre a resposta o levou a acreditar que ela fazia parte da biblioteca padrão?
Dustin Wyatt

25

Depois de examinar as respostas a essas perguntas e outras relacionadas, juntei alguns códigos usando alguns dos métodos sugeridos para criptografar e ocultar dados secretos. Este código é especificamente para quando o script precisa ser executado sem a intervenção do usuário (se o usuário o iniciar manualmente, é melhor que ele coloque a senha e apenas a mantenha na memória como a resposta a esta pergunta sugere). Este método não é superseguro; fundamentalmente, o script pode acessar as informações secretas para que qualquer pessoa que tenha acesso total ao sistema tenha o script e seus arquivos associados e possa acessá-los. O que isso faz id obscurece os dados da inspeção casual e deixa os próprios arquivos de dados seguros se forem examinados individualmente ou juntos sem o script.

Minha motivação para isso é um projeto que pesquisa algumas de minhas contas bancárias para monitorar transações - preciso que ele seja executado em segundo plano, sem que eu reinsira as senhas a cada minuto ou dois.

Basta colar este código no topo do seu script, alterar o saltSeed e usar store () retrieve () e require () no seu código conforme necessário:

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

A segurança desse método seria significativamente melhorada se as permissões do sistema operacional fossem definidas nos arquivos secretos para permitir apenas que o próprio script os lesse e se o próprio script fosse compilado e marcado como executável apenas (não legível). Parte disso poderia ser automatizado, mas não me incomodei. Provavelmente, seria necessário configurar um usuário para o script e executá-lo como esse usuário (e definir a propriedade dos arquivos do script para esse usuário).

Eu adoraria quaisquer sugestões, críticas ou outros pontos de vulnerabilidade que alguém possa pensar. Sou muito novo na escrita de código criptográfico, então o que fiz quase certamente poderia ser melhorado.


22

Existem algumas opções para armazenar senhas e outros segredos que um programa Python precisa usar, particularmente um programa que precisa ser executado em segundo plano, onde não pode simplesmente pedir ao usuário para digitar a senha.

Problemas a evitar:

  1. Verificar a senha no controle de origem, onde outros desenvolvedores ou até mesmo o público podem vê-la.
  2. Outros usuários no mesmo servidor lendo a senha de um arquivo de configuração ou código-fonte.
  3. Ter a senha em um arquivo de origem onde outras pessoas possam vê-la por cima do ombro enquanto você a edita.

Opção 1: SSH

Nem sempre isso é uma opção, mas provavelmente é a melhor. Sua chave privada nunca é transmitida pela rede, o SSH apenas executa cálculos matemáticos para provar que você tem a chave certa.

Para fazer funcionar, você precisa do seguinte:

  • O banco de dados ou o que você está acessando precisa estar acessível por SSH. Tente pesquisar por "SSH" e qualquer serviço que você está acessando. Por exemplo, "ssh postgresql" . Se este não for um recurso em seu banco de dados, vá para a próxima opção.
  • Crie uma conta para executar o serviço que fará chamadas ao banco de dados e gere uma chave SSH .
  • Adicione a chave pública ao serviço que você vai chamar ou crie uma conta local nesse servidor e instale a chave pública lá.

Opção 2: Variáveis ​​de Ambiente

Este é o mais simples, então pode ser um bom lugar para começar. É bem descrito no aplicativo Twelve Factor . A ideia básica é que seu código-fonte apenas extraia a senha ou outros segredos das variáveis ​​de ambiente e, em seguida, configure essas variáveis ​​de ambiente em cada sistema em que executa o programa. Também pode ser um toque agradável se você usar valores padrão que funcionarão para a maioria dos desenvolvedores. Você deve equilibrar isso contra tornar seu software "seguro por padrão".

Aqui está um exemplo que extrai o servidor, o nome de usuário e a senha das variáveis ​​de ambiente.

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)

Veja como definir variáveis ​​de ambiente em seu sistema operacional e considere a possibilidade de executar o serviço em sua própria conta. Dessa forma, você não tem dados confidenciais em variáveis ​​de ambiente ao executar programas em sua própria conta. Ao configurar essas variáveis ​​de ambiente, tome cuidado extra para que outros usuários não possam lê-las. Verifique as permissões do arquivo, por exemplo. Claro que qualquer usuário com permissão de root será capaz de lê-los, mas isso não pode ser ajudado.

Opção 3: arquivos de configuração

Isso é muito semelhante às variáveis ​​de ambiente, mas você lê os segredos de um arquivo de texto. Ainda acho as variáveis ​​de ambiente mais flexíveis para coisas como ferramentas de implantação e servidores de integração contínua. Se você decidir usar um arquivo de configuração, Python suporta vários formatos na biblioteca padrão, como JSON , INI , netrc e XML . Você também pode encontrar pacotes externos como PyYAML e TOML . Pessoalmente, acho JSON e YAML os mais simples de usar, e YAML permite comentários.

Três coisas a serem consideradas com arquivos de configuração:

  1. Onde está o arquivo? Talvez um local padrão como ~/.my_app, e uma opção de linha de comando para usar um local diferente.
  2. Certifique-se de que outros usuários não possam ler o arquivo.
  3. Obviamente, não envie o arquivo de configuração para o código-fonte. Você pode querer enviar um modelo que os usuários possam copiar para seu diretório pessoal.

Opção 4: módulo Python

Alguns projetos simplesmente colocam seus segredos diretamente em um módulo Python.

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

Em seguida, importe esse módulo para obter os valores.

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

db_connect(db_server, db_user, db_password)

Um projeto que usa essa técnica é o Django . Obviamente, você não deve se comprometer settings.pycom o controle de origem, embora possa querer enviar um arquivo chamado settings_template.pyque os usuários possam copiar e modificar.

Vejo alguns problemas com essa técnica:

  1. Os desenvolvedores podem enviar acidentalmente o arquivo para o controle de origem. Adicioná-lo .gitignorereduz esse risco.
  2. Parte do seu código não está sob controle de origem. Se você for disciplinado e apenas colocar strings e números aqui, isso não será um problema. Se você começar a escrever classes de filtro de registro aqui, pare!

Se seu projeto já usa essa técnica, é fácil fazer a transição para variáveis ​​de ambiente. Basta mover todos os valores de configuração para variáveis ​​de ambiente e alterar o módulo Python para ler essas variáveis ​​de ambiente.


Olá. Se seu projeto já usa essa técnica, é fácil fazer a transição para variáveis ​​de ambiente. Eu sei como definir variáveis ​​de ambiente no Windows 10 manualmente, mas posso acessá-los do meu código python usando os.getenv(). Como devemos fazer isso se o código está sendo compartilhado? Se o código for baixado por outro desenvolvedor, como ele deve se certificar de que as variáveis ​​de ambiente já estão definidas para ele?
a_sid

Tento passar um valor padrão razoável para os.getenv()@a_sid, para que o código seja executado pelo menos para um usuário que não definiu as variáveis ​​de ambiente. Se não houver um valor padrão bom, gere um erro claro quando obtiver None. Fora isso, coloque comentários claros no arquivo de configurações. Se eu não entendi algo, sugiro que você faça uma pergunta separada.
Don Kirkby

8

Não faz muito sentido tentar criptografar a senha: a pessoa de quem você está tentando escondê-la tem o script Python, que terá o código para descriptografá-la. A maneira mais rápida de obter a senha será adicionar uma instrução de impressão ao script Python antes de usar a senha com o serviço de terceiros.

Portanto, armazene a senha como uma string no script e codifique-a em base64 de forma que apenas ler o arquivo não seja o suficiente, então, termine o dia.


Vou precisar editar o nome de usuário e a senha periodicamente e embrulhar tudo em um EXE para o Windoze; Eu editei a postagem para refletir isso. Devo simplesmente basear64 onde quer que eu acabe armazenando?
Naftuli Kay

Concordo que "criptografar" a senha não ajuda, uma vez que a senha em texto simples deve ser obtida de qualquer maneira de forma automática e, portanto, deve ser obtida de tudo o que está armazenado. Mas existem abordagens viáveis.
wberry

Pensei ter reconhecido seu nome, você estava no painel de iniciantes e especialistas no TalkPython, como um iniciante, sua mensagem realmente ressoou em mim, obrigado!
Manakin

6

Acho que o melhor que você pode fazer é proteger o arquivo de script e o sistema em que ele está sendo executado.

Basicamente, faça o seguinte:

  • Use as permissões do sistema de arquivos (chmod 400)
  • Senha forte para a conta do proprietário no sistema
  • Reduz a capacidade do sistema ser comprometido (firewall, desabilitar serviços desnecessários, etc)
  • Remova os privilégios administrativos / root / sudo para aqueles que não precisam deles

Infelizmente, é o Windows, vou envolvê-lo em um EXE e vou precisar alterar a senha de vez em quando, portanto, embutir não será uma opção.
Naftuli Kay

1
O Windows ainda tem permissões do sistema de arquivos. Armazene a senha em um arquivo externo e remova o acesso de todos, exceto o seu. Você provavelmente também terá que remover seus privilégios administrativos.
Corey D de

Sim, usar permissões é a única opção de segurança confiável aqui. Obviamente, qualquer administrador ainda será capaz de acessar os dados (pelo menos em distribuições windows / linux usuais), mas essa é uma batalha já perdida.
Voo de

É verdade. Quando a descriptografia de senha é automatizada, isso é tão bom quanto ter uma senha em texto simples. A verdadeira segurança está em bloquear a conta do usuário com acesso. O melhor que pode ser feito é conceder permissões somente leitura para apenas essa conta de usuário. Possivelmente crie um usuário especial, específico e somente para aquele serviço.
Sepero

1

os sistemas operacionais geralmente oferecem suporte para proteger os dados do usuário. no caso do Windows, parece que é http://msdn.microsoft.com/en-us/library/aa380261.aspx

você pode chamar apis win32 de python usando http://vermeulen.ca/python-win32api.html

tanto quanto eu entendo, isso irá armazenar os dados para que possam ser acessados ​​apenas a partir da conta usada para armazená-los. se você deseja editar os dados, pode fazê-lo escrevendo um código para extrair, alterar e salvar o valor.


Esta parece ser a melhor escolha para mim, mas acho que esta resposta é muito incompleta para aceitá-la, uma vez que não há exemplos reais.
ArtOfWarfare

1
Existem alguns exemplos de uso dessas funções em Python aqui: stackoverflow.com/questions/463832/using-dpapi-with-python
ArtOfWarfare

1

Usei a criptografia porque tive problemas para instalar (compilar) outras bibliotecas comumente mencionadas em meu sistema. (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)

Meu script está sendo executado em um sistema / sala fisicamente seguro. Eu criptografo credenciais com um "script de criptografia" em um arquivo de configuração. E então descriptografe quando eu precisar usá-los. O "script de criptografia" não está no sistema real, apenas o arquivo de configuração criptografado está. Alguém que analisa o código pode facilmente quebrar a criptografia analisando o código, mas você ainda pode compilá-lo em um EXE, se necessário.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.