Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cc2camera/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

__all__ = ["__version__", "commands"]

__version__ = "0.8.0"
__version__ = "0.9.0"
89 changes: 63 additions & 26 deletions cc2camera/image.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
Strict, self-contained recovery builder for the Elegoo Centauri Carbon 2
stock camera firmware family observed in two independent 8 MiB dumps.
stock camera firmware family observed in four independent 8 MiB dumps.

The tool:
* validates every invariant firmware byte against fingerprints derived
Expand All @@ -19,6 +19,7 @@
from __future__ import annotations

import argparse
from datetime import date
import hashlib
import json
import lzma
Expand Down Expand Up @@ -66,18 +67,22 @@
(5, 0x020000, "config"),
)

# The HWCONFIG type-12 record contains a unit-specific two-byte check value and
# a 94-byte encrypted/encoded UOID in its known 256-byte prefix. Some cameras
# append opaque bytes to that payload. The record length bounds those bytes;
# recovery preserves them exactly but normalizes them for invariant hashing.
# The HWCONFIG type-12 record contains a unit-specific three-byte check value,
# a 94-byte encrypted/encoded UOID, and a four-byte little-endian calendar date
# in its known 256-byte prefix. Some cameras append opaque bytes to that
# payload. The record length bounds those bytes; recovery preserves all of
# these unit-specific values exactly but normalizes them for invariant hashing.
HW_RECORD_START = 0x7D2000
HW_RECORD_PAYLOAD_START = HW_RECORD_START + 4
HW_KNOWN_PAYLOAD_END = HW_RECORD_PAYLOAD_START + 0x100
HW_SUPPORTED_PAYLOAD_LENGTHS = (0x100, 0x105)
HW_CHECK_START = 0x7D200B
HW_CHECK_START = 0x7D200A
HW_CHECK_END = 0x7D200D
HW_UOID_START = 0x7D2011
HW_UOID_END = 0x7D206F
HW_DATE_START = HW_UOID_END
HW_DATE_END = 0x7D2073
HW_SUPPORTED_DATES = frozenset((date(2026, 3, 2), date(2026, 4, 1)))

JFFS2_MAGIC = 0x1985
JFFS2_NODE_ACCURATE = 0x2000
Expand All @@ -94,8 +99,8 @@
b"dev_config.cfg",
}

SERIAL_PATTERN = re.compile(rb"^serial=(12PSSSS4[A-Z0-9]{28})\n$")
UOID_PATTERN = re.compile(rb"^12PSSSS4[A-Za-z0-9+/=]{86}$")
SERIAL_PATTERN = re.compile(rb"^serial=(12PSSSS[34][A-Z0-9]{28})\n$")
UOID_PATTERN = re.compile(rb"^12PSSSS[34][A-Za-z0-9+/=]{86}$")

# Exact byte ranges shared by every supported HWCONFIG variant.
REFERENCE_SEGMENTS = {
Expand All @@ -107,17 +112,18 @@
"hwconfig_between_identity_fields": (0x7D200D, 0x7D2011, "3c3351dc1dedcd627419e02de4fc8202e2d507d786c26f142b767fd9859d0cb4"),
}

