Guias

Guias · Parte 4 de 8

Construir uma PoE selada

Uma PoE simples prova que determinado conteúdo existiu. Uma PoE selada prova a mesma coisa mantendo o conteúdo em si sob sigilo: você criptografa os bytes para uma ou mais chaves de destinatários, armazena apenas o texto cifrado e ancora o registro na cadeia. Qualquer pessoa pode ver que o registro existe e verificar sua estrutura; somente o detentor de uma chave privada correspondente consegue descriptografar a carga útil. Consulte PoE selada para conhecer o formato do envelope e Selado até ser retirado para o modelo de ameaças.

Endereçar seus destinatários

Um destinatário é identificado por uma string no estilo age. Há dois tipos, e o prefixo os distingue:

  • age1… — uma chave clássica X25519 (32 bytes).
  • age1pqc… — uma chave híbrida X-Wing (ML-KEM-768 + X25519, 1216 bytes).

X-Wing (mlkem768x25519) é o KEM padrão — ele permanece seguro diante de um eventual adversário quântico no futuro, e toda identidade sempre tem um endereço age1pqc….

O destinatário lhe passa essa string por um canal à parte. Você a decodifica de volta para a chave pública bruta de que o helper de selagem precisa, usando parseAgeRecipient:

import { parseAgeRecipient } from '@cardanowall/sdk-ts';

const them = parseAgeRecipient('age1pqc…'); // { kem: 'mlkem768x25519', publicKey: Uint8Array }

Se você mesmo tiver um seed de 32 bytes, o recipientsFromSeed devolve os seus dois endereços — compartilhe um deles para que outras pessoas possam selar registros para você, e inclua sua própria chave na lista de destinatários para manter acesso de leitura ao que envia:

import { recipientsFromSeed } from '@cardanowall/sdk-ts';

const me = recipientsFromSeed(mySeed); // { age: 'age1…', age1pqc: 'age1pqc…' }

Selar e publicar

A selagem passa por um gateway, que constrói e transmite a transação Cardano e guarda o texto cifrado de cada item por você. Aponte o cliente para o gateway que você usa; o SDK não depende de gateway algum.

O publishSealed recebe as chaves públicas brutas dos destinatários, então colete o publicKey de cada endereço decodificado. Todos os destinatários precisam compartilhar um único KEM — mantenha as chaves age1pqc… juntas. items é uma lista, então você pode selar todo um conjunto de arquivos para os mesmos destinatários em um único registro; aqui, ela contém apenas uma carga útil:

import { Label309Client, parseAgeRecipient } from '@cardanowall/sdk-ts';

const client = new Label309Client({
  baseUrl: 'https://your-gateway.example',
  apiKey: process.env.CW_API_KEY,
});

const content = new TextEncoder().encode('the secret payload');
const recipients = ['age1pqc…recipient', me.age1pqc].map((r) => parseAgeRecipient(r).publicKey);

const result = await client.poe.publishSealed({
  items: [{ content }],
  recipients,
  maxUsdMicros: 2_000_000n, // refuse to spend more than $2
  // kem defaults to 'mlkem768x25519' (X-Wing); pass 'x25519' only for age1… keys.
});

console.log(result.response.id, result.uris);

Não há uma chamada quote separada nem uma estimativa própria de recordBytes: o publishSealed mede o registro selado exato, trava um preço, envia cada texto cifrado e o submete — tudo em uma única chamada. maxUsdMicros é o seu teto; a publicação é recusada se o preço cotado o ultrapassar, e o teto é reavaliado contra um preço atualizado se um envio lento sobreviver ao primeiro bloqueio, de modo que um envio grande nunca ultrapassa a sua cotação. Seu seed e o texto claro nunca saem da sua máquina às claras.

Por padrão, a afirmação on-chain de cada item é o seu digest sha2-256. Para vincular um segundo digest ao mesmo registro, calcule o hash do item sob os dois algoritmos: passe hashAlgs: ['sha2-256', 'blake2b-256'] ao publishSealed no SDK, ou repita --hash-alg sha2-256 --hash-alg blake2b-256 no comando seal da CLI. Os dois algoritmos do registro se comportam de forma idêntica em todos os modos de selagem.

Selar com uma frase secreta

Nem todo mundo tem uma chave. Às vezes você quer selar para pessoas que compartilham uma frase secreta, não uma chave — uma senha de equipe, uma frase dita em voz alta numa chamada, um valor que já está entre os secrets da sua CI. Dê ao seal uma frase secreta em vez de --to, e qualquer pessoa que a conheça consegue abrir o registro: sem endereço age, sem troca de chaves.

cardanowall seal \
  --file ./contract.pdf \
  --passphrase-file ./pw.txt \
  --base-url https://your-gateway.example \
  --api-key "$CW_API_KEY"

Um registro é selado para destinatários ou para uma frase secreta, nunca para os dois — passar --passphrase junto com --to/--to-self é recusado. Mantenha a frase fora da linha de comando: --passphrase-file, --passphrase-stdin ou a variável de ambiente CARDANOWALL_PASSPHRASE a leem sem que ela vá parar no histórico do shell. A chave de conteúdo é derivada da frase com Argon2id, então uma frase secreta forte e de alta entropia é tudo o que se interpõe entre o texto cifrado e quem tenta adivinhá-la — este caminho é tão seguro quanto a frase que você escolher.

Para abri-lo, não é preciso nada além da frase. O verificador autônomo descriptografa e reconfere o hash do conteúdo; a inbox grava o texto claro recuperado em um arquivo:

cardanowall verify <tx-hash> --passphrase-file ./pw.txt
cardanowall inbox decrypt <tx-hash> --passphrase-file ./pw.txt --out ./recovered.pdf

A substituição funciona da mesma forma nos dois modos. Uma nova selagem é sempre um registro totalmente novo — a criptografia é aleatorizada a cada vez (uma nova chave de conteúdo e um novo nonce, mais um novo salt Argon2id no caminho com frase secreta), então os bytes nunca se deduplicam. Para marcar uma nova selagem como sucessora de uma anterior —um arquivo corrigido, um conjunto de destinatários rotacionado— aponte-a de volta com --supersedes:

cardanowall seal --file ./contract-v2.pdf --to age1pqc…recipient --supersedes <tx-hash>

Retomar após uma falha

A selagem gera a cada vez uma nova chave de conteúdo, um novo nonce e novo material KEM por slot, então uma nova tentativa ingênua criptografaria de novo, pagaria um segundo envio de texto cifrado e produziria bytes de registro diferentes. Para tarefas de CI e cargas úteis de vários gigabytes, divida o fluxo: o sealPrepare criptografa offline e devolve um artefato portátil que você pode guardar; o submitSealed executa a metade on-line sobre ele.

import { sealPrepare, preparedSealToJson, preparedSealFromJson } from '@cardanowall/sdk-ts';

// Phase 1 — pure and offline: encrypt every item to the recipient set.
const prepared = sealPrepare({ items: [{ content }], recipients });
await save(preparedSealToJson(prepared)); // the portable prepared_seal_json_v1 artifact

// Phase 2 — online: quote, upload each ciphertext, publish. If it throws after
// an upload, the error's `uploads` carry validated receipts — pass them back as
// `uploaded` and the finished uploads are never paid for again.
const submission = await client.poe.submitSealed({
  prepared: preparedSealFromJson(await load()),
  maxUsdMicros: 2_000_000n,
});
console.log(submission.response.id, submission.uris);

O publishSealed acima é apenas sealPrepare + submitSealed em uma única chamada; os SDKs de Python e Rust expõem o mesmo par.

Se a publicação falhar no meio do caminho

A CLI se recupera da mesma forma, sem o código de duas fases. Uma execução seal --to … que morre depois do envio de um texto cifrado grava um <seal-fingerprint>.l309-seal-resume.json livre de segredos (os envios concluídos, e nenhuma chave ou texto claro). Rode de novo com cardanowall seal --resume <seal-fingerprint>.l309-seal-resume.json: ela refaz a cotação, conclui apenas os envios ainda pendentes e publica. Uma execução assinada precisa do seu seed de novo; selagens com frase secreta não guardam nenhum estado de retomada, então rode-as do começo.

Com Python

O cardanowall-sdk é um gêmeo byte a byte — mesmo KEM padrão, mesmo envelope:

import asyncio
import os
from cardanowall import Label309Client, parse_age_recipient

async def main():
    content = b"the secret payload"
    recipients = [parse_age_recipient("age1pqc…recipient").public_key]

    async with Label309Client(
        base_url="https://your-gateway.example",
        api_key=os.environ["CW_API_KEY"],
    ) as client:
        submission = await client.poe.publish_sealed(
            items=[content],           # a list of plaintext items (bytes or str)
            recipients=recipients,
            max_usd_micros=2_000_000,  # refuse to spend more than $2
        )
        print(submission.response["id"], submission.uris)

asyncio.run(main())

Para um fluxo retomável, o seal_prepare(...) (de cardanowall.client) devolve o artefato preparado e o client.poe.submit_sealed(prepared=...) executa a metade on-line — as mesmas duas fases acima.

Com Rust

O crate cardanowall sela por meio do mesmo cliente de gateway. O parse_age_recipient decodifica cada endereço para a chave bruta, e o KEM assume por padrão o híbrido X-Wing:

use cardanowall::client::{
    Label309Client, Label309ClientConfig, PublishSealedInput, SealPrepareItem,
};
use cardanowall::recipient::parse_age_recipient;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Label309Client::new(Label309ClientConfig {
        base_url: Some("https://your-gateway.example".into()),
        api_key: std::env::var("CW_API_KEY").ok(),
    })?;

    let content = b"the secret payload".to_vec();
    let recipients = vec![parse_age_recipient("age1pqc…recipient")?.public_key];

    // One-shot: seal, quote the exact record size, upload, publish. The KEM
    // defaults to mlkem768x25519 (X-Wing); `with_kem` selects x25519.
    let submission = client.poe().publish_sealed(
        &PublishSealedInput::new(vec![SealPrepareItem::new(&content)], recipients)
            .with_max_usd_micros(2_000_000), // refuse to spend more than $2
    )?;

    println!("{}", submission.response.id);
    Ok(())
}

Para um fluxo retomável, o seal_prepare devolve um PreparedSeal que você pode guardar e o client.poe().submit_sealed(&SubmitSealedInput::new(&prepared)) executa a metade on-line. A CLI também sela pela linha de comando, com as mesmas fases nos bastidores: cardanowall seal --file <path> --to <address>.

Assim que o registro for liquidado, cada destinatário o descobre, descriptografa a carga útil com sua chave privada e recalcula o hash do texto claro para fechar o ciclo — essa é a parte do destinatário em Verificar um registro.

Sele também para você mesmo

O publishSealed nunca adiciona você à lista de destinatários sem avisar. Se você não incluir uma de suas próprias chaves, vai publicar um registro que jamais conseguirá ler de volta. Inclua me.age1pqc (ou me.age) entre os destinatários sempre que quiser manter acesso ao que enviou.