Armazenando senhas de forma segura

Dica ao leitor: não deixe de ler a seção “O mecanismo correto”.

Diga aí, você anota a senha do seu email em um post-it e deixa ele colado em seu monitor, visível a qualquer um? Aposto que não, pois seria muito fácil para alguém acessar sua conta e mandar emails engraçadinhos para seus contatos. Da mesma forma que você protege sua senha, os aplicativos que utilizam informações de login e senha para autenticar usuários (como o seu serviço de email) também devem cuidar das senhas armazenadas neles, não devendo nunca guardá-las em texto puro. Vamos ver nesse post como funciona a autenticação, e as melhores práticas para implementar esse serviço na sua aplicação de forma razoavelmente segura.

Autenticação

Você já parou para pensar em como funciona a autenticação em um serviço de email? Primeiro, você digita o seu nome de usuário e senha em um formulário web, como o exemplo abaixo:

Usuário: pythonhelp
  Senha: *******

Esses dados são então enviados para o servidor que está fornecendo o serviço de autenticação do seu email. Lá dentro, o serviço de autenticação irá procurar por um usuário chamado pythonhelp no banco de dados de usuários. Se encontrá-lo, irá realizar uma comparação (mais para frente veremos que não é uma simples comparação de duas strings) para verificar se a senha fornecida no formulário web corresponde à senha do usuário. Em caso positivo, a autenticação ocorre com sucesso e você pode então acessar sua conta de email. Em caso negativo, aquela mensagenzinha chata avisando que você errou seu nome de usuário ou sua senha aparece na tela. (A propósito, você já percebeu que a maioria dos serviços não informa se o que erramos foi o nome de usuário ou se foi a senha? Esse tipo de informação é usualmente interessante para um invasor em potencial.)

Deixando de lado alguns detalhes, a autenticação funciona basicamente da forma descrita acima. Antes de vermos como isso tudo poderia ser implementado, veremos como NÃO deve ser implementado um serviço de autenticação.

Como NÃO implementar autenticação

Vamos desenvolver o serviço de autenticação para a nossa aplicação. Para isso, criamos uma tabela no banco de dados chamada USUARIOS, que contém duas informações sobre cada usuário: seu nome de usuário e sua senha. Como você pode ver abaixo, armazenamos a senha dos usuários em texto puro, ou seja, as senhas estão visíveis a qualquer pessoa que obtiver acesso ao banco de dados.

+--------------+
| USUARIOS     |
+--------------+------------------------+
|    NOME      |        SENHA           |
+--------------+------------------------+
| joaozinho    | teste                  |
+--------------+------------------------+
| pedrinho     | teste123               |
+--------------+------------------------+
| maria        | t35t3                  |
+--------------+------------------------+

Para fazer a autenticação, basta que o usuário forneça a sua senha e que comparemos a senha fornecida com a que está armazenada no BD.

Isso até funciona, mas eu é que não forneceria a minha senha para um sistema meia-boca desses que vai armazená-la em texto puro no banco de dados. Sabe por quê? Porque uma vez que alguém obtenha acesso ao banco de dados do sistema, basta isso para quebrar a privacidade de todos os usuários:

user@host:~/$ sqlite usuarios.db
sqlite> select * from usuarios;
joaozinho|teste
pedrinho|teste123
maria|t35t3

You're doing it wrong!

Que coisa, não? As senhas estão expostas. Falha de segurança gravíssima! Sabendo que a maioria dos usuários usa a mesma senha para os logins em vários sites, dá pra ter uma idéia do estrago né?

Lição número 1: JAMAIS ARMAZENE SENHAS EM TEXTO PURO!

Um jeito melhor

Agora que você já sabe como não fazer, vamos ver uma forma um pouquinho melhor (ainda não a correta) de implementar um mecanismo de autenticação.

Dessa vez nós não vamos armazenar as senhas dos usuários no BD. O que vamos armazenar é uma informação relacionada à senha e gerada a partir dela, o chamado hash da senha.

O que é o Hash?

O hash de um valor é o resultado da aplicação de uma função de hashing a tal valor. Esse resultado é, em geral, muito diferente do valor original. Uma função de hashing H recebe como entrada um valor x e retorna como resultado o hash h correspondente àquele valor:

H(x) -> h

Vamos calcular o hash do valor 'teste123':

H('teste123') -> 'aa1bf4646de67fd9086cf6c79007026c'

