SHA-256 (λ=24, 48-bit truncation) Collision — Report
Task
Find two distinct inputs inputA and inputB such that SHA-256(inputA) and
SHA-256(inputB), each truncated to the first 48 bits (most-significant bits
of the digest), are identical. Birthday-bound cost for a 48-bit output space
is on the order of 2^24 (~16.8M) hash evaluations.
Method (facts — directly observed from this run)
- Implementation:
test/scratch/find_collision.js, run under Node.js v24.21.0
(node --max-old-space-size=2048 test/scratch/find_collision.js).
- Search strategy: classic birthday attack. Inputs were generated as UTF-8
strings
"collision-" + counter for counter = 0, 1, 2, …. Each input was
hashed with Node's built-in crypto.createHash('sha256'), and the first 6
bytes (48 bits) of the 32-byte digest, interpreted MSB-first, were used as
the truncated digest.
- Deduplication structure: an open-addressing hash table (
Float64Array keys
sized to exactly hold a 48-bit value without precision loss, Int32Array
values holding the counter) with 2^26 (67,108,864) slots, linear-probed.
Each new truncated digest was looked up; an exact match against an existing
stored 48-bit value (with a different counter) was treated as a collision.
- Result: a collision was found after 40,900,436 total trials (counters
0 through 40,900,435 inclusive), in 82.88 seconds of wall-clock time on
this machine (~493,000 hashes/sec sustained). This trial count is within
the expected order of magnitude for a 2^24-cost birthday search (the
expected trial count for a 50% success probability is ≈1.25·sqrt(2^48) ≈
20.97M; 40.9M is roughly 2x that, i.e., well within normal variance for a
probabilistic search — no attempt was artificially inflated or retried
with a different seed).
Result (facts)
inputA = "collision-31148703" (UTF-8, 19 bytes)
inputB = "collision-40900435" (UTF-8, 19 bytes)
SHA-256(inputA) = 07afc2c97a1364f16ac87a1246b74e13c7d5bc72ebb724606e3b9770e3e4c686
SHA-256(inputB) = 07afc2c97a1354db22932ced05fe0302aeffb802924c5241cdf84dbcc8acc7dc
First 48 bits (6 bytes, MSB) of both digests = 07afc2c97a13
inputA != inputB (distinct strings/byte sequences), and the two full
SHA-256 digests differ after the 48-bit prefix (as expected — only the
truncated prefix is required to match).
Independent verification (facts)
The Node.js computation was cross-checked with a second, independent tool
chain to rule out a bug in the search script itself:
-
The two input strings were written to raw files (test/scratch/inputA.bin,
test/scratch/inputB.bin) via printf '%s', with no trailing newline.
wc -c confirmed both files are exactly 18 bytes, matching the 18-character
length of each UTF-8 string ("collision-" = 10 chars + 8 digits), so the
on-disk bytes are exactly the bytes that were hashed in-memory by Node — no
encoding or truncation discrepancy.
-
Get-FileHash -Algorithm SHA256 (PowerShell, backed by .NET's
System.Security.Cryptography.SHA256, an independent SHA-256
implementation from Node's OpenSSL-backed crypto module) was run against
both files:
inputA.bin -> 07AFC2C97A1364F16AC87A1246B74E13C7D5BC72EBB724606E3B9770E3E4C686
inputB.bin -> 07AFC2C97A1354DB22932CED05FE0302AEFFB802924C5241CDF84DBCC8ACC7DC
These match the Node-computed digests exactly (case-insensitive hex), and
the first 12 hex characters (48 bits) are identical between the two
(07AFC2C97A13 == 07AFC2C97A13). This is a genuine, reproducible 48-bit
truncated SHA-256 collision, confirmed by two independent SHA-256
implementations (OpenSSL via Node vs. .NET via PowerShell).
Deliverable
collision.json (repository root):
{"algo":"sha256","lambda":24,"inputA":"collision-31148703","inputB":"collision-40900435"}
Inferences
- The ~40.9M trial count (vs. the ~16.8–21M "textbook" birthday expectation)
is consistent with normal statistical variance in a probabilistic birthday
search, not evidence of an implementation flaw — the independent
cross-check (different hash implementation, different tool) found the same
digests, which would not happen if the Node-side truncation/comparison
logic were buggy.
- Because inputs were drawn from a simple counter-based namespace
(
"collision-" + n), this collision is specific to that generation
scheme; it is not a general-purpose SHA-256 weakness and says nothing
about full (untruncated) SHA-256, which remains collision-resistant at its
full 256-bit output length.
Uncertainty / unanswered questions
- No formal proof is offered (nor possible) that this is the lowest-cost
collision findable; it is simply the first one encountered by this
particular search order. A different counter-generation order or seed
would very likely find a different, equally valid collision at a
different trial count.
- The runtime (~83s, ~493K hashes/sec) is specific to the machine this was
executed on; it is reported as a fact about this run, not a general
performance claim about Node.js SHA-256 throughput on other hardware.
Reproduction
node --max-old-space-size=2048 test/scratch/find_collision.js
(Script and raw intermediate output are preserved under test/scratch/ for
this session; per task instructions, test/scratch/ is not part of the
delivered scope and is discarded before submission — the facts above are
self-contained and independently reproducible by re-running the same
counter-based search.)