Guías

Guías · Parte 4 de 8

Construir una PoE sellada

Una PoE simple prueba que cierto contenido existió. Una PoE sellada prueba lo mismo manteniendo en secreto el propio contenido: cifra los bytes para una o varias claves de destinatario, almacena únicamente el texto cifrado y ancla el registro en cadena. Cualquiera puede ver que el registro existe y verificar su estructura; solo quien posea una clave privada correspondiente puede descifrar la carga útil. Consulte PoE sellada para el formato del sobre y Sellada hasta su reclamación para el modelo de amenaza.

Identificar a los destinatarios

Un destinatario se identifica mediante una cadena al estilo de age. Hay dos tipos, y el prefijo los distingue:

  • age1…: una clave clásica X25519 (32 bytes).
  • age1pqc…: una clave híbrida X-Wing (ML-KEM-768 + X25519, 1216 bytes).

X-Wing (mlkem768x25519) es el KEM predeterminado: se mantiene seguro frente a un futuro adversario cuántico, y toda identidad dispone siempre de una dirección age1pqc….

Un destinatario le entrega su cadena por un canal externo. Para decodificarla y recuperar la clave pública sin procesar que necesita el asistente de sellado, use parseAgeRecipient:

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

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

Si usted mismo dispone de una semilla de 32 bytes, recipientsFromSeed le proporciona sus dos direcciones propias: comparta una para que otros puedan sellarle registros, e incluya su propia clave en la lista de destinatarios para conservar el acceso de lectura a lo que envía:

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

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

Sellar y publicar

El sellado se realiza a través de una pasarela, que construye y difunde la transacción de Cardano y almacena el texto cifrado de cada elemento por usted. Apunte el cliente a la pasarela que utilice; el SDK es independiente de la pasarela.

publishSealed recibe claves públicas de destinatario sin procesar, así que recopile el publicKey de cada dirección decodificada. Todos los destinatarios deben compartir un mismo KEM: agrupe las claves age1pqc…. items es una lista, así que puede sellar todo un conjunto de archivos para los mismos destinatarios en un solo registro; aquí contiene una única 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);

No hay una llamada quote aparte ni una estimación propia de recordBytes: publishSealed mide el registro sellado exacto, fija un precio, sube cada texto cifrado y lo envía, todo en una sola llamada. maxUsdMicros es su límite máximo; la publicación se rechaza si el precio ofertado lo supera, y vuelve a comprobar el límite contra un precio nuevo si una subida lenta sobrevivió al primer bloqueo, de modo que una subida grande nunca puede dejar atrás su oferta. Su semilla y el texto plano nunca salen de su máquina sin cifrar.

De forma predeterminada, la afirmación en cadena de cada elemento es su resumen sha2-256. Para vincular un segundo resumen al mismo registro, calcule el hash del elemento bajo ambos algoritmos a la vez: pase hashAlgs: ['sha2-256', 'blake2b-256'] a publishSealed en el SDK, o repita --hash-alg sha2-256 --hash-alg blake2b-256 en el seal de la CLI. Los dos algoritmos registrados se comportan de forma idéntica en todos los modos de sellado.

Sellar con una frase de contraseña

No todo el mundo tiene una clave. A veces quiere sellar para personas que comparten una frase secreta, no una clave: una contraseña de equipo, una frase leída en voz alta en una llamada, un valor que ya reside en sus secretos de CI. Dé a seal una frase de contraseña en lugar de --to, y cualquiera que la conozca podrá abrir el registro: sin dirección age, sin intercambio de claves.

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

Un registro se sella para destinatarios o para una frase de contraseña, nunca para ambos: pasar --passphrase junto con --to/--to-self se rechaza. Mantenga la frase fuera de la línea de comandos: --passphrase-file, --passphrase-stdin o la variable de entorno CARDANOWALL_PASSPHRASE la leen sin que acabe en el historial del shell. La clave de contenido se deriva de la frase con Argon2id, así que lo único que se interpone entre el texto cifrado y quien quiera adivinarla es una frase de contraseña fuerte y de alta entropía: este camino es tan seguro como la frase que elija.

Abrirlo no necesita nada más que la frase. El verificador autónomo descifra y vuelve a comprobar el hash del contenido; el comando inbox escribe el texto plano recuperado en un archivo:

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

El reemplazo funciona igual en cualquiera de los dos modos. Un nuevo sellado es siempre un registro completamente nuevo: el cifrado se aleatoriza cada vez (una clave de contenido y un nonce nuevos, y una sal Argon2id nueva en el camino de la frase de contraseña), de modo que los bytes nunca se deduplican. Para marcar un sellado nuevo como el sucesor de uno anterior —un archivo corregido, un conjunto de destinatarios rotado— apúntelo de vuelta con --supersedes:

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

Reanudar tras un fallo

El sellado extrae cada vez una clave de contenido, un nonce y material KEM por ranura nuevos, así que un reintento ingenuo volvería a cifrar, pagaría una segunda subida de texto cifrado y produciría bytes de registro distintos. Para trabajos de CI y cargas útiles de varios gigabytes, divida el flujo: sealPrepare cifra sin conexión y devuelve un artefacto portátil que puede conservar; submitSealed ejecuta la mitad en línea sobre él.

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);

publishSealed, más arriba, es simplemente sealPrepare + submitSealed en una sola llamada; los SDK de Python y Rust exponen el mismo par.

Si la publicación falla a medias

La CLI se recupera de la misma manera, sin el código de dos fases. Una ejecución de seal --to … que muere tras subir un texto cifrado escribe un <seal-fingerprint>.l309-seal-resume.json sin secretos (las subidas completadas, y ninguna clave ni texto plano). Vuelva a ejecutar con cardanowall seal --resume <seal-fingerprint>.l309-seal-resume.json: vuelve a cotizar, termina solo las subidas que aún faltan y publica. Una ejecución firmada necesita de nuevo su semilla; los sellados con frase de contraseña no guardan ningún estado de reanudación, así que reejecútelos desde el principio.

Con Python

cardanowall-sdk es un gemelo byte por byte: mismo KEM predeterminado, mismo sobre:

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 un flujo reanudable, seal_prepare(...) (de cardanowall.client) devuelve el artefacto preparado y client.poe.submit_sealed(prepared=...) ejecuta la mitad en línea: las mismas dos fases que arriba.

Con Rust

El crate cardanowall sella a través del mismo cliente de pasarela. parse_age_recipient decodifica cada dirección a la clave sin procesar, y el KEM adopta de forma predeterminada el 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 un flujo reanudable, seal_prepare devuelve un PreparedSeal que puede conservar y client.poe().submit_sealed(&SubmitSealedInput::new(&prepared)) ejecuta la mitad en línea. La CLI también sella desde la línea de comandos, con las mismas fases por debajo: cardanowall seal --file <path> --to <address>.

Una vez que el registro se consolida, cada destinatario lo descubre, descifra la carga útil con su clave privada y recalcula el hash del texto plano para cerrar el ciclo: esa es la mitad del destinatario de Verificar un registro.

Séllese también a usted mismo

publishSealed nunca lo añade a la lista de destinatarios de forma silenciosa. Si no incluye una de sus propias claves, publica un registro que nunca podrá volver a leer. Incluya me.age1pqc (o me.age) entre los destinatarios siempre que quiera conservar el acceso a lo que envió.