Vamos agora calcular o hash do valor 'teste12':

H('teste12') -> '0940004e70ce8d82b440d3c1244dfdee'

Vamos calcular novamente o hash do valor 'teste123':

H('teste123') -> 'aa1bf4646de67fd9086cf6c79007026c'

Agora vamos aplicar a função de hash ao valor 'aa1bf4646de67fd9086cf6c79007026c' (que é o hash de 'teste123'):

H(‘aa1bf4646de67fd9086cf6c79007026c’) -> ‘0728a200630cec4b33e33e20646bc54a’

Observando com atenção, você pode notar algumas coisas sobre as funções de hash:

  1. A função de hash gera um resultado cujo valor é muito diferente do valor original.
  2. Quando alteramos levemente o valor de entrada para a função de hash, o valor retornado por ela muda completamente (veja os exemplos de aplicação nas entradas 'teste123' e 'teste12'). Isso é chamado de efeito avalanche.
  3. Quando aplicamos H novamente à entrada 'teste123', obtivemos o valor idêntico ao obtido pela primeira vez. Ou seja, a função de hash é determinística.
  4. O último exemplo nos mostra que uma função de hash não é reversível, isto é, dado o hash de um valor, não conseguiremos descobrir o valor original através desse hash.

A partir das observações acima, podemos inferir algumas propriedades que as funções de hash possuem:

  1. É improvável (muito improvável mesmo) que você modifique a entrada da função sem modificar o resultado dela.
  2. É impossível gerar o valor de entrada a partir do resultado.
  3. É muito difícil (muito mesmo) encontrar dois valores para os quais a função de hashing produza o mesmo resultado.
  4. Sempre que aplicada ao mesmo valor x, uma mesma função H(x) irá retornar o mesmo resultado h.

* Nos exemplos acima, usei o algoritmo de hashing MD5, embora existam vários outros que poderiam ser igualmente usados, como SHA-1, SHA-256, etc.

Autenticação com hash

Conhecendo as propriedades acima, podemos criar um mecanismo de autenticação mais seguro. O objetivo é armazenar as informações de senha de forma mais protegida, em vez de deixá-la exposta em texto puro.

A primeira idéia pode ser simplesmente armazenar apenas o hash da senha. Parece uma boa idéia, afinal, a propriedade 2 diz que se alguém roubar o valor do hash da sua senha, não conseguirá obter a senha propriamente dita, e a propriedade 4 nos possibilita verificar se a senha está correta comparando o hash do que o usuário digitou com o hash armazenado no banco de dados.

Veja abaixo como ficaria o nosso novo BD de usuários. Para cada usuário, armazenamos o hash da senha que ele forneceu no cadastro.

+-----------+
| USUARIOS  |                                  
+-----------+----------------------------------+
| NOME      |         HASH DA SENHA            |
+-----------+----------------------------------+
| joaozinho | 698dc19d489c4e4db73e28a713eab07b |
+-----------+----------------------------------+
| pedrinho  | aa1bf4646de67fd9086cf6c79007026c |
+-----------+----------------------------------+
| maria     | c7ac7410983dc7efbb2e5c062c515b7d |
+-----------+----------------------------------+

Quando o usuário quiser se autenticar no sistema, teremos em mãos a senha fornecida por ele na tela de login, mas não podemos compará-la diretamente ao valor armazenado no banco, pois o que está armazenado é o hash da senha. O serviço de autenticação deverá aplicar a função de hash sobre a senha fornecida pelo usuário e comparar o resultado com o que está armazenado no BD. Se o valor obtido for idêntico ao hash armazenado no banco de dados para aquele usuário, a autenticação ocorre com sucesso. Caso contrário, erro de autenticação.

Agora as senhas estarão um pouquinho mais seguras. Isso mesmo, somente um pouco, pois existem meios para descobrir o valor da senha original através do hash dela. A próxima seção vai descrever um pouco isso.

Ataques ao banco de dados de senhas (ou hashes delas)

Uma coisa que acontece com frequência é um sistema sofrer uma invasão e os invasores realizarem uma cópia do seu banco de dados. Considere que os invasores roubaram o seguinte banco de dados:

