<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Samir Chatwiti</title>
    <description>The latest articles on DEV Community by Samir Chatwiti (@chatwiti).</description>
    <link>https://dev.to/chatwiti</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3870633%2F105e4c05-e9b1-40ce-a406-ffb222d77c21.jpeg</url>
      <title>DEV Community: Samir Chatwiti</title>
      <link>https://dev.to/chatwiti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chatwiti"/>
    <language>en</language>
    <item>
      <title>Costa Rica v4.4: what's inside the 50-digit clave (and why your comprobante gets rejected)</title>
      <dc:creator>Samir Chatwiti</dc:creator>
      <pubDate>Fri, 31 Jul 2026 11:53:22 +0000</pubDate>
      <link>https://dev.to/chatwiti/costa-rica-v44-whats-inside-the-50-digit-clave-and-why-your-comprobante-gets-rejected-35pd</link>
      <guid>https://dev.to/chatwiti/costa-rica-v44-whats-inside-the-50-digit-clave-and-why-your-comprobante-gets-rejected-35pd</guid>
      <description>&lt;p&gt;Costa Rica's electronic invoicing moved to version 4.4 with Resolución MH-DGT-RES-0027-2024 — 146 changes, mandatory for all taxpayers since 1 September 2025, a brand-new document type (the Recibo Electrónico de Pago), and a new mandatory node (ProveedorSistemas, identifying the software that produced the document). If you maintain an ERP or billing integration for Costa Rica, you migrated recently, and you probably still get rejections you don't immediately understand.&lt;/p&gt;

&lt;p&gt;Most of them trace back to one field: the clave numérica.&lt;/p&gt;

&lt;p&gt;The 50-digit clave, decomposed&lt;br&gt;
The clave isn't a random ID. It's a structured composite:&lt;/p&gt;

&lt;p&gt;positions 1–3: country code (506)&lt;br&gt;
positions 4–9: emission date as DDMMAA&lt;br&gt;
positions 10–21: the issuer's cédula&lt;br&gt;
positions 22–41: the consecutivo — which must equal the document's , and whose own digits encode the branch, terminal and document type (which must match the XML root element!)&lt;br&gt;
position 42: situación (normal / contingencia / sin internet)&lt;br&gt;
positions 43–50: security code&lt;br&gt;
So the clave cross-references the emission date, the issuer, the consecutivo and the document type. Generate any of those inconsistently — a consecutivo counter that drifted, a clave built before a date rollover, a credit note wrapped in an invoice root — and the document is internally contradictory even though it validates against the XSD.&lt;/p&gt;

&lt;p&gt;A real rejection, explained&lt;br&gt;
Here's actual validator output for a signed v4.4 invoice where the consecutivo, a CABYS code and the total were altered (excerpt):&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "valid": false,&lt;br&gt;
  "documentType": "FacturaElectronica",&lt;br&gt;
  "version": "4.4",&lt;br&gt;
  "signatureValid": false,&lt;br&gt;
  "errorCount": 5,&lt;br&gt;
  "findings": [&lt;br&gt;
    { "level": "error", "ruleId": "CR-CLAVE-CONSECUTIVO",&lt;br&gt;
      "text": "Clave consecutivo segment (positions 22-41) '00100001010000000001' does not match the document's  '00100001010000000009'.",&lt;br&gt;
      "location": "Clave / NumeroConsecutivo" },&lt;br&gt;
    { "level": "error", "ruleId": "CR-CODE-CABYS",&lt;br&gt;
      "text": "CodigoCABYS '85110' must be exactly 13 digits (Anexos y Estructuras v4.4; national CABYS product/service catalogue code).",&lt;br&gt;
      "location": "CodigoCABYS" },&lt;br&gt;
    { "level": "error", "ruleId": "CR-MATH-COMPROBANTE",&lt;br&gt;
      "text": "TotalComprobante (99999.00000) != TotalVentaNeta + TotalImpuesto + TotalOtrosCargos - TotalIVADevuelto (11300.00000).",&lt;br&gt;
      "location": "ResumenFactura/TotalComprobante" }&lt;br&gt;
  ]&lt;br&gt;
}&lt;br&gt;
Plus a CR-SIG-DOC-DIGEST mismatch — the XAdES-EPES signature caught the tampering too, because the recomputed document digest no longer matches what was signed.&lt;/p&gt;

&lt;p&gt;Note what the official clave lookup can't tell you: VerificaTuFactura and the ATV consultation only answer "has Hacienda received this clave?" — nothing about a document you haven't sent yet, and nothing about why it's malformed.&lt;/p&gt;

&lt;p&gt;Pre-flight in one call&lt;br&gt;
The output comes from the Costa Rica e-Invoice Validator v4.4 (Hacienda) — keyless REST API, free tier available:&lt;/p&gt;

&lt;p&gt;curl --request POST \&lt;br&gt;
  --url &lt;a href="https://costa-rica-e-invoice-validator-v4-4-hacienda.p.rapidapi.com/validate" rel="noopener noreferrer"&gt;https://costa-rica-e-invoice-validator-v4-4-hacienda.p.rapidapi.com/validate&lt;/a&gt; \&lt;br&gt;
  --header 'Content-Type: application/xml' \&lt;br&gt;
  --header 'x-rapidapi-host: costa-rica-e-invoice-validator-v4-4-hacienda.p.rapidapi.com' \&lt;br&gt;
  --header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \&lt;br&gt;
  --data-binary @comprobante.xml&lt;br&gt;
It auto-detects all 8 v4.4 document types (including the new REP), validates the official Hacienda XSDs, decomposes and cross-checks the clave, enforces the v4.4 ProveedorSistemas requirement, checks CABYS and the other code lists, reconciles totals/IVA, and verifies the XAdES-EPES signature end to end (document digest, SignedProperties digest, RSA-SHA256, EPES policy presence). Keyless and stateless — no ATV login, no certificate, in-memory validation only.&lt;/p&gt;