# These hashes use the type-12 record's canonical 256-byte payload length and
# zero bytes in place of any declared extension. This retains exact checking of
# all known bytes without treating an unknown opaque extension as firmware.
# These hashes use the type-12 record's canonical 256-byte payload length,
# exclude the structurally validated unit fields, and use zero bytes in place
# of any declared extension. This retains exact checking of every other byte
# without treating unit data or an unknown opaque extension as firmware.
HWCONFIG_BEFORE_IDENTITY_SHA256 = (
"0e1514680c4e25e5c431adae5d4cb98bac14746b6e8fc30eb346ec75249f7cc5"
"e9ba6b36ab55dd0284e7cfcaf8c6ece3b4903bf953dcae34026244a5febd701d"
)
HWCONFIG_AFTER_UOID_SHA256 = (
"98d0beba4c7328a7237bc1a18fdd5e3da64c9f009e3b1c253ea39167a6dabd97"
HWCONFIG_AFTER_UNIT_FIELDS_SHA256 = (
"f2e10823638187acb4572437debe618bb1c27d0a8796d40445c3132cdd805601"
)
NORMALIZED_INVARIANT_SHA256 = (
"7346221d7814c8ef4412ced5f3795891f077f89ba64cf61d087e01fc5344f62f"
"4dee29ee9f996779a8af0f4e4a66ebad3c6c1ca353cf8b4287b4f1f9a8b12823"
)

ORIGINAL_PATCH_SHA256 = "5591f5350feabb73fd29e21ae72ee9c3c9dab0c6bb78e02178267e5cb2ab4780"
Expand Down Expand Up @@ -1178,12 +1184,12 @@ def normalized_hwconfig_before_identity(image: bytes) -> bytes:
)


