mirror of
https://github.com/Astatin3/CC2.git
synced 2026-06-08 16:08:00 -06:00
Sucessfully repack OTA!
This commit is contained in:
+2
-1
@@ -8,4 +8,5 @@ aes = "0.8.4"
|
||||
cbc = "0.1.2"
|
||||
clap = { version = "4.5.37", features = ["derive"] }
|
||||
rand = "0.8.5"
|
||||
sha2 = "0.10.9"
|
||||
rsa = "0.9.8"
|
||||
sha2 = { version = "0.10.9", features = ["oid"] }
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# OTA Unpack And Repack Guide
|
||||
|
||||
This guide describes how to unpack and repack a Centauri Carbon 2 OTA update using
|
||||
the tools in this directory.
|
||||
|
||||
## Build The Tool
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
```
|
||||
|
||||
The compiled tool is:
|
||||
|
||||
```bash
|
||||
target/debug/sig
|
||||
```
|
||||
|
||||
## Inspect A `.sig` File
|
||||
|
||||
Print parsed header information:
|
||||
|
||||
```bash
|
||||
target/debug/sig info path/to/file.sig
|
||||
```
|
||||
|
||||
Print raw unknown header bytes too:
|
||||
|
||||
```bash
|
||||
target/debug/sig info --raw path/to/file.sig
|
||||
```
|
||||
|
||||
Important fields:
|
||||
|
||||
- `encrypted`: whether the payload is AES-CBC encrypted.
|
||||
- `filesize`: plaintext payload size.
|
||||
- `stored_sha256`: SHA-256 of the payload stored in the header.
|
||||
- `payload_sha256_matches`: whether the stored hash matches the actual payload.
|
||||
- `signature_valid`: whether the RSA signature at `0x100..0x200` verifies.
|
||||
|
||||
## Unpack A Single `.sig`
|
||||
|
||||
```bash
|
||||
target/debug/sig unpack input.sig -o output-file
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
target/debug/sig unpack ota-package-list.json.sig -o ota-package-list.json
|
||||
target/debug/sig unpack cc2_eeb001_01.03.02.36_20260326171745.swu.sig -o update.swu
|
||||
target/debug/sig unpack release.zip.sig -o release.zip
|
||||
```
|
||||
|
||||
`unpack` removes the 512-byte `.sig` header. If the payload is encrypted, it also
|
||||
decrypts it with the Carbon 2 AES key and trims the result to the plaintext size
|
||||
stored in the header.
|
||||
|
||||
## Repack A Single `.sig`
|
||||
|
||||
Use `repack` to wrap a file in a valid signed `.sig` header:
|
||||
|
||||
```bash
|
||||
target/debug/sig repack input-file -o output.sig
|
||||
```
|
||||
|
||||
For encrypted inner files, use a template from the original package:
|
||||
|
||||
```bash
|
||||
target/debug/sig repack modified.swu \
|
||||
--template original.swu.sig \
|
||||
-o modified.swu.sig
|
||||
```
|
||||
|
||||
```bash
|
||||
target/debug/sig repack ota-package-list.json \
|
||||
--template original-ota-package-list.json.sig \
|
||||
-o ota-package-list.json.sig
|
||||
```
|
||||
|
||||
Template mode preserves metadata such as package type, version, IV, filename, and
|
||||
unknown header fields, then rewrites the size, payload hash, and RSA signature.
|
||||
|
||||
To force encryption without a template:
|
||||
|
||||
```bash
|
||||
target/debug/sig repack input-file --encrypt -o input-file.sig
|
||||
```
|
||||
|
||||
Without `--encrypt` and without a template, `repack` writes a plain signed `.sig`.
|
||||
|
||||
## Unpack A Full OTA Update
|
||||
|
||||
The full OTA package has this structure:
|
||||
|
||||
```text
|
||||
release.zip.sig
|
||||
└── release.zip
|
||||
├── cc2_...swu.sig
|
||||
│ └── cc2_...swu
|
||||
│ └── SWU CPIO archive contents
|
||||
└── ota-package-list.json.sig
|
||||
└── ota-package-list.json
|
||||
```
|
||||
|
||||
Use the helper script:
|
||||
|
||||
```bash
|
||||
scripts/undo-release-package.sh \
|
||||
cc2-01.03.02.36-ee76546c665bb272b43798813f60f8dd-release-abroad.zip.sig \
|
||||
release-unpacked
|
||||
```
|
||||
|
||||
This creates:
|
||||
|
||||
```text
|
||||
release-unpacked/
|
||||
├── templates/
|
||||
│ ├── release.zip.sig
|
||||
│ ├── cc2_...swu.sig
|
||||
│ └── ota-package-list.json.sig
|
||||
├── release-signed/
|
||||
│ ├── cc2_...swu.sig
|
||||
│ └── ota-package-list.json.sig
|
||||
├── release/
|
||||
│ ├── cc2_...swu
|
||||
│ └── ota-package-list.json
|
||||
├── swu/
|
||||
│ ├── .swu-manifest.json
|
||||
│ ├── sw-description
|
||||
│ ├── resource
|
||||
│ ├── uboot
|
||||
│ ├── boot0
|
||||
│ ├── kernel
|
||||
│ ├── rootfs
|
||||
│ └── cpio_item_md5
|
||||
└── repack.env
|
||||
```
|
||||
|
||||
Edit files under `release-unpacked/swu/` to change SWU contents. For example, to
|
||||
replace the rootfs image:
|
||||
|
||||
```bash
|
||||
cp -p test/rootfs_modified release-unpacked/swu/rootfs
|
||||
```
|
||||
|
||||
## Repack A Full OTA Update
|
||||
|
||||
After editing the unpacked contents, run:
|
||||
|
||||
```bash
|
||||
scripts/redo-release-package.sh \
|
||||
release-unpacked \
|
||||
repacked.zip.sig
|
||||
```
|
||||
|
||||
The script:
|
||||
|
||||
- Rebuilds the SWU CPIO archive from `release-unpacked/swu/`.
|
||||
- Recomputes `cpio_item_md5`.
|
||||
- Re-signs the SWU `.sig` using the original SWU `.sig` as a template.
|
||||
- Updates the hash in `ota-package-list.json`.
|
||||
- Re-signs `ota-package-list.json.sig` using its original template.
|
||||
- Rebuilds the release ZIP while preserving original ZIP timestamps and modes.
|
||||
- Re-signs the outer `.zip.sig` using the original outer `.sig` as a template.
|
||||
|
||||
If no files were changed, the rebuilt package should be bit-perfect:
|
||||
|
||||
```bash
|
||||
cmp repacked.zip.sig original.zip.sig
|
||||
```
|
||||
|
||||
If files were changed, the rebuilt package should differ, but all `.sig` headers
|
||||
should still validate:
|
||||
|
||||
```bash
|
||||
target/debug/sig info repacked.zip.sig
|
||||
```
|
||||
|
||||
To validate inner signatures, unpack the release ZIP and run `info` on the inner
|
||||
`.sig` files:
|
||||
|
||||
```bash
|
||||
target/debug/sig unpack repacked.zip.sig -o /tmp/release.zip
|
||||
unzip -q /tmp/release.zip -d /tmp/release
|
||||
target/debug/sig info /tmp/release/cc2_eeb001_01.03.02.36_20260326171745.swu.sig
|
||||
target/debug/sig info /tmp/release/ota-package-list.json.sig
|
||||
```
|
||||
|
||||
Each should show:
|
||||
|
||||
```text
|
||||
payload_sha256_matches: true
|
||||
signature_valid: true
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Inner `.sig` files are encrypted and signed.
|
||||
- The outer `.zip.sig` is plain but signed.
|
||||
- The RSA signature is stored in header bytes `0x100..0x200`.
|
||||
- The signature signs the SHA-256 digest stored at `0xE0..0x100`.
|
||||
- Reusing old signatures after modifying payloads will not work. Always use
|
||||
`repack` or `redo-release-package.sh` so signatures are regenerated.
|
||||
@@ -0,0 +1 @@
|
||||
This is a pile of badly made tooling to repack CC2 OTAs
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 || $# -gt 2 ]]; then
|
||||
printf 'Usage: %s input-dir [output.zip]\n' "$0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
indir=$1
|
||||
output=${2:-release.zip}
|
||||
|
||||
python3 - "$indir" "$output" <<'PY'
|
||||
import pathlib
|
||||
import stat
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
|
||||
def fail(message):
|
||||
print(message, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if len(sys.argv) != 3:
|
||||
fail("Usage: build-release-zip.sh input-dir [output.zip]")
|
||||
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
output = pathlib.Path(sys.argv[2])
|
||||
|
||||
if not root.is_dir():
|
||||
fail(f"input directory does not exist: {root}")
|
||||
|
||||
entries = sorted(path for path in root.iterdir() if path.is_file())
|
||||
if not entries:
|
||||
fail(f"input directory contains no regular files: {root}")
|
||||
|
||||
|
||||
def zip_date_time(path):
|
||||
modified = path.stat().st_mtime
|
||||
return tuple(__import__("time").localtime(modified)[:6])
|
||||
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with zipfile.ZipFile(output, "w") as archive:
|
||||
for src in entries:
|
||||
mode = stat.S_IMODE(src.stat().st_mode)
|
||||
info = zipfile.ZipInfo(src.name, date_time=zip_date_time(src))
|
||||
info.create_system = 3
|
||||
info.external_attr = (stat.S_IFREG | mode) << 16
|
||||
info.compress_type = zipfile.ZIP_DEFLATED
|
||||
archive.writestr(
|
||||
info,
|
||||
src.read_bytes(),
|
||||
compress_type=zipfile.ZIP_DEFLATED,
|
||||
compresslevel=6,
|
||||
)
|
||||
|
||||
print(f"wrote {output}")
|
||||
PY
|
||||
Executable
+100
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 || $# -gt 2 ]]; then
|
||||
printf 'Usage: %s unpacked-dir [output.zip.sig]\n' "$0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
workdir=$1
|
||||
output=${2:-$workdir/repacked.zip.sig}
|
||||
env_file="$workdir/repack.env"
|
||||
|
||||
if [[ ! -f "$env_file" ]]; then
|
||||
printf 'missing repack metadata: %s\n' "$env_file" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC1090
|
||||
source "$env_file"
|
||||
|
||||
required=(
|
||||
"$workdir/templates/release.zip.sig"
|
||||
"$workdir/templates/$SWU_SIG_NAME"
|
||||
"$workdir/templates/$OTA_SIG_NAME"
|
||||
"$workdir/release-signed/$SWU_SIG_NAME"
|
||||
"$workdir/release-signed/$OTA_SIG_NAME"
|
||||
"$workdir/release/$OTA_NAME"
|
||||
"$workdir/swu/.swu-manifest.json"
|
||||
)
|
||||
|
||||
for path in "${required[@]}"; do
|
||||
if [[ ! -e "$path" ]]; then
|
||||
printf 'missing required repack input: %s\n' "$path" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
cargo build --quiet
|
||||
|
||||
sig_bin=target/debug/sig
|
||||
build_dir="$workdir/build"
|
||||
release_dir="$build_dir/release"
|
||||
rebuilt_swu="$build_dir/$SWU_NAME"
|
||||
rebuilt_ota="$build_dir/$OTA_NAME"
|
||||
rebuilt_zip="$build_dir/$ZIP_NAME"
|
||||
|
||||
rm -rf "$build_dir"
|
||||
mkdir -p "$release_dir"
|
||||
|
||||
scripts/build-swu.sh "$workdir/swu" "$rebuilt_swu"
|
||||
"$sig_bin" repack "$rebuilt_swu" --template "$workdir/templates/$SWU_SIG_NAME" -o "$release_dir/$SWU_SIG_NAME"
|
||||
cp "$workdir/release/$OTA_NAME" "$rebuilt_ota"
|
||||
|
||||
python3 - "$rebuilt_ota" "$workdir/release/$OTA_NAME" "$release_dir/$SWU_SIG_NAME" "$SWU_SIG_NAME" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
ota_path = pathlib.Path(sys.argv[1])
|
||||
source_ota_path = pathlib.Path(sys.argv[2])
|
||||
swu_sig_path = pathlib.Path(sys.argv[3])
|
||||
swu_sig_name = sys.argv[4]
|
||||
|
||||
data = json.loads(ota_path.read_text())
|
||||
digest = hashlib.sha256(swu_sig_path.read_bytes()).hexdigest()
|
||||
matched = False
|
||||
for package in data.get("packages", []):
|
||||
if package.get("file") == swu_sig_name:
|
||||
package["hash"] = digest
|
||||
matched = True
|
||||
|
||||
if not matched:
|
||||
raise SystemExit(f"ota package list does not reference {swu_sig_name}")
|
||||
|
||||
updated = json.dumps(data, separators=(", ", ": "))
|
||||
ota_path.write_text(updated)
|
||||
source_ota_path.write_text(updated)
|
||||
PY
|
||||
|
||||
"$sig_bin" repack "$rebuilt_ota" --template "$workdir/templates/$OTA_SIG_NAME" -o "$release_dir/$OTA_SIG_NAME"
|
||||
|
||||
touch -r "$workdir/release-signed/$SWU_SIG_NAME" "$release_dir/$SWU_SIG_NAME"
|
||||
touch -r "$workdir/release-signed/$OTA_SIG_NAME" "$release_dir/$OTA_SIG_NAME"
|
||||
chmod --reference="$workdir/release-signed/$SWU_SIG_NAME" "$release_dir/$SWU_SIG_NAME"
|
||||
chmod --reference="$workdir/release-signed/$OTA_SIG_NAME" "$release_dir/$OTA_SIG_NAME"
|
||||
|
||||
scripts/build-release-zip.sh "$release_dir" "$rebuilt_zip"
|
||||
"$sig_bin" repack "$rebuilt_zip" --template "$workdir/templates/release.zip.sig" -o "$output"
|
||||
|
||||
printf 'rebuilt %s\n' "$output"
|
||||
|
||||
if [[ -n "${ORIGINAL_SIG:-}" && -f "$ORIGINAL_SIG" ]]; then
|
||||
if cmp -s "$output" "$ORIGINAL_SIG"; then
|
||||
printf 'bit-perfect match: %s\n' "$ORIGINAL_SIG"
|
||||
else
|
||||
printf 'warning: rebuilt output differs from %s\n' "$ORIGINAL_SIG" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 || $# -gt 2 ]]; then
|
||||
printf 'Usage: %s input.zip.sig [output-dir]\n' "$0" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
input=$1
|
||||
outdir=${2:-release-unpacked}
|
||||
|
||||
if [[ ! -f "$input" ]]; then
|
||||
printf 'input does not exist: %s\n' "$input" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -e "$outdir" && -n "$(ls -A "$outdir")" ]]; then
|
||||
printf 'output directory is not empty: %s\n' "$outdir" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$outdir" "$outdir/templates" "$outdir/release-signed" "$outdir/release" "$outdir/swu"
|
||||
|
||||
cargo build --quiet
|
||||
|
||||
sig_bin=target/debug/sig
|
||||
base=$(basename "$input")
|
||||
zip_name=${base%.sig}
|
||||
zip_path="$outdir/$zip_name"
|
||||
|
||||
cp -p "$input" "$outdir/templates/release.zip.sig"
|
||||
"$sig_bin" unpack "$input" -o "$zip_path"
|
||||
unzip -q "$zip_path" -d "$outdir/release-signed"
|
||||
|
||||
mapfile -t signed_files < <(python3 - "$zip_path" <<'PY'
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
with zipfile.ZipFile(sys.argv[1]) as archive:
|
||||
for item in archive.infolist():
|
||||
if not item.is_dir():
|
||||
print(item.filename)
|
||||
PY
|
||||
)
|
||||
|
||||
swu_sig_name=''
|
||||
ota_sig_name=''
|
||||
for name in "${signed_files[@]}"; do
|
||||
case "$name" in
|
||||
*.swu.sig) swu_sig_name=$name ;;
|
||||
ota-package-list.json.sig) ota_sig_name=$name ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$swu_sig_name" || -z "$ota_sig_name" ]]; then
|
||||
printf 'release zip must contain one .swu.sig and ota-package-list.json.sig\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
swu_name=${swu_sig_name%.sig}
|
||||
ota_name=${ota_sig_name%.sig}
|
||||
|
||||
cp -p "$outdir/release-signed/$swu_sig_name" "$outdir/templates/$swu_sig_name"
|
||||
cp -p "$outdir/release-signed/$ota_sig_name" "$outdir/templates/$ota_sig_name"
|
||||
|
||||
"$sig_bin" unpack "$outdir/release-signed/$swu_sig_name" -o "$outdir/release/$swu_name"
|
||||
"$sig_bin" unpack "$outdir/release-signed/$ota_sig_name" -o "$outdir/release/$ota_name"
|
||||
scripts/extract-swu.sh "$outdir/release/$swu_name" "$outdir/swu"
|
||||
|
||||
cat > "$outdir/repack.env" <<EOF
|
||||
ORIGINAL_SIG=$input
|
||||
ZIP_NAME=$zip_name
|
||||
SWU_SIG_NAME=$swu_sig_name
|
||||
SWU_NAME=$swu_name
|
||||
OTA_SIG_NAME=$ota_sig_name
|
||||
OTA_NAME=$ota_name
|
||||
EOF
|
||||
|
||||
printf 'unpacked %s to %s\n' "$input" "$outdir"
|
||||
+43
-20
@@ -29,21 +29,37 @@ pub struct Cli {
|
||||
/// repacking commands without changing the top-level parser API.
|
||||
#[derive(Subcommand)]
|
||||
pub enum Command {
|
||||
/// Strip the .sig wrapper and write the unpacked payload.
|
||||
Strip(StripArgs),
|
||||
/// Unpack the .sig wrapper and write the contained payload.
|
||||
#[command(alias = "strip")]
|
||||
Unpack(UnpackArgs),
|
||||
|
||||
/// Encrypt a file and wrap it in a .sig container.
|
||||
Encrypt(EncryptArgs),
|
||||
/// Print parsed .sig header information.
|
||||
Info(InfoArgs),
|
||||
|
||||
/// Repack a file into a signed .sig container.
|
||||
#[command(alias = "encrypt")]
|
||||
Repack(RepackArgs),
|
||||
}
|
||||
|
||||
/// Arguments accepted by the `strip` subcommand.
|
||||
/// Arguments accepted by the `info` subcommand.
|
||||
#[derive(Args)]
|
||||
pub struct InfoArgs {
|
||||
/// Input `.sig` file to inspect.
|
||||
pub input: PathBuf,
|
||||
|
||||
/// Print exact hex bytes for unknown/reserved header regions.
|
||||
#[arg(long)]
|
||||
pub raw: bool,
|
||||
}
|
||||
|
||||
/// Arguments accepted by the `unpack` subcommand.
|
||||
///
|
||||
/// The command mirrors the browser-based unpacker at
|
||||
/// <https://docs.opencentauri.cc/extras/cc2_update_decrypt.html>: read a `.sig`
|
||||
/// file, remove the header, decrypt the payload when needed, trim it to the
|
||||
/// payload size declared in the header, then write the resulting package bytes.
|
||||
#[derive(Args)]
|
||||
pub struct StripArgs {
|
||||
pub struct UnpackArgs {
|
||||
/// Input `.sig` file.
|
||||
///
|
||||
/// The file must begin with the Centauri/Elegoo `ELEG` magic value and must
|
||||
@@ -59,20 +75,19 @@ pub struct StripArgs {
|
||||
pub output: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Arguments accepted by the `encrypt` subcommand.
|
||||
/// Arguments accepted by the `repack` subcommand.
|
||||
///
|
||||
/// This command performs the inverse of `strip` for the subset of the format the
|
||||
/// tool understands: it encrypts a plaintext payload with the Carbon 2 AES key,
|
||||
/// writes a 512-byte `.sig` header, and appends the ciphertext. It does not yet
|
||||
/// create or verify any vendor signature material beyond the metadata needed for
|
||||
/// decryption.
|
||||
/// This command performs the inverse of `unpack`: it writes a 512-byte `.sig`
|
||||
/// header, signs the payload hash, and appends either plaintext or encrypted
|
||||
/// payload bytes. When a template is supplied, unknown header metadata and the
|
||||
/// template encryption mode are preserved by default.
|
||||
#[derive(Args)]
|
||||
pub struct EncryptArgs {
|
||||
pub struct RepackArgs {
|
||||
/// Input plaintext package file.
|
||||
///
|
||||
/// The bytes are copied into the encrypted payload with PKCS#7 padding. The
|
||||
/// original unpadded length is stored in the header so `strip` can recover
|
||||
/// the exact input bytes.
|
||||
/// With `--encrypt`, the bytes are copied into the encrypted payload with
|
||||
/// PKCS#7 padding. The original unpadded length is stored in the header so
|
||||
/// `unpack` can recover the exact input bytes.
|
||||
pub input: PathBuf,
|
||||
|
||||
/// Output `.sig` path.
|
||||
@@ -82,9 +97,18 @@ pub struct EncryptArgs {
|
||||
#[arg(short, long)]
|
||||
pub output: Option<PathBuf>,
|
||||
|
||||
/// Filename to store inside the `.sig` header.
|
||||
/// Encrypt the payload before writing it.
|
||||
///
|
||||
/// Defaults to the input file name. The `.sig` format reserves 48 bytes for
|
||||
/// Without a template, repack writes a plain signed `.sig` unless this flag
|
||||
/// is set. With a template, the command preserves the template encryption
|
||||
/// mode unless this flag is set, in which case output is encrypted.
|
||||
#[arg(long)]
|
||||
pub encrypt: bool,
|
||||
|
||||
/// Override the filename stored inside the `.sig` header.
|
||||
///
|
||||
/// Defaults to the template header filename when `--template` is provided,
|
||||
/// otherwise to the input file name. The `.sig` format reserves 48 bytes for
|
||||
/// this field, so values longer than 47 bytes are rejected to preserve the
|
||||
/// trailing NUL terminator used by existing packages.
|
||||
#[arg(long)]
|
||||
@@ -94,8 +118,7 @@ pub struct EncryptArgs {
|
||||
///
|
||||
/// Use this when you need reproducible output that can match an existing
|
||||
/// encrypted package byte-for-byte. The command reuses the template IV and
|
||||
/// opaque header metadata, then rewrites size, filename, and payload hash
|
||||
/// fields for the new encrypted payload.
|
||||
/// metadata, then rewrites size, filename, payload hash, and RSA signature.
|
||||
#[arg(long)]
|
||||
pub template: Option<PathBuf>,
|
||||
}
|
||||
|
||||
+266
-26
@@ -19,6 +19,11 @@ use cbc::cipher::{
|
||||
block_padding::{NoPadding, Pkcs7},
|
||||
};
|
||||
use rand::{RngCore, rngs::OsRng};
|
||||
use rsa::{
|
||||
RsaPrivateKey, RsaPublicKey,
|
||||
pkcs1v15::Pkcs1v15Sign,
|
||||
pkcs8::{DecodePrivateKey, DecodePublicKey},
|
||||
};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// Length of the fixed `.sig` header in bytes.
|
||||
@@ -37,6 +42,47 @@ const AES_KEY: [u8; 32] = [
|
||||
0x4E, 0x16, 0x1A, 0x8B, 0xEB, 0xB8, 0xF7, 0x20, 0x73, 0x7E, 0xE6, 0x0E, 0x7F, 0x8C, 0x7E, 0x68,
|
||||
];
|
||||
|
||||
/// RSA private key used to sign the SHA-256 digest stored in `.sig` headers.
|
||||
const RSA_PRIVATE_KEY_PEM: &str = r#"-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCgET+oNgUA1acA
|
||||
vyESnNsTy+Ae+k6Q/LLM5bh4sLfFeu0TT9Ka4SQa6LffDLJvaqjt++jsB2PwPvZc
|
||||
clHcG5yC2bNikz+x2WaHCQOI26vzy8oh8x+wLI77GfVG82mWpo6gHfyu51LBvCDo
|
||||
aDfghSUL7B9lIaCDb0dbBfpjTTrQr0UcUET9RvsrjMkyLI5jfBnd1VNTvFD9gngc
|
||||
wAmZ7s65UIEkKjmQZH+xK+gDa+oZBJpjz7vgXEezIVSDuZQKQFB/4E5YErftRwNp
|
||||
h7rngDTpyFYjaAvD9ZoM90exFWtRWMvjt2HNtDUOHjRI4C5XToqm/FfPxYAproJU
|
||||
PyE6ckFrAgMBAAECggEAIzAHhWvyp59QKiraE2RmCLEN4OF3ugnDKKXraqS2kXQX
|
||||
f+JRUvjhXgUAvsjkxPd2kXKKXrC1OJAuyl3bPv7W5jEDbU0feHJpRpAltcVMxLht
|
||||
BA+VTL5O5EZtlB5YfOS6f9p3vN9fYvV/anfWqMW8QiWzNSEyTxJ8ZjcnNwM4Rb2Y
|
||||
hMxDpQ8EMIK4Hwchmv8V2vNIY1CnIFz+i1U722PnnR6wDO4obFX2wh+6KCBuTrwW
|
||||
babpNhGpW2P/f33HoeVhoJkt4k7kogM45cqPwC9i3YKpEgeHdo6aRs+h/hhFk9BV
|
||||
NKks3lK6kY20KvGLyhauh9usKZRiONxn/w0YmB4CeQKBgQC2tuIqXg1KARaZ7FkY
|
||||
Bw/2sr2uCbiN3vgP30RMWNtrtCluZ49HSphV8Td69qgI6hXyFBzm1uaLA8gAZ6DR
|
||||
r9VYu6uQTLZDhMkd36pbcCQsvmF1Q9yifBd3nBHZWt6eek61mbK0MrBq2ofEDSAd
|
||||
dPV1VEAMthB20yAT7qI33XLj7QKBgQDgRPhATfkdVcRE0D4+XkIUmNUF7MLzRfxq
|
||||
ZvXhhxci0d2TtkIKhMzNFQrmIaIp8ex8mpbmyebmsLX6lINjvQAzmHGuk5niOR8w
|
||||
T5Lkj8WY9E7lw+clApo5TXLJunHxEdtamASdjRdI1lAGNhG4SzH/ea6OtR/HeziA
|
||||
kQ6NMOI/twKBgBlQjVVBYqX2MKNy04U4tUWAzjbmseM2GThZvqS1SvFJLNRXFMrT
|
||||
0vdVTFKFChLyG8hGcRqqe5aXF4a21Nk4e16n4cVEW5xPMW4qJvg0OU7ZsbcFh/Qb
|
||||
LUUtImvy4xUh7PXMLa45t6eWT2kiSGjMY5W17onUT8OmzLL2RRNoYxqhAoGABLZO
|
||||
RQOeZVhk/FEnzaWrW8VuTGaSHgxtZkrthaSR/uBL+IuOzavGpdR4Wyd/wcPchS22
|
||||
V/kMCfLSkAZI0HKrK2pbkSB2zkMG/bveSUEgFLulYLyCAcwRM30GGWj6dec7Jacm
|
||||
Ca1qPNSL7+V479dcoJKM8WCq30Uehc0Gcj8BsfcCgYBTicTc3S/o2jsJLRtXzFap
|
||||
eooocDD8Q0xIFRMZplDOvAnWXq6QJ0Xyeqlx9dk41dwWvnHHJGazZPZA4wijyI0T
|
||||
orTbpLkOKpMz0hlqy9j2/k+vgf004+xfDSAywtOPop9csXxFWPB5qmdjTONv5Zpn
|
||||
bzzXqOTAU1QZvdTdOBvbUw==
|
||||
-----END PRIVATE KEY-----"#;
|
||||
|
||||
/// RSA public key used to verify the header signature.
|
||||
const RSA_PUBLIC_KEY_PEM: &str = r#"-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAoBE/qDYFANWnAL8hEpzb
|
||||
E8vgHvpOkPyyzOW4eLC3xXrtE0/SmuEkGui33wyyb2qo7fvo7Adj8D72XHJR3Buc
|
||||
gtmzYpM/sdlmhwkDiNur88vKIfMfsCyO+xn1RvNplqaOoB38rudSwbwg6Gg34IUl
|
||||
C+wfZSGgg29HWwX6Y0060K9FHFBE/Ub7K4zJMiyOY3wZ3dVTU7xQ/YJ4HMAJme7O
|
||||
uVCBJCo5kGR/sSvoA2vqGQSaY8+74FxHsyFUg7mUCkBQf+BOWBK37UcDaYe654A0
|
||||
6chWI2gLw/WaDPdHsRVrUVjL47dhzbQ1Dh40SOAuV06KpvxXz8WAKa6CVD8hOnJB
|
||||
awIDAQAB
|
||||
-----END PUBLIC KEY-----"#;
|
||||
|
||||
type Aes256CbcDec = cbc::Decryptor<Aes256>;
|
||||
type Aes256CbcEnc = cbc::Encryptor<Aes256>;
|
||||
|
||||
@@ -59,6 +105,72 @@ struct Header {
|
||||
iv: [u8; 16],
|
||||
}
|
||||
|
||||
/// Human-readable view of every known `.sig` header field.
|
||||
///
|
||||
/// The format still has unknown regions. Those are exposed as non-zero counts
|
||||
/// and SHA-256 digests so different files can be compared without dumping large
|
||||
/// binary blobs to the terminal.
|
||||
#[derive(Debug)]
|
||||
pub struct HeaderInfo {
|
||||
/// Total input file length.
|
||||
pub total_len: usize,
|
||||
/// Number of bytes after the fixed 512-byte header.
|
||||
pub payload_len: usize,
|
||||
/// Big-endian magic as raw bytes.
|
||||
pub magic: [u8; 4],
|
||||
/// Raw package type byte at `0x04`.
|
||||
pub package_type_raw: u8,
|
||||
/// Low seven bits of the package type byte.
|
||||
pub package_type: u8,
|
||||
/// Whether the high bit of the package type byte is set.
|
||||
pub is_encrypted: bool,
|
||||
/// Header version major byte at `0x05`.
|
||||
pub version_major: u8,
|
||||
/// Header version minor byte at `0x06`.
|
||||
pub version_minor: u8,
|
||||
/// Currently unknown byte at `0x07`.
|
||||
pub byte_07: u8,
|
||||
/// Plaintext payload size from `0x08..0x10`.
|
||||
pub filesize: u64,
|
||||
/// NUL-terminated header filename from `0x10..0x40`.
|
||||
pub filename: String,
|
||||
/// Unknown/reserved bytes from `0x40..0x90`.
|
||||
pub reserved_40_90: RegionInfo,
|
||||
/// Encryption offset from `0x90..0x98`.
|
||||
pub encrypt_offset: u64,
|
||||
/// Encrypted payload length from `0x98..0xA0`.
|
||||
pub encrypt_length: u64,
|
||||
/// AES-CBC IV from `0xA0..0xB0`.
|
||||
pub iv_hex: String,
|
||||
/// Encrypted payload length/size mirror from `0xB0..0xB8`.
|
||||
pub encrypt_filesize: u64,
|
||||
/// Unknown/reserved bytes from `0xB8..0xE0`.
|
||||
pub reserved_b8_e0: RegionInfo,
|
||||
/// Stored SHA-256 digest from `0xE0..0x100`.
|
||||
pub stored_sha256_hex: String,
|
||||
/// SHA-256 of the actual bytes after the header.
|
||||
pub payload_sha256_hex: String,
|
||||
/// Whether `stored_sha256_hex` matches `payload_sha256_hex`.
|
||||
pub payload_sha256_matches: bool,
|
||||
/// Whether the RSA signature at `0x100..0x200` verifies the stored SHA-256.
|
||||
pub signature_valid: bool,
|
||||
/// Opaque 256-byte region from `0x100..0x200`.
|
||||
pub opaque_100_200: RegionInfo,
|
||||
}
|
||||
|
||||
/// Summary for an unknown or reserved header byte range.
|
||||
#[derive(Debug)]
|
||||
pub struct RegionInfo {
|
||||
/// Number of bytes in the region.
|
||||
pub len: usize,
|
||||
/// Number of non-zero bytes in the region.
|
||||
pub nonzero_count: usize,
|
||||
/// SHA-256 digest of the region.
|
||||
pub sha256_hex: String,
|
||||
/// Exact region bytes as lowercase hexadecimal.
|
||||
pub bytes_hex: String,
|
||||
}
|
||||
|
||||
/// Unpack a complete `.sig` file held in memory.
|
||||
///
|
||||
/// The input must include both the 512-byte header and the wrapped payload. The
|
||||
@@ -84,13 +196,101 @@ pub fn unpack_sig(raw: &[u8]) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Pack plaintext bytes into an encrypted `.sig` container.
|
||||
/// Return the NUL-terminated filename stored in a `.sig` header.
|
||||
pub fn header_filename(raw: &[u8]) -> Result<String, Box<dyn Error>> {
|
||||
if raw.len() < HEADER_LEN {
|
||||
return Err("input is too small to contain a .sig header".into());
|
||||
}
|
||||
|
||||
let header = &raw[..HEADER_LEN];
|
||||
let magic = u32::from_be_bytes(header[0..4].try_into()?);
|
||||
if magic != MAGIC {
|
||||
return Err(format!("invalid .sig magic: 0x{magic:08x}").into());
|
||||
}
|
||||
|
||||
let filename = &header[0x10..0x40];
|
||||
let end = filename
|
||||
.iter()
|
||||
.position(|byte| *byte == 0)
|
||||
.unwrap_or(filename.len());
|
||||
|
||||
Ok(std::str::from_utf8(&filename[..end])?.to_owned())
|
||||
}
|
||||
|
||||
/// Parse and summarize all known `.sig` header fields.
|
||||
pub fn header_info(raw: &[u8]) -> Result<HeaderInfo, Box<dyn Error>> {
|
||||
if raw.len() < HEADER_LEN {
|
||||
return Err("input is too small to contain a .sig header".into());
|
||||
}
|
||||
|
||||
let header = &raw[..HEADER_LEN];
|
||||
let magic = header[0..4].try_into()?;
|
||||
if u32::from_be_bytes(magic) != MAGIC {
|
||||
return Err(format!("invalid .sig magic: 0x{:08x}", u32::from_be_bytes(magic)).into());
|
||||
}
|
||||
|
||||
let package_type_raw = header[0x04];
|
||||
let payload = &raw[HEADER_LEN..];
|
||||
let stored_sha256_hex = bytes_to_hex(&header[0xE0..0x100]);
|
||||
let payload_sha256_hex = bytes_to_hex(&Sha256::digest(payload));
|
||||
let payload_sha256_matches = stored_sha256_hex == payload_sha256_hex;
|
||||
|
||||
Ok(HeaderInfo {
|
||||
total_len: raw.len(),
|
||||
payload_len: payload.len(),
|
||||
magic,
|
||||
package_type_raw,
|
||||
package_type: package_type_raw & 0x7F,
|
||||
is_encrypted: package_type_raw & 0x80 != 0,
|
||||
version_major: header[0x05],
|
||||
version_minor: header[0x06],
|
||||
byte_07: header[0x07],
|
||||
filesize: u64::from_le_bytes(header[0x08..0x10].try_into()?),
|
||||
filename: header_filename(raw)?,
|
||||
reserved_40_90: region_info(&header[0x40..0x90]),
|
||||
encrypt_offset: u64::from_le_bytes(header[0x90..0x98].try_into()?),
|
||||
encrypt_length: u64::from_le_bytes(header[0x98..0xA0].try_into()?),
|
||||
iv_hex: bytes_to_hex(&header[0xA0..0xB0]),
|
||||
encrypt_filesize: u64::from_le_bytes(header[0xB0..0xB8].try_into()?),
|
||||
reserved_b8_e0: region_info(&header[0xB8..0xE0]),
|
||||
stored_sha256_hex,
|
||||
payload_sha256_hex,
|
||||
payload_sha256_matches,
|
||||
signature_valid: verify_payload_signature(&header[0xE0..0x100], &header[0x100..0x200]),
|
||||
opaque_100_200: region_info(&header[0x100..0x200]),
|
||||
})
|
||||
}
|
||||
|
||||
fn region_info(bytes: &[u8]) -> RegionInfo {
|
||||
RegionInfo {
|
||||
len: bytes.len(),
|
||||
nonzero_count: bytes.iter().filter(|byte| **byte != 0).count(),
|
||||
sha256_hex: bytes_to_hex(&Sha256::digest(bytes)),
|
||||
bytes_hex: bytes_to_hex(bytes),
|
||||
}
|
||||
}
|
||||
|
||||
fn bytes_to_hex(bytes: &[u8]) -> String {
|
||||
const HEX: &[u8; 16] = b"0123456789abcdef";
|
||||
let mut out = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
out.push(HEX[(byte >> 4) as usize] as char);
|
||||
out.push(HEX[(byte & 0x0F) as usize] as char);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Pack plaintext bytes into a plain signed `.sig` container.
|
||||
///
|
||||
/// The generated file is intentionally conservative: it writes the fields needed
|
||||
/// by the known Carbon 2 unpacker and by [`unpack_sig`], but it does not claim to
|
||||
/// recreate every vendor metadata field. The payload is encrypted with the fixed
|
||||
/// Carbon 2 AES-256 key and a fresh random IV stored in the header.
|
||||
/// recreate every vendor metadata field.
|
||||
pub fn pack_sig(payload: &[u8], filename: &str) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
pack_sig_inner(payload, filename, None, None, false)
|
||||
}
|
||||
|
||||
/// Pack plaintext bytes into an encrypted signed `.sig` container.
|
||||
pub fn pack_sig_encrypted(payload: &[u8], filename: &str) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
let mut iv = [0u8; 16];
|
||||
OsRng.fill_bytes(&mut iv);
|
||||
|
||||
@@ -107,7 +307,7 @@ pub fn pack_sig_with_iv(
|
||||
filename: &str,
|
||||
iv: [u8; 16],
|
||||
) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
pack_sig_inner(payload, filename, iv, None)
|
||||
pack_sig_inner(payload, filename, Some(iv), None, true)
|
||||
}
|
||||
|
||||
/// Pack plaintext bytes using the IV and opaque metadata from an existing `.sig` header.
|
||||
@@ -122,38 +322,53 @@ pub fn pack_sig_with_template(
|
||||
payload: &[u8],
|
||||
filename: &str,
|
||||
template: &[u8],
|
||||
encrypt: Option<bool>,
|
||||
) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
if template.len() < HEADER_LEN {
|
||||
return Err("template is too small to contain a .sig header".into());
|
||||
}
|
||||
|
||||
let iv = template[0xA0..0xB0].try_into()?;
|
||||
pack_sig_inner(payload, filename, iv, Some(&template[..HEADER_LEN]))
|
||||
let template_header = &template[..HEADER_LEN];
|
||||
let template_is_encrypted = template_header[0x04] & 0x80 != 0;
|
||||
let encrypt = encrypt.unwrap_or(template_is_encrypted);
|
||||
let iv = if encrypt {
|
||||
Some(template_header[0xA0..0xB0].try_into()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
pack_sig_inner(payload, filename, iv, Some(template_header), encrypt)
|
||||
}
|
||||
|
||||
fn pack_sig_inner(
|
||||
payload: &[u8],
|
||||
filename: &str,
|
||||
iv: [u8; 16],
|
||||
iv: Option<[u8; 16]>,
|
||||
header_template: Option<&[u8]>,
|
||||
encrypt: bool,
|
||||
) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
let filename_bytes = filename.as_bytes();
|
||||
if filename_bytes.len() >= 48 {
|
||||
return Err("header filename must be 47 bytes or shorter".into());
|
||||
}
|
||||
|
||||
let ciphertext = encrypt_payload(payload, &iv)?;
|
||||
let encrypted_sha256 = Sha256::digest(&ciphertext).into();
|
||||
let mut output = Vec::with_capacity(HEADER_LEN + ciphertext.len());
|
||||
let payload_out = if encrypt {
|
||||
let iv = iv.ok_or("encrypted repack requires an IV")?;
|
||||
encrypt_payload(payload, &iv)?
|
||||
} else {
|
||||
payload.to_vec()
|
||||
};
|
||||
let payload_sha256 = Sha256::digest(&payload_out).into();
|
||||
let mut output = Vec::with_capacity(HEADER_LEN + payload_out.len());
|
||||
output.extend_from_slice(&build_header(
|
||||
filename_bytes,
|
||||
payload.len(),
|
||||
ciphertext.len(),
|
||||
&iv,
|
||||
encrypted_sha256,
|
||||
payload_out.len(),
|
||||
iv,
|
||||
payload_sha256,
|
||||
header_template,
|
||||
encrypt,
|
||||
)?);
|
||||
output.extend_from_slice(&ciphertext);
|
||||
output.extend_from_slice(&payload_out);
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
@@ -169,14 +384,15 @@ fn pack_sig_inner(
|
||||
fn build_header(
|
||||
filename: &[u8],
|
||||
payload_len: usize,
|
||||
encrypted_len: usize,
|
||||
iv: &[u8; 16],
|
||||
encrypted_sha256: [u8; 32],
|
||||
output_payload_len: usize,
|
||||
iv: Option<[u8; 16]>,
|
||||
payload_sha256: [u8; 32],
|
||||
template: Option<&[u8]>,
|
||||
encrypt: bool,
|
||||
) -> Result<[u8; HEADER_LEN], Box<dyn Error>> {
|
||||
let payload_len = u64::try_from(payload_len).map_err(|_| "payload is too large")?;
|
||||
let encrypted_len =
|
||||
u64::try_from(encrypted_len).map_err(|_| "encrypted payload is too large")?;
|
||||
let output_payload_len =
|
||||
u64::try_from(output_payload_len).map_err(|_| "output payload is too large")?;
|
||||
|
||||
let mut header = [0u8; HEADER_LEN];
|
||||
if let Some(template) = template {
|
||||
@@ -184,20 +400,44 @@ fn build_header(
|
||||
}
|
||||
|
||||
header[0..4].copy_from_slice(&MAGIC.to_be_bytes());
|
||||
header[0x04] = 0x83;
|
||||
header[0x05] = 0x01;
|
||||
header[0x06] = 0x02;
|
||||
if template.is_none() || (header[0x04] & 0x80 != 0) != encrypt {
|
||||
header[0x04] = if encrypt { 0x83 } else { 0x04 };
|
||||
header[0x05] = 0x01;
|
||||
header[0x06] = 0x02;
|
||||
}
|
||||
header[0x08..0x10].copy_from_slice(&payload_len.to_le_bytes());
|
||||
header[0x10..0x40].fill(0);
|
||||
header[0x10..0x10 + filename.len()].copy_from_slice(filename);
|
||||
header[0x98..0xA0].copy_from_slice(&encrypted_len.to_le_bytes());
|
||||
header[0xA0..0xB0].copy_from_slice(iv);
|
||||
header[0xB0..0xB8].copy_from_slice(&encrypted_len.to_le_bytes());
|
||||
header[0xE0..0x100].copy_from_slice(&encrypted_sha256);
|
||||
if encrypt {
|
||||
let iv = iv.ok_or("encrypted header requires an IV")?;
|
||||
header[0x98..0xA0].copy_from_slice(&output_payload_len.to_le_bytes());
|
||||
header[0xA0..0xB0].copy_from_slice(&iv);
|
||||
header[0xB0..0xB8].copy_from_slice(&output_payload_len.to_le_bytes());
|
||||
} else {
|
||||
header[0x98..0xA0].fill(0);
|
||||
header[0xA0..0xB0].fill(0);
|
||||
header[0xB0..0xB8].copy_from_slice(&payload_len.to_le_bytes());
|
||||
}
|
||||
header[0xE0..0x100].copy_from_slice(&payload_sha256);
|
||||
let signature = sign_payload_hash(&payload_sha256)?;
|
||||
header[0x100..0x200].copy_from_slice(&signature);
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
fn sign_payload_hash(payload_sha256: &[u8; 32]) -> Result<Vec<u8>, Box<dyn Error>> {
|
||||
let key = RsaPrivateKey::from_pkcs8_pem(RSA_PRIVATE_KEY_PEM)?;
|
||||
Ok(key.sign(Pkcs1v15Sign::new::<Sha256>(), payload_sha256)?)
|
||||
}
|
||||
|
||||
fn verify_payload_signature(payload_sha256: &[u8], signature: &[u8]) -> bool {
|
||||
let Ok(key) = RsaPublicKey::from_public_key_pem(RSA_PUBLIC_KEY_PEM) else {
|
||||
return false;
|
||||
};
|
||||
key.verify(Pkcs1v15Sign::new::<Sha256>(), payload_sha256, signature)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
/// Parse the fixed-width `.sig` header.
|
||||
///
|
||||
/// Offsets are based on the browser implementation:
|
||||
|
||||
+73
-16
@@ -22,31 +22,86 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
match cli.command {
|
||||
Command::Strip(args) => strip(args)?,
|
||||
Command::Encrypt(args) => encrypt(args)?,
|
||||
Command::Unpack(args) => unpack(args)?,
|
||||
Command::Info(args) => info(args)?,
|
||||
Command::Repack(args) => repack(args)?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute the `encrypt` subcommand.
|
||||
///
|
||||
/// The command reads a plaintext payload, wraps it with a fresh-IV encrypted
|
||||
/// `.sig` header via [`crypto::pack_sig`], and writes the resulting container.
|
||||
fn encrypt(args: cli::EncryptArgs) -> Result<(), Box<dyn Error>> {
|
||||
/// Execute the `info` subcommand.
|
||||
fn info(args: cli::InfoArgs) -> Result<(), Box<dyn Error>> {
|
||||
let raw = fs::read(&args.input)?;
|
||||
let filename = match args.filename {
|
||||
Some(filename) => filename,
|
||||
None => args
|
||||
let info = crypto::header_info(&raw)?;
|
||||
|
||||
println!("file: {}", args.input.display());
|
||||
println!("total_len: {}", info.total_len);
|
||||
println!("header_len: {}", crypto::HEADER_LEN);
|
||||
println!("payload_len: {}", info.payload_len);
|
||||
println!(
|
||||
"magic: {}",
|
||||
std::str::from_utf8(&info.magic).unwrap_or("<non-ascii>")
|
||||
);
|
||||
println!("package_type_raw: 0x{:02x}", info.package_type_raw);
|
||||
println!("package_type: {}", info.package_type);
|
||||
println!("encrypted: {}", info.is_encrypted);
|
||||
println!("version: {}.{}", info.version_major, info.version_minor);
|
||||
println!("byte_07: 0x{:02x}", info.byte_07);
|
||||
println!("filesize: {}", info.filesize);
|
||||
println!("filename: {}", info.filename);
|
||||
print_region("reserved_40_90", &info.reserved_40_90, args.raw);
|
||||
println!("encrypt_offset: {}", info.encrypt_offset);
|
||||
println!("encrypt_length: {}", info.encrypt_length);
|
||||
println!("iv: {}", info.iv_hex);
|
||||
println!("encrypt_filesize: {}", info.encrypt_filesize);
|
||||
print_region("reserved_b8_e0", &info.reserved_b8_e0, args.raw);
|
||||
println!("stored_sha256: {}", info.stored_sha256_hex);
|
||||
println!("payload_sha256: {}", info.payload_sha256_hex);
|
||||
println!("payload_sha256_matches: {}", info.payload_sha256_matches);
|
||||
println!("signature_valid: {}", info.signature_valid);
|
||||
print_region("opaque_100_200", &info.opaque_100_200, args.raw);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_region(name: &str, region: &crypto::RegionInfo, raw: bool) {
|
||||
println!("{name}_len: {}", region.len);
|
||||
println!("{name}_nonzero_count: {}", region.nonzero_count);
|
||||
println!("{name}_sha256: {}", region.sha256_hex);
|
||||
if raw {
|
||||
println!("{name}_bytes_hex:");
|
||||
for chunk in region.bytes_hex.as_bytes().chunks(64) {
|
||||
println!(" {}", std::str::from_utf8(chunk).unwrap_or(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute the `repack` subcommand.
|
||||
///
|
||||
/// The command reads a plaintext payload, wraps it in a signed `.sig` header,
|
||||
/// and optionally encrypts the payload.
|
||||
fn repack(args: cli::RepackArgs) -> Result<(), Box<dyn Error>> {
|
||||
let raw = fs::read(&args.input)?;
|
||||
let template = match args.template {
|
||||
Some(template) => Some(fs::read(template)?),
|
||||
None => None,
|
||||
};
|
||||
let filename = match (args.filename, template.as_deref()) {
|
||||
(Some(filename), _) => filename,
|
||||
(None, Some(template)) => crypto::header_filename(template)?,
|
||||
(None, None) => args
|
||||
.input
|
||||
.file_name()
|
||||
.ok_or("input path does not have a file name")?
|
||||
.to_string_lossy()
|
||||
.into_owned(),
|
||||
};
|
||||
let packed = if let Some(template) = args.template {
|
||||
let template = fs::read(template)?;
|
||||
crypto::pack_sig_with_template(&raw, &filename, &template)?
|
||||
let packed = if let Some(template) = template {
|
||||
let encrypt = if args.encrypt { Some(true) } else { None };
|
||||
crypto::pack_sig_with_template(&raw, &filename, &template, encrypt)?
|
||||
} else if args.encrypt {
|
||||
crypto::pack_sig_encrypted(&raw, &filename)?
|
||||
} else {
|
||||
crypto::pack_sig(&raw, &filename)?
|
||||
};
|
||||
@@ -58,19 +113,21 @@ fn encrypt(args: cli::EncryptArgs) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute the `strip` subcommand.
|
||||
/// Execute the `unpack` subcommand.
|
||||
///
|
||||
/// `strip` removes the 512-byte `.sig` wrapper and writes the contained package
|
||||
/// `unpack` removes the 512-byte `.sig` wrapper and writes the contained package
|
||||
/// bytes. For encrypted packages, [`crypto::unpack_sig`] performs the AES-CBC
|
||||
/// decryption before returning the payload.
|
||||
fn strip(args: cli::StripArgs) -> Result<(), Box<dyn Error>> {
|
||||
fn unpack(args: cli::UnpackArgs) -> Result<(), Box<dyn Error>> {
|
||||
let raw = fs::read(&args.input)?;
|
||||
let filename = crypto::header_filename(&raw)?;
|
||||
let unpacked = crypto::unpack_sig(&raw)?;
|
||||
let output_path = args
|
||||
.output
|
||||
.unwrap_or_else(|| default_output_path(&args.input));
|
||||
|
||||
fs::write(&output_path, unpacked)?;
|
||||
println!("header filename: {filename}");
|
||||
println!("wrote {}", output_path.display());
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -83,6 +83,7 @@ fn encrypts_ota_package_list_fixture() {
|
||||
EXPECTED_OTA_PACKAGE_LIST,
|
||||
"ota-package-list.json",
|
||||
ENCRYPTED_OTA_PACKAGE_LIST,
|
||||
None,
|
||||
)
|
||||
.expect("failed to pack embedded plaintext fixture");
|
||||
|
||||
|
||||
BIN
Binary file not shown.
Binary file not shown.
Executable
BIN
Binary file not shown.
@@ -1 +0,0 @@
|
||||
{"packages": [{"file": "cc2_eeb001_01.03.02.36_20260326171745.swu.sig", "hash": "46aa459b61340a84ff52e2865347d42a568351919441fb9db6e185424220b192"}], "version": "01.03.02.36"}
|
||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user