&lt;p&gt;The TRIBU-CR angle&lt;br&gt;
Costa Rica is migrating its platform to TRIBU-CR, so the schema/annex set will keep moving — v4.4 was itself a 146-change step. If you hand-rolled your validation, every sub-version is a rebuild. A validator whose XSDs and code lists are swappable data turns that churn into someone else's problem.&lt;/p&gt;

&lt;p&gt;Scope note: trust-chain verification to the SINPE/MICITT roots, live CABYS catalogue existence and the clave's acceptance require Hacienda's online services — valid: true means "structurally ready", not "accepted".&lt;/p&gt;

&lt;p&gt;I maintain validators like this for five mandated e-invoicing regimes (Malaysia, Dominican Republic, Nigeria, Bolivia, Costa Rica) — overview with links: github.com/SamirChatwiti/e-invoice-validators.&lt;/p&gt;

</description>
      <category>costarica</category>
      <category>einvoicing</category>
      <category>api</category>
      <category>tax</category>
    </item>
    <item>
      <title>Bolivia's CUF: the invoice control code you can't fake (and how to verify it in one call)</title>
      <dc:creator>Samir Chatwiti</dc:creator>
      <pubDate>Fri, 31 Jul 2026 11:40:43 +0000</pubDate>
      <link>https://dev.to/chatwiti/bolivias-cuf-the-invoice-control-code-you-cant-fake-and-how-to-verify-it-in-one-call-43co</link>
      <guid>https://dev.to/chatwiti/bolivias-cuf-the-invoice-control-code-you-cant-fake-and-how-to-verify-it-in-one-call-43co</guid>
      <description>&lt;p&gt;Bolivia's Sistema de Facturación Electrónica en Línea (SFV/SIAT) reached full coverage on 1 October 2025 — the last taxpayer groups are now required to emit digitally signed XML invoices to the SIN. Every one of those invoices carries a CUF (Código Único de Facturación): a long hex string that most integrations treat as an opaque token.&lt;/p&gt;

&lt;p&gt;It isn't opaque. The CUF is deterministically derived from the invoice header — NIT of the issuer, emission timestamp, branch (sucursal), invoice number, point of sale, sector, modality — run through the SIN's Módulo 11 self-check digit algorithm and encoded in Base16, with the current CUFD control code appended.&lt;/p&gt;

&lt;p&gt;Which means two things:&lt;/p&gt;

&lt;p&gt;If any header field and the CUF disagree, the invoice is inconsistent — even if the XML is schema-valid.&lt;br&gt;
You can verify a CUF offline, by rebuilding it. Most validators just regex it. Rebuilding it is the real check.&lt;br&gt;
Rebuilding a real CUF&lt;br&gt;
Run the SIN's own published example invoice through a validator that reimplements the algorithm:&lt;/p&gt;

&lt;p&gt;BO-CUF-VERIFY: CUF VERIFIES: its Base16 prefix reproduces from the header via the&lt;br&gt;
SIN Modulo 11 self-check digit (modalidad=1, tipoEmision=1, tipoFactura=1);&lt;br&gt;
recovered CUFD control code = '67A75AC82F24C74'.&lt;br&gt;
The official example reproduces bit for bit — and the CUFD control code pops out of the decomposition.&lt;/p&gt;

&lt;p&gt;Now tamper with one digit of that CUF and change montoTotal. Real output:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "valid": false,&lt;br&gt;
  "documentType": "facturaElectronicaCompraVenta",&lt;br&gt;
  "signatureValid": false,&lt;br&gt;
  "errorCount": 3,&lt;br&gt;
  "warningCount": 3,&lt;br&gt;
  "findings": [&lt;br&gt;
    { "level": "warning", "ruleId": "BO-CUF-VERIFY",&lt;br&gt;
      "text": "CUF does NOT reconcile: no standard modalidad/tipoEmision/tipoFactura combination reproduces its Base16 prefix from the header (nitEmisor=1003579028, fechaEmision=20211007090124178, sucursal=0, numeroFactura=1, pos=0, sector=1) using the SIN Modulo 11 algorithm. The CUF or a header field may be inconsistent.",&lt;br&gt;
      "location": "cabecera/cuf" },&lt;br&gt;
    { "level": "error", "ruleId": "BO-MATH-TOTAL",&lt;br&gt;
      "text": "montoTotal (500) != sum(subTotal) - descuentoAdicional (99).",&lt;br&gt;
      "location": "cabecera/montoTotal" }&lt;br&gt;
  ]&lt;br&gt;
}&lt;br&gt;
The validator tells you which header fields it tried to reconcile against — that's the difference between "your CUF is wrong somewhere" and an actionable finding.&lt;/p&gt;

&lt;p&gt;The full pre-flight in one call&lt;br&gt;
The output above comes from the Bolivia SIAT e-Invoice Validator (SFV) — keyless REST API, free tier available:&lt;/p&gt;

&lt;p&gt;curl --request POST \&lt;br&gt;
  --url &lt;a href="https://bolivia-siat-e-invoice-validator-sfv.p.rapidapi.com/validate" rel="noopener noreferrer"&gt;https://bolivia-siat-e-invoice-validator-sfv.p.rapidapi.com/validate&lt;/a&gt; \&lt;br&gt;
  --header 'Content-Type: application/xml' \&lt;br&gt;
  --header 'x-rapidapi-host: bolivia-siat-e-invoice-validator-sfv.p.rapidapi.com' \&lt;br&gt;
  --header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \&lt;br&gt;
  --data-binary @factura.xml&lt;br&gt;
Layers checked: official SIN XSDs per document type (including the SignatureSchema) → SIN parametric code ranges (documento de identidad, método de pago, moneda, unidad de medida) → CUF/CUFD reconstruction → totals math per the SIN's Validaciones por Documento Sector → enveloped XML-DSig (document digest + RSA-SHA256 against the embedded certificate).&lt;/p&gt;

&lt;p&gt;Keyless and stateless: no SIN account, no private key, nothing stored — the invoice is validated in-memory and discarded. Safe to run in CI on every generated document.&lt;/p&gt;

