<?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: trampto</title>
    <description>The latest articles on DEV Community by trampto (@trampto).</description>
    <link>https://dev.to/trampto</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%2F4243627%2F58fb740a-649a-40cc-8b04-3774ce1a646a.png</url>
      <title>DEV Community: trampto</title>
      <link>https://dev.to/trampto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/trampto"/>
    <language>en</language>
    <item>
      <title>The invoice scam that changes one line: why "it looks right" is not verification</title>
      <dc:creator>trampto</dc:creator>
      <pubDate>Sun, 15 Nov 2026 13:09:27 +0000</pubDate>
      <link>https://dev.to/trampto/the-invoice-scam-that-changes-one-line-why-it-looks-right-is-not-verification-54di</link>
      <guid>https://dev.to/trampto/the-invoice-scam-that-changes-one-line-why-it-looks-right-is-not-verification-54di</guid>
      <description>&lt;p&gt;A supplier sends you an invoice. Same logo, same layout, same amount, same reference. You pay it. Two weeks later the supplier calls: they never got the money.&lt;/p&gt;

&lt;p&gt;Somewhere between their outbox and your accounts team, one line changed: the IBAN.&lt;/p&gt;

&lt;p&gt;This is one of the most common business payment scams, and it works for a boring reason: &lt;strong&gt;nobody checks the bank details, because everything else looks right.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it usually happens
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A compromised mailbox.&lt;/strong&gt; The attacker reads real conversations and waits for an invoice to be sent, then forwards an "updated" copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A lookalike domain.&lt;/strong&gt; &lt;code&gt;supplier-invoices.com&lt;/code&gt; instead of &lt;code&gt;supplier.com&lt;/code&gt;. The email says "we changed banks, please use the attached invoice".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An edited PDF.&lt;/strong&gt; Changing a few characters in a PDF takes seconds, and with AI tools the result is visually perfect: fonts, spacing, everything.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The amount is the same, so nothing feels off. That is the whole trick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the usual checks fail
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Looking carefully&lt;/strong&gt; doesn't work when the edit is pixel-perfect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The file name and date&lt;/strong&gt; prove nothing: they're trivially copied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"It came from their email"&lt;/strong&gt; only means it came from &lt;em&gt;an&lt;/em&gt; email that looks like theirs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real question isn't "does this invoice look genuine?" but &lt;strong&gt;"is this exactly the file the supplier issued?"&lt;/strong&gt; And that question has a mathematical answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification with a fingerprint
&lt;/h2&gt;

&lt;p&gt;A cryptographic hash (SHA-256) turns any file into a 64-character fingerprint. Change one character of the IBAN and the fingerprint changes completely.&lt;/p&gt;

&lt;p&gt;So the flow becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The supplier &lt;strong&gt;seals&lt;/strong&gt; the invoice when they issue it: its fingerprint is registered.&lt;/li&gt;
&lt;li&gt;The invoice travels with a &lt;strong&gt;verification link&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Before paying, you open the link or drop the PDF on the verify page. Your browser recomputes the fingerprint and checks it against the register.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Match: it's byte-for-byte the invoice they issued. No match: someone changed something, even if you can't see what.&lt;/p&gt;

&lt;p&gt;This is what we built &lt;a href="https://trampto.com" rel="noopener noreferrer"&gt;TRAMPTO&lt;/a&gt; to do. Sealing runs in the browser, so the invoice is never uploaded, and verifying is free and needs no account.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't replace
&lt;/h2&gt;

&lt;p&gt;To be clear about the limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It proves the file &lt;strong&gt;hasn't changed since it was sealed&lt;/strong&gt;. If the attacker controls the supplier's own system &lt;em&gt;before&lt;/em&gt; sealing, the seal won't catch that.&lt;/li&gt;
&lt;li&gt;It doesn't replace good habits: &lt;strong&gt;confirm any bank-detail change by phone&lt;/strong&gt;, using a number you already had, never one from the email.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for the most common version of the scam, an edited PDF in transit, a fingerprint check turns "it looks right" into "it is right".&lt;/p&gt;

&lt;h2&gt;
  
  
  Your turn
&lt;/h2&gt;

&lt;p&gt;Has your company (or a client) ever received an invoice with changed bank details? How did you catch it, or how did it slip through? I'd love to hear the stories.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>fintech</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Making PDFs tamper-evident in the browser with SHA-256 (and why we hash the sealed file, not the original)</title>
      <dc:creator>trampto</dc:creator>
      <pubDate>Fri, 13 Nov 2026 23:08:56 +0000</pubDate>
      <link>https://dev.to/trampto/making-pdfs-tamper-evident-in-the-browser-with-sha-256-and-why-we-hash-the-sealed-file-not-the-4k4c</link>
      <guid>https://dev.to/trampto/making-pdfs-tamper-evident-in-the-browser-with-sha-256-and-why-we-hash-the-sealed-file-not-the-4k4c</guid>
      <description>&lt;p&gt;AI has made it trivial to edit an invoice, a quote or a contract so cleanly that nobody can spot the change by eye. A PDF has no memory: the file name, the date and the layout all look the same after someone changes "1,800 €" to "4,800 €".&lt;/p&gt;

&lt;p&gt;So instead of trying to &lt;em&gt;detect&lt;/em&gt; a forgery by how it looks, we built &lt;a href="https://trampto.com" rel="noopener noreferrer"&gt;TRAMPTO&lt;/a&gt; to &lt;em&gt;prove&lt;/em&gt; that a document hasn't changed, with plain math. This post walks through how it works, the one design decision that bit us, and what this approach does &lt;strong&gt;not&lt;/strong&gt; prove.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea in one line
&lt;/h2&gt;

