AVISO: Este projeto migrou para o repositório https://github.com/ramalho/dadoware

Diceware: método seguro para gerar senhas

Compilação da lista de palavras

Neste notebook comecei com uma grande lista palavras da língua portuguesa, que fui sucessivamente filtrando com diversos critérios até chegar próximo da quantidade necessária para o método diceware. Então embaralhei as 7808 palavras restantes e fatiei a lista para ficar com a quantidade exata: 7776. No último passo, gerei o arquivo 7776palavras.txt com linhas de "11111 abaci" a "66666 zurpa".

Para começar, confirmei a quantidade de palavras necessárias para usar 5 dados na escolha de palavras:


In [1]:
6**5


Out[1]:
7776

O ponto de partida foi uma lista 326.716 palavras da língua portuguesa que compilei a partir de várias fontes. Aqui eu leio o arquivo, verifico que são todas palavras únicas e exibo uma amostra com as 10 primeiras e as 10 últimas:


In [2]:
completa = [lin.strip() for lin in open('palavras.txt').readlines()]
len(completa), len(completa) == len(set(completa))


Out[2]:
(326716, True)

In [3]:
completa[:10], completa[-10:]


Out[3]:
(['a',
  'ª',
  'á',
  'à',
  'ã',
  'a-amilase',
  'á-bê-cê',
  'á-é-i-ó-u',
  'a-histórico',
  'à-propos'],
 ['zurzidura',
  'zurzir',
  'zuzins',
  'zwinglianismo',
  'zwingliano',
  'µg',
  'µL',
  'µm',
  'µmol',
  'µUI'])

O primeiro filtro foi pegar só palavras de até 5 caracteres:


In [4]:
ate5 = [p for p in completa if len(p) <= 5]
len(ate5)


Out[4]:
18098

Em seguida, fiquei apenas com as palavras formadas exclusivamente por letras minúsculas de "a" a "z", sem acentos ou hífens:


In [5]:
import string
so_ascii = []
for palavra in ate5:
    if all(c in string.ascii_lowercase for c in palavra):
        so_ascii.append(palavra)
    
len(so_ascii)


Out[5]:
8950

In [6]:
so_ascii[:10], so_ascii[-10:]


Out[6]:
(['a',
  'ab',
  'abaci',
  'abade',
  'abafo',
  'abal',
  'abama',
  'abana',
  'abar',
  'abaso'],
 ['zulos',
  'zulu',
  'zum',
  'zunar',
  'zunda',
  'zunir',
  'zupar',
  'zura',
  'zurca',
  'zurpa'])

Examinando a lista obtida até agora, percebi que haviam muitas palavras que eram apenas plurais, como "acres", "botas" etc. Com este código eu removi as palavras que são iguais a outras palavras apenas com um "s" colado no final. Por exemplo, removi "abios" porque a lista já tem a palavra "abio".


In [7]:
ocorrencias = set(so_ascii)
singulares = []
print('Plurais removidas:')
for pal in so_ascii:
    if pal[-1] == 's' and pal[:-1] in ocorrencias:
        print(pal, end=' ')
    else:
        singulares.append(pal)


Plurais removidas:
abios abius acres agues aias aipos alas algas alhos almas alpes altas altos amas anas angas anjos anos antas anus arcas arcos ares artes arus as asas aspas assas atos aves babas bacus bagas bagos baios balas belas belos bias bicos bips birus blues boas bobas bobos bocas bodes bofes bogas boias bois bolas boles bolos botas botos breus brios bufas bufos bules buris buxos buzos cabas cabos cacas cacos caias cais cajus camas canas capas caras caris caros casas casos catas cavas cebas cegas cegos ceias ceras cias cilas cocas cocos colas colos cones copas copos cotas covas coxas coxos cris crus cubos cucas cucos cuias cujos cujus curas cus dados damas das datas dedos dias didis dinas ditos doces donas donos dores dos doses dunas duras duros dus efes efues eis eixos enxus eros ervas es exs exus facas faces fadas fados faias fatos favas feias feios fenos feras fetos figos finas finos fios fitas fixos focas fofas fofos foges fogos foles fomes fotos freis frias frios fulas fulos fumos furos fusos gaios galos gatas gatos gelos gemas gilas giros godes golas gols gomas gotas grous guias gutas heras horas https iafis igbas iinis ilhas imbus incas iogas iris is iscas ivas jacas jacus jades japus jatis jatos jecas jejes jesus jiles jipis jocos jogos joias joios jotas jucas jutas labas lagos lamas lanas lapas las latas ledos leis leoas leos lesas lesos leves ligas limas limos lipes lisas lisos lixas lixos lobos loios lonas lopes loros lotos luas lulas lulus lumes lutos luvas luzes mails maios mais malas mamas manas manos mapas mas mates matos maus meias meios menas menos meros mesas meus mexes miaos micas micos mijas mijos mimos minas miras miris modas modos molas moles mols monas monos motos ms mucos mudas mudos mulas muros musas nabos nadas nados nagas nas natas naus naves negas netos neves nomes nonos nos novas novos nuas numes nunes obis obras ocas ocos ohms oiros oitis olhas olhos olmos ondas os ossos otis ouros ovos pacas pacos pacus pafs pagos pais palas palos panos papos parus patas patis patos pauls paus pebas pegas pegos peles pelos penas pepes peras perus pesos petas petos pias piaus picas picos pinos pios pipas pires piris pisos pitas pitos pneus polos pomas pomos potes povos pras proas ps pubas pulas pulos punas punks puros pus putas puxas rabos raias raios ramas ramos ranus raras rasas rasos ratos recos redes regos reis remas remos retos ricas ricos rijos rimas rios ritas rocas rodas rolas rolos rosas rotas rotos roxas roxos ruas rubis rufes rufos ruges rumos rutas sacas sacis sacos safas sagus saias sais sajus salas sapos saras saxes sebas sebes sebos secas secos sedas seios seis selos senas senes setes seus sexos shows sinas sinos siris sitos socos sojas solas solos sonos soros sotos spins sujas sujos sumos sus talos tatus taxis tebas tecas temas tesas tesos tetas tetos tias ticos tios tipas tipis tipos tiras tiros tocos todos tojos tolas tolos topos tops tubos tudos tufos tuias tupis udus umes unas unhas urnas ursos urzes us uvas uxis vacas vais valas vales valos varas vars vazas veios velas veras vias vidas vides vilas viras viros vis vivas vivos vogas volts voos votos vs watts zebus zeros zonas zoos 

In [8]:
len(singulares)


Out[8]:
8404

Estamos chegando perto das 7776 palavras que precisamos. Aqui fiquei só com as palavras de 4 ou 5 letras:


In [9]:
tam4ou5 = [p for p in singulares if len(p) in (4,5)]
len(tam4ou5)


Out[9]:
7850

In [10]:
sobrando = len(tam4ou5) - 6**5
sobrando


Out[10]:
74

Ainda precisamos descartar 74 palavras. Procurei na Web uma lista de palavrões, e achei nesta página. Carreguei as palavras, convertendo em minúsculas e criando um set. A notação {… for … in …} é uma set comprehension e produz um objeto set. O resultado foi um conjunto de 301 palavrões.


In [11]:
palavroes = {lin.strip().lower() for lin in open('palavroes.txt', encoding='latin1').readlines()}

In [12]:
len(palavroes)


Out[12]:
301

Transformando a lista tam4ou5 em um set fica muito fácil remover os palavrões fazendo apenas uma subtração entre conjuntos. O resultado eu converti em list novamente, para poder embaralhar a seguir.


In [13]:
limpas = list(set(tam4ou5) - palavroes)
len(limpas), len(limpas) - 6**5


Out[13]:
(7808, 32)

Ainda temos 32 palavras a mais. Na diagramação que fiz depois, a palavra "mamum" causou problemas por ter 3 "m" e então fica mais larga que as demais. Vamos todas as palavras com mais de 2 "m":


In [14]:
import collections
mmm = []
for palavra in limpas:
    if collections.Counter(palavra)['m'] > 1:
        mmm.append(palavra)
mmm


Out[14]:
['momar',
 'mumua',
 'mamal',
 'mimbo',
 'mesmo',
 'imamo',
 'tmema',
 'imame',
 'mumbo',
 'mirmo',
 'moim',
 'mimi',
 'meoma',
 'merim',
 'mamoa',
 'ambom',
 'mulim',
 'omoma',
 'emfim',
 'miama',
 'menim',
 'amim',
 'modem',
 'pemom',
 'tomim',
 'mium',
 'cumim',
 'mimar',
 'mamar',
 'mulme',
 'marim',
 'motum',
 'meum',
 'miami',
 'mambo',
 'amamu',
 'muim',
 'motim',
 'mome',
 'mugem',
 'molim',
 'mioma',
 'muame',
 'simum',
 'mumo',
 'mombe',
 'homum',
 'momo',
 'mamum',
 'murim',
 'mmol',
 'mirim',
 'marmo',
 'mocim',
 'metim',
 'amame',
 'mulum',
 'homem',
 'emamo',
 'madim',
 'camim',
 'mutum',
 'mama',
 'mambi',
 'mimo',
 'membi',
 'mimbi',
 'maxim',
 'mujem',
 'omeme',
 'meme',
 'mumu',
 'mucum',
 'morim',
 'mormo',
 'magma',
 'mamua',
 'comum',
 'murum',
 'mutom',
 'malim',
 'armim',
 'armum',
 'memi',
 'migma']

Ok, vamos tirar esta da lista:


In [15]:
limpas.remove('mamum')
len(limpas), len(limpas) - 6**5


Out[15]:
(7807, 31)

Ainda sobram 31. Para ficar só com a quantidade certa eu embaralhei a lista limpas e peguei uma fatia com as 6**5 palavras iniciais da lista.


In [16]:
import random
random.shuffle(limpas)
final = sorted(limpas[:6**5])
len(final), final[:10], final[-10:]


Out[16]:
(7776,
 ['abaci',
  'abade',
  'abafo',
  'abal',
  'abama',
  'abana',
  'abar',
  'abaso',
  'abati',
  'abatu'],
 ['zular',
  'zulos',
  'zulu',
  'zunar',
  'zunda',
  'zunir',
  'zupar',
  'zura',
  'zurca',
  'zurpa'])

Vamos conferir mais uma vez se temos a quantidade certa de palavras e não temos nenhuma repetida:


In [17]:
len(final), len(final) == len(set(final))


Out[17]:
(7776, True)

Com a lista final em mãos, agora falta só gerar as chaves de 11111 a 66666 para criar um índice fácil de usar com 5 dados de 6 faces.


In [18]:
import itertools
dados5 = list(''.join(dados) for dados in itertools.product('123456', repeat=5))
len(dados5)


Out[18]:
7776

In [19]:
pares = list(zip(dados5, final))
pares[:10], pares[-10:]


Out[19]:
([('11111', 'abaci'),
  ('11112', 'abade'),
  ('11113', 'abafo'),
  ('11114', 'abal'),
  ('11115', 'abama'),
  ('11116', 'abana'),
  ('11121', 'abar'),
  ('11122', 'abaso'),
  ('11123', 'abati'),
  ('11124', 'abatu')],
 [('66653', 'zular'),
  ('66654', 'zulos'),
  ('66655', 'zulu'),
  ('66656', 'zunar'),
  ('66661', 'zunda'),
  ('66662', 'zunir'),
  ('66663', 'zupar'),
  ('66664', 'zura'),
  ('66665', 'zurca'),
  ('66666', 'zurpa')])

O último passo é escrever o arquivo 7776palavras.txt. Usando o comando wc no shell confiro se o arquivo tem o número esperado de linhas:


In [20]:
with open('7776palavras.txt', 'wt', encoding='ascii') as saida:
    for par in pares:
        saida.write('%s %s\n' % par)
        
!wc '7776palavras.txt'


    7776   15552   91106 7776palavras.txt

Carregando esse arquivo no LibreOffice, criei dois PDF: 18 páginas A4, 36 páginas A5.

Basta imprimir, juntar um dado de 6 faces e gerar senhas seguras sem usar o computador.