&lt;p&gt;Practical takeaways&lt;br&gt;
Don't regex the CUF — rebuild it. A format check passes tampered and stale CUFs alike; the Módulo 11 reconstruction doesn't.&lt;br&gt;
Validate before the SIN sees it. Assert on ruleIds (BO-STRUCT-&lt;em&gt;, BO-CODE-&lt;/em&gt;, BO-CUF-&lt;em&gt;, BO-MATH-&lt;/em&gt;, BO-SIG-*) in your tests.&lt;br&gt;
Scope honestly: whether a NIT is registered or a CUFD is currently live requires the SIN's online services; the trust chain to Bolivia's national CA (ADSIB) isn't checkable offline. valid: true means "consistent and ready", not "accepted".&lt;br&gt;
I maintain validators like this for five mandated e-invoicing regimes (Malaysia, Dominican Republic, Nigeria, Bolivia, Costa Rica) — overview with links: github.com/SamirChatwiti/e-invoice-validators.&lt;/p&gt;

</description>
      <category>bolivia</category>
      <category>einvoicing</category>
      <category>api</category>
      <category>tax</category>
    </item>
    <item>
      <title>Nigeria's e-invoicing mandate: the validation layer generic Peppol tools won't give you</title>
      <dc:creator>Samir Chatwiti</dc:creator>
      <pubDate>Fri, 31 Jul 2026 11:24:32 +0000</pubDate>
      <link>https://dev.to/chatwiti/nigerias-e-invoicing-mandate-the-validation-layer-generic-peppol-tools-wont-give-you-1kib</link>
      <guid>https://dev.to/chatwiti/nigerias-e-invoicing-mandate-the-validation-layer-generic-peppol-tools-wont-give-you-1kib</guid>
      <description>&lt;p&gt;Nigeria's e-invoicing mandate is live. Under the Nigeria Tax Administration Act 2025, the NRS (formerly FIRS) runs a pre-clearance system — the Merchant-Buyer Solution (FIRSMBS) — where every invoice is submitted, validated, and only becomes a legal document once it comes back with an IRN (Invoice Reference Number), a CSID cryptographic stamp and a QR code. Large taxpayers (₦5B+ turnover) have been in since 1 November 2025; the phased rollout continues. Non-compliance costs about ₦1,000,000 up front plus ₦10,000 per day.&lt;/p&gt;

&lt;p&gt;The format is Peppol BIS Billing 3.0-based UBL 2.1, so a common integrator reflex is: "I'll run it through a free Peppol validator, green check, ship it."&lt;/p&gt;

&lt;p&gt;That reflex produces invoices the NRS rejects.&lt;/p&gt;

&lt;p&gt;What the generic tools don't check&lt;br&gt;
Free Peppol/EN 16931 validators stop at the European base layer. The Nigerian profile adds requirements they know nothing about:&lt;/p&gt;

&lt;p&gt;Nigerian TIN on both parties — seller and buyer, as PartyTaxScheme/CompanyID or PartyIdentification/ID schemeID='TIN', in the FIRS format.&lt;br&gt;
Per-line VAT — Nigeria expects VAT broken out at line level, not only in the document totals.&lt;br&gt;
NGN expectations and local unit codes.&lt;br&gt;
The IRN / CSID / QR clearance blocks — their presence and structure on cleared documents.&lt;br&gt;
Every one of those is invisible to a vanilla BIS check, and every one of them is a rejection at the NRS.&lt;/p&gt;

&lt;p&gt;A real failing invoice&lt;br&gt;
Here's the actual validator output for a UBL invoice missing the buyer's TIN and the IRN block:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "valid": false,&lt;br&gt;
  "documentType": "380",&lt;br&gt;
  "hasIRN": false,&lt;br&gt;
  "hasCSID": true,&lt;br&gt;
  "hasQR": true,&lt;br&gt;
  "errorCount": 2,&lt;br&gt;
  "findings": [&lt;br&gt;
    { "level": "error", "ruleId": "NG-RULE-BUYER-TIN",&lt;br&gt;
      "text": "Nigerian TIN for the customer (buyer) is missing. NRS requires a Tax Identification Number for both parties (as PartyTaxScheme/CompanyID or PartyIdentification/ID schemeID='TIN').",&lt;br&gt;
      "location": "cac:AccountingCustomerParty" },&lt;br&gt;
    { "level": "error", "ruleId": "NG-IRN-ABSENT",&lt;br&gt;
      "text": "No Invoice Reference Number (IRN) found. A cleared NRS invoice carries an IRN (cbc:UUID or cac:AdditionalDocumentReference[cbc:ID='IRN']). Structure check only.",&lt;br&gt;
      "location": "cbc:UUID / cac:AdditionalDocumentReference" }&lt;br&gt;
  ]&lt;br&gt;
}&lt;br&gt;
Note the booleans (hasIRN, hasCSID, hasQR): handy for a quick gate — "is this document post-clearance complete?" — without parsing the findings.&lt;/p&gt;

