Entendendo crypto com javascript Publicado em:
Objetivos 🎯
Esclarecer conceitos de crypto utilizados em aplicativos de carteiras digitais e exemplificar como estes códigos funcionam por debaixo dos panos.
O post seguirá os exemplos do site learnmeabitcoin.com feitos em ruby e irá portar-los para javascript (node).
Conceitos 📋
Frase Mnemônica (Mnemonic Seed):
É uma frase contendo um conjunto de palavras que serão usadas para criação/recuperação de uma conta ou carteira digital.
Semente (Seed):
É um valor alfanumérico gerado através da frase mnemônica.
Chave pública e privada:
São chaves criadas através de uma seed. A chave privada é responsável por assinar mensagens, e a pública é responsável por validar pacotes.
Assinatura:
Valor alfanumérico gerado com a a chave privada para garantir autenticidade de mensagens.
Tutorial 🔭
Passo-a-passo:
- Gerar frase mnemônica;
- Gerar seed;
- Gerar chave pública e privada;
- Assinar mensagem e verificar assinatura.
1. Gerar frase mnemônica:
import { readFileSync } from 'node:fs'
import { createHash, randomBytes } from 'node:crypto'
// Lista de palavras para seed
// Seguir para exemplo https://github.com/bitcoin/bips/blob/master/bip-0039/portuguese.txt
/**
* Nesse exemplo, o arquivo `words.json` é uma lista de palavras
* @example
* [
* "abacate",
* "abaixo",
* ...
* ]
*/
const words = JSON.parse(readFileSync('./words.json', { encoding: 'utf-8' }))
/**
* Transforma buffer hexadecimal em array de binário
* @param {Buffer} buf
* @returns {string[]}
* @example
* const buffer = new Buffer().fill(0xb4)
* bufferToBinary(buffer) // -> ['10110100']
*/
//
const bufferToBinary = (buf) =>
[...buf.values()].map((byte) => Number(byte).toString(2).padStart(8, '0'))
// Calcular entropia inicial de 128 bits
const bytes = randomBytes(16)
// Organizar valor de entropia em array de binário
const entropy = bufferToBinary(bytes)
// Aplicar hash (SHA256) na entropia para criar checksum
const hashedEntropy = bufferToBinary(
createHash('sha256').update(entropy.join('')).digest()
)
// Recuperar a quantidade de bits necessária para usar como checksum
// Regra: 1bit a cada 32 bytes
const size = entropy.join('').length / 32
const checksum = hashedEntropy.join('').slice(0, size)
// Agrupar entropia + checksum em formato de 11bits
const full = [...entropy, checksum]
const pieces = full.join('').match(/\d{11}/g)
// Popular frase mnemônica
const sentence = []
for (const piece of pieces || []) {
sentence.push(words[parseInt(piece, 2) - 1])
}
// Ex: entortar ramal ralado copeiro grunhido fretar inerente acima palheta minoria adentro granada
console.log('Frase mnemônica:', sentence.join(' '))
2. Gerar seed através de frase:
import { pbkdf2 } from 'node:crypto'
// Frase mnemônica de exemplo
const mnemonic = 'patente derramar secar edificar galho talher'
const passphrase = '' // Senha opcional
const salt = `mnemonic${passphrase}` // "mnemonic" (padrão) + senha
const iterations = 2048
const keylength = 64
// Utilizando pbkdf2 para criar uma seed de 64 bytes
pbkdf2(mnemonic, salt, iterations, keylength, 'sha512', (err, derivedKey) => {
if (err) throw err
console.log('Mnemonic seed:', derivedKey.toString('hex')) // Ex: 'ee933c...8c5d00'
})
3. Gerar chave pública e privada mestre:
import { createHmac, createECDH } from 'crypto'
// Seed gerada através de frase mnemônica
const seed =
'ee933cc9789f0f3b2377883d193aa717961db37b317f798f9b2f7ad763fbda24' +
'115e37e4f7eca541f7feebdfafa8e8f9a20ea9f632710f155a63a1a6c88c5d00'
// Criar hash (SHA512) de sua seed usando a seed da aplicação como key
// Ex: 'Bitcoin seed'
const hmac = createHmac('sha512', 'Bitcoin seed')
.update(Buffer.from(seed, 'hex'))
.digest()
const masterPrivateKey = hmac.subarray(0, 32)
const masterChainCode = hmac.subarray(32, 64)
// Gerar chaves pública e privada
const ecdh = createECDH('secp256k1')
ecdh.setPrivateKey(masterPrivateKey)
console.log('Chave privada:', ecdh.getPrivateKey('hex'))
console.log('Chave pública:', ecdh.getPublicKey('hex', 'uncompressed'))
4. Assinar dados usando ECDSA:
import {
createSign,
createECDH,
createVerify,
createPublicKey,
createPrivateKey,
} from 'node:crypto'
// Criar objeto de chaves pública e privada
// Referência: https://zanechua.com/blog/generate-ec-private-public-key-pair-node/
/**
* Cria par de chaves pública e privada em formato der
*
* @param {string} rawPublicHexKey hex
* @param {string} rawPrivateHexKey hex
*/
function buildKeyPair(rawPublicHexKey, rawPrivateHexKey) {
const pubKey = Buffer.from(rawPublicHexKey, 'hex')
const privKey = Buffer.from(rawPrivateHexKey, 'hex')
const pubA = Buffer.from(
'3056301006072A8648CE3D020106052B8104000A034200',
'hex'
)
const publicKeyDer = Buffer.concat([pubA, pubKey])
const publicKeyObject = createPublicKey({
key: publicKeyDer,
type: 'spki',
format: 'der',
})
const privA = Buffer.from('30740201010420', 'hex')
const privB = Buffer.from('A00706052B8104000AA144034200', 'hex')
const privateKeyDer = Buffer.concat([privA, privKey, privB, pubKey])
const privateKeyObject = createPrivateKey({
type: 'sec1',
format: 'der',
key: privateKeyDer.toString('hex'),
encoding: 'hex',
})
return { publicKey: publicKeyObject, privateKey: privateKeyObject }
}
// Recuperar chave privada mestre
const masterPrivateKey = Buffer.from(
'07acbf6a29fc19b91d43d1bc9da2aa9588a1fd3fdad9c33681fee523c16f8d03', // Chave de exemplo
'hex'
)
const ecdh = createECDH('secp256k1')
ecdh.setPrivateKey(masterPrivateKey)
const rawPublicKey = ecdh.getPublicKey('hex', 'uncompressed')
const rawPrivateKey = ecdh.getPrivateKey('hex')
// Criando par de chaves no formato der
const { publicKey, privateKey } = buildKeyPair(rawPublicKey, rawPrivateKey)
// Assinando mensagem
const message = 'Hello world'
const signature = createSign('sha256').update(message).sign(privateKey)
console.log('Assinatura:', signature.toString('hex'))
// Verificando assinatura
const isVerified = createVerify('sha256')
.update(message)
.verify(publicKey, signature)
console.log('Verificado:', isVerified)