E enviando mensagens secretas pela web distribuída
Parte do nosso plano de trazer o texto 1 milhão de peers para a rede IPFS envolve tornar divertido e fácil participar da web distribuída. Estamos levando esse apelo à ação a sério e já começamos a criar produtos reais que resolvem problemas reais de maneira segura e descentralizada (sem exigir que as pessoas aprendam um monte de novas tecnologias de criptografia/web). Mas, às vezes, desenvolvedores e amadores querem colocar as mãos na massa. É para isso que serve este post.
Vamos construir um ĐApp simples do zero, usando tecnologias modernas de desenvolvimento web, magia (criptografia) e o Sistema de Arquivos Interplanetários (IPFS). E vamos hospedá-lo de forma permanente e segura na web descentralizada. O produto final? Um ĐApp que permite aos usuários enviar mensagens efêmeras e criptografadas com segurança pela web sem a necessidade de servidores centralizados. Um Dead Drop descentralizado, se você quiser. Então vamos mergulhar nisso.
Primeiro, vou assumir que a) você está familiarizado com o desenvolvimento node (e que tenha o node instalado), b) você está ciente de que o IPFS é uma coisa (confira este vídeo se não estiver), c) você já ouviu falar de criptografia, e d) você acha que espiões e mensagens secretas são legais.
Configuração
Com essas suposições em mente, vamos começar. Você vai querer criar um novo projeto de node a partir da linha de comando. Eu chamei o meu encryptoid, e os únicos não padrões que usei foram definir o entry point
para dist/index.html
e test
para standard
(o que é realmente apenas um bom linter).
mkdir encryptoid
cd encryptoid
npm init
Seu arquivo package.json deve ficar assim:
{
"name": "encryptoid",
"version": "1.0.0",
"description": "Browser dapp for encrypting and sending ephemeral secret messages over ipfs",
"main": "dist/index.html",
"scripts": {
"test": "standard"
},
"author": "Carson Farmer <[email protected]>",
"license": "MIT"
}
Agora vamos adicionar algumas devDependencies
. Como queremos executar nosso ĐApp no navegador, mas escrever módulos modernos no estilo de nó usando a sintaxe ECMAScript 2015(ES6), precisaremos usar browserify
e amigos para transpilar nosso Javascript.
yarn add --dev browserify babelify babel-core babel-polyfill babel-preset-env
Também vamos usar alguns outros módulos para minify, lint, watch e simplify nosso código, e servir nosso ĐApp enquanto o construímos:
yarn add --dev envify npm-run-all shx standard uglifyify watchify ecstatic
Em seguida, precisamos configurar nossa estrutura de pastas. Eu gosto de manter as coisas bem modularizadas, então vamos configurar algo assim:
encryptoid
|____LICENSE
|____README.md
|____package.json
|____src
| |____index.html
| |____images
| | |____logo.png
| |____main.js
| |____style.css
|____dist
| |____…
Com dist
sendo o local de saída para nossos módulos transpilados e arquivos de suporte (html, css, imagens, etc). Agora, com essa estrutura de pastas em mente, vamos modificar package.json
para configurar nossos scripts de compilação:
...
"scripts": {
"start": "ecstatic dist",
"clean": "shx rm -rf dist",
"build": "run-s build:*",
"build:copy": "run-p build:copy:*",
"build:copy:html": "shx mkdir -p dist && shx cp src/index.html dist/index.html",
"build:copy:css": "shx mkdir -p dist && shx cp src/style.css dist/style.css",
"build:js": "browserify src/main.js -o dist/bundle.js -g uglifyify",
"watch": "npm-run-all build:* --parallel watch:*",
"watch:js": "watchify -t envify src/main.js -o dist/bundle.js -v",
"watch:serve": "ecstatic --cache=0 dist",
"test": "standard"
},
"browserify": {
"transform": [
["babelify", {"presets": ["env"]}],
["envify"]
]
},
...
Há muito aqui, mas os componentes de build
são as peças mais importantes. Nosso script build:js
principal executará nosso arquivo Javascript src/main.js
por meio de browserify
e uglifyify
usando as configurações especificadas na entrada “browserify”
(que inclui executá-lo por meio de babelify
e envify
), e os scripts baseados em cópia moverão nosso vários arquivos de suporte para nossa pasta dist
. A única parte 'personalizada' aqui é que estamos usando o módulo shx para executar comandos shell multiplataforma em nossos scripts.
Primeiros passos
Ok, já temos nossa estrutura pronta, agora vamos para a tarefa em mãos: colocando as mãos na massa escrevendo código. Vamos começar com o básico, vamos criar um arquivo index.html
em nosso diretório src
para suportar nosso ĐApp, junto com um estilo mínimo (style.css
) e um arquivo Javascript 'vazio' (main.js
) com algo como console.log('hello world')
.
(HTML)
<!doctype html>
<html>
<head>
<meta charset="utf8">
<title>Encryptoid ĐApp</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="main">
<h1>Welcome to Encryptoid!</h1>
</div>
<script type="text/javascript" src="bundle.js"></script>
</body>
</html>
(CSS)
html, body {
font-family: "Lucida Sans Typewriter", "Lucida Console", "Bitstream Vera Sans Mono", monospace;
height: 100%;
}
#main {
width: 50%;
margin: 0 auto;
padding-top: 2%;
}
Se executarmos yarn build
, então yarn start
e navegarmos até http://0.0.0.0:8000
devemos obter uma página web simples com “Welcome to Encryptoid!”
espalhado pela tela. Nada 'ĐAppy' ainda. Então agora vamos adicionar algumas funcionalidades.
Primeiro, adicionaremos algumas dependências de tempo de execução. Para instanciar um nó de mesmo nível IPFS, usaremos window.ipfs-fallback
e, para realizar os cálculos criptográficos, usaremos js-libp2p-crypto
. O módulo de criptografia é bastante autoexplicativo, mas a funcionalidade ipfs-fallback
requer alguma explicação.
O pessoal da IPFS lançou a extensão da web IPFS Companion que simplifica o acesso aos recursos IPFS executando um peer Javascript IPFS em seu navegador. Ele pode expor um nó IPFS incorporado como window.ipfs
em todas as páginas da Web! Isso torna possível para qualquer ĐApp baseado em IPFS detectar se window.ipfs
existe e optar por usá-lo em vez de criar um peer js-ipfs único
. Não é necessário para executar ĐApps, mas faz com que funcionem melhor, porque você não precisa ativar um peer IPFS inteiro toda vez que carregar um novo ĐApp. Mas, obviamente, não podemos esperar que nossos usuários instalem uma extensão de navegador antes de poder usar nosso ĐApp, que é onde entra o window.ipfs-fallback
. Este módulo detectará a presença de window.ipfs
e o usará se disponível , ou voltará automaticamente para baixar a versão mais recente do IPFS da CDN, caso não seja. Legal e Limpo:
yarn add window.ipfs-fallback js-libp2p-crypto
Agora que temos esses módulos, vamos adicionar algumas funcionalidades Javascript ao nosso arquivo src/main.js
:
import 'babel-polyfill' // We need this for async/await polyfills
// Import our two IPFS-based modules
import getIpfs from 'window.ipfs-fallback'
import crypto from 'libp2p-crypto'
let ipfs
// Setup a very simple async setup function to run on page load
const setup = async () => {
try {
ipfs = await getIpfs() // Init an IPFS peer node
const id = await ipfs.id() // Get the peer id info
console.log(`running ${id.agentVersion} with ID ${id.id}`)
} catch(err) {
console.log(err) // Just pass along the error
}
}
setup()
Estamos usando padrões async/await para tornar o código agradável e legível, então esperamos que o código acima seja bastante autoexplicativo. Em resumo, estamos chamando e esperando que nossa função getIpfs
retorne um nó de peer IPFS e, em seguida, consultando esse peer por seu idinfo
e registrando isso no console. Também poderíamos adicionar um pequeno indicador de 'pronto' à página para que nossos usuários saibam que estão prontos para acessar a web distribuída.
Neste ponto, já temos um ĐApp baseado em IPFS em funcionamento. Poderíamos encerrar o dia e seguir em frente - que foi o que fizemos quando criamos o IPFS ĐApp Template para outros desenvolverem - ou poderíamos começar a ser um pouco mais criativos... então vamos fazer isso. Para permitir que os usuários escrevam mensagens secretas, precisaremos de duas entradas de texto, uma para a própria mensagem e outra para a senha super secreta. Também queremos alguma maneira de mostrar o endereço da mensagem criptografada e talvez algum estilo para se adequar ao nosso tema de espionagem?
Sob nosso cabeçalho de boas-vindas, vamos adicionar duas entradas de texto em um formulário e um botão para fazer o trabalho. Também colocaremos uma div de output
para exibir a saída para nós:
<h1>Welcome to Encryptoid!</h1>
<form id="secret">
<label for="message">Message</label>
<textarea id="message" required rows="5"></textarea>
<label for="password">Password</label>
<input id="password" type="text" required minlength="10" maxlength="100">
</form>
<button id="button" type="button">Encrypt</button>
<div id="output"></div>
Agora, você vai querer reiniciar seu processo yarn watch
, e vamos começar a editar src/main.js
novamente:
import 'babel-polyfill' // We need this for async/await polyfills
// Import our two IPFS-based modules
import getIpfs from 'window.ipfs-fallback'
import crypto from 'libp2p-crypto'
let ipfs
// Setup a very simple async setup function to run on page load
const setup = async () => {
try {
ipfs = await getIpfs() // Init an IPFS peer node
const button = document.getElementById('button')
button.addEventListener("click", (e) => {
e.preventDefault()
const message = document.getElementById('message')
const password = document.getElementById('password')
// Compute a derived key to use in AES encryption algorithm
const key = crypto.pbkdf2(password.value, 'encryptoid', 5000, 24, 'sha2-256')
// We're only using the key once, so a fixed IV should be ok
const iv = Buffer.from([...Array(16).keys()])
// Create AES encryption object
crypto.aes.create(Buffer.from(key), iv, (err, cipher) => {
if (!err) {
cipher.encrypt(Buffer.from(message.value), async (err, encrypted) => {
if (!err) {
const hashed = (await ipfs.files.add(encrypted))[0]
output.innerText = `/ipfs/${hashed.hash}`
}
})
}
})
})
} catch(err) {
console.log(err) // Just pass along the error
}
}
A maior mudança aqui é que adicionamos um event listener em nosso botão Criptografar. Neste event listener estamos fazendo várias coisas:
- Na linha 19, estamos usando a função de derivação de chave PBKDF2 (Password-BasedKey Derivation Function 2) para produzir uma chave derivada, que usamos como chave criptográfica em operações subsequentes. Embora geralmente seja uma boa ideia usar um salt aleatório ao fazer hash de senhas, nunca armazenamos senhas aqui, então usamos apenas um salt fixo. Mantemos 5.000 iterações aqui, com um tamanho de chave de 24 bytes.
- Na linha 20, criamos um vetor de inicialização de buffer (IV) de comprimento fixo(16). Como estamos usando nossa chave derivada apenas uma vez, isso deve estar ok. Mas, idealmente, usaríamos um IV (Vetor de Inicialização) aleatório.
- A verdadeira 'mágica' acontece nas linhas 22 a 27. Começamos criando um objeto de criptografia AES no modo CTR (já que temos uma chave de 32 comprimentos, estamos usando AES 256) usando nossa chave e IV (Vetor de Inicialização).
- Em seguida, na linha 24, fazemos a criptografia real. Passamos um
Buffer
de nossa mensagem de texto simples para o método de criptografia da cifra, cujo texto cifrado é passado para nosso retorno de chamada (infelizmente,libp2p-crypto
não parece suportar promises, daí os callbacks aqui). - Finalmente, na linha 26, adicionamos a mensagem criptografada ao IPFS e aguardamos o hash CID retornado. Se você não estiver familiarizado com os hashes CID e a mecânica do IPFS, recomendo dar uma olhada aqui.
- Para fornecer uma maneira de acessar a mensagem criptografada, simplesmente produzimos o hash CID. Poderíamos então enviar esse hash para o destinatário pretendido e fazer com que descriptografassedescriptografassem seu conteúdo. Você pode visualizar o texto cifrado da mensagem criptografada acessando-a pelo gateway público: https://ipfs.io/ipfs/Qm….
Para fazer a descriptografia real, o processo é quase idêntico. Vou deixar os detalhes da implementação real como um exercício para o leitor (libp2p-crypto fornece alguns bons exemplos), mas a principal diferença seria que o aes.createcallback
pode ser mais ou menos assim:
crypto.aes.create(Buffer.from(key), iv, (err, cipher) => {
if (!err) {
cipher.decrypt(Buffer.from(message.value, 'base64'), async (err, plain) => {
if (!err) {
output.innerText = `${plain.toString('utf-8')}`
}
})
}
})
Você pode dar uma olhada em como implementamos isso em nosso repositório GitHub, que lançaremos como um aplicativo de demonstração completo no IPFS em breve.
E é isso por enquanto! Você sabe que tem um ĐApp baseado em IPFS totalmente funcional que realmente faz algo útil (criptografa mensagens secretas e as publica na web distribuída). Como estamos executando este ĐApp em um navegador e não fixando os arquivos em um servidor em qualquer lugar, eles eventualmente terão coleta de lixo. Provavelmente após cerca de 24 a 48 horas (a menos que as pessoas continuem acessando). Nesse ponto, sua mensagem desaparecerá para sempre (ou pelo menos até você criá-la novamente). E a melhor parte disso tudo? As mensagens criptografadas são acessadas através da rede IPFS peer2peer. Não há servidores centralizados para bisbilhotar suas comunicações aqui. Esse é o poder da descentralização.
Não há servidores centralizados para bisbilhotar suas comunicações aqui. Esse é o poder da descentralização. - tweet isso
E por falar em criptografia, na Textile, estamos desenvolvendo uma maneira totalmente nova de permitir backup de fotos seguro (pense em criptografia) e compartilhamento na Web distribuída (pense em IPFS). Venha nos conferir e entre na lista de espera do Textile Photos para solicitar acesso antecipado ao futuro.
Esse artigo é uma tradução de Carson Farmer feita por @bananlabs. Você pode encontrar o artigo original aqui
Oldest comments (0)