&lt;p&gt;Compute a SHA-256 fingerprint of the document, register it, and give the recipient a link where they can recompute it themselves. Change one byte and the fingerprint no longer matches.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hashing in the browser with Web Crypto
&lt;/h2&gt;

&lt;p&gt;No libraries needed. The whole thing runs client-side with the Web Crypto API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&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="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Copy only this view's bytes: a Uint8Array can be a window&lt;/span&gt;
  &lt;span class="c1"&gt;// into a larger ArrayBuffer, and hashing the whole buffer&lt;/span&gt;
  &lt;span class="c1"&gt;// would give you a different (wrong) fingerprint.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="n"&gt;byteOffset&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="n"&gt;byteOffset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;byteLength&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nc"&gt;ArrayBuffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;digest&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;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"SHA-256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&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="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"0"&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&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;p&gt;That &lt;code&gt;slice(byteOffset, …)&lt;/code&gt; line looks redundant until it isn't. If the bytes come from a parser or a file reader that hands you a view over a bigger buffer, hashing &lt;code&gt;bytes.buffer&lt;/code&gt; directly silently fingerprints the wrong data. Two codebases that both "compute SHA-256 of the file" will then disagree, and every verification fails.&lt;/p&gt;

&lt;p&gt;Because the hash is computed on the device, &lt;strong&gt;the document itself never leaves the browser&lt;/strong&gt;. Only the fingerprint, the file name and a seal ID go to the register.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sealing: two hashes, not one
&lt;/h2&gt;

&lt;p&gt;When a document is sealed, we compute two fingerprints:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;contentHash&lt;/code&gt;&lt;/strong&gt;: SHA-256 of the original file, before we touch it.&lt;/li&gt;
&lt;li&gt;We add a tiny visual micro-seal to each page (about 6 mm, 15% opacity, bottom-right corner, respecting page rotation) and write &lt;code&gt;contentHash&lt;/code&gt; into the PDF metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;stampedHash&lt;/code&gt;&lt;/strong&gt;: SHA-256 of the &lt;strong&gt;final sealed PDF&lt;/strong&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;contentHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdfBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;          &lt;span class="c1"&gt;// original&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;pdfDoc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nc"&gt;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdfBytes&lt;/span&gt;&lt;span class="p"&gt;)&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;const&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="n"&gt;pdfDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPages&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="nf"&gt;drawDiscreetMark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;pdfDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setSubject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sb"&gt;`TRAMPTO-HASH:${contentHash}`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sealedBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;pdfDoc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="n"&gt;stampedHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sha256Hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sealedBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// public fingerprint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The public verification link is &lt;code&gt;/v/{stampedHash}&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision that matters: hash the &lt;em&gt;sealed&lt;/em&gt; file
&lt;/h2&gt;

&lt;p&gt;It's tempting to make the public fingerprint the hash of the original. Don't. The file people actually send around is the sealed one. If the link pointed at the original's hash, a recipient holding the sealed PDF could never reproduce it.&lt;/p&gt;

&lt;p&gt;The order is an invariant: &lt;strong&gt;hash original → seal → hash sealed&lt;/strong&gt;. If you ever change the order, the seal drawing, or touch the PDF after computing &lt;code&gt;stampedHash&lt;/code&gt;, every link you've already issued stops verifying. We treat that sequence like a database migration: it doesn't change casually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying
&lt;/h2&gt;

&lt;p&gt;The recipient opens the link (or drops the file on the verify page). The browser recomputes SHA-256 of the file they have and looks it up in the register:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Match&lt;/strong&gt; → the file is byte-for-byte the one that was sealed, and we show when it was sealed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No match&lt;/strong&gt; → something changed, even a single byte.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One detail worth copying: distinguish &lt;strong&gt;"not found"&lt;/strong&gt; from &lt;strong&gt;"network error"&lt;/strong&gt;. If the backend is down, we must not tell someone their genuine document is fake. A failed lookup throws and asks to retry; only a clean "no row" means "not verified".&lt;/p&gt;

&lt;h2&gt;
  
  
  Other formats
&lt;/h2&gt;

&lt;p&gt;Word, PowerPoint, images and text are converted to PDF in the browser first (heavy libraries like &lt;code&gt;pdf-lib&lt;/code&gt; and &lt;code&gt;mammoth&lt;/code&gt; are loaded on demand with dynamic &lt;code&gt;import()&lt;/code&gt; so the app opens instantly). Then the same sealing flow runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this does &lt;em&gt;not&lt;/em&gt; prove
&lt;/h2&gt;

&lt;p&gt;Being honest about limits matters more than the feature list:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It proves integrity, not authorship.&lt;/strong&gt; It shows the file hasn't changed since it was sealed. It doesn't prove who wrote it or that its content is true.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's not a qualified e-signature.&lt;/strong&gt; It doesn't replace eIDAS qualified signatures or seals where the law requires them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "when" relies on our register.&lt;/strong&gt; The sealing date comes from TRAMPTO's database, not from a blockchain or a qualified timestamp authority.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For quotes, contracts, invoices, reports and certificates, where the risk is "someone quietly changed the numbers", that's usually exactly the guarantee you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://trampto.com" rel="noopener noreferrer"&gt;trampto.com&lt;/a&gt;: verifying is always free, and the first 3 seals are free. I'd love feedback from other devs, especially on the verification UX for people who have never heard of a hash.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>security</category>
      <category>pdf</category>
    </item>
  </channel>
</rss>
