AI analysis · SeedSigner

SeedSigner image entropy

An AI analysis of how SeedSigner turns a photograph into a seed phrase. Measured on instrumented builds of the shipped release, the capture path byte-identical to stock, on four physical devices, from the first preview frame to the final photograph. Every claim is reproducible, and the raw captures are published so you can check them.

Version
0.8.7
History
Since v0.4.3
Captures
32 runs, 4 units
Model
Claude Fable 5
Revision
0804db1d
The short version

A normally lit scene provides thousands of times the bits of entropy that a seed needs.

Two documents

This document explains what the camera contributes, what the measurements found, and what you have to do yourself. It is written for a person deciding how to use the device, and everything you need in order to generate a seed safely is here.

The measurements themselves, the methods that produced them, the full release history, every concern raised with its disposition, and what was not verified are in a companion document: data and methodology. It takes an AI-first approach, organized for checking claims one by one rather than for reading straight through. Work through it yourself if you want to, and if you would rather have a claim on this page checked than take it on trust, pointing a model at it is the quickest route.

1The implementation

Reviewed at tag 0.8.7, commit e0a80d4b. SeedSigner builds seed entropy as a running SHA-256 chain in tools_views.py, folding in four inputs in order:

#InputSource
1CPU serial numberL151–158
2time.time()L161
3Live preview frames, a rolling window of up to 50L165–167
4The full-resolution final imageL170

The first two inputs contribute almost nothing. SeedSigner OS has no real-time clock and no network, so the system clock starts at zero every boot. time.time() returns fractional seconds since power-on (e.g. 123.4567891). Its unpredictability is therefore the sub-second timing of a button press plus however long the user took to reach the feature: bounded, and not something to lean on. The CPU serial is a fixed per-device value and not secret. The code's own comment calls this “modest entropy”, which is fair. The security of this feature rests on inputs 3 and 4.

Those two carry everything, and the raw quantity they hand over is not small. On the standard 240-pixel display:

StageResolutionFormatBits each
Preview frames (up to 50)240 × 240RGBA, 32 bits/px*1,382,400*
Final image480 × 480RGB, 24 bits/px, JPEG-decoded5,529,600

* Preview frames are stored and hashed as RGBA, but the alpha channel is a constant 255 in every pixel of every frame: it never varies, so it can never carry anything. The count keeps only the 24 RGB bits per pixel that can.

Fifty preview frames at 1,382,400 bits each, plus the final image, is 74,649,600 bits going into the chain. A 24-word seed needs 256.

Those are the most over-readable numbers in this document, so take them for what they are. Nothing guarantees that any of those bits carry anything of value. A frame is a container, and how much unpredictability it holds depends on what the camera was pointed at and on the state of the sensor. Establishing what the containers actually carry is what the rest of this analysis does.

A margin is not a score

SHA-256 emits 256 bits, regardless of how much data goes in. Once at least 256 bits of unpredictability have gone into it, everything beyond that is extraneous. It is still hashed and it still changes the result, but it cannot make the seed any harder to attack.

So 256 is a threshold rather than a target to beat, and a large margin does not mean a proportionally stronger seed. It means the threshold is cleared with room to spare. Anyone attacking a seed made this way is attacking 256 bits, not a multi-million-bit photograph. The margin matters at the thin end, not the thick end, which is why this analysis spends most of its time where the sensor is starved of light.

Every figure quoted here is measured against 256, the 24-word case, because it is the stricter of the two. A 12-word seed keeps only the first 128 bits of the digest (tools_views.py L172–174), so its threshold is 128 and every margin below doubles against it.

2You cannot take the same photograph twice

A reasonable worry about generating a seed from a camera: what if you photograph the same thing twice and get the same seed? Point the device at the same desk on two different days, or take two shots in a row without moving, and it feels as though the result might repeat.

For the final photograph, it never has, and in practice cannot, and this section is the demonstration. Two photographs of the same scene are never the same photograph. Be precise about the claim: it is not that no individual pixel repeats. Plenty do. What never repeats is the frame taken as a whole. Two things drive it.

  1. The camera moves. Held in a hand, it is impossible to avoid some movement changing where it is aimed.
  2. More importantly, the sensor is imperfect. Every read adds noise of its own, from heat in the sensor and from the readout electronics that turn what it collected into numbers. A camera module costing a few dollars is a very noisy one, but that is not really the point: a perfect sensor does not exist, at any price. Every frame carries a fresh dose of its sensor's own imprecision.

And a hash has no notion of similarity. Change a single pixel by one step and the end result is a completely different seed, so nearly the same photograph is worth nothing to anyone trying to repeat your result. Only an exact byte-for-byte match would do it.

Where these images come from

SeedSigner releases do not save captured images. The preview frames and the final photograph exist only in memory, are fed through the SHA-256 chain, and are gone; nothing is written to the microSD card, and there is nothing on the device to read back afterward. (Verified on the reviewed release, whose OS runs entirely from RAM with no persistent filesystem.)

Every frame shown in this analysis therefore comes from a purpose-built instrumented build, modified just for the purposes of this analysis: v0.8.7 plus a small patch that writes the exact bytes the app is about to hash out to the microSD card, an instant before hashing them. The patch leaves the camera code untouched, so the bytes are precisely what a release would have hashed in the same scene. How the build is made, verified against the release, and reproduced is in the companion document.

Below are two consecutive captures of a lit bookshelf, the device resting on a table, taken back to back as fast as the hardware can capture: the second followed the first by 0.64 seconds. Nothing was moved or touched between them. These are the exact bytes v0.8.7 fed into its SHA-256 chain, dumped before hashing.

Capture 1, as recorded
First capture: a shelf of books photographed square on, spines and titles legible.
Capture 2, next in the burst
Second capture of the same shelf, visually identical to the first.
The same shelf, twice, 0.64 seconds apart.

To the eye they are the same photograph. In the data, 80.82% of the frame's color values are different between the two. Subtract one capture from the other and amplify what is left, and you can see where: the entire scene subtracts away, and what remains is what changed between the two exposures. Notice that the change covers the frame rather than collecting in one place. That is the signature of sensor noise; something moving in the room would leave its change where the something was.

Difference ×12
The difference between the two captures, amplified twelve times: a dense field of color noise across the whole frame.
Difference ×40
The same difference amplified forty times, filling the frame.
The difference between the two captures, amplified at fixed linear gains of ×12 and ×40, clipped.

Magnify a small patch and let the two captures alternate, and you can watch it happen: the lettering holds perfectly still while the surface crawls. 97.5% of the pixels in that crop differ between the two. Differing is a low bar, and that is the point: a pixel counts if even one of its three color values moved by a single step, and most of these moved by only a step or two. A change that slight is nearly invisible to the eye, and it is everything to a hash.

Captures 1 and 2 alternating, 96×96 px at 6×
Animation alternating between the same magnified region of the two captures every half second. The lettering holds still while the surface crawls.
Their difference, ×12
Magnified detail of the difference between the two captures: dense multicolored noise.
Left: the same region of both captures, swapped every half second. The animation is lossless, so every pixel that appears to move is a pixel that moved. Right: the difference itself, amplified ×12.

The scale of it, across the four-device lit baseline series:

80.82%
of this pair's color values differ between two consecutive captures
98.89%
of the frame's byte positions changed at least once across the ten-capture series
0
repeated final images in 320 captures across every retained series, lit and dark

That last figure is the one that answers the worry. Every final photograph taken in this work was distinct from every other, under the most favorable conditions for a repeat that could reasonably be arranged: a device that never moved, a scene that never changed, and shots taken back to back as fast as the hardware allows.

One scope note, because it matters later: that claim is about the final photograph. The preview frames in input 3 are a different story (section 4).

3A scene with nothing in it

The bookshelf above is a busy scene: edges, text, texture, color. The advice that circulates for camera entropy is to photograph something like it, and the worry that circulates alongside is the opposite case. What if there is nothing to look at? Is a blank wall a weak seed?

It was measured directly. A blank white wall, lit by ordinary window light, photographed on a Pi Zero resting motionless: the maximally boring capture this feature can produce, no scene detail, no camera motion, no artificial light. These are the exact bytes the instrumented v0.8.7 build fed into its chain.

Capture 1, as recorded
First capture: a featureless pale wall with a soft light gradient, nothing in frame.
Capture 2, three frames later
Second capture of the same wall, visually identical to the first.
The blank wall, twice, on a motionless device. The wall is white; auto white balance renders it with a pale blue-green cast, and the soft gradient is part room light, part natural vignetting in the camera's optics. This is the most-alike pair among the 45 pairings of the run's ten captures (frames 03 and 06).

There is nothing to tell apart by looking. In the data, even this most-alike pair differs in 77.69% of the frame's color values. In the magnified crop, everything that moves is noise, because noise is all there is. And the same subtraction as before shows the whole frame at once: the wall subtracts away, and a dense field of per-pixel sensor noise remains.

Captures 1 and 2 alternating, 96×96 px at 6×
Animation alternating between the same magnified region of the two wall captures every half second: the surface crawls with noise.
Difference ×40
The difference between the two wall captures amplified forty times: dense multicolored noise filling the entire frame.
Left: the center of the frame, 96×96 pixels at 6×, alternating between the captures every half second, lossless; 88.3% of the pixels in the crop differ. Right: the difference between the two captures, amplified ×40 (fixed linear gain, clipped), the same processing as the bookshelf plates so the two sets compare directly.

The numbers behind the plates. Like every figure in this analysis, they come from the more conservative of the two estimation methods used, the one sized to what an attacker guessing a seed actually faces; the method and its limits are in the companion document.

1,490,163
bits, stationary blank wall, worst of the 45 pairings of its ten captures. 5,820× what a seed needs
20 of 20
preview frames distinct in the stationary run's window. Hand shake is not what the entropy rests on
<10%
difference in measured unpredictability, blank wall versus bookshelf, same device

A featureless scene is not a degenerate scene. The blank wall landed within 10% of the same unit's bookshelf figure, and essentially every byte position in the frame (691,192 of 691,200) changed at least once across its ten captures, against 98.89% for the bookshelf. Scene structure contributes almost nothing to these figures; the entropy is per-pixel sensor noise, and a uniformly lit field exercises it as fully as a cluttered one. The worry about the blank wall conflates two different things, a featureless scene and a light-starved sensor, and the measurements separate them cleanly: what decides everything is whether light reaches the sensor at all, not what the light is bouncing off. The next section is about what happens when it stops arriving.

The approach is well established

Cloudflare generates part of its production randomness from a camera aimed at a wall of lava lamps, LavaRand, in service since 2017; Silicon Graphics patented the idea in 1996. Those systems point a camera at a deliberately unpredictable process. The measurements here say the choice of subject is the least important part: point it at anything, so long as the anything is lit.

4When no light reaches the sensor

Everything above happened in the light. This section is what the same hardware and the same code do when essentially no light reaches the sensor at all, and it is where this analysis found the things the project should fix. To be clear about the bottom line before the detail: every capture whose full chain was measured, including every light-starved one, cleared the 256 bits a 24-word seed needs, and no capture anywhere in this work was shown to fall short. (One earlier round, behind engineered light seals, measured final images alone, without the preview layer; its lowest final was 173 bits, and whether those chains cleared cannot be known.) What follows is about how thin the clearance ran, and about a layered design quietly becoming a single layer without anything noticing.

Be precise about the condition, because “dark” undersells it: in these captures the lens was completely or effectively blocked. That is not a dim room; it is the device set down lens-down on a desk in shadow, or the lens deliberately sealed. Nothing between ordinary lighting and that near-blackout was measured, and the few runs where a small light leak crept in measured roughly seven to over a hundred times higher than their properly blocked siblings, so everything below lives at the blackout end of the axis, not merely in low light. The route that matters is the mundane one: face-down on a desk in ordinary shadow, no seal, no enclosure, no intent. (Deliberately engineered light seals were also tried; the plain desk produced lower figures than any of them. The companion document carries that comparison.)

What the final image has left

Capture, as recorded
A face-down capture as recorded: black.
Same capture, ×60
The same capture amplified sixty times: a flat green field.
A face-down capture, as recorded and amplified ×60 (fixed linear gain, clipped).

Read the amplified plate carefully, because the flat green field is not the entropy. It is the green channel sitting at exactly 1 across nearly the whole frame, a constant pedestal that amplification turns into a wash; green rises first because the sensor has twice as many green photosites. The information is in the sparse variation on top of that pedestal, and there is almost none of it left.

Subtracting this run's two most-alike captures makes the point numerically: 105 of the frame's 691,200 values differ, 0.0152%, and the pair measures 113 bits, below the 256 a 24-word seed needs. Set beside it, the lit bookshelf difference from section 2, at one fifth the amplification: 1.5 million bits against 113. The distance between those two plates is what light is worth.

Worst pair difference ×60, dilated
The difference between the two darkest captures: a nearly black field with a scattering of isolated specks.
Lit difference ×12, for comparison
The lit bookshelf difference at one fifth the amplification: dense noise across the whole frame.
Left: the difference between the run's two most-alike captures, amplified ×60 and 5×5 max-dilated; without the dilation its 105 changed values are invisible. Right: the lit bookshelf difference, ×12, no dilation.

That 113-bit final image is the lowest measured anywhere in this work, and it was not unique: face-down desk runs on one board, across two capture rounds and separate boots, produced final images of 113, 177, 190 and 221 bits, each below the 256-bit requirement on its own. Session-to-session spread under a nominally unchanged setup is enormous: the same desk position also produced 26,318 bits minutes earlier with nothing moved, a run the light test flags as contaminated by stray light, which is itself the lesson: an imperceptible leak can swing the figure a hundredfold between sessions. So these are points from a wide distribution, not a floor. But they establish the reachable state: setting the device down lens-down on an ordinary desk can push the final photograph below what a 24-word seed needs, with no intent and no equipment.

What the safety net actually held

A final image below 256 was supposed to be what the preview layer exists for: up to 50 more frames, each a separate exposure, chained ahead of it. These captures record what that layer actually held, and with the lens blocked the answer is: almost nothing, and sometimes exactly nothing.

A dark live preview frame, ×60
A live dark preview frame amplified sixty times: a nearly solid green field.
Two adjacent live frames, difference ×60
The difference between two adjacent live preview frames: sparse green specks on black.
Left: live frame 18 of the 113-bit run's preview window, amplified ×60 (fixed linear gain, clipped). Right: the difference between adjacent live frames 18 and 19, 820 changed color bytes (0.47%), amplified ×60, no dilation. The black constant frame that filled 45 of the window's 50 slots is stated rather than pictured: its SHA-256 (6f77f852…) is computable from the panel size alone.

The left plate is one of only four live frames in the 113-bit run's 50-slot window, and it looks like the amplified final image for a reason: it is the same near-binary green plane, 99.4% of its pixels at green = 1, the whole frame one rounding event, the green channel crossing the pipeline's threshold everywhere at once. Of the other 46 slots, 45 held a pure-black constant frame, byte for byte, and one held a cached repeat of a live frame. The right plate is why that distinction matters: two adjacent live frames differ in just 820 of their color bytes, and that sparse flicker is the whole of the layer's genuine per-read contribution.

The mechanism: with essentially no signal arriving, the preview path quantizes the frame all the way to zero. When that happens, a slot in the window holds pure black, every value identical, and an attacker can compute that frame, and its contribution to the chain, from the panel dimensions alone. Hashing a value the attacker already knows adds zero unpredictability, no matter how many times it is hashed. In the measured dark windows on the standard panel, 46 to 50 of the 50 slots held that constant or a repeat.

On one of the three boards it went all the way: in six out of six lens-blocked runs, across two boots, all 50 slots were the known constant, and the preview layer contributed exactly zero. The seed rested on the final image alone. The thinnest of those runs totaled 389 bits against the required 256: a 1.52× margin, on an estimator that if anything overstates. (For a 12-word seed the same run clears its 128-bit requirement at 3.0×.)

Three findings sharpen this from an anecdote into a design problem:

  1. The failure is common-cause. Both layers watch the same scene through the same auto-exposure. The light starvation that thins the final image is the same starvation that collapses the preview window, and it also thins what little the surviving live frames carry (the worst adjacent live pair measured 469 bits). The layers do not fail independently, so counting layers is not counting safety.
  2. Which layer goes first is a per-unit accident. Under the same blackout condition, auto-exposure settled at its gain ceiling on three of the four units and 10% lower on the fourth, stably, in every recorded session. Every unit at the ceiling that had its window measured produced at least a few live frames; the below-ceiling unit produced none, ever. The correlation is unanimous but is exactly that, a correlation over four units; nothing in the code controls or even observes which side of the knife-edge a device sits on.
  3. There is a second route to the same state. Holding a button down through the preview causes the code to return before appending any frame, deterministically, on every measured attempt: zero preview frames, in any lighting. This is a quirk introduced in v0.7.0 by an unrelated crash fix. A blocked lens and a held button are independent, accidental, and both reduce the four-input chain to one meaningful input.
You cannot trust the screen here

In this blocked-lens condition, what the screen shows tells you nothing about what the capture carried. In our testing, a pure black review screen came from captures holding over a hundred times what a seed needs, and a faint noisy image from captures far below it. The working is in the companion document.

What the review screen displayed
The review screen's display for a capture from the 113-bit run: a saturated green field broken by patches of black speckle.
The review screen's own output (autocontrast, cutoff 2, rendered at the image library version the release ships) for a capture from the run measuring 113 bits, less than half of what a 24-word seed needs. It looks like noise; the measurement says it carried almost none.

What would actually fix it (for the project, not the user): append the frame before checking the button, so a held button cannot skip the window; and more fundamentally, have the code check what this analysis measured, a count of distinct frame digests in the window, before accepting a capture. Appending more frames is no fix on its own, because with the lens blocked the appended frames are the known constant. The companion document carries the full accounting and the measured evidence behind each recommendation.

Scope, stated honestly in both directions: this is a real design finding, not a break. No capture was ever shown to produce a weak seed; the thinnest margin observed was 1.52× on a deliberately conservative estimator, in a state (face-down in shadow, or a held button) that no ordinary capture visits. But the margins in that state are decided by per-unit accidents and session-to-session luck spanning a factor of a hundred or more, the design's redundancy argument does not hold there, and nothing on the device would notice if a future hardware or tuning change ate the remaining 1.52×.

5What you should do

Everything above compresses to one instruction and two habits.

Capture in a lit scene

Tap the button, do not hold it

Watch the preview, and know what watching buys you

6What you have to trust

The measurements say the camera path supplies abundant entropy in the light and cleared the requirement everywhere it was measured. They cannot say your particular seed did, and no measurement ever can. That gap is worth stating precisely, because image entropy sits in a different trust relationship from the other ways SeedSigner can make a seed, and the difference is structural rather than a matter of code quality.

Three steps, and only two of them transfer

Trusting a camera-generated seed is really a chain of three:

  1. Verify the process. Read the code and establish what it does with the bytes. That is what these two documents are: the chain composition, the release history, and measurements of what the code actually feeds into SHA-256.
  2. Verify you are running that code. Source review says nothing about the binary on your microSD card. Release images have been reproducible since v0.7.0, by the project's own statement, and the build procedure is documented separately, so the strong form is to build from source and confirm you get the image the project published, byte for byte. The weaker form is to check the published manifest hash and its PGP signature. That matters more than it sounds: the project's own README states that release images are prepared and signed by one individual, who is the sole holder of the signing keys. Reproducible builds exist precisely so that trust does not have to end there. Skipping this step makes step 1 decorative.
  3. Choose the scene and watch the capture. The last control in the chain is performed by a person, and after section 4 its scope is clear: watching confirms the camera is alive, and choosing a lit scene is what actually provisions the entropy. Verified code, verified to be the code that is running, still cannot make that choice for you.

Steps 1 and 2 are done once and are the same for everyone. Step 3 is the only per-capture control, and only you can ever perform it.

Why the input cannot be exported

The obvious fix would be for the device to write out the bytes it hashed, so someone could check them elsewhere. Consider what that would take. The entropy inputs are enormous: 691,200 bytes for the final image, plus fifty preview frames at 230,400 bytes each, roughly 12 MB in total. SeedSigner's only data channels are the camera coming in and the screen going out, and a QR code carries a few kilobytes; 12 MB is upwards of four thousand of them. The only practical channel would be the microSD card.

And that image data is your secret. It is what the seed is derived from, so holding it is effectively holding the seed. SeedSigner is built never to write a secret to persistent storage. On the reviewed release the entropy inputs stay in RAM and are released once the seed is derived.

Image entropy puts an extraordinary quantity of unpredictability into your seed. The price is that you can only verify the process, but not the individual outcomes.

Dice differs on exactly this point, and it is the only difference that matters here: rolls are small enough to write on paper, so an individual dice result stays externally checkable forever, while the camera data exists nowhere but the device that hashed it (the dice analysis works through that method).

None of which makes camera entropy weaker than dice. A lit capture feeds the chain orders of magnitude more unpredictability than a dice session does. But once a camera seed is generated, no one can go back and confirm anything about it, which is why the process verification above is the whole of what can be offered.

7Checking this analysis

Every figure quoted here was computed from raw camera frames that are published alongside this document, roughly 340 MB across 32 capture runs on 4 physical devices, exactly as the device handed them to SHA-256. Nothing rests on a number you have to take on trust.

The working is in the companion document, data and methodology: the canonical figures with the estimator behind each one, the preview-window structure of every dark run, how the instrumented builds were made and provenance-locked to the release, every release from 2021 checked one by one, every concern raised with its disposition and the weakest point of that disposition, what was not verified, and the commands that recompute every number from the published frames.

It takes an AI-first approach: dense, declarative, and ordered so a reviewer can work claim by claim rather than follow an argument. That structure is what makes it cheap to check, and pointing a model at it is the fastest way to get an answer. It is also perfectly readable if you would rather go through it yourself.

Corrections and challenges are welcome, particularly on the measurements. There is no published external review of this path that we are aware of: no independent measurement and no third-party reproduction of these figures. The raw captures ship precisely so that can change. Work that contradicts these numbers is more useful than work that confirms them. When responding, cite the revision you reviewed: the reporting guidance explains what to record and why it matters once these documents are revised.