Руководства · Часть 4 из 8
Создайте запечатанное подтверждение существования
Обычное подтверждение существования (PoE) показывает сам факт того, что некое содержимое существовало. Запечатанное подтверждение существования доказывает то же самое, но при этом держит содержимое в тайне: вы шифруете байты для одного или нескольких ключей получателей, храните только шифртекст, а запись закрепляете в блокчейне. Любой может увидеть, что запись существует, и проверить её структуру; расшифровать полезную нагрузку способен только обладатель подходящего закрытого ключа. Формат конверта описан в разделе Sealed PoE, а модель угроз — в разделе Запечатано до получения.
Кому адресовать запись
Получатель задаётся строкой в стиле age. Таких строк два вида, и различает их
префикс:
age1…— классический ключ X25519 (32 байта).age1pqc…— гибридный ключ X-Wing (ML-KEM-768 + X25519, 1216 байт).
X-Wing (mlkem768x25519) — это KEM по умолчанию: он остаётся стойким против
будущего квантового противника, и у каждой идентичности всегда есть адрес
age1pqc….
Получатель передаёт вам свою строку по внешнему каналу. Декодируйте её обратно в
сырой открытый ключ, который нужен помощнику запечатывания, с помощью
parseAgeRecipient:
import { parseAgeRecipient } from '@cardanowall/sdk-ts';
const them = parseAgeRecipient('age1pqc…'); // { kem: 'mlkem768x25519', publicKey: Uint8Array }Если у вас есть собственный 32-байтовый сид, recipientsFromSeed вернёт оба ваших
адреса: одним поделитесь, чтобы другие могли запечатывать записи для вас, а
собственный ключ добавьте в список получателей, чтобы сохранить доступ к тому, что
отправляете:
import { recipientsFromSeed } from '@cardanowall/sdk-ts';
const me = recipientsFromSeed(mySeed); // { age: 'age1…', age1pqc: 'age1pqc…' }Запечатывание и публикация
Запечатывание идёт через шлюз: он собирает и рассылает транзакцию Cardano и хранит шифртекст каждого элемента за вас. Направьте клиент на тот шлюз, которым пользуетесь; SDK не привязан к конкретному шлюзу.
publishSealed принимает сырые открытые ключи получателей, поэтому возьмите поле
publicKey из каждого разобранного адреса. Все получатели должны использовать
один и тот же KEM — держите ключи age1pqc… в одной группе. items — это список,
так что можно запечатать целый набор файлов для одних и тех же получателей в одной
записи; здесь он содержит одну полезную нагрузку:
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);Отдельного вызова quote и собственной оценки recordBytes больше не нужно:
publishSealed измеряет точный размер запечатанной записи, фиксирует цену,
выгружает каждый шифртекст и отправляет запись — всё за один вызов. maxUsdMicros —
ваш потолок: публикация отклоняется, если названная цена его превышает, и потолок
повторно проверяется по свежей цене, если медленная выгрузка пережила первую
фиксацию, так что крупная выгрузка никогда не обгонит свою котировку. Ваш сид и
открытый текст никогда не покидают вашу машину в открытом виде.
По умолчанию ончейн-утверждением каждого элемента служит его цифровой отпечаток sha2-256.
Чтобы привязать к той же записи второй отпечаток, захешируйте элемент сразу под обоими
алгоритмами: передайте hashAlgs: ['sha2-256', 'blake2b-256'] в publishSealed в SDK или
повторите --hash-alg sha2-256 --hash-alg blake2b-256 у команды seal в CLI. Оба алгоритма
из реестра ведут себя одинаково в любом режиме запечатывания.
Запечатывание на парольную фразу
Ключ есть не у всех. Иногда нужно запечатать запись для людей, у которых есть общая
секретная фраза, а не ключ: командный пароль, фраза, продиктованная в разговоре, значение,
уже лежащее в секретах вашей CI. Передайте seal парольную фразу вместо --to — и открыть
запись сможет любой, кто её знает: ни адреса age, ни обмена ключами.
cardanowall seal \
--file ./contract.pdf \
--passphrase-file ./pw.txt \
--base-url https://your-gateway.example \
--api-key "$CW_API_KEY"Запись запечатывается либо на получателей, либо на парольную фразу — никогда на то и
другое сразу: передать --passphrase вместе с --to/--to-self не разрешается. Держите
фразу вне командной строки: --passphrase-file, --passphrase-stdin и переменная окружения
CARDANOWALL_PASSPHRASE считывают её так, что она не оседает в истории оболочки. Ключ
содержимого растягивается из фразы с помощью Argon2id, поэтому между шифртекстом и тем, кто
пытается его подобрать, стоит только надёжная парольная фраза с высокой энтропией — этот путь
ровно настолько безопасен, насколько надёжна выбранная вами фраза.
Чтобы открыть запись, не нужно ничего, кроме самой фразы. Автономный верификатор расшифровывает её и заново сверяет хеш содержимого; а inbox записывает восстановленный открытый текст в файл:
cardanowall verify <tx-hash> --passphrase-file ./pw.txt
cardanowall inbox decrypt <tx-hash> --passphrase-file ./pw.txt --out ./recovered.pdfЗамещение работает одинаково в обоих режимах. Повторное запечатывание — это всегда совершенно
новая запись: шифрование каждый раз рандомизируется (новый ключ содержимого и новый nonce, а
на пути с парольной фразой — ещё и новая соль Argon2id), так что байты никогда не
дедуплицируются. Чтобы пометить новое запечатывание как преемника предыдущего — исправленный
файл, обновлённый набор получателей — укажите на него через --supersedes:
cardanowall seal --file ./contract-v2.pdf --to age1pqc…recipient --supersedes <tx-hash>Возобновление после сбоя
При каждом запечатывании берутся новый ключ содержимого, новый nonce и новый
KEM-материал для каждого слота, поэтому наивный повтор заново зашифрует данные,
оплатит повторную выгрузку шифртекста и породит другие байты записи. Для задач CI и
полезных нагрузок в несколько гигабайт разделите поток: sealPrepare шифрует офлайн
и возвращает переносимый артефакт, который можно сохранить; submitSealed выполняет
над ним онлайн-половину.
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 выше — это просто sealPrepare + submitSealed в одном вызове; SDK
для Python и Rust предоставляют ту же пару.
Если публикация прервётся на полпути
CLI восстанавливается точно так же, без двухфазного кода. Запуск seal --to …, который обрывается
после выгрузки шифртекста, пишет файл <seal-fingerprint>.l309-seal-resume.json без секретов
(завершённые выгрузки — и ни ключей, ни открытого текста). Перезапустите командой cardanowall seal --resume <seal-fingerprint>.l309-seal-resume.json: она заново запрашивает цену, доводит до конца
только оставшиеся выгрузки и публикует. Подписанному запуску снова нужен его сид; запечатывания на
парольную фразу не хранят состояния возобновления, поэтому их придётся запускать с начала.
На Python
cardanowall-sdk — побайтный близнец: тот же KEM по умолчанию, тот же конверт:
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())Для возобновляемого потока seal_prepare(...) (из cardanowall.client) возвращает
подготовленный артефакт, а client.poe.submit_sealed(prepared=...) выполняет
онлайн-половину — те же две фазы, что и выше.
На Rust
Крейт cardanowall запечатывает через тот же клиент шлюза. parse_age_recipient
декодирует каждый адрес в сырой ключ, а KEM по умолчанию использует гибрид 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(())
}Для возобновляемого потока seal_prepare возвращает PreparedSeal, который можно
сохранить, а client.poe().submit_sealed(&SubmitSealedInput::new(&prepared))
выполняет онлайн-половину. CLI тоже умеет запечатывать из командной строки, с теми
же фазами под капотом: cardanowall seal --file <path> --to <address>.
Как только запись зафиксируется в блокчейне, каждый получатель обнаруживает её, расшифровывает полезную нагрузку своим закрытым ключом и заново вычисляет хеш открытого текста, замыкая цикл, — это и есть та половина из раздела Проверка записи, что относится к получателю.
Запечатайте и для себя
publishSealed никогда не добавляет вас в список получателей молча. Если вы не включите туда один
из собственных ключей, то опубликуете запись, которую сами уже никогда не прочитаете. Добавляйте
me.age1pqc (или me.age) в список получателей всякий раз, когда хотите сохранить доступ к тому,
что отправили.