&lt;p&gt;One call, the whole stack&lt;br&gt;
The output comes from the Nigeria NRS/FIRS E-Invoice Validator (keyless REST API, free tier available). It layers the checks: official OASIS UBL 2.1 XSD → EN 16931 / Peppol BIS 3.0 base fields and ISO code lists → the Nigeria-specific rules (NG-RULE-&lt;em&gt;) → IRN/CSID/QR structure (NG-IRN-&lt;/em&gt;, NG-CSID-&lt;em&gt;, NG-QR-&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;curl --request POST \&lt;br&gt;
  --url &lt;a href="https://nigeria-nrs-firs-e-invoice-validator.p.rapidapi.com/validate" rel="noopener noreferrer"&gt;https://nigeria-nrs-firs-e-invoice-validator.p.rapidapi.com/validate&lt;/a&gt; \&lt;br&gt;
  --header 'Content-Type: application/xml' \&lt;br&gt;
  --header 'x-rapidapi-host: nigeria-nrs-firs-e-invoice-validator.p.rapidapi.com' \&lt;br&gt;
  --header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \&lt;br&gt;
  --data-binary @invoice.xml&lt;br&gt;
XML or JSON body, JSON verdict either way, always HTTP 200 with "valid": true|false. Keyless and stateless — no NRS login, no certificate, the invoice is validated in-memory and discarded — so it runs happily in CI or inside an Access Point Provider's onboarding flow.&lt;/p&gt;

&lt;p&gt;Honest limits&lt;br&gt;
The CSID is signed by the NRS itself; verifying its cryptographic authenticity would require the NRS signing certificate, which nobody outside the authority holds. So IRN/CSID/QR checks are structural — present and well-formed. valid: true is a pre-clearance readiness signal, not proof of clearance.&lt;/p&gt;

&lt;p&gt;That's exactly what you want in CI though: catch the TIN you forgot, the per-line VAT you flattened, the IRN block your serializer dropped — before the NRS does, and before the daily penalties start.&lt;/p&gt;

&lt;p&gt;I maintain validators like this for five mandated e-invoicing regimes (Malaysia, Dominican Republic, Nigeria, Bolivia, Costa Rica) — overview with links: github.com/SamirChatwiti/e-invoice-validators.&lt;/p&gt;

</description>
      <category>nigeria</category>
      <category>einvoicing</category>
      <category>api</category>
      <category>tax</category>
    </item>
    <item>
      <title>Dominican Republic e-CF: how one tampered byte invalidates your invoice (and how to catch it)</title>
      <dc:creator>Samir Chatwiti</dc:creator>
      <pubDate>Fri, 31 Jul 2026 11:12:16 +0000</pubDate>
      <link>https://dev.to/chatwiti/dominican-republic-e-cf-how-one-tampered-byte-invalidates-your-invoice-and-how-to-catch-it-59fl</link>
      <guid>https://dev.to/chatwiti/dominican-republic-e-cf-how-one-tampered-byte-invalidates-your-invoice-and-how-to-catch-it-59fl</guid>
      <description>&lt;p&gt;Under Ley 32-23, the Dominican Republic requires businesses to issue Comprobantes Fiscales Electrónicos (e-CF): digitally signed XML documents transmitted to the DGII in real time. The rollout is phased by taxpayer size, and the small/medium wave is underway — which means a lot of teams are wiring e-CF generation into ERPs and billing systems right now.&lt;/p&gt;

&lt;p&gt;Here's the thing about signed tax documents that surprises integrators: the signature freezes every byte. Touch the XML after signing — reformat it, "fix" an amount, let a proxy re-encode it — and the document is cryptographically dead, even if it still looks fine to the eye.&lt;/p&gt;

&lt;p&gt;An experiment: falsify a real e-CF&lt;br&gt;
Take a genuinely signed e-CF (type 31, Factura de Crédito Fiscal Electrónica), then do two small things a buggy pipeline could do:&lt;/p&gt;

&lt;p&gt;change the e-NCF prefix from E31… to E32…&lt;br&gt;
bump MontoTotal to a different number&lt;br&gt;
Then run it through a validator that recomputes everything. Real output:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "valid": false,&lt;br&gt;
  "documentType": "31",&lt;br&gt;
  "signatureValid": false,&lt;br&gt;
  "errorCount": 2,&lt;br&gt;
  "warningCount": 2,&lt;br&gt;
  "findings": [&lt;br&gt;
    { "level": "error", "ruleId": "DO-NCF-TYPE",&lt;br&gt;
      "text": "e-NCF type digits '32' do not match TipoeCF '31'.",&lt;br&gt;
      "location": "IdDoc/eNCF" },&lt;br&gt;
    { "level": "error", "ruleId": "DO-MATH-TOTAL",&lt;br&gt;
      "text": "MontoTotal (999999.99) != MontoGravadoTotal + MontoExento + TotalITBIS + MontoImpuestoAdicional (637.2).",&lt;br&gt;
      "location": "Totales/MontoTotal" },&lt;br&gt;
    { "level": "warning", "ruleId": "DO-SIG-DOC-DIGEST",&lt;br&gt;
      "text": "Document digest MISMATCH: signed DigestValue cBg59omGSW41xtQT7Q4nL54/Ueq6UObAEfP+/gf1aYg= does not match the recomputed SHA-256. The document content differs from what was signed, or a different canonicalization was used.",&lt;br&gt;
      "location": "ds:Reference[&lt;a class="mentioned-user" href="https://dev.to/uri"&gt;@uri&lt;/a&gt;='']" }&lt;br&gt;
  ]&lt;br&gt;
}&lt;br&gt;
Three independent layers caught it: the e-NCF consistency rule, the ITBIS/totals math, and the signature digest — the recomputed SHA-256 of the document no longer matches what was signed.&lt;/p&gt;

&lt;p&gt;That last one is the check most homemade validators skip, because it requires re-canonicalizing the XML exactly as the signer did (enveloped transform, inclusive C14N) before hashing. Skip it, and your pipeline will happily emit documents the DGII will reject.&lt;/p&gt;

&lt;p&gt;Running the check yourself&lt;br&gt;
The output above comes from the Dominican Republic e-CF Validator (DGII) — a keyless REST API (free tier available). One call:&lt;/p&gt;

&lt;p&gt;curl --request POST \&lt;br&gt;
  --url &lt;a href="https://dominican-republic-e-cf-validator-dgii.p.rapidapi.com/validate" rel="noopener noreferrer"&gt;https://dominican-republic-e-cf-validator-dgii.p.rapidapi.com/validate&lt;/a&gt; \&lt;br&gt;
  --header 'Content-Type: application/xml' \&lt;br&gt;
  --header 'x-rapidapi-host: dominican-republic-e-cf-validator-dgii.p.rapidapi.com' \&lt;br&gt;
  --header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \&lt;br&gt;
  --data-binary @ecf.xml&lt;br&gt;
It checks the e-CF v1.0 structure (XSD), DGII code lists, the e-NCF number and its consistency with the document type, ITBIS/totals reconciliation, and the full enveloped signature — document digest and RSA-SHA256 SignatureValue against the embedded certificate. On an untampered, genuinely signed e-CF it returns "valid": true, "signatureValid": true.&lt;/p&gt;

&lt;p&gt;Keyless and stateless: no DGII account, no certificate upload, the document is validated in-memory and discarded. That makes it safe to run from CI on every generated document, not just in production.&lt;/p&gt;

&lt;p&gt;Practical takeaways&lt;br&gt;
Sign last. Any serialization step after signing (pretty-printing, encoding changes, namespace "cleanup") kills the digest.&lt;br&gt;
Validate at generation time — assert on ruleIds (DO-STRUCT-&lt;em&gt;, DO-NCF-&lt;/em&gt;, DO-MATH-&lt;em&gt;, DO-SIG-&lt;/em&gt;) in your test suite.&lt;br&gt;
Don't trust "it parses" as "it's valid". XML well-formedness says nothing about e-NCF consistency, ITBIS math, or the signature.&lt;br&gt;
Scope note: a keyless pre-flight can't verify the trust chain to a DGII/INDOTEL-recognized CA or perform the real-time submission — valid: true means "ready to submit", not "accepted by the DGII".&lt;/p&gt;

&lt;p&gt;I maintain validators like this for five mandated e-invoicing regimes (Malaysia, Dominican Republic, Nigeria, Bolivia, Costa Rica) — overview with links: github.com/SamirChatwiti/e-invoice-validators.&lt;/p&gt;

</description>
      <category>einvoicing</category>
      <category>api</category>
      <category>security</category>
      <category>tax</category>
    </item>
    <item>
      <title>Why MyInvois rejects your e-invoice — and how to catch it before LHDN does</title>
      <dc:creator>Samir Chatwiti</dc:creator>
      <pubDate>Fri, 31 Jul 2026 11:00:22 +0000</pubDate>
      <link>https://dev.to/chatwiti/why-myinvois-rejects-your-e-invoice-and-how-to-catch-it-before-lhdn-does-n3m</link>
      <guid>https://dev.to/chatwiti/why-myinvois-rejects-your-e-invoice-and-how-to-catch-it-before-lhdn-does-n3m</guid>
      <description>&lt;p&gt;Malaysia's MyInvois mandate is now real for most businesses — Phase 4 pulled companies with RM1-5M turnover into the system in January 2026. If you build or integrate the software that emits those e-invoices (an ERP, a POS, a SaaS billing module), you have discovered the painful part: LHDN rejects documents, and it rejects them after you submit them.&lt;/p&gt;

&lt;p&gt;This post walks through the most common rejection causes I've seen, and a way to catch all of them in CI before anything reaches LHDN.&lt;/p&gt;

&lt;p&gt;The usual suspects&lt;br&gt;
A MyInvois document (UBL 2.1, XML or JSON) fails for a handful of recurring reasons:&lt;/p&gt;

&lt;p&gt;Structure — the document doesn't validate against the official OASIS UBL 2.1 XSD: wrong element order, missing wrappers, bad namespaces.&lt;br&gt;
Missing mandatory fields — invoice type code, issue date/time, currency, supplier or buyer TIN, MSIC code, line items, TaxTotal, LegalMonetaryTotal.&lt;br&gt;
Code-list violations — a currency that isn't ISO-4217, an MSIC code that doesn't exist, a state code or unit code outside the LHDN tables.&lt;br&gt;
Totals that don't add up — TaxInclusiveAmount ≠ TaxExclusiveAmount + tax, line extensions that don't sum.&lt;br&gt;
A broken XAdES signature — the enveloped signature's document digest doesn't match (the document was touched after signing), the SignedProperties digest is wrong, or the signing certificate is expired / lacks the Document Signing EKU.&lt;br&gt;
The first four are boring but frequent. The fifth is the sneaky one: serialize your XML differently after signing — pretty-print it, re-encode it, let a middleware "fix" a namespace — and the digest no longer matches.&lt;/p&gt;

&lt;p&gt;Pre-flight, not post-mortem&lt;br&gt;
The pattern that works: validate every document at the moment it's generated, not when LHDN bounces it. Keyless validation (no certificate, no MyInvois account, no token) means you can run it anywhere — CI pipelines, a pre-submit hook, a partner-onboarding gate.&lt;/p&gt;

&lt;p&gt;Here's the whole check as one HTTP call, using the Malaysia MyInvois E-Invoice Validator (free tier available):&lt;/p&gt;

&lt;p&gt;curl --request POST \&lt;br&gt;
  --url &lt;a href="https://malaysia-myinvois-e-invoice-validator.p.rapidapi.com/validate" rel="noopener noreferrer"&gt;https://malaysia-myinvois-e-invoice-validator.p.rapidapi.com/validate&lt;/a&gt; \&lt;br&gt;
  --header 'Content-Type: application/xml' \&lt;br&gt;
  --header 'x-rapidapi-host: malaysia-myinvois-e-invoice-validator.p.rapidapi.com' \&lt;br&gt;
  --header 'x-rapidapi-key: YOUR_RAPIDAPI_KEY' \&lt;br&gt;
  --data-binary @invoice.xml&lt;br&gt;
You always get HTTP 200 with a JSON verdict. Here is a real response for an invoice with a missing supplier TIN and a bogus currency:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
  "valid": false,&lt;br&gt;
  "documentType": "01",&lt;br&gt;
  "errorCount": 3,&lt;br&gt;
  "warningCount": 5,&lt;br&gt;
  "findings": [&lt;br&gt;
    { "level": "error", "ruleId": "MY-CORE-SUPPLIER-TIN",&lt;br&gt;
      "text": "Supplier TIN (PartyIdentification schemeID='TIN') is missing.",&lt;br&gt;
      "location": "cac:AccountingSupplierParty" },&lt;br&gt;
    { "level": "error", "ruleId": "MY-CODE-CURRENCY",&lt;br&gt;
      "text": "DocumentCurrencyCode 'XYZ' is not an ISO-4217 code.",&lt;br&gt;
      "location": "cbc:DocumentCurrencyCode" }&lt;br&gt;
  ]&lt;br&gt;
}&lt;br&gt;
Every finding has a stable ruleId (MY-STRUCT-&lt;em&gt;, MY-CORE-&lt;/em&gt;, MY-CODE-&lt;em&gt;, MY-MATH-&lt;/em&gt;, MY-SIG-*), so you can assert on specific rules in tests, or map them to user-facing messages in your product.&lt;/p&gt;