+-----------+
| USUARIOS  |                                  
+-----------+----------------------------------+
| NOME      |         HASH DA SENHA            |
+-----------+----------------------------------+
| joaozinho | 698dc19d489c4e4db73e28a713eab07b |  <-- hash de "teste"
+-----------+----------------------------------+
| pedrinho  | aa1bf4646de67fd9086cf6c79007026c |  <-- hash de "teste123"
+-----------+----------------------------------+
| maria     | 2a1bdbdad93b1081007abc4b419d8f0b |  <-- hash de "t35t3"
+-----------+----------------------------------+
|    ...    |               ...                |
+-----------+----------------------------------+

O que eles poderiam fazer com esses dados? De acordo com as propriedades que vimos sobre as funções de hashing, não seria possível extrair o valor original que gerou o hash, então você imaginaria que as senhas estariam seguras dessa maneira.

Teoricamente, sim. Mas agora imagine que o invasor (uma pessoa muuuuuuito paciente), de posse do hash da senha do usuário joaozinho (698dc19d489c4e4db73e28a713eab07b), comece a fazer alguns testes com valores comumente usados como senhas:

>>> import hashlib
>>> print hashlib.md5('123').hexdigest()
202cb962ac59075b964b07152d234b70
>>> print hashlib.md5('abcde').hexdigest()
ab56b4d92b40713acc5af89985d4b786
>>> print hashlib.md5('bla').hexdigest()
128ecf542a35ac5270a87dc740918404
>>> print hashlib.md5('teste').hexdigest()
698dc19d489c4e4db73e28a713eab07b

Opa! O invasor acabou de descobrir a senha do joaozinho (teste), pois o hash obtido para essa string é o mesmo hash que está armazenado para o joaozinho no BD. Se ele continuar testando valores que considera prováveis de serem usados como senha, ele pode, eventualmente, acabar fazendo:

>>> print hashlib.md5('teste123').hexdigest()
aa1bf4646de67fd9086cf6c79007026c

Então ele terá descoberto a senha do usuário pedrinho, pois o hash gerado para teste123 possui o mesmo valor que está armazenado para esse usuário. A esse tipo de ataque, chamamos de Ataque por força bruta.

Imagino que você esteja pensando que o invasor terá muito trabalho para descobrir a senha do usuário maria, testando milhões de possibilidades antes de chegar em t35t3. Pois é. Se ele for testando manualmente, dificilmente irá descobrir a senha. Mas imagine agora que o invasor tenha escrito um programa que gere uma tabela gigantesca contendo possíveis senhas e seus hashes:

+----------------------------------+---------------------+
|              HASH                |       SENHA         |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 698dc19d489c4e4db73e28a713eab07b | teste               |
+----------------------------------+---------------------+
| e959088c6049f1104c84c9bde5560a13 | teste1              |
+----------------------------------+---------------------+
| 38851536d87701d2191990e24a7f8d4e | teste2              |
+----------------------------------+---------------------+
| 56c1056afb34f0d5ad809821d417a52b | t3st3               |
+----------------------------------+---------------------+
| 2a1bdbdad93b1081007abc4b419d8f0b | t35t3               |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 128ecf542a35ac5270a87dc740918404 | bla                 |
+----------------------------------+---------------------+
| 14a310c18e7ee2627b3de4ff82b11e76 | bl4                 |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+

Tendo essa tabela pré-calculada, uma vez que o invasor esteja de posse do hash da senha da maria (2a1bdbdad93b1081007abc4b419d8f0b), basta pesquisar por esse hash na tabela de hashes. Se encontrar um registro que possua tal hash, basta obter a senha que o acompanha.

É evidente que essa tabela deve ser gigantesca, gigantesca mesmo, para conter uma boa quantidade de combinações de valores de senhas. Para “facilitar o trabalho”, invasores do mundo todo colaboram na criação e busca em tabelas desse tipo. Essas tabelas são conhecidas como Rainbow Tables.

Não só isso, os próprios usuários colaboram com os invasores, usando as mesmas senhas em vários lugares, ou usando palavras simples de adivinhar, de forma que bem antes duma tabela dessas cobrir todas as possibilidades de hash, ela já pode ser extremamente útil para os invasores.

Lição número 2: O HASH SOZINHO NÃO FAZ MILAGRE!

Evitando as Rainbow Tables

Para lidar com isso, existe uma técnica chamada de salgar senhas (tradução literal do inglês “salting passwords”), que consiste em adicionar um “temperinho” na senha antes de armazenar. A idéia é gerar uma string contendo alguns valores aleatórios e concatenar essa string à senha do usuário na hora gerar o hash. Assim, ao invés de tomar somente a senha do usuário como entrada, a função de hashing passa a tomar como entrada também a string aleatória (chamada de salt, ou sal). Esse sal também é armazenado no BD para que posteriormente seja possível que o serviço de autenticação realize a verificação.

