Skip to content

Commit 6f3ab74

Browse files
authored
feat: F5 JPEG steganography support (#235)
## Summary Adds JPEG steganography support using the F5 algorithm, enabling users to hide and extract secret data in JPEG images alongside the existing PNG and WAV support. - JPEG hide/unveil works transparently via file extension detection - Password serves dual purpose: encryption (ChaCha20-Poly1305) AND F5 permutation seed - No public API changes — same builder pattern as PNG/WAV ## What is F5? F5 is a steganographic algorithm designed specifically for JPEG images. Unlike naive LSB embedding in spatial domain: - **Frequency domain embedding**: Data is hidden in quantized DCT coefficients, surviving JPEG's lossy compression - **Matrix encoding**: Minimizes embedding changes using (1, n, k) codes — on average only 1 coefficient change per k bits embedded - **Permutation-based shuffling**: When a password is provided, coefficients are accessed in a pseudorandom order derived from the password, making extraction without the password computationally infeasible This makes F5 more statistically secure than naive approaches — changes to the coefficient histogram are minimal and spread across the image. ## Usage Examples ### CLI ```bash # Hide a message in a JPEG (without password) stegano hide --in photo.jpg --out secret.jpg --message "Hello World" # Hide with password (encrypted + shuffled) stegano hide --in photo.jpg --out secret.jpg --message "Secret!" --password "MyPass123" # Unveil stegano unveil --in secret.jpg --out ./output/ --password "MyPass123" cat ./output/secret-message.txt ``` ### Rust API ```rust use stegano_core::api::{hide, unveil}; // Hide with password hide::prepare() .with_message("Secret message") .with_image("photo.jpg") .using_password("MyPassword") .with_output("stego.jpg") .execute()?; // Unveil unveil::prepare() .from_secret_file("stego.jpg") .using_password("MyPassword") .into_output_folder("./output/") .execute()?; ``` ### Cross-format (PNG → JPEG) ```rust hide::prepare() .with_message("From PNG to JPEG") .with_image("source.png") // PNG input .with_output("output.jpg") // JPEG output .execute()?; ``` ## Security Model | Mode | Encryption | Coefficient Access | Security | |------|------------|-------------------|----------| | No password | None | Sequential | Low — extraction is straightforward | | With password | ChaCha20-Poly1305 | Permuted (password-seeded) | High — attacker needs password for both extraction order and decryption | ## Test Plan - [x] Unit tests for JPEG hide/unveil with and without password - [x] Wrong password fails extraction - [x] Binary file roundtrip test - [x] PNG→JPEG cross-format test - [x] All existing PNG/WAV tests still pass (60 tests) - [x] CLI smoke tests pass --------- Signed-off-by: Sven Kanoldt <sven@d34dl0ck.me>
1 parent ded19f0 commit 6f3ab74

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

72 files changed

+4759
-265
lines changed

.cargo/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ lint = "clippy --all-targets"
55
benchmarks = "bench --features benchmarks --locked"
66
ntest = "nextest run --locked"
77
coverage = "llvm-cov --workspace --codecov --output-path codecov.json"
8+
t = "nextest run --locked"

.github/workflows/build.yml

Lines changed: 119 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,40 +11,138 @@ on:
1111
required: true
1212

1313
jobs:
14-
build:
15-
uses: steganogram/.github/.github/workflows/build.yml@main
14+
check:
15+
name: check
16+
strategy:
17+
fail-fast: false
18+
matrix:
19+
os: ["macos-latest", "ubuntu-latest", "windows-latest"]
20+
runs-on: ${{ matrix.os }}
21+
steps:
22+
- uses: actions/checkout@v4
23+
with:
24+
submodules: recursive
25+
- name: setup | rust
26+
uses: steganogram/.github/.github/actions/rust-toolchain@main
27+
- run: cargo check
28+
29+
lint:
30+
name: lint
31+
strategy:
32+
fail-fast: false
33+
matrix:
34+
os: ["macos-latest", "ubuntu-latest", "windows-latest"]
35+
cargo-cmd:
36+
- fmt --all -- --check
37+
- clippy --all-targets -- -D warnings
38+
runs-on: ${{ matrix.os }}
39+
steps:
40+
- uses: actions/checkout@v4
41+
with:
42+
submodules: recursive
43+
- name: setup | rust
44+
uses: steganogram/.github/.github/actions/rust-toolchain@main
45+
- run: cargo ${{ matrix['cargo-cmd'] }}
46+
47+
tests:
48+
name: tests
49+
strategy:
50+
fail-fast: false
51+
matrix:
52+
os: ["macos-latest", "ubuntu-latest", "windows-latest"]
53+
channel: ["nightly", "stable"]
54+
runs-on: ${{ matrix.os }}
55+
continue-on-error: ${{ matrix.channel == 'nightly' }}
56+
steps:
57+
- uses: actions/checkout@v4
58+
with:
59+
submodules: recursive
60+
- name: setup | rust
61+
uses: steganogram/.github/.github/actions/rust-toolchain@main
62+
with:
63+
channel: ${{ matrix.channel }}
64+
default: true
65+
profile: minimal
66+
- name: cargo test
67+
if: matrix.os != 'windows-latest'
68+
run: |
69+
if [ -f "Cargo.lock" ]; then
70+
cargo test --all --locked
71+
else
72+
cargo test --all
73+
fi
74+
- name: cargo test
75+
if: matrix.os == 'windows-latest'
76+
run: |
77+
if (Test-Path "Cargo.lock") {
78+
cargo test --all --locked
79+
} else {
80+
cargo test --all
81+
}
82+
83+
audit:
84+
name: security audit
85+
runs-on: ubuntu-latest
86+
steps:
87+
- uses: actions/checkout@v4
88+
with:
89+
submodules: recursive
90+
- name: setup | rust
91+
uses: steganogram/.github/.github/actions/rust-toolchain@main
92+
- uses: taiki-e/install-action@v2
93+
with:
94+
tool: cargo-deny
95+
- name: audit
96+
run: cargo deny check advisories bans sources
97+
continue-on-error: true
98+
99+
docs:
100+
name: docs
101+
runs-on: ubuntu-latest
102+
steps:
103+
- uses: actions/checkout@v4
104+
with:
105+
submodules: recursive
106+
- name: setup | rust
107+
uses: steganogram/.github/.github/actions/rust-toolchain@main
108+
- name: check documentation
109+
env:
110+
RUSTDOCFLAGS: -D warnings
111+
run: cargo doc --no-deps
16112

17113
coverage:
18114
name: code coverage
19-
uses: steganogram/.github/.github/workflows/coverage.yml@main
20-
secrets:
21-
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
115+
runs-on: ubuntu-latest
116+
steps:
117+
- uses: actions/checkout@v4
118+
with:
119+
submodules: recursive
120+
- name: setup | rust
121+
uses: steganogram/.github/.github/actions/rust-toolchain@main
122+
with:
123+
channel: nightly
124+
- uses: taiki-e/install-action@cargo-llvm-cov
125+
- name: run code coverage
126+
run: cargo +nightly llvm-cov --all-features --workspace --lcov --output-path lcov.info
127+
- name: upload to codecov.io
128+
uses: codecov/codecov-action@v5
129+
continue-on-error: true
130+
with:
131+
token: ${{ secrets.CODECOV_TOKEN }}
132+
files: lcov.info
133+
fail_ci_if_error: true
22134

23135
benchmark:
24136
name: benchmark
25137
runs-on: ubuntu-latest
26138
continue-on-error: true
27139
steps:
28140
- uses: actions/checkout@v4
141+
with:
142+
submodules: recursive
29143
- name: setup | rust
30144
uses: steganogram/.github/.github/actions/rust-toolchain@main
31145
with:
32146
channel: nightly
33147
- name: run benchmarks
34148
run: cargo +nightly benchmarks
35-
36-
# pkg-deb:
37-
# name: binaray package .deb
38-
# needs: check
39-
# runs-on: ubuntu-latest
40-
# steps:
41-
# - uses: actions/checkout@v4
42-
# - name: cargo deb
43-
# uses: sassman/rust-deb-builder@v1
44-
# with:
45-
# package: stegano-cli
46-
# - name: Archive deb artifact
47-
# uses: actions/upload-artifact@v2
48-
# with:
49-
# name: stegano-cli-amd64-static.deb
50-
# path: target/x86_64-unknown-linux-musl/debian/stegano-cli*.deb

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
target
22
tmp
3+
.planning/

.gitmodules

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[submodule "crates/stegano-f5-jpeg-decoder"]
2+
path = crates/stegano-f5-jpeg-decoder
3+
url = git@github.com:steganogram/stegano-f5-jpeg-decoder.git
4+
[submodule "crates/stegano-f5-jpeg-encoder"]
5+
path = crates/stegano-f5-jpeg-encoder
6+
url = git@github.com:steganogram/stegano-f5-jpeg-encoder.git

0 commit comments

Comments
 (0)