&lt;p&gt;The signature layer goes deep for a keyless check: document digest recomputation (with the exact canonicalization MyInvois expects), SignedProperties digest, RSA-SHA256 SignatureValue against the embedded X.509, certificate validity window, Document Signing EKU, and the XAdES SigningCertificate binding.&lt;/p&gt;

&lt;p&gt;What a pre-flight can't do&lt;br&gt;
Honesty matters in compliance tooling. A keyless validator cannot: verify the trust chain to the LHDNM / Pos Digicert CA, confirm a TIN actually exists, or perform the submission itself (the unique ID and QR come back from LHDN). valid: true means "structurally ready to submit", not "accepted".&lt;/p&gt;

&lt;p&gt;Wrap-up&lt;br&gt;
Validate at generation time, not at submission time.&lt;br&gt;
Assert on ruleIds in CI so regressions in your invoice generator are caught by the build.&lt;br&gt;
Treat the XAdES signature as part of your test surface — most "mystery" rejections are digest mismatches introduced after signing.&lt;br&gt;
I maintain validators like this for five mandated e-invoicing regimes (Malaysia, Dominican Republic, Nigeria, Bolivia, Costa Rica) — overview with links here: github.com/SamirChatwiti/e-invoice-validators.&lt;/p&gt;

</description>
      <category>malaysia</category>
      <category>einvoicing</category>
      <category>api</category>
      <category>tax</category>
    </item>
    <item>
      <title>Why I’m finally open-sourcing my architectural work after 20 years building private enterprise systems</title>
      <dc:creator>Samir Chatwiti</dc:creator>
      <pubDate>Fri, 10 Apr 2026 23:17:01 +0000</pubDate>
      <link>https://dev.to/chatwiti/why-im-finally-open-sourcing-my-architectural-work-after-20-years-building-private-enterprise-3ibn</link>
      <guid>https://dev.to/chatwiti/why-im-finally-open-sourcing-my-architectural-work-after-20-years-building-private-enterprise-3ibn</guid>
      <description>&lt;p&gt;Hey everyone! 👋&lt;/p&gt;