O que isso traz de segurança para o sistema? Vamos visualizar uma tabela que armazena o nome do usuário, o hash da concatenação entre senha e sal, e o sal. A senha de cada usuário é a mesma senha mostrada lá no início do post.

+-----------+
| USUARIOS  |                                  
+-----------+----------------------------------+-----------+
| NOME      |       HASH DE (SENHA+SAL)        |    SAL    |
+-----------+----------------------------------+-----------+
| joaozinho | e3e923b2c0d3890270a2cb6d52b13bf6 |   h6ja8   |
+-----------+----------------------------------+-----------+
| pedrinho  | 046b8f04069efab205d9f7bcc099e3d3 |   5uoWB   |
+-----------+----------------------------------+-----------+
| maria     | 960e18e4dc393660fdf3caba8634f38e |   jtm7a   |
+-----------+----------------------------------+-----------+

Veja como foram gerados os hashes armazenados:

>>> print hashlib.md5('teste'+'h6ja8').hexdigest()
e3e923b2c0d3890270a2cb6d52b13bf6
>>> print hashlib.md5('teste123'+'5uoWB').hexdigest()
046b8f04069efab205d9f7bcc099e3d3
>>> print hashlib.md5('t35t3'+'jtm7a').hexdigest()
960e18e4dc393660fdf3caba8634f38e

Está achando estranho o fato de termos armazenado o sal em texto puro no BD? Pois é, se o invasor roubar nossa base, ele terá de lambuja o valor do sal. Mas isso não é um problema, pois o objetivo principal de salgarmos a senha é impossibilitar a utilização de rainbow tables, afinal a idéia principal por trás dessas tabelas é o cálculo prévio dos hashes de vários valores. Ao roubar o sal e o hash de uma senha, o invasor teria que recalcular toda a rainbow table para poder descobrir a senha do usuário. E essa tarefa é muito custosa.

Assim, se cada usuário possuir um sal diferente, a rainbow table terá que ser recalculada para cada usuário, tornando essa atividade praticamente impossível.

Considere que o invasor possui a rainbow table abaixo, bem como o hash (e3e923b2c0d3890270a2cb6d52b13bf6) e o sal (h6ja8) do usuário joaozinho (roubados da base de dados de usuários).

+----------------------------------+---------------------+
|              HASH                |       SENHA         |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 698dc19d489c4e4db73e28a713eab07b | teste               |
+----------------------------------+---------------------+
| e959088c6049f1104c84c9bde5560a13 | teste1              |
+----------------------------------+---------------------+
| 38851536d87701d2191990e24a7f8d4e | teste2              |
+----------------------------------+---------------------+
| 507eb04c9c427e9f961e47a7204fac41 | teste3              |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+
| 128ecf542a35ac5270a87dc740918404 | bla                 |
+----------------------------------+---------------------+
| df5ea29924d39c3be8785734f13169c6 | blabla              |
+----------------------------------+---------------------+
| ...                              | ...                 |
+----------------------------------+---------------------+

Para obter a senha do usuário, o invasor terá que recalcular toda a tabela. Por exemplo, terá que calcular o hash de “testeh6ja8”, de “teste1h6ja8”, e assim por diante, até encontrar o hash correspondente. Considerando que ela pode ter zilhões de registros, essa é uma tarefa bastante demorada. Imagine recalcular toda a tabela para todas as possíveis combinações de valores no sal.

Perceba que o sal que utilizamos é bastante curto. Quanto mais longo for o sal, mais protegidas contra rainbow tables as senhas estarão, pois o número de combinações possíveis de valores para o sal aumentam exponencialmente.

Dessa forma, o sal derruba a maior força das rainbow tables, que é a busca rápida por elementos (que ocorre graças ao cálculo prévio dos valores). Ao usar valores de sal suficientemente longos, estamos aumentando MUITO (MUITO MESMO) a quantidade de entradas que uma rainbow table deve ter para servir como uma base de hashes pré-calculados (segundo a Wikipedia, hoje em dia são usados salts de até 128 bits). Se, além disso, usarmos um sal diferente para cada usuário, então tornamos impossível o uso de tabelas pré-computadas para a descoberta das senhas dos nossos usuários.