def normalized_hwconfig_after_uoid(
def normalized_hwconfig_after_unit_fields(
image: bytes, record: dict[str, Any]
) -> bytes:
extension_length = record["extension_length"]
return (
image[HW_UOID_END:HW_KNOWN_PAYLOAD_END]
image[HW_DATE_END:HW_KNOWN_PAYLOAD_END]
+ b"\0" * extension_length
+ image[record["record_end"]:CONFIG_START]
)
Expand All @@ -1200,10 +1206,32 @@ def invariant_bytes(
+ (0x100).to_bytes(2, "little")
+ image[HW_RECORD_PAYLOAD_START:HW_CHECK_START]
+ image[HW_CHECK_END:HW_UOID_START]
+ normalized_hwconfig_after_uoid(image, record)
+ normalized_hwconfig_after_unit_fields(image, record)
)


def decode_hwconfig_date(image: bytes) -> str:
year = int.from_bytes(image[HW_DATE_START:HW_DATE_START + 2], "little")
month = image[HW_DATE_START + 2]
day = image[HW_DATE_START + 3]
try:
value = date(year, month, day)
except ValueError as exc:
raise ValidationError(
"The unit-specific HWCONFIG date field is not a valid "
f"little-endian year/month/day value ({year:04d}-{month:02d}-{day:02d})"
) from exc
if value not in HW_SUPPORTED_DATES:
supported = ", ".join(
item.isoformat() for item in sorted(HW_SUPPORTED_DATES)
)
raise ValidationError(
"The unit-specific HWCONFIG date field is not one of the physically "
f"observed values {supported} (value={value.isoformat()})"
)
return value.isoformat()


def identify_hwconfig_variant(image: bytes) -> tuple[str, dict[str, Any]]:
record_type = int.from_bytes(
image[HW_RECORD_START:HW_RECORD_START + 2], "little"
Expand Down Expand Up @@ -1337,11 +1365,11 @@ def analyze_image(
HWCONFIG_BEFORE_IDENTITY_SHA256,
normalized_hwconfig_before_identity(image),
),
"hwconfig_after_uoid": (
HW_UOID_END,
"hwconfig_after_unit_fields": (
HW_DATE_END,
CONFIG_START,
HWCONFIG_AFTER_UOID_SHA256,
normalized_hwconfig_after_uoid(image, hwconfig_record),
HWCONFIG_AFTER_UNIT_FIELDS_SHA256,
normalized_hwconfig_after_unit_fields(image, hwconfig_record),
),
}
for name, (start, end, expected_hash, normalized_bytes) in (
Expand All @@ -1361,8 +1389,8 @@ def analyze_image(
if not ok:
errors.append(
f"{name} 0x{start:06X}-0x{end - 1:06X} does not match "
"the supported reference firmware after normalizing the "
"HWCONFIG extension"
"the supported reference firmware after normalizing "
"HWCONFIG unit fields and extension"
)

actual_invariant_hash = sha256(invariant_bytes(image, hwconfig_record))
Expand Down Expand Up @@ -1393,11 +1421,17 @@ def analyze_image(
)

check_value = image[HW_CHECK_START:HW_CHECK_END]
if check_value in (b"\x00\x00", b"\xFF\xFF"):
if check_value in (b"\x00" * 3, b"\xFF" * 3):
warnings.append(
"The unit-specific two-byte HWCONFIG check value is all-zero/all-FF"
"The unit-specific three-byte HWCONFIG check value is all-zero/all-FF"
)

try:
hwconfig_date = decode_hwconfig_date(image)
except ValidationError as exc:
errors.append(str(exc))
hwconfig_date = "invalid"

config = image[CONFIG_START:CONFIG_END]
try:
config_info = extract_serial_and_config_info(
Expand Down Expand Up @@ -1438,6 +1472,7 @@ def analyze_image(
"system_state": system_state,
"system_patch_sha256": patch_hash,
"hwconfig_check_hex": check_value.hex(),
"hwconfig_date": hwconfig_date,
"uoid": uoid,
"uoid_sha256": sha256(uoid),
"serial_payload": config_info["serial_payload"],
Expand Down Expand Up @@ -1543,6 +1578,7 @@ def format_analysis(analysis: dict[str, Any], show_serial: bool = False) -> str:
Unit-specific data
------------------
HWCONFIG check bytes: {analysis['hwconfig_check_hex']}
HWCONFIG unit date: {analysis['hwconfig_date']}
HWCONFIG UOID: {uoid_display}
UOID SHA-256: {analysis['uoid_sha256']}
serial.cfg source: {analysis['serial_source']}
Expand Down Expand Up @@ -1779,6 +1815,7 @@ def build_recovery(
"hwconfig_extension_sha256": analysis[
"hwconfig_extension_sha256"
],
"hwconfig_date": analysis["hwconfig_date"],
"system_state_before": analysis["system_state"],
"system_state_after": output_analysis["system_state"],
"serial_source": analysis["serial_source"],
Expand Down
12 changes: 10 additions & 2 deletions hardware-recovery/CC2_RECOVERY_VERIFICATION.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# CC2 camera recovery tool — independent verification

> **Historical verification record:** This document describes the v1.1.0 tool
> and the two physical dumps available on 2026-08-22. It is retained as an
> audit record, not as the current compatibility contract. See
> [TECHNICAL_DETAILS.md](TECHNICAL_DETAILS.md) for the current supported
> HWCONFIG/identity structures and [TEST_RESULTS.md](TEST_RESULTS.md) for the
> four-camera validation record.

Review date: 2026-08-22
Reviewed tool: `cc2_sig_tool.py` v1.1.0

Expand Down Expand Up @@ -51,9 +58,10 @@ At the image level, yes. Both supplied bricked configs retain one unambiguous CR

The result for both real units is deterministic and matches the previously documented output hashes. This is sufficient to verify image construction, identity preservation, and filesystem consistency. Only an actual flash/readback/boot test can establish electrical and runtime recovery for a particular board.

## Cross-serial behavior and residual risk
## Historical cross-serial behavior and residual risk

Cross-serial support was directly verified on two real units and structurally tested on a third synthetic identity.
At the time of this review, cross-serial support was directly verified on two
real units and structurally tested on a third synthetic identity.

The validator normalizes the observed five-byte opaque HWCONFIG extension and
excludes the known unit-specific HWCONFIG fields and mutable config log from
Expand Down
36 changes: 29 additions & 7 deletions hardware-recovery/REFERENCE_FINGERPRINTS.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"start": "0x7E0000",
"validation": "JFFS2 structure/CRCs, known names, unambiguous serial"
},
"format_version": 4,
"format_version": 5,
"full_size": 8388608,
"invariant_definition": {
"excluded_or_normalized_ranges": [
Expand All @@ -27,14 +27,19 @@
},
{
"end_inclusive": "0x7D200C",
"reason": "unit-specific HWCONFIG check value",
"start": "0x7D200B"
"reason": "unit-specific three-byte HWCONFIG check value",
"start": "0x7D200A"
},
{
"end_inclusive": "0x7D206E",
"reason": "unit-specific 94-byte HWCONFIG UOID",
"start": "0x7D2011"
},
{
"end_inclusive": "0x7D2072",
"reason": "unit-specific little-endian year/month/day field",
"start": "0x7D206F"
},
{
"end_inclusive": "0x7D2108",
"reason": "observed five-byte opaque type-12 extension, normalized to zero bytes when present",
Expand All @@ -43,14 +48,14 @@
],
"included": "All bytes from 0x000000 through 0x7DFFFF except or normalized as listed below"
},
"normalized_invariant_sha256": "7346221d7814c8ef4412ced5f3795891f077f89ba64cf61d087e01fc5344f62f",
"normalized_invariant_sha256": "4dee29ee9f996779a8af0f4e4a66ebad3c6c1ca353cf8b4287b4f1f9a8b12823",
"hwconfig_record": {
"record_type": 12,
"known_payload_length": 256,
"supported_payload_lengths": [256, 261],
"extension_handling": "For payload length 261, preserve all five extension bytes exactly, report their SHA-256, and normalize them to zero for invariant comparison; reject other payload lengths",
"normalized_before_identity_sha256": "0e1514680c4e25e5c431adae5d4cb98bac14746b6e8fc30eb346ec75249f7cc5",
"normalized_after_uoid_sha256": "98d0beba4c7328a7237bc1a18fdd5e3da64c9f009e3b1c253ea39167a6dabd97",
"normalized_before_identity_sha256": "e9ba6b36ab55dd0284e7cfcaf8c6ece3b4903bf953dcae34026244a5febd701d",
"normalized_after_unit_fields_sha256": "f2e10823638187acb4572437debe618bb1c27d0a8796d40445c3132cdd805601",
"observed_payloads": [
{
"payload_length": 256,
Expand All @@ -59,9 +64,21 @@
{
"payload_length": 261,
"extension_hex": "0000029840"
},
{
"payload_length": 261,
"extension_hex": "0000000840"
}
]
},
"identity_fields": {
"check_range": "0x7D200A-0x7D200C",
"date_encoding": "little-endian uint16 year, uint8 month, uint8 day; only physically observed values are accepted",
"date_range": "0x7D206F-0x7D2072",
"observed_date_values": ["2026-03-02", "2026-04-01"],
"serial_prefixes": ["12PSSSS3", "12PSSSS4"],
"uoid_prefixes": ["12PSSSS3", "12PSSSS4"]
},
"reference_images": [
{
"label": "Discord bricked image",
Expand All @@ -87,9 +104,14 @@
"label": "Third camera bricked image",
"sha256": "75d676ba4e478572777761d8caea43a6774a51d812050e4ba6ffa371a8ccc1e1",
"state": "stock system, exhausted config, 261-byte HWCONFIG record"
},
{
"label": "Fourth camera bricked image",
"sha256": "574c71e6572093ea1b65c3ca9c3e5e1dd1e55d71443ca7b282db8ce700c45c85",
"state": "stock system, exhausted config, 12PSSSS3 identity, 261-byte HWCONFIG record"
}
],
"scope": "Exact CC2 stock-camera firmware family and the 256- and 261-byte type-12 payload lengths observed in three independent cameras; the five-byte extension contents are normalized",
"scope": "Exact CC2 stock-camera firmware family, 12PSSSS3 and 12PSSSS4 identities, and the 256- and 261-byte type-12 payload lengths observed in four independent cameras; unit check/date fields and five-byte extension contents are normalized",
"segments": {
"boot": {
"end_exclusive": "0x040000",
Expand Down
Loading
Loading