duly / web/lib/doc.js · raw
// Invoice documents hold client names and line items, which do not belong on a// public chain. Each is encrypted here with its own key. The server stores only// ciphertext; the chain stores its SHA-256, so any tampering is visible; and the// key travels in the part of the link after #, which browsers never send anywhere.import { b64decode, b64encode, concat, hex, sha256 } from './near.js';const te = new TextEncoder();const td = new TextDecoder();async function seal(keyBytes, plain) {  const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, ['encrypt']);  const iv = crypto.getRandomValues(new Uint8Array(12));  return concat(iv, new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plain)));}async function open(keyBytes, sealed) {  if (sealed.length < 28) throw new Error('Damaged document');  const key = await crypto.subtle.importKey('raw', keyBytes, 'AES-GCM', false, ['decrypt']);  return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv: sealed.slice(0, 12) }, key, sealed.slice(12)));}export const toUrlKey = (b) => b64encode(b).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');export const fromUrlKey = (s) => {  if (!/^[A-Za-z0-9_-]{43}$/.test(s)) throw new Error('Bad link key');  return b64decode(s.replace(/-/g, '+').replace(/_/g, '/') + '=');};/** Encrypt a document. `wrap` lets the merchant reopen it from any device. */export async function sealDocument(doc, docKey) {  const contentKey = crypto.getRandomValues(new Uint8Array(32));  const sealed = await seal(contentKey, te.encode(JSON.stringify(doc)));  return {    ct: b64encode(sealed),    wrap: b64encode(await seal(docKey, contentKey)),    hash: hex(await sha256(sealed)),    contentKey,  };}/** Throws if the ciphertext is not the one the chain committed to. */export async function openDocument(ctB64, contentKey, expectedHash) {  const sealed = b64decode(ctB64);  if (expectedHash && hex(await sha256(sealed)) !== expectedHash) {    throw new Error('This document does not match the invoice on-chain');  }  return JSON.parse(td.decode(await open(contentKey, sealed)));}export async function unwrapContentKey(wrapB64, docKey) {  return open(docKey, b64decode(wrapB64));}