Mas, ainda assim, hashes salgados não são a solução definitiva para armazenamento de senhas. Leia a próxima seção para descobrir o porquê.

O mecanismo correto

Mesmo sendo um mecanismo muito mais seguro do que armazenando a senha em texto puro ou o hash simples, o armazenamento de hashes de senhas salgadas ainda não é a melhor solução. Apesar de amplamente utilizados, os algoritmos de hashing como MD5 e SHA-1 não são recomendados para o armazenamento de senhas, pois são bastante velozes para realizar o hashing de um valor. Um algoritmo rápido torna a geração de ataques via força bruta e rainbow tables mais fácil, pois o tempo necessário para gerar as tabelas acaba sendo pequeno.

Um algoritmo para uso no armazenamento de senhas tem como requisito ser lento. Lento o suficiente para atrapalhar o atacante, mas não o bastante para atrapalhar o usuário.

Uma forma recomendada de armazenar as senhas dos usuários é usando o bcrypt. O BCrypt é um mecanismo criptográfico criado para lidar com senhas. Assim sendo, uma de suas características é ser demorado para geração do hash.

Para ter uma idéia da diferença na velocidade dos dois mecanismos, observe o resultados de uns testes que fiz usando o timeit:

$ python -m timeit -s "import bcrypt; salt = bcrypt.gensalt()" "bcrypt.hashpw('teste', salt)"
10 loops, best of 3: 243 msec per loop

$ python -m timeit -s "import hashlib" "hashlib.md5('teste')"
1000000 loops, best of 3: 0.409 usec per loop

Enquanto o MD5 gera os hashes em uma média de 0.409 microsegundos por hash, o bcrypt leva em média 243 milisegundos por hash.

Instalando o bcrypt

A implementação Python do bcrypt não está disponível com a biblioteca-padrão, portanto é necessário instalá-la através do gerenciador de pacotes pip:

sudo pip install py-bcrypt

Obs.: o módulo é escrito em linguagem C, portanto o código será compilado. Para isso, é necessário possuir instalados (em sistema Ubuntu): build-essential e python-dev.

Alternativamente, num sistema Debian ou Ubuntu você pode instalar o pacote no repositório do apt:

sudo apt-get install python-bcrypt

Usando o bcrypt

Assim como a maioria dos módulos Python, o bcrypt é facinho de usar. Vamos fazer alguns exemplos.

Gerando o hash de uma senha

>>> import bcrypt
>>> print bcrypt.hashpw('teste123', bcrypt.gensalt())
$2a$12$axTaPizQ6q2V8VCLseqEq.Rwm1aZVx7oimvjPmmLWTNE3uW15xpIu

Execute o código acima no seu interpretador Python local e perceba a diferença no tempo de resposta entre o bcrypt e o MD5.

Verificando a senha do usuário

Para verificar se determinado hash é correto, devemos passá-lo como argumento para a função hashpw, juntamente com a senha:

>>> print bcrypt.hashpw("teste123", "$2a$12$axTaPizQ6q2V8VCLseqEq.Rwm1aZVx7oimvjPmmLWTNE3uW15xpIu")
$2a$12$axTaPizQ6q2V8VCLseqEq.Rwm1aZVx7oimvjPmmLWTNE3uW15xpIu

Se ela retornar o mesmo hash que foi passado como argumento, é porque o hash corresponde à senha passada como primeiro argumento.

Podemos então criar uma função de validação de senha de usuário:

def valida_senha(senha_digitada, hash_senha):
    return bcrypt.hashpw(senha_digitada, hash_senha) == hash_senha

Um sistema de autenticação usando BD

Vamos agora implementar um mecanismo para autenticação de usuários, usando sqlite3 e bcrypt.

# -*- encoding:utf-8 -*-
import bcrypt
import sqlite3

def valida_senha(senha_digitada, hash_senha):
    return bcrypt.hashpw(senha_digitada, hash_senha) == hash_senha

def insere_usuario(conexao, usuario, senha):
    hash_senha = bcrypt.hashpw(senha, bcrypt.gensalt())
    conexao.execute('insert into USUARIOS values ("%s", "%s")' % (usuario, hash_senha))
    conexao.commit()

def usuario_autenticado(conexao, usuario, senha):
    cursor = conexao.execute('select SENHA from USUARIOS where NOME = "%s"' % (usuario,))
    dados = cursor.fetchone()
    hash_senha = str(dados[0])
    return valida_senha(senha, hash_senha)

