DEV Community

Craig Solomon
Craig Solomon

Posted on

A hash is one-way until someone can guess the file

Here's a question worth asking about anything you've timestamped, or anything you've published a hash of. If someone could guess what's in the file, could they confirm the guess from the hash alone?

For some files, yes. And "it's SHA-256, it's one-way" doesn't save you.

The comfortable answer

When you timestamp a file with ProofLedger, the file never leaves your machine. You hash the bytes locally and send the hash. The hash gets anchored to Polygon and Bitcoin. That's the whole engine. It doesn't look inside the file, and it doesn't care what kind of file it is.

So the comfortable reasoning goes like this. The file stays private. Only a digest goes public. You can't reverse SHA-256. Done.

That reasoning is right about the function. It's wrong about the situation.

Where it breaks

One-way means you can't go from the hash back to the input. It says nothing about going forward. Anyone can hash a candidate and compare. If the set of plausible inputs is small, nobody has to reverse anything. They just try them all.

And the lookup is public on purpose. Anyone can hit the verify endpoint without an account:

sha256sum decision.txt
curl "https://proofledger.io/api/v1/verify?hash=<sha256>"
Enter fullscreen mode Exit fullscreen mode

That's what it's for. Opposing counsel or an auditor should be able to check a proof without asking me, and without asking you. If the only way to verify a timestamp is to ask whoever issued it, it isn't worth much.

But "anyone can check whether this hash has a proof" also means "anyone with a guess can check whether their guess has a proof." Same endpoint, same property, read from the other side.

Here's the concrete version. Say the file you timestamped is just this:

board vote: approve
Enter fullscreen mode Exit fullscreen mode

Someone who knows a vote happened doesn't need your file. They need a list:

import hashlib

candidates = [
 b"board vote: approve\n",
 b"board vote: reject\n",
 b"board vote: defer\n",
]

for c in candidates:
 print(hashlib.sha256(c).hexdigest(), c)
Enter fullscreen mode Exit fullscreen mode

Check each digest against the public lookup, and whichever one has a proof is your answer. The lookup is rate limited to 120 requests per hour per IP. That slows down a wide search. A narrow one doesn't even notice it. And the endpoint isn't the only place the hash lives. Once it's anchored it's on chain, and the chains are public.

There's one thing working in your favor here, and it's worth being precise about. The guesser needs the exact bytes. Trailing newline or not, CRLF or LF, a space after the colon. Every one of those changes the digest. So if your file is freeform and messy, guessing it is hopeless.

The catch is that structured files aren't messy. A config with one flag in it. A JSON blob where the only field that varies is an amount. A template where the only thing filled in is a name. If the format is known, the bytes are predictable, and all that's left to guess is the part that matters.

A photo or a compiled build artifact? Nobody's guessing those. So the risk isn't hashes in general. It's small, structured, predictable inputs, and you're the only one who knows which of your files look like that.

The fix, and what it costs

The fix is to make the input unguessable. Put random bytes in it before you hash. The simplest way is a salt you keep:

import hashlib, secrets

salt = secrets.token_hex()

with open("decision.txt", "rb") as f:
 data = f.read()

digest = hashlib.sha256(salt.encode() + data).hexdigest()

print("salt: ", salt)
print("digest:", digest)
Enter fullscreen mode Exit fullscreen mode

POST /api/v1/proof takes a hash, not a file, so whatever digest you compute is what gets anchored. The engine doesn't know a salt went in, and it doesn't need to.

It doesn't salt anything for you either, and I'd rather keep it that way. If the service generated and held the salt, that'd be a secret the issuer keeps. Then checking the proof means asking the issuer, which is the exact thing the public verify path exists to avoid.

Now the part that matters. Salting isn't free, and the costs are the reason this is a decision and not a default.

First, the verifier now needs the file and the salt. Hash the file on its own, look it up, and you'll find nothing. You haven't removed the need to share a secret. You've moved it. The file stays private until you choose to hand over the salt alongside it.

Second, lose the salt and the proof is dead. The anchor is still sitting on two chains, perfectly valid, attached to a digest nobody can reproduce. So the salt needs to be stored somewhere at least as durable as the file. If it only lives in a terminal scrollback, it's gone.

Third, the construction has to be written down. Salt first or file first? Hex string or raw bytes? Any separator? Whatever tool checks the proof later, whether that's the public endpoint, the verify-proof package running offline, or the GitHub Action in CI, it's checking the digest you submitted. The person verifying has to rebuild that digest your way first. If your way only exists in your head, they can't.

There's one variant that dodges the first cost. Put the random value inside the file itself, as a comment line or an extra field. Then the file alone reproduces the hash, and the plain lookup works again for anyone you've given the file to. The trade is that the file is no longer quite the artifact you'd naturally have. Sometimes that's fine. If the point is "this exact export from this exact system existed," it isn't.

The test

Pick the most sensitive file you've hashed somewhere public. A timestamp, a checksum in a release note, a digest in a log.

Then actually write the candidates list. Don't just think about whether you could. Open an editor and type out every version of that file someone with outside knowledge would consider plausible, byte for byte, newlines included. Hash each one.

If you can finish the list, that hash was never protecting the contents. It's an answer waiting for someone to ask the right question. From there you either salt the next one and accept the costs above, or you decide the contents were never really the secret. The timing was. Both are legitimate. Just pick one on purpose.

I built and run ProofLedger, and the verify endpoint and the other two are documented at proofledger.io/api.html if you want to see exactly what's public.


Disclosure: this article was drafted by an AI agent I built and run, from facts I supplied about my own project.

Top comments (0)