Skip to content

Verify an encrypted note

An encrypted note is one you encrypted on your own machine before sending — we only ever receive the ciphertext, so that's what we hash and anchor to Bitcoin. The proof therefore shows your ciphertext existed and hasn't changed since a certain Bitcoin block, without us, or anyone you hand the proof to, ever seeing your plaintext. One extra step, which only you can do, ties that anchored ciphertext to your real content. This is the hands-on version of the verification reference.

Verifying a different kind of note? See append-only notes or editable notes.

What the entries are. An encrypted note is an ordinary plain note, so its proof follows whichever kind it is: a per-entry chain if it's an append-only log, or dated daily snapshots if it's editable. In every case each entry's content is your ciphertext, not your text — so steps 1–5 below prove the ciphertext is intact and anchored, and the decrypt step proves it's yours.

The extra step — decrypt and compare

Only you hold the key, so only you can show the anchored ciphertext is really your note. Pull an entry's anchored bytes out of the proof, then decrypt them with the same tool and key you encrypted with:

# 1. pull entry 0's anchored bytes (the ciphertext you pasted) out of the proof
python3 -c 'import json, base64; open("ciphertext.b64","wb").write(base64.b64decode(json.load(open("proof.json"))["entries"][0]["content"]))'

# 2. decrypt with YOUR key, using the same tool you encrypted with. For the
#    openssl example from the Encrypted notes page that is:
openssl enc -d -aes-256-gcm -pbkdf2 -a -in ciphertext.b64 -out plaintext.txt
cat plaintext.txt        # this must be your note

If plaintext.txt is your content, that entry's anchored bytes decrypt to your note — and steps 1–5 below then prove those exact bytes are fixed in Bitcoin. (Change the [0] to check another entry; use whatever tool and key you actually encrypted with.) Nothing here is sent to us: you decrypt locally, and you reveal the plaintext only to whoever you choose.

The chain you're checking. A proof links five things together, and each step below verifies one link: your exact bytes → their SHA-256 hash → a "leaf" tagged with your note's identity and position → folded into that day's single Merkle root → committed to the Bitcoin blockchain. If every link holds, the content provably existed by a certain time and hasn't changed since — and none of it depends on trusting Fresh Jots.

What you'll need

  • Your proof file. On the Provenance dashboard click Download next to a note and save it as proof.json. (Or fetch it with your API token: curl -H "Authorization: Bearer YOUR_TOKEN" https://freshjots.com/api/v1/notes/<id>/proof > proof.json.)
  • Python 3 — already on macOS and most Linux; on Windows install it from python.org. Every check below is a short Python command, and nothing gets installed.
  • The OpenTimestamps client, for the final Bitcoin step only: pip install opentimestamps-client (this gives you the ots command).

Run everything below in the same folder as proof.json.

1. Content integrity — these are your exact bytes

Each entry stores the exact bytes we received (base64-encoded) and their SHA-256 hash. Decode the bytes, hash them, and confirm the hash matches — proof that the recorded content is byte-for-byte what we stored.

python3 -c '
import json, base64, hashlib
e = json.load(open("proof.json"))["entries"][0]
print("your bytes:", base64.b64decode(e["content"]).decode(errors="replace"))
print("computed  :", hashlib.sha256(base64.b64decode(e["content"])).hexdigest())
print("claimed   :", e["content_hash"])'

The last two lines must be identical; the first shows you the exact bytes we stored — your note's text (or, for an encrypted note, the ciphertext you sent). (Change the [0] to check a different entry.)

2. Leaf value — bound to your note's identity

A bare hash proves nothing — anyone can hash anything. So each entry becomes a "leaf": SHA-256(0x00 ‖ note_token ‖ prev_hash ‖ content_hash), folding in the note's public token and its position in the chain. This leaf is the value that actually reaches Bitcoin, so the commitment is to your content, in your note, in order.

python3 -c '
import json, hashlib
p = json.load(open("proof.json")); e = p["entries"][0]
leaf = hashlib.sha256(b"\x00" + p["note_token"].encode() + bytes.fromhex(e["prev_hash"]) + bytes.fromhex(e["content_hash"])).hexdigest()
print("leaf value:", leaf)'

You'll fold this value up to the root in the next step. (The 0x00 prefix keeps a leaf from ever being mistaken for a tree node.)

3. Merkle branch — folds into the day's root

Every leaf from every account on a given day is combined into one tree, and only its single root_hash goes to Bitcoin. The entry's branch is the short path from your leaf up to that root: fold each step with SHA-256(0x01 ‖ left ‖ right), putting the sibling on its stated side. If it lands on the anchor's root, your entry really is inside that day's Bitcoin commitment.

python3 -c '
import json, hashlib
def sha(b): return hashlib.sha256(b).digest()
p = json.load(open("proof.json")); e = p["entries"][0]
acc = sha(b"\x00" + p["note_token"].encode() + bytes.fromhex(e["prev_hash"]) + bytes.fromhex(e["content_hash"]))
for s in e["branch"]:
    sib = bytes.fromhex(s["hash"])
    acc = sha(b"\x01" + (acc + sib if s["side"] == "right" else sib + acc))
print("folds to   :", acc.hex())
print("anchor root:", p["anchors"][e["anchor_id"]]["root_hash"])'

The two lines must match. (A brand-new entry may not be anchored yet: if anchor_id is null it's hashed but not yet in a day's Bitcoin commitment — check back within a day.)

4. Order — nothing inserted, reordered, or dropped

When a note has more than one entry, each entry's prev_hash equals the previous entry's content_hash — an unbroken chain. For an append-only log that's the order entries were written; for an editable note it's the order of its dated snapshots. Either way, an unbroken chain proves nothing was inserted, reordered, or removed from the middle.

python3 -c '
import json
es = json.load(open("proof.json"))["entries"]
print("chain intact:", all(es[i]["prev_hash"] == es[i-1]["content_hash"] for i in range(1, len(es))))'

It should print True (a single-entry note trivially passes). (Honest limit: the chain proves nothing was changed in the middle, but not that nothing was withheld after the last entry you hold.)

5. Bitcoin — the root is committed to the blockchain

Finally, confirm the anchor's root_hash really is in Bitcoin. Each anchor ships its OpenTimestamps proof (ots). Write both to disk:

python3 -c '
import json, base64
a = json.load(open("proof.json"))["anchors"]; k = list(a)[0]
open("anchor", "wb").write(bytes.fromhex(a[k]["root_hash"]))
open("anchor.ots", "wb").write(base64.b64decode(a[k]["ots"]))
print("wrote anchor + anchor.ots for anchor", k)'

Then read the Bitcoin attestation out of it:

ots info anchor.ots     # look for BitcoinBlockHeaderAttestation(<height>)
ots verify anchor.ots   # if you run a Bitcoin node, prints the exact block time

ots info needs no Bitcoin node: it prints the block height the root was committed in, plus that block's Merkle root. Look the height up on any public block explorer and confirm the Merkle root matches — that block's timestamp is when your content provably existed. (ots verify automates this but needs access to a Bitcoin node. If ots info shows only PendingAttestation, the anchor isn't Bitcoin-confirmed yet — give it up to a day.)

Shortcut: check 1–4 in one go

Save this as verify.py and run python3 verify.py proof.json. It runs checks 1–4 over every entry and tells you exactly what passed; then finish with the Bitcoin step above.

import sys, json, base64, hashlib

def sha(b): return hashlib.sha256(b).digest()

proof   = json.load(open(sys.argv[1] if len(sys.argv) > 1 else "proof.json"))
token   = proof["note_token"].encode()
anchors = proof["anchors"]
prev    = bytes(32)          # the first entry's prev_hash is 32 zero bytes
ok      = True

for i, e in enumerate(proof["entries"]):
    content = base64.b64decode(e["content"])
    ch, ph  = bytes.fromhex(e["content_hash"]), bytes.fromhex(e["prev_hash"])

    if sha(content) != ch:                     # 1. content integrity
        print(i, "FAIL: content != content_hash"); ok = False; continue
    if ph != prev:                             # 4. order
        print(i, "FAIL: prev_hash out of order"); ok = False
    prev = ch

    leaf = sha(b"\x00" + token + ph + ch)      # 2. leaf value
    if e.get("anchor_id") is None:
        print(i, "OK: hashed, not yet Bitcoin-anchored"); continue

    acc = leaf                                  # 3. Merkle branch
    for s in e["branch"]:
        sib = bytes.fromhex(s["hash"])
        acc = sha(b"\x01" + (acc + sib if s["side"] == "right" else sib + acc))
    if acc == bytes.fromhex(anchors[e["anchor_id"]]["root_hash"]):
        print(i, "OK: folds to anchor", e["anchor_id"])
    else:
        print(i, "FAIL: does not fold to the anchor root"); ok = False

print("PASS - content, order and Merkle paths check out." if ok else "FAIL - see above.")
print("Now confirm each anchor is in Bitcoin with the `ots` step.")

When it all passes

You've shown — trusting no one, not even us — that your ciphertext existed and was unchanged as of a specific Bitcoin block, and (with your key) that it decrypts to your real content. So you get tamper-evidence and privacy: the proof commits to your note without ever exposing it, and no one who holds the proof learns a thing about what the note says unless you choose to decrypt it for them.

Honest limits. The guarantee is only as strong as your own encryption and key handling — if you lose your key, no one, including us, can recover the note. The proof also can't show the content is true, or that it wasn't altered before you encrypted it. And note that the encrypted marker is your declaration: we hash whatever bytes you send, so if you mark a note encrypted but paste plaintext, the proof anchors that plaintext. See Encrypted notes for the full picture.