mirror of
https://github.com/freemkv/freemkv-unlock.git
synced 2026-10-07 18:13:11 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
274e9c66f5 | ||
|
|
d05b9bce18 | ||
|
|
f9df7e0fb3 | ||
|
|
9a4f29b04c | ||
|
|
6a8253f889 | ||
|
|
2684f5f28e | ||
|
|
f0d936f4b6 | ||
|
|
5fd090fc8e | ||
|
|
44bd80e8df | ||
|
|
fb3d045509 | ||
|
|
f9182df204 | ||
|
|
53d9e15d3d | ||
|
|
65e02f4b79 | ||
|
|
10126cdbf6 | ||
|
|
887d154014 | ||
|
|
e9ed2ffa85 | ||
|
|
3a5f92da91 | ||
|
|
58dc46037e | ||
|
|
0ed613d7a9 | ||
|
|
4663cdf2bf | ||
|
|
e7a60d461c | ||
|
|
e7490240a1 | ||
|
|
3c535ec874 | ||
|
|
21cfb17d11 | ||
|
|
33e65f739c | ||
|
|
ac94c80163 | ||
|
|
94cad15a98 |
@@ -0,0 +1,48 @@
|
||||
version: 2
|
||||
|
||||
# Dependency updates land on `dev`, never on `main`.
|
||||
#
|
||||
# `main` here is a RELEASE POINTER that release.sh moves to each tag. A bot
|
||||
# commit on it would put work there that no tag contains, which is exactly the
|
||||
# state that aborted the 1.6.2 cascade at the last step -- so pointing
|
||||
# Dependabot at main would recreate that failure on a schedule.
|
||||
updates:
|
||||
- package-ecosystem: cargo
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
# One PR per week for the routine bumps instead of one per crate. Eight
|
||||
# repos times a handful of crates is a volume nobody reads, and an
|
||||
# unread PR queue is indistinguishable from no updates at all.
|
||||
groups:
|
||||
minor-and-patch:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
ignore:
|
||||
# The freemkv crates depend on each other by GIT TAG, re-pinned by
|
||||
# release.sh as part of the release commit. Dependabot cannot see that
|
||||
# cascade, so a PR bumping one of these would fight the release process
|
||||
# and could pin a version whose tag does not exist yet.
|
||||
- dependency-name: freemkv-unlock
|
||||
- dependency-name: libfreemkv
|
||||
- dependency-name: freemkv-keysources
|
||||
- dependency-name: freemkv-i18n
|
||||
- dependency-name: freemkv-engine
|
||||
|
||||
# The workflows are now real infrastructure -- the release cascade, the
|
||||
# cross-platform hash matrix, the disc gate -- so their actions need the same
|
||||
# attention as the crates.
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
target-branch: dev
|
||||
schedule:
|
||||
interval: weekly
|
||||
open-pull-requests-limit: 5
|
||||
groups:
|
||||
actions:
|
||||
update-types:
|
||||
- minor
|
||||
- patch
|
||||
@@ -0,0 +1,98 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
# dev -> qa -> main. `dev` is where work lands and is meant to be pushed
|
||||
# to often: these are the FAST checks, so a mistake surfaces in minutes.
|
||||
# `qa` is the release candidate — it runs these too, plus the expensive
|
||||
# suite in qa.yml. `main` only ever moves at release time, to a tagged
|
||||
# commit that was already green on qa.
|
||||
branches: [main, dev, qa]
|
||||
pull_request:
|
||||
|
||||
# freemkv-unlock is the BASE of the whole ecosystem: libfreemkv path-deps it,
|
||||
# bdemu path-deps it with the `emulation` feature, and every other crate pulls
|
||||
# it in transitively. It has no freemkv dependencies of its own, so no sibling
|
||||
# checkout is needed here — but it is also the crate whose breakage takes all
|
||||
# seven others down with it, which is why it gets the full OS matrix rather
|
||||
# than a token Linux run.
|
||||
#
|
||||
# Cargo.lock is gitignored (library convention), so `--locked` is deliberately
|
||||
# NOT used: with no committed lockfile it fails rather than resolves.
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: freemkv-unlock-ci
|
||||
- run: cargo fmt --check
|
||||
# --all-targets so the test code is linted too, not just the library.
|
||||
- run: cargo clippy --all-targets -- -D warnings
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: freemkv-unlock-ci
|
||||
- run: cargo test --tests
|
||||
# `emulation` is off by default and is the face bdemu builds against. A
|
||||
# feature nothing compiles is a feature that breaks at release time.
|
||||
- run: cargo test --tests --features emulation
|
||||
|
||||
# The crate ships on macOS and Windows through its consumers, so it has to
|
||||
# compile there. Build the tests (not just `cargo check`) — that forces
|
||||
# codegen and catches errors in test-only code, which `check` skips.
|
||||
check-macos:
|
||||
# dev is the FAST lane: this job still runs, but on the release-candidate
|
||||
# branches rather than on every push to dev. Nothing is deleted and no
|
||||
# platform stops being checked before a release -- qa.yml independently
|
||||
# covers macOS and Windows, and the jobs unique to this file (the Intel
|
||||
# macOS build, the Windows release build) run here on qa and main. A push
|
||||
# to dev is meant to be cheap and frequent; waiting on three runner pools
|
||||
# to agree is what a release candidate is for.
|
||||
#
|
||||
# `if` SKIPS the job (it does not queue). A queued job would be far worse
|
||||
# than a slow one: release.sh's CI gate refuses while any run for the
|
||||
# commit is still in progress, so a never-scheduled job blocks releases
|
||||
# silently -- see the note on real-media in qa.yml.
|
||||
if: github.ref_name == 'qa' || github.ref_name == 'main'
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: freemkv-unlock-ci
|
||||
- run: cargo build --tests --all-features
|
||||
|
||||
check-windows:
|
||||
# dev is the FAST lane: this job still runs, but on the release-candidate
|
||||
# branches rather than on every push to dev. Nothing is deleted and no
|
||||
# platform stops being checked before a release -- qa.yml independently
|
||||
# covers macOS and Windows, and the jobs unique to this file (the Intel
|
||||
# macOS build, the Windows release build) run here on qa and main. A push
|
||||
# to dev is meant to be cheap and frequent; waiting on three runner pools
|
||||
# to agree is what a release candidate is for.
|
||||
#
|
||||
# `if` SKIPS the job (it does not queue). A queued job would be far worse
|
||||
# than a slow one: release.sh's CI gate refuses while any run for the
|
||||
# commit is still in progress, so a never-scheduled job blocks releases
|
||||
# silently -- see the note on real-media in qa.yml.
|
||||
if: github.ref_name == 'qa' || github.ref_name == 'main'
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
shared-key: freemkv-unlock-ci
|
||||
- run: cargo build --tests --all-features
|
||||
@@ -0,0 +1,35 @@
|
||||
name: leak-guard
|
||||
|
||||
# Self-contained public-repo leak gate. Public CI cannot reach the private
|
||||
# tooling, so this encodes only the generic net: internal-infra references,
|
||||
# tracked CLAUDE.md/.claude paths, and AI-attribution in commit messages.
|
||||
# No project-specific reverse-engineering vocabulary lives here.
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
leak-guard:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Compute commit range
|
||||
id: range
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
||||
base="${{ github.event.pull_request.base.sha }}"
|
||||
head="${{ github.event.pull_request.head.sha }}"
|
||||
echo "range=$base..$head" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
before="${{ github.event.before }}"
|
||||
after="${{ github.sha }}"
|
||||
# New branch / first push: github.event.before is all-zeros.
|
||||
if [ -z "$before" ] || [ "$before" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "range=$after" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "range=$before..$after" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
fi
|
||||
- name: Run leak-guard
|
||||
run: bash ci/leak-guard.sh "${{ steps.range.outputs.range }}"
|
||||
@@ -0,0 +1,118 @@
|
||||
name: qa
|
||||
|
||||
# ── The qa gate: "is this production worth?" ────────────────────────────────
|
||||
#
|
||||
# dev -> qa -> main.
|
||||
#
|
||||
# `dev` is for committing often. ci.yml answers "is it green" in minutes with
|
||||
# fmt, clippy and the unit suite, so a mistake surfaces while it is still cheap
|
||||
# to fix. `qa` is the release-candidate branch, and THIS workflow is the claim
|
||||
# that a commit is production worth: everything expensive that can run without
|
||||
# physical media. `main` only ever receives a qa that went green here.
|
||||
#
|
||||
# Sibling repos are checked out at `qa`, NOT `dev`. A qa run that resolved its
|
||||
# dependencies from dev tips would be validating a combination that is not the
|
||||
# one being released, which is the exact failure this branch exists to prevent.
|
||||
#
|
||||
# What this gate CANNOT cover: `disc://` and real `iso://` need physical media,
|
||||
# and no hosted runner has an optical drive or the image hoard. Those run on a
|
||||
# self-hosted runner (see the media job at the end) and are the one leg that
|
||||
# stays on hardware.
|
||||
on:
|
||||
push:
|
||||
branches: [qa]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# ── Name the candidate ────────────────────────────────────────────────
|
||||
#
|
||||
# Every push to `qa` is a release candidate, so every push gets a tag:
|
||||
# v<version>-rc<N>, N incrementing. That is the answer to "which build is on
|
||||
# qa right now, and is it the one I tested?" — a question that otherwise gets
|
||||
# answered from memory.
|
||||
#
|
||||
# This runs FIRST and does not depend on the gates, deliberately. A red
|
||||
# candidate needs a name more than a green one does: "rc3 failed
|
||||
# release-tests on windows" is a sentence you can act on; "qa is red" is not.
|
||||
# Red on qa is a working gate, not an incident — it is the branch saying this
|
||||
# is not production worth yet. Fix on dev, get dev green, push qa again.
|
||||
#
|
||||
# release.yml excludes v*-rc* so a candidate never publishes a release.
|
||||
rc-tag:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Stamp the next rc
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
v=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
|
||||
[ -n "$v" ] || { echo "no version in Cargo.toml" >&2; exit 1; }
|
||||
# Numeric sort on the rc ordinal: -rc10 must beat -rc9, and a plain
|
||||
# lexical sort gets that backwards from the tenth candidate on.
|
||||
n=$(git tag -l "v$v-rc*" | sed "s|^v$v-rc||" | sort -n | tail -1)
|
||||
tag="v$v-rc$(( ${n:-0} + 1 ))"
|
||||
git tag "$tag"
|
||||
git push origin "$tag"
|
||||
echo "### Candidate \`$tag\`" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
# The debug suite runs on every dev push. Release is a DIFFERENT build:
|
||||
# overflow checks are off, debug_assert! is compiled out, and inlining
|
||||
# changes what the optimiser can prove. A test that only passes in debug is
|
||||
# a test that never guarded the binary anyone actually ships.
|
||||
release-tests:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: ['ubuntu-latest', 'macos-latest']
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-unlock
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-unlock
|
||||
- run: cargo test --release --tests
|
||||
working-directory: freemkv-unlock
|
||||
|
||||
# clippy's output is target-dependent: cfg-gated code only gets linted on
|
||||
# the target it compiles for. Linting solely on the dev machine's host
|
||||
# target is how a lint that CI rejects reaches a push.
|
||||
cross-lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-unlock
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
with:
|
||||
components: clippy
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-unlock
|
||||
- run: rustup target add x86_64-unknown-linux-gnu
|
||||
- run: cargo clippy --all-targets --target x86_64-unknown-linux-gnu -- -D warnings
|
||||
working-directory: freemkv-unlock
|
||||
|
||||
# Windows compiles the tests but does not run them, matching the policy
|
||||
# ci.yml already set. The value here is codegen: the #[cfg(windows)] halves
|
||||
# of the SCSI transport and platform layers compile on no other runner, so
|
||||
# without this they are first built at release time.
|
||||
windows-build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
path: freemkv-unlock
|
||||
- uses: dtolnay/rust-toolchain@1.97.0
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: freemkv-unlock
|
||||
- run: cargo build --release --tests
|
||||
working-directory: freemkv-unlock
|
||||
+29
-1
@@ -1,6 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
## [1.5.2] — UNRELEASED
|
||||
## [1.6.4] — 2026-08-15
|
||||
|
||||
### Changed
|
||||
|
||||
- **No functional change.** This crate ships alongside the rest of freemkv at a
|
||||
matching version; its behaviour is untouched.
|
||||
|
||||
## [1.6.3] — 2026-08-10
|
||||
|
||||
### Changed
|
||||
|
||||
- **Housekeeping only — nothing about how a drive is unlocked has changed.** The
|
||||
cryptography and compression crates this crate builds on were moved to their
|
||||
current releases and brought into line with the versions the rest of freemkv
|
||||
uses, so one version of each is built instead of two.
|
||||
|
||||
## [1.6.2] — 2026-08-08
|
||||
|
||||
Version sync with the workspace. No functional change in this crate.
|
||||
|
||||
## [1.6.1] — 2026-08-07
|
||||
|
||||
Version sync with the workspace. No functional change in this crate.
|
||||
|
||||
## [1.6.0] — 2026-08-03
|
||||
|
||||
Version sync with the workspace. No functional change in this crate.
|
||||
|
||||
## [1.5.2] — 2026-07-22
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ This Code of Conduct applies within all community spaces, and also applies when
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at owner@example.invalid. All complaints will be reviewed and investigated promptly and fairly.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported privately to the project maintainer via GitHub at https://github.com/MattJackson. All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for your interest!
|
||||
|
||||
- **Report a bug** — open an issue
|
||||
- **Fix a bug** — fork, branch, PR
|
||||
- **Suggest a feature** — open an issue first
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
cargo test
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
+7
-7
@@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "freemkv-unlock"
|
||||
version = "1.5.2"
|
||||
version = "1.6.4"
|
||||
edition = "2024"
|
||||
rust-version = "1.86"
|
||||
rust-version = "1.97"
|
||||
license = "MIT"
|
||||
description = "Unlock layer for the freemkv toolchain: the Unlocker contract + self-contained firmware/AACS/CSS unlocker modules. libfreemkv depends on this and dispatches via all_unlockers()."
|
||||
repository = "https://github.com/freemkv/freemkv-unlock"
|
||||
@@ -18,11 +18,11 @@ emulation = []
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
base64 = "0.22.1"
|
||||
base64 = "0.23"
|
||||
tracing = "0.1"
|
||||
num-bigint = "0.4"
|
||||
num-bigint = "0.5"
|
||||
num-traits = "0.2"
|
||||
sha1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
aes = "0.8"
|
||||
rand = "0.8"
|
||||
sha2 = "0.11"
|
||||
aes = "0.9"
|
||||
rand = "0.10"
|
||||
|
||||
@@ -14,9 +14,17 @@ clients of libfreemkv are oblivious to unlockers entirely (as they are to the
|
||||
SCSI layer).
|
||||
|
||||
```rust
|
||||
use freemkv_unlock::UnlockError;
|
||||
|
||||
// Drive-prep: try each unlocker's feature unlock until one claims the drive.
|
||||
// `NotApplicable` means "not this unlocker's drive" — move on; a transport
|
||||
// error means a dead bus — abort. `unlock_bus` follows the same contract for
|
||||
// removing per-disc bus encryption.
|
||||
for u in freemkv_unlock::all_unlockers() {
|
||||
if u.matches(&ctx) {
|
||||
return u.unlock(&mut scsi, &ctx);
|
||||
match u.unlock_features(&mut scsi, &ctx) {
|
||||
Ok(unlocked) => return Ok(unlocked),
|
||||
Err(UnlockError::NotApplicable) => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported versions
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | --------- |
|
||||
| 1.6.x | Yes |
|
||||
| < 1.6 | No |
|
||||
|
||||
Only the current 1.6.x line receives security fixes.
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Report vulnerabilities privately through GitHub Security Advisories:
|
||||
https://github.com/freemkv/freemkv-unlock/security/advisories/new
|
||||
|
||||
Do not open a public issue for a security report. Include the affected
|
||||
version, steps to reproduce, and the impact you believe the issue has.
|
||||
|
||||
## Response time
|
||||
|
||||
You will get an initial response within 7 days.
|
||||
Executable
+120
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# leak-guard.sh — self-contained public-repo leak gate.
|
||||
#
|
||||
# This is the LAST line of defense in CI. It is intentionally self-contained:
|
||||
# public CI cannot reach the private tooling, so this script encodes ONLY the
|
||||
# generic net — internal infrastructure references, agent-context files, and
|
||||
# AI-attribution in commit messages. It deliberately contains NO project-
|
||||
# specific reverse-engineering vocabulary (those words would themselves be a
|
||||
# leak). The richer private scanner stays private.
|
||||
#
|
||||
# Fails (exit 1) if any of the following appear in the repo:
|
||||
# 1. a tracked CLAUDE.md or .claude/ path (agent context — never public),
|
||||
# 2. tracked file content matching the internal-infra net,
|
||||
# 3. a commit message (in the given range) with AI attribution.
|
||||
#
|
||||
# Usage:
|
||||
# leak-guard.sh [<commit-range>]
|
||||
# <commit-range> optional git rev-list range to scan commit messages
|
||||
# (e.g. "abc..def"). If omitted, commit-message scan is
|
||||
# skipped (path + content checks always run).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Absolute path to this script, resolved before any cd, so we can exclude it
|
||||
# from the content scan (it necessarily contains the detection patterns).
|
||||
SELF_ABS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
|
||||
|
||||
REPO="$(git rev-parse --show-toplevel)"
|
||||
cd "$REPO"
|
||||
|
||||
fail=0
|
||||
note() { printf ' ✗ %s\n' "$1"; fail=1; }
|
||||
|
||||
# Internal-infra net — GENERIC ONLY. This script ships in the public repo, so
|
||||
# the patterns themselves must not name any org-specific identifier (doing so
|
||||
# would itself leak the infra they guard). We catch the leak *class*:
|
||||
# - RFC1918 private IPv4 ranges (10/8, 172.16/12, 192.168/16),
|
||||
# - private/internal/non-routable TLDs (.internal/.local/.lan/.corp/.invalid),
|
||||
# - docker.internal.
|
||||
# The full org-specific net (literal hostnames, service names, repo paths,
|
||||
# vendor tooling, …) lives ONLY in the private scanner and never ships here.
|
||||
INFRA_RE='\b10\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|\b172\.(1[6-9]|2[0-9]|3[01])\.[0-9]{1,3}\.[0-9]{1,3}|\b192\.168\.[0-9]{1,3}\.[0-9]{1,3}|\.internal\b|\.local\b|\.lan\b|\.corp\b|\.invalid\b|docker\.internal'
|
||||
# Home-path net — GENERIC ONLY. Catches an absolute developer home path
|
||||
# committed into a tracked file (a macOS /Users/<user>/… or Linux /home/<user>/…
|
||||
# path). This names NO specific user — it matches the leak *class* (any home
|
||||
# path), so the pattern itself reveals nothing org- or person-specific. A real
|
||||
# leak (e.g. /Users/alice/Developer/x slipping into a public RELEASE.md) trips
|
||||
# this regardless of whose machine it came from. The username segment is a
|
||||
# literal-username class ([A-Za-z0-9._-]) so dynamic/templated paths that build
|
||||
# the user at runtime — shell `/home/$USER/`, doc `/home/<rip>/`, Rust
|
||||
# `/home/{user}/` — do NOT false-positive; only a baked-in literal home leaks.
|
||||
HOMEPATH_RE='/Users/[A-Za-z0-9._-]+/|/home/[A-Za-z0-9._-]+/'
|
||||
# AI-attribution net (case-insensitive). "claude" matches only as a standalone
|
||||
# word — NOT preceded by a dot/slash/alnum and NOT followed by .md — so legit
|
||||
# mentions of CLAUDE.md / .claude/ in a commit message don't false-positive.
|
||||
ATTR_RE='co-authored-by|generated with|🤖|(?<![.\/A-Za-z0-9])claude(?!\.md)'
|
||||
|
||||
echo "── leak-guard: tracked agent-context paths ──"
|
||||
while IFS= read -r f; do
|
||||
case "$f" in
|
||||
CLAUDE.md|*/CLAUDE.md|.claude|.claude/*|*/.claude|*/.claude/*)
|
||||
note "tracked agent-context file: $f (CLAUDE.md/.claude must never be tracked in a public repo)" ;;
|
||||
esac
|
||||
done < <(git ls-files)
|
||||
|
||||
# Match a PCRE against a file, emitting "LINE: MATCH". The pattern is passed as
|
||||
# an argument (not interpolated into a //) so metacharacters like the "/" in a
|
||||
# path-style token can't break the regex. Reads raw bytes so non-UTF-8 blobs
|
||||
# don't abort the scan.
|
||||
pcre_matches() {
|
||||
perl -e '
|
||||
my ($file, $re) = @ARGV;
|
||||
open(my $fh, "<:raw", $file) or exit 0;
|
||||
my $rx; eval { $rx = qr/$re/i }; exit 0 if $@;
|
||||
while (my $l = <$fh>) { if ($l =~ /$rx/) { print "$.: $&\n"; } }
|
||||
' "$1" "$2" 2>/dev/null
|
||||
}
|
||||
|
||||
# This script's own source necessarily contains the detection patterns (e.g.
|
||||
# the regex tokens in INFRA_RE), so scanning it would always self-flag. Skip it.
|
||||
SELF="$(git ls-files --full-name -- "$SELF_ABS" 2>/dev/null | head -1)"
|
||||
|
||||
echo "── leak-guard: internal-infra references in tracked files ──"
|
||||
while IFS= read -r f; do
|
||||
case "$f" in *.png|*.jpg|*.jpeg|*.ico|*.gif|*.bin|*.crate|*.gz|*.zip|*.pdf) continue ;; esac
|
||||
[ -n "$SELF" ] && [ "$f" = "$SELF" ] && continue
|
||||
[ -f "$f" ] || continue
|
||||
while IFS= read -r hit; do
|
||||
[ -z "$hit" ] && continue
|
||||
note "internal-infra reference: $f:$hit"
|
||||
done < <(pcre_matches "$f" "$INFRA_RE")
|
||||
while IFS= read -r hit; do
|
||||
[ -z "$hit" ] && continue
|
||||
note "[HOME-PATH] absolute home path: $f:$hit (no local home path may be committed to a public repo)"
|
||||
done < <(pcre_matches "$f" "$HOMEPATH_RE")
|
||||
done < <(git ls-files)
|
||||
|
||||
RANGE="${1:-}"
|
||||
if [ -n "$RANGE" ]; then
|
||||
echo "── leak-guard: AI-attribution in commit messages ($RANGE) ──"
|
||||
while IFS= read -r sha; do
|
||||
[ -z "$sha" ] && continue
|
||||
msg="$(git log -1 --format='%B' "$sha" 2>/dev/null || true)"
|
||||
# Pass the pattern as an argument (not interpolated into a //) so the
|
||||
# lookbehind char class and "/" don't break the regex.
|
||||
hit="$(printf '%s' "$msg" | perl -e '
|
||||
my $re = $ARGV[0]; my $rx = qr/$re/i;
|
||||
while (my $l = <STDIN>) { if ($l =~ /($rx)/) { print "$1\n"; last; } }
|
||||
' "$ATTR_RE" | head -1 || true)"
|
||||
[ -n "$hit" ] && note "commit ${sha:0:12}: message contains \"$hit\" (owner rule: zero AI attribution, ever)"
|
||||
done < <(git rev-list "$RANGE" 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
echo
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo "✗ leak-guard: blocking finding(s) above — DO NOT MERGE/PUBLISH"
|
||||
exit 1
|
||||
fi
|
||||
echo "✓ leak-guard: clean"
|
||||
+21
-21
@@ -391,8 +391,8 @@ fn ecdsa_sign(priv_key: &[u8; 20], data: &[u8]) -> ([u8; 20], [u8; 20]) {
|
||||
// two); a biased ECDSA nonce is a known key-recovery weakness, so
|
||||
// we reject and redraw any candidate >= n instead.
|
||||
let mut k_bytes = [0u8; 20];
|
||||
use rand::RngCore;
|
||||
rand::thread_rng().fill_bytes(&mut k_bytes);
|
||||
use rand::Rng;
|
||||
rand::rng().fill_bytes(&mut k_bytes);
|
||||
let k = BigUint::from_bytes_be(&k_bytes);
|
||||
if k.is_zero() || k >= n {
|
||||
continue;
|
||||
@@ -490,8 +490,8 @@ fn ecdsa_sign_p256(priv_key: &[u8; 32], data: &[u8]) -> ([u8; 32], [u8; 32]) {
|
||||
// (avoid the modulo bias that reducing raw RNG bytes mod n would
|
||||
// introduce).
|
||||
let mut k_bytes = [0u8; 32];
|
||||
use rand::RngCore;
|
||||
rand::thread_rng().fill_bytes(&mut k_bytes);
|
||||
use rand::Rng;
|
||||
rand::rng().fill_bytes(&mut k_bytes);
|
||||
let k = BigUint::from_bytes_be(&k_bytes);
|
||||
if k.is_zero() || k >= n {
|
||||
continue;
|
||||
@@ -703,8 +703,8 @@ fn generate_host_key_pair_p256() -> ([u8; 32], [u8; 32], [u8; 32]) {
|
||||
|
||||
let (d, q) = loop {
|
||||
let mut priv_bytes = [0u8; 32];
|
||||
use rand::RngCore;
|
||||
rand::thread_rng().fill_bytes(&mut priv_bytes);
|
||||
use rand::Rng;
|
||||
rand::rng().fill_bytes(&mut priv_bytes);
|
||||
// d == 0 (prob ~1/n) would yield the point at infinity / an
|
||||
// all-zero key and degenerate the bus key — reject and retry,
|
||||
// matching the AACS 1.0 sibling generate_host_key_pair.
|
||||
@@ -735,8 +735,8 @@ fn generate_host_key_pair() -> ([u8; 20], [u8; 20], [u8; 20]) {
|
||||
|
||||
let (d, q) = loop {
|
||||
let mut priv_bytes = [0u8; 20];
|
||||
use rand::RngCore;
|
||||
rand::thread_rng().fill_bytes(&mut priv_bytes);
|
||||
use rand::Rng;
|
||||
rand::rng().fill_bytes(&mut priv_bytes);
|
||||
let d = BigUint::from_bytes_be(&priv_bytes) % &n;
|
||||
if d.is_zero() {
|
||||
continue;
|
||||
@@ -771,13 +771,13 @@ fn generate_host_key_pair() -> ([u8; 20], [u8; 20], [u8; 20]) {
|
||||
/// padding.
|
||||
fn aes_cmac_16(data: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
|
||||
use aes::Aes128;
|
||||
use aes::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
|
||||
use aes::cipher::{Array, BlockCipherEncrypt, KeyInit};
|
||||
|
||||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||||
let cipher = Aes128::new(&(*key).into());
|
||||
|
||||
// For single-block CMAC:
|
||||
// 1. Generate subkey K1
|
||||
let mut l = GenericArray::clone_from_slice(&[0u8; 16]);
|
||||
let mut l: Array<u8, _> = [0u8; 16].into();
|
||||
cipher.encrypt_block(&mut l);
|
||||
|
||||
let mut k1 = [0u8; 16];
|
||||
@@ -795,7 +795,7 @@ fn aes_cmac_16(data: &[u8; 16], key: &[u8; 16]) -> [u8; 16] {
|
||||
for i in 0..16 {
|
||||
block[i] = data[i] ^ k1[i];
|
||||
}
|
||||
let mut ga = GenericArray::clone_from_slice(&block);
|
||||
let mut ga: Array<u8, _> = block.into();
|
||||
cipher.encrypt_block(&mut ga);
|
||||
|
||||
let mut mac = [0u8; 16];
|
||||
@@ -903,8 +903,8 @@ pub fn aacs_authenticate(
|
||||
|
||||
// Step 3: Generate host nonce and ephemeral key pair
|
||||
let mut host_nonce = [0u8; 20];
|
||||
use rand::RngCore;
|
||||
rand::thread_rng().fill_bytes(&mut host_nonce);
|
||||
use rand::Rng;
|
||||
rand::rng().fill_bytes(&mut host_nonce);
|
||||
let (host_key, host_key_point_x, host_key_point_y) = generate_host_key_pair();
|
||||
|
||||
// Step 4: Send host certificate + nonce (SEND KEY format 0x01)
|
||||
@@ -1066,8 +1066,8 @@ fn aacs2_authenticate_p256(
|
||||
|
||||
// Step 3: Generate host nonce + P-256 ephemeral key pair
|
||||
let mut host_nonce = [0u8; 20];
|
||||
use rand::RngCore;
|
||||
rand::thread_rng().fill_bytes(&mut host_nonce);
|
||||
use rand::Rng;
|
||||
rand::rng().fill_bytes(&mut host_nonce);
|
||||
let (host_eph_key, host_eph_pub_x, host_eph_pub_y) = generate_host_key_pair_p256();
|
||||
|
||||
// Step 4: Send AACS 2.0 host certificate + nonce
|
||||
@@ -1490,8 +1490,8 @@ mod tests {
|
||||
|
||||
// Generate random P-256 key pair
|
||||
let mut priv_bytes = [0u8; 32];
|
||||
use rand::RngCore;
|
||||
rand::thread_rng().fill_bytes(&mut priv_bytes);
|
||||
use rand::Rng;
|
||||
rand::rng().fill_bytes(&mut priv_bytes);
|
||||
let d = BigUint::from_bytes_be(&priv_bytes) % &n;
|
||||
let priv_key: [u8; 32] = to_bytes_be_padded(&d, 32).try_into().unwrap();
|
||||
|
||||
@@ -1514,9 +1514,9 @@ mod tests {
|
||||
|
||||
let mut pa = [0u8; 32];
|
||||
let mut pb = [0u8; 32];
|
||||
use rand::RngCore;
|
||||
rand::thread_rng().fill_bytes(&mut pa);
|
||||
rand::thread_rng().fill_bytes(&mut pb);
|
||||
use rand::Rng;
|
||||
rand::rng().fill_bytes(&mut pa);
|
||||
rand::rng().fill_bytes(&mut pb);
|
||||
let da = BigUint::from_bytes_be(&pa) % &n;
|
||||
let db = BigUint::from_bytes_be(&pb) % &n;
|
||||
let priv_a: [u8; 32] = to_bytes_be_padded(&da, 32).try_into().unwrap();
|
||||
|
||||
+3
-3
@@ -9,7 +9,7 @@ mod error;
|
||||
mod handshake;
|
||||
|
||||
use aes::Aes128;
|
||||
use aes::cipher::{BlockDecrypt, KeyInit, generic_array::GenericArray};
|
||||
use aes::cipher::{Array, BlockCipherDecrypt, KeyInit};
|
||||
|
||||
use crate::scsi::ScsiTransport;
|
||||
use crate::{DiscKind, UnlockCtx, UnlockError, Unlocked, Unlocker};
|
||||
@@ -17,8 +17,8 @@ use crate::{DiscKind, UnlockCtx, UnlockError, Unlocked, Unlocker};
|
||||
/// AES-128-ECB decrypt a single 16-byte block — used to decrypt the bus key /
|
||||
/// read_data_key the drive returns after the handshake.
|
||||
pub(crate) fn aes_ecb_decrypt(key: &[u8; 16], data: &[u8; 16]) -> [u8; 16] {
|
||||
let cipher = Aes128::new(GenericArray::from_slice(key));
|
||||
let mut block = GenericArray::clone_from_slice(data);
|
||||
let cipher = Aes128::new(&(*key).into());
|
||||
let mut block: Array<u8, _> = (*data).into();
|
||||
cipher.decrypt_block(&mut block);
|
||||
let mut out = [0u8; 16];
|
||||
out.copy_from_slice(&block);
|
||||
|
||||
+2
-2
@@ -299,8 +299,8 @@ fn establish_authenticated_session(scsi: &mut dyn ScsiTransport) -> Result<u8> {
|
||||
// handshake.
|
||||
let mut host_challenge = [0u8; 10];
|
||||
{
|
||||
use rand::RngCore;
|
||||
rand::thread_rng().fill_bytes(&mut host_challenge);
|
||||
use rand::Rng;
|
||||
rand::rng().fill_bytes(&mut host_challenge);
|
||||
}
|
||||
let mut hc_buf = [0u8; 16];
|
||||
hc_buf[0] = 0x00;
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ decryption is a separate concern, handled by the consumer.
|
||||
|
||||
Clients never name this module directly —
|
||||
[libfreemkv](https://github.com/freemkv/libfreemkv) dispatches through
|
||||
`freemkv_unlock::all_unlockers()`, and this module answers `matches()` /
|
||||
`unlock()` when the drive identity is one it supports.
|
||||
`freemkv_unlock::all_unlockers()`, and this module answers `unlock_features()` /
|
||||
`unlock_bus()` when the drive identity is one it supports.
|
||||
|
||||
## Scope: non-persistent unlock only
|
||||
|
||||
|
||||
+9
-9
@@ -141,7 +141,7 @@ pub struct ProfileMatch {
|
||||
/// translatable English message.
|
||||
fn decode_hex(s: &str) -> std::result::Result<Vec<u8>, &'static str> {
|
||||
let bytes = s.as_bytes();
|
||||
if bytes.len() % 2 != 0 {
|
||||
if !bytes.len().is_multiple_of(2) {
|
||||
return Err("hex");
|
||||
}
|
||||
let mut out = Vec::with_capacity(bytes.len() / 2);
|
||||
@@ -304,19 +304,19 @@ pub fn find_by_drive_id(profiles: &Profiles, drive_id: &crate::DriveId) -> Optio
|
||||
(Platform::Mt1959B, &profiles.mt1959_b),
|
||||
(Platform::Renesas, &profiles.renesas),
|
||||
] {
|
||||
if !prod.is_empty() {
|
||||
if let Some(p) = list.iter().find(|p| {
|
||||
if !prod.is_empty()
|
||||
&& let Some(p) = list.iter().find(|p| {
|
||||
p.identity.vendor_id.trim() == v
|
||||
&& p.identity.product_id.trim() == prod
|
||||
&& p.identity.product_revision.trim() == r
|
||||
&& p.identity.vendor_specific.trim() == vs
|
||||
&& p.identity.firmware_date.trim() == date
|
||||
}) {
|
||||
return Some(ProfileMatch {
|
||||
profile: p.clone(),
|
||||
platform,
|
||||
});
|
||||
}
|
||||
})
|
||||
{
|
||||
return Some(ProfileMatch {
|
||||
profile: p.clone(),
|
||||
platform,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(p) = list.iter().find(|p| {
|
||||
|
||||
Reference in New Issue
Block a user