&lt;p&gt;I'm Samir, a Principal Software Architect and Tech Lead based in Casablanca, Morocco 🇲🇦.&lt;/p&gt;

&lt;p&gt;With over 20 years in software engineering (including 7 years as a Director of Operations), I've spent most of my career building private, enterprise-grade systems and internal client platforms. Recently, I've shifted into independent consulting and decided to start open-sourcing my architectural work!&lt;/p&gt;

&lt;p&gt;Currently, I'm heavily focused on Zero-Trust Architectures and Proof-of-Presence protocols. I'm building out the WITI ecosystem, and I recently open-sourced its core module: QRPruf (a high-security cryptographic certifier built with Flutter/Dart and Riverpod 3.0). My other technical loves are Laravel (PHP) and AI integrations (Gemini/Replicate).&lt;/p&gt;

&lt;p&gt;I joined DEV to connect with other senior engineers, share some deep dives into Clean Architecture, and learn from this amazing community.&lt;/p&gt;

&lt;p&gt;Feel free to say hi, I’m always open to talking about mobile architecture, security, or just geeking out over code!&lt;/p&gt;

&lt;p&gt;Check out what I'm building here: github.com/sanadidari (Stars are always appreciated! ⭐)&lt;/p&gt;

</description>
      <category>hello</category>
      <category>architecture</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>Building a Zero-Trust Proof-of-Presence Protocol with Flutter &amp; Supabase</title>
      <dc:creator>Samir Chatwiti</dc:creator>
      <pubDate>Thu, 09 Apr 2026 23:02:59 +0000</pubDate>
      <link>https://dev.to/chatwiti/building-a-zero-trust-proof-of-presence-protocol-with-flutter-supabase-1hd5</link>
      <guid>https://dev.to/chatwiti/building-a-zero-trust-proof-of-presence-protocol-with-flutter-supabase-1hd5</guid>
      <description>&lt;h1&gt;
  
  
  Building a Zero-Trust Proof-of-Presence Protocol with Flutter &amp;amp; Supabase
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;How I designed a cryptographic certification system for judicial officers — GPS, AES-GCM, SHA-256, and Mocktail tests.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Imagine you are a judicial officer (huissier de justice) in Morocco. Your job requires you to physically appear at a location — a home, a court, a business — and serve a legal document. That act of presence is legally binding.&lt;/p&gt;

&lt;p&gt;The question: &lt;strong&gt;how do you prove, with cryptographic certainty, that you were physically present at a specific location, at a specific time, and that the evidence you captured was not tampered with afterward?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the problem I solved building &lt;strong&gt;QRPruf&lt;/strong&gt; — a zero-trust proof-of-presence protocol embedded in a Flutter mobile app.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "Zero-Trust" Means in This Context
&lt;/h2&gt;

&lt;p&gt;Zero-trust doesn't mean "trust nothing from the network." Here it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No server is trusted to compute the proof&lt;/strong&gt; — all hashing and encryption happens on-device, in an Isolate&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No timestamp can be faked&lt;/strong&gt; — GPS coordinates and device time are cryptographically bound to the file hash at capture time&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No file can be silently replaced&lt;/strong&gt; — AES-GCM authentication tags detect any tampering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No identity can be assumed&lt;/strong&gt; — every proof payload is signed with a canonical SHA-256 that includes the user's subject ID&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Camera / Mic / GPS]
        │
        ▼
[ProofCryptoService]          ← On-device, runs in Dart Isolate
  • SHA-256 hash of raw file
  • AES-GCM encryption (256-bit key)
  • IV prepended to ciphertext
        │
        ▼
[Proof Payload]
  • proof_id, subject_id, timestamp
  • GPS coordinates
  • item hashes + sizes
  • canonical_hash (SHA-256 of sorted JSON)
        │
        ▼
[Supabase Edge Function]      ← Server-side verification only
  • Verifies canonical hash
  • Issues signed proof certificate
  • Stores encrypted blobs in Storage
        │
        ▼
[QR Code]                     ← Shareable proof certificate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical design decision: &lt;strong&gt;the server never sees the original files, only the encrypted blobs and their hashes.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Crypto Layer: ProofCryptoService
&lt;/h2&gt;

&lt;p&gt;This service runs entirely inside a Dart &lt;code&gt;Isolate&lt;/code&gt; via &lt;code&gt;compute()&lt;/code&gt; — no UI thread blocking, no memory spikes on large video files.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:io'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter/foundation.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:crypto/crypto.dart'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:encrypt/encrypt.dart'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:path_provider/path_provider.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProofCryptoService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;ProofCryptoService&lt;/span&gt; &lt;span class="n"&gt;_instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProofCryptoService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_internal&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;factory&lt;/span&gt; &lt;span class="n"&gt;ProofCryptoService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;ProofCryptoService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;_internal&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;/// Runs in a background Isolate — safe for large files&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_encryptIsolate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'inputPath'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;readAsBytesSync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 1. Hash the original (before encryption)&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. AES-GCM encryption — 256-bit key, 96-bit IV&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromBase64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'keyBase64'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IV&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSecureRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Encrypter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;mode:&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AESMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gcm&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encryptBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;iv:&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Write [IV (12 bytes) | ciphertext] to disk — avoids RAM duplication&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'outputPath'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;openSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;mode:&lt;/span&gt; &lt;span class="n"&gt;FileMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeFromSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeFromSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypted&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;closeSync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'sha256'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;'size'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;encryptFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Key&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;skipEncryption&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Videos: hash-only to avoid OOM&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skipEncryption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_hashOnlyIsolate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'inputPath'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;dir&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;getApplicationDocumentsDirectory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;tempPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${dir.path}&lt;/span&gt;&lt;span class="s"&gt;/enc_&lt;/span&gt;&lt;span class="si"&gt;${DateTime.now().microsecondsSinceEpoch}&lt;/span&gt;&lt;span class="s"&gt;.bin'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_encryptIsolate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="s"&gt;'inputPath'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;inputPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'outputPath'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'keyBase64'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;'path'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tempPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why AES-GCM specifically?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;GCM (Galois/Counter Mode) provides both confidentiality &lt;em&gt;and&lt;/em&gt; authentication. If a single byte of the encrypted file is changed after encryption, decryption throws an exception — the authentication tag fails. This is what makes the proof tamper-evident.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;compute()&lt;/code&gt; for the Isolate?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Flutter's &lt;code&gt;compute()&lt;/code&gt; runs a top-level function in a separate Isolate. For a 50MB video file, this means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The main thread stays responsive&lt;/li&gt;
&lt;li&gt;Memory is not duplicated between threads (Dart Isolates don't share heap)&lt;/li&gt;
&lt;li&gt;AES operation on large files won't trigger OOM kills&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Verification Layer: ProofVerifier
&lt;/h2&gt;

&lt;p&gt;A proof is only useful if anyone can verify it independently. The verification logic is intentionally pure — no network, no platform dependencies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:convert'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:crypto/crypto.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:qrpruf/models/proof.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProofVerifier&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Proof&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;canonical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_canonicalize&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="s"&gt;'proof_id'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'proof_version'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;proofVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'status'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;              &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'created_at'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toIso8601String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="s"&gt;'timestamp_primary'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timestampPrimary&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toIso8601String&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="s"&gt;'timezone'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'subject_id'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;          &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;subjectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'items_count'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;itemsCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'hash_algorithm'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;      &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;hashAlgorithm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'signature_algorithm'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;signatureAlgorithm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'public_key_id'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;       &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publicKeyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'auth_method'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;         &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'confidence_level'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;confidenceLevel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'purpose_type'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;        &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;purposeType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s"&gt;'purposes'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;            &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;purposes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;computed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;canonical&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;computed&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;proofHash&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;/// Sort all keys recursively before JSON encoding.&lt;/span&gt;
  &lt;span class="c1"&gt;/// This makes the hash key-order independent.&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="n"&gt;_canonicalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="n"&gt;jsonEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_sortValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;dynamic&lt;/span&gt; &lt;span class="n"&gt;_sortValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;dynamic&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_sortValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nl"&gt;k:&lt;/span&gt; &lt;span class="n"&gt;_sortValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The canonical hash is key-order independent.&lt;/strong&gt; This matters because JSON serializers across platforms (Dart, JavaScript, Python) don't guarantee key ordering. By sorting keys before hashing, the same proof verifies correctly regardless of which platform serialized it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing with Mocktail
&lt;/h2&gt;

&lt;p&gt;Testing cryptographic code requires determinism. Mocktail (the Dart mocking library — no code generation) lets us test the full stack without hitting real Supabase endpoints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing ProofVerifier — pure logic, no mocks needed
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:convert'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:crypto/crypto.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_test/flutter_test.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:qrpruf/models/proof.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:qrpruf/utils/proof_verifier.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Replicate the canonicalization in the test — independently&lt;/span&gt;
&lt;span class="kt"&gt;String&lt;/span&gt; &lt;span class="nf"&gt;_computeExpectedHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Proof&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;dynamic&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;dynamic&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;List&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="kt"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;keys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nl"&gt;k:&lt;/span&gt; &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;'proof_id'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;proofId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;'auth_method'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;authMethod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// ... all fields&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jsonEncode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ProofVerifier'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'returns true for correct hash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildTestProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;hash:&lt;/span&gt; &lt;span class="n"&gt;_computeExpectedHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseProof&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProofVerifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;isTrue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'returns false for tampered hash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildTestProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;hash:&lt;/span&gt; &lt;span class="s"&gt;'a'&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProofVerifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;isFalse&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'is deterministic — same proof verifies twice'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;proof&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buildTestProof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;hash:&lt;/span&gt; &lt;span class="n"&gt;_computeExpectedHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseProof&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProofVerifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;isTrue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProofVerifier&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proof&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;isTrue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Testing ProofCryptoService — file system + Isolate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dart"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:io'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'dart:convert'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:flutter_test/flutter_test.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:crypto/crypto.dart'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:encrypt/encrypt.dart'&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="s"&gt;'package:qrpruf/features/proofs/data/proof_crypto_service.dart'&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'AES-GCM roundtrip'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'encrypt then decrypt returns original bytes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSecureRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;        &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IV&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSecureRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;encrypter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Encrypter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;mode:&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AESMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gcm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;original&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'preuve judiciaire confidentielle'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encryptBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;iv:&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;decrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypter&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decryptBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;iv:&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decrypted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;original&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'wrong key throws — GCM auth tag mismatch'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;key1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSecureRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;key2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSecureRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;IV&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSecureRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;enc1&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Encrypter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;mode:&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AESMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gcm&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;enc2&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Encrypter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;mode:&lt;/span&gt; &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;AESMode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;gcm&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;enc1&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encryptBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'secret'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nl"&gt;iv:&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;enc2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;decryptBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nl"&gt;iv:&lt;/span&gt; &lt;span class="n"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;throwsA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anything&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'ProofCryptoService.encryptFile — skipEncryption'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;late&lt;/span&gt; &lt;span class="n"&gt;Directory&lt;/span&gt; &lt;span class="n"&gt;tempDir&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;tempDir&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;Directory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;systemTemp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;createTemp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'qrpruf_test_'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="n"&gt;tearDown&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tempDir&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;recursive:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'returns correct SHA-256 for known content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kd"&gt;async&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;utf8&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'contenu connu'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="si"&gt;${tempDir.path}&lt;/span&gt;&lt;span class="s"&gt;/sample.jpg'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;writeAsBytesSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ProofCryptoService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encryptFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSecureRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nl"&gt;skipEncryption:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;equals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crypto&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sha256&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;'throws when file does not exist'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProofCryptoService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encryptFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s"&gt;'/nonexistent/file.jpg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="n"&gt;encrypt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fromSecureRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="nl"&gt;skipEncryption:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;throwsA&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isA&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;()),&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The CI Pipeline
&lt;/h2&gt;

&lt;p&gt;Every push to &lt;code&gt;main&lt;/code&gt; runs this GitHub Actions workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;QRPruf Flutter CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;quality-gate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;subosito/flutter-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;channel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stable&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flutter pub get&lt;/span&gt;

      &lt;span class="c1"&gt;# Generate Riverpod annotations BEFORE tests&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dart run build_runner build --delete-conflicting-outputs&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flutter analyze&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flutter test --coverage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The key insight:&lt;/strong&gt; &lt;code&gt;build_runner&lt;/code&gt; runs before &lt;code&gt;flutter test&lt;/code&gt;. Generated code (Riverpod annotations) must exist before tests can import it. This is a common CI mistake that makes the green badge meaningless.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Isolates are not optional for crypto on mobile.&lt;/strong&gt;&lt;br&gt;
AES-256 on a 30MB video on a mid-range Android device takes ~800ms. On the main thread, that freezes the UI. In an Isolate, the user sees a progress indicator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Canonical hashing is the hardest part to get right.&lt;/strong&gt;&lt;br&gt;
The first version of &lt;code&gt;ProofVerifier&lt;/code&gt; failed intermittently because JSON key ordering differed between the app (Dart) and the verification server (Node.js). Sorting keys recursively before hashing fixed it permanently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;skipEncryption&lt;/code&gt; for video is a pragmatic tradeoff.&lt;/strong&gt;&lt;br&gt;
Dart's pure AES engine runs out of RAM on files larger than ~100MB. For large videos, we hash the original and upload a non-encrypted stream with TLS. The hash still proves tamper-evidence — only confidentiality is reduced, and large video files in court contexts are less sensitive than text documents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Mocktail &amp;gt; Mockito for Flutter.&lt;/strong&gt;&lt;br&gt;
No &lt;code&gt;build_runner&lt;/code&gt;, no &lt;code&gt;@GenerateMocks&lt;/code&gt;, no code generation step to maintain. Just &lt;code&gt;class MockX extends Mock implements X {}&lt;/code&gt; — one line.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;A judicial officer opens QRPruf, selects the act type, captures images/audio/video on-site. Within seconds, a QR code is generated that encodes a cryptographic proof — verifiable by any court system without trusting the officer's device, the network, or the server.&lt;/p&gt;

&lt;p&gt;The proof survives even if Supabase is compromised: the canonical hash computed on-device will not match a server-generated replacement.&lt;/p&gt;

&lt;p&gt;That is what zero-trust means in practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/sanadidari/qrpruf" rel="noopener noreferrer"&gt;github.com/sanadidari/qrpruf&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live: &lt;a href="https://qrpruf.com" rel="noopener noreferrer"&gt;qrpruf.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Built as part of the WITI Institutional Intelligence ecosystem — a full-stack platform for Morocco's judicial corps.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tags: #flutter #dart #security #cryptography #supabase #testing&lt;/em&gt;&lt;/p&gt;

</description>
      <category>flutter</category>
      <category>dart</category>
      <category>security</category>
      <category>supabase</category>
    </item>
  </channel>
</rss>