# alguns testes
if __name__=='__main__':
    conexao = sqlite3.connect('arquivo.db')
    insere_usuario(conexao, 'maria', 'teste')
    insere_usuario(conexao, 'joao', 'teste123')
    if usuario_autenticado(conexao, 'joao', 'teste123'):
        print 'joao está autenticado!'
    else:
        print 'Xiiiii...'

 

Antes de terminar …

O post mostrou quão simples é a implementação de um mecanismo seguro para armazenamento das senhas dos usuários em um banco de dados. Agora você não tem mais desculpas para implementar a autenticação usando senhas armazenadas em texto puro.

Se você armazena as senhas em texto puro, é fácil alterar seus algoritmos de autenticação e gerar os hashes das senhas existentes (faça backup antes, claro :)). Vale a pena gastar um tempinho corrigindo isso em seu sistema.

Finalizando, o py-bcrypt é tão simples de ser utilizado que me deixa à vontade para deixar como lição final:

LIÇÃO FINAL: USE O BCRYPT PARA ARMAZENAMENTO DE SENHAS DE USUÁRIOS!

LIÇÃO DEFINITIVA: NÃO SEJA RELAPSO COM AS INFORMAÇÕES DOS SEUS USUÁRIOS!

Leia mais sobre o assunto

Em inglês:

P.S.: obrigado ao eliasdorneles por ter revisado esse post. 🙂

Anúncios

all() e any()

A linguagem Python é recheada de atalhos úteis, que não só facilitam nossa vida na hora de resolver problemas, mas que também fazem você pensar soluções de forma diferente. Este post é sobre duas funções Python que agem sobre listas de booleanos, e se revelam úteis na prática.

Imagine que você tenha uma lista de inteiros, e você precisa verificar se todos são pares. Você pode pensar: “Fácil, é só percorrer a lista, e verificar para cada elemento se ele é divisível por 2. Se algum não for, posso setar uma variável avisando que é falso e tá feito!” Tendo o raciocínio pronto, você senta o traseiro na cadeira e escreve um código tipo esse:

listaDeInteiros = [2, 6, 4, 7, -2]
todosSaoPares = True
for i in listaDeInteiros:
    if i % 2 == 0:
        todosSaoPares = False
        break
if todosSaoPares:
    print 'Todos sao pares'
else:
    print 'Tem algum impar'

E está ótimo, o código funciona numa boa, as variáveis estão claras, e tudo o mais. Todavia, com o passar do tempo você se dá conta que esse tipo de problema começa a repetir. Seguidamente você precisa verificar se uma condição é verdadeira (ou falsa) para os elementos de uma lista, e acaba fazendo muitas muitas versõezinhas parecidas desse código. Agora você pode precisar descobrir se todos os elementos de uma lista são inteiros positivos, e lá vai você de novo:

listaDeInteiros = [2, 6, 4, 7, -2]
todosSaoPositivos = True
for i in listaDeInteiros:
    if i <= 0:
        todosSaoPositivos = False
        break
if todosSaoPositivos:
    print 'Todos sao positivos'
else:
    print 'Tem algum negativo'

Perceba como o código é bem similar ao anterior, praticamente só altera a condição, e os nomes das coisas. Devido a frequência com que aparecem problemas parecidos com esse, Python fornece um jeito mais sucinto de escrever esse tipo de lógica. Usando as funções pré-definidas all e any, seu código ficará mais fácil tanto de escrever como de ler, uma vez que você as aprende. Veja como ficaria o código para verificar se todos os elementos são pares usando all:

listaDeInteiros = [2, 6, 4, 7, -2]
if all(i % 2 == 0 for i in listaDeInteiros):
    print 'Todos sao pares'
else:
    print 'Tem algum impar'

Podemos obter resultado semelhante usando a função any. Tente perceber todas as diferenças:

listaDeInteiros = [2, 6, 4, 7, -2]
if not any(i % 2 != 0 for i in listaDeInteiros):
    print 'Todos sao pares'
else:
    print 'Tem algum impar'

all()

A função all() é muito simples e também muito útil. A documentação oficial da função diz o seguinte:

`all(iterável)`
Retorna `True` se todos os elementos do iterável forem verdadeiros (ou se o iterável for vazio).

Ou seja, a função recebe como parâmetro um objeto iterável (uma lista ou uma tupla, por exemplo) e verifica se todos os elementos desse iterável possuem o valor True. Como o próprio nome diz, all() (todos, em português) verifica se todos os elementos contidos em uma sequência são verdadeiros.

Isso pode ser muito útil quando temos uma lista e precisamos, antes de qualquer coisa, verificar se os elementos dessa lista satisfazem determinada condição.

Por exemplo, antes de passarmos uma lista de strings para uma função que não trata strings vazias, poderíamos fazer o seguinte:

if all(s != '' for s in lista_de_strings):
    funcao_que_nao_trata_strings_vazias(lista_de_strings)
else:
    print "Erro: strings vazias existem na lista!"

No exemplo acima usamos uma generator expression (s != '' for s in lista_de_strings) para gerar o iterável contendo booleanos para passar como entrada para a função all().

Simples, expressivo e eficiente.

any()

A função builtin any() é parecida com a função all(), porém ela retorna True se algum dos elementos do iterável for True. Por exemplo, poderíamos usá-la para verificar se pelo menos um dos elementos de lista_de_strings é uma string vazia.

if any(s == '' for s in lista_de_strings):
    print "Erro: strings vazias existem na lista!"
else:
    funcao_que_nao_trata_strings_vazias(lista_de_strings)

all() e any() como alternativas a and e or

Uma expressão booleana como:

if cond1 and cond2 and cond3 and cond4 and cond5:
    # faça algo

será verdadeira somente se todas as condições testadas forem verdadeiras. Como poderíamos escrever a expressão acima utilizando a função all()?

if all([cond1, cond2, cond3, cond4, cond5]):
    # faça algo

E uma expressão composta por ors lógicos, como poderia ser reescrita?

if cond1 or cond2 or cond3 or cond4 or cond5:
    # faça algo

A expressão acima (cond1 or cond2 or cond3 or cond4 or cond5) será verdadeira se alguma das condições for verdadeira. Ou seja, poderíamos usar a função any():

if any([cond1, cond2, cond3, cond4, cond5]):
    # faça algo

As funções all() e any() podem então ser vistas como aplicações dos operadores and e or aos elementos de sequências.

all() e any() combinados com map()

Já vimos como as funções all() e any() são práticas. Vamos ver agora que, quando combinadas com outros recursos que Python oferece, essas funções passam a ser mais poderosas, dando maior expressividade ao código escrito.

Anteriormente, vimos o funcionamento da função map(): ela aplica uma função a cada elemento de uma sequência e retorna o resultado disso em uma nova lista. Por exemplo:

>>> import math
>>> lista1 = [1, 4, 9, 16, 25]
>>> lista2 = map(math.sqrt, lista1)
>>> print lista2
[1.0, 2.0, 3.0, 4.0, 5.0]

Como poderíamos combinar a função map() com as funções all() ou any()? Vamos a um exemplo.

Considere que temos duas listas contendo números inteiros ([1,2,3,4,5] e [1,4,2,3,5]) e precisamos saber se essas listas possuem elementos de mesmo valor posicionados nas mesmas posições em ambas as listas. Isso poderia ser feito da seguinte forma:

>>> import operator
>>> map(operator.eq, [1,2,3,4,5], [1,4,2,3,5])
[True, False, False, False, True]
>>> any( map(operator.eq, [1,2,3,4,5], [1,4,2,3,5]) )
True

Na segunda linha, usamos a função map() para comparar a igualdade (operator.eq) dos elementos das duas listas (mais informações sobre o módulo operator aqui). Tal linha aplica a função eq (que recebe dois argumentos, os elementos a serem comparados) a pares de elementos das duas listas passadas como argumentos, iniciando por 1 e 1, passando por 2 e 4, 3 e 2, 4 e 3, e, por fim, 5 e 5. Cada comparação gera um booleano que é adicionado à lista que a função map() retorna como resultado. Então, basta que apliquemos a função any() para verificar se para algum dos pares de elementos o eq retornou True. Se em qualquer das posições a lista de resultado do map tiver o valor True, significa que as duas listas possuem elemento cujo valor e posição sejam iguais nas duas listas.

Para finalizar

all() e any() fazem parte do grupo de recursos de Python que todo pythonista deve ter no bolso para utilizar sempre que for preciso. Além de evitar longas linhas de expressões booleanas, essas funções nos permitem dar maior legibilidade ao nosso código.

Ah, fique atento, pois ambas as funções estão disponíveis somente a partir da versão 2.5 do Python.