Skip to content

Commit d8ef5cb

Browse files
authored
Merge pull request #5 from SecureToolsProject/feat/sprint-4-jpeg-cleaner-verification
✨[Feat] Sprint 4 JPEG Cleaner and Verification
2 parents 2fd70ab + 739bfce commit d8ef5cb

17 files changed

Lines changed: 735 additions & 74 deletions

‎CHANGELOG.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ All notable changes will be documented here. The project intends to follow seman
66

77
### Added
88

9+
- Deterministic JPEG Privacy Clean for whole EXIF, XMP, Photoshop/IPTC, and COM segments.
10+
- Checked single-allocation JPEG reconstruction preserving ICC, unknown APP, structural, scan, and trailing bytes.
11+
- Structured JPEG presence/absence verification and typed unsupported/incomplete-input errors.
12+
- Compact canonical coverage for determinism, idempotency, multiple scans and metadata instances, malformed TIFF removal, and exact subviews.
13+
914
- Shared bounded little- and big-endian TIFF/EXIF decoder.
1015
- Iterative IFD0, ExifIFD, GPSIFD, and next-IFD traversal.
1116
- IFD entry/depth limits and repeated-offset cycle protection.
@@ -17,6 +22,11 @@ All notable changes will be documented here. The project intends to follow seman
1722
- Bounded JPEG marker traversal and EXIF/XMP/ICC/IPTC container detection.
1823
- Binary boundary, JPEG container, TIFF endian, malformed, cycle, and integration tests.
1924

25+
### Changed
26+
27+
- JPEG parser records internal fill-aware rewrite ranges while retaining existing public source offsets.
28+
- Parse-limit validation is shared by inspection and cleaning.
29+
2030
### Foundation
2131

2232
- Repository and TypeScript library scaffold.

‎README.md‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ Current implementation:
1212
- JPEG EXIF, XMP, ICC, Photoshop/IPTC, and comment container detection;
1313
- shared little- and big-endian TIFF/EXIF decoder;
1414
- iterative IFD0, ExifIFD, GPSIFD, and next-IFD traversal with cycle and depth protection;
15-
- common TIFF, EXIF, and GPS field decoding with exact rational values.
15+
- common TIFF, EXIF, and GPS field decoding with exact rational values;
16+
- deterministic whole-segment JPEG Privacy Clean with byte-preserving reconstruction;
17+
- structured JPEG verification for observable container presence or absence.
1618

17-
Not implemented: MakerNote or thumbnail decoding, XMP/IPTC/ICC payload parsing, PNG/WebP container parsing, metadata cleaning, and verification.
19+
Not implemented: MakerNote or thumbnail decoding, XMP/IPTC/ICC payload parsing, PNG/WebP container parsing or cleaning, and PNG/WebP verification.
1820

1921
## Format status
2022

@@ -38,11 +40,13 @@ import {
3840

3941
GPS rational components remain exact numerator/denominator pairs; decimal coordinates are not derived. Unknown TIFF tags and MakerNote are represented structurally without dumping or recursively parsing their payloads.
4042

41-
`cleanMetadata` and `verifyMetadata` still throw a typed `NotImplementedError`.
43+
`cleanMetadata` supports JPEG. Its default policy removes complete EXIF, standard/extended XMP, Photoshop/IPTC, and COM segments while preserving ICC, JFIF/JFXX, Adobe APP14, unknown APP segments, structural data, scan bytes, and trailing bytes. It returns a separate output, container-level change evidence, and an inspection report of that output.
44+
45+
`verifyMetadata` supports JPEG expectations of `absent`, `present`, or `ignore` for EXIF, XMP, IPTC, comments, and ICC. The default checks the four privacy-clean removal targets. A single-file verification can observe presence or absence; it cannot prove that bytes came from an original file.
4246

4347
## Security philosophy
4448

45-
Every byte is untrusted. All offsets are interpreted within bounded views, traversal is iterative and limited, repeated IFD offsets are rejected, and malformed entries recover without unchecked access. Unknown structures remain unknown and should be preserved by future cleaning. See the [security model](docs/security-model.md), [architecture](docs/architecture.md), and [cleaning policy](docs/cleaning-policy.md).
49+
Every byte is untrusted. All offsets are interpreted within bounded views, traversal is iterative and limited, repeated IFD offsets are rejected, and malformed entries recover without unchecked access. Unknown JPEG APP structures remain unknown and are preserved by cleaning. See the [security model](docs/security-model.md), [architecture](docs/architecture.md), and [cleaning policy](docs/cleaning-policy.md).
4650

4751
## Non-goals
4852

‎docs/architecture.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,17 @@ Unknown tags retain namespace, tag number, TIFF type, count, entry offset, and s
3232
- `container-partial`: JPEG traversal stopped on corruption, truncation, or a limit.
3333
- `metadata-partial`: JPEG container traversal completed and common TIFF/EXIF decoding was attempted; XMP/IPTC/ICC and unknown fields remain incomplete.
3434
- `metadata-inspected`: reserved for future broader decoders.
35+
36+
## JPEG clean and verify flow
37+
38+
```text
39+
input JPEG
40+
→ bounded JPEG parser and existing APP classification
41+
→ direct keep/remove policy
42+
→ checked retained ranges
43+
→ one output allocation and ordered byte copies
44+
→ inspectMetadata(output)
45+
→ structured verification checks
46+
```
47+
48+
The parser remains the structural source of truth. Internal rewrite ranges include marker fill bytes associated with a removed marker while public source offsets retain their existing meaning. Cleaning does not invoke TIFF decoding on the source: a structurally bounded EXIF APP1 can be removed even if its TIFF body is malformed. The post-write inspection and verifier use the normal inspection layer.

‎docs/cleaning-policy.md‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
1-
# Cleaning Policy Direction
1+
# Cleaning Policy
22

3-
Cleaning is not implemented in Sprint 0. This document records the intended conservative policy for future work.
3+
JPEG Privacy Clean removes complete recognized metadata containers. It never rewrites TIFF/EXIF fields, XMP XML, IPTC blocks, comments, or ICC payloads.
44

5-
An initial privacy-clean mode should remove EXIF, GPS, XMP, IPTC, comments, and privacy-relevant textual metadata. It should preserve the encoded image payload, required container structures, ICC and other color profiles, rendering-critical metadata, and unknown structures unless the relevant format specification proves removal is safe.
5+
| JPEG structure | Default action |
6+
| -------------------------------------- | -------------- |
7+
| EXIF APP1 | Remove |
8+
| Standard XMP APP1 | Remove |
9+
| Extended XMP APP1 | Remove |
10+
| Photoshop/IPTC APP13 | Remove |
11+
| COM | Remove |
12+
| ICC APP2 | Preserve |
13+
| JFIF/JFXX APP0 | Preserve |
14+
| Adobe APP14 | Preserve |
15+
| Unknown APP | Preserve |
16+
| Structural markers and image/scan data | Preserve |
17+
| Data after EOI | Preserve |
618

7-
For v0.1, whole EXIF containers are preferred over selective TIFF rewriting:
19+
Every recognized instance is handled independently and retained content keeps its original order and bytes. Unknown APP removal is intentionally unavailable in Sprint 4. Callers may override the four removal booleans and ICC preservation; `preserveColorProfiles` remains a deprecated alias for `preserveIcc`.
820

9-
```text
10-
JPEG APP1 EXIF → remove whole EXIF APP1
11-
PNG eXIf → remove whole eXIf chunk
12-
WebP EXIF → remove whole EXIF chunk
13-
```
21+
v0.1 removes the entire EXIF APP1, including malformed TIFF bodies whose JPEG segment boundary is valid. Selective GPS or tag rewriting and TIFF reserialization are deferred.
1422

15-
Selective EXIF field rewriting is postponed. This reduces offset-rewrite complexity and makes cleaner behavior easier to audit. Unaffected bytes should remain byte-for-byte identical whenever the container format permits it, and output must be re-inspected rather than trusted merely because a write completed.
23+
`cleanMetadata` returns a new `Uint8Array`, container-level removed/preserved records, diagnostics, and an inspection report of the produced JPEG. A structurally incomplete JPEG is rejected before allocation. PNG, WebP, and unknown inputs return a typed unsupported-format error.

‎docs/format-support.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
| MakerNote decoding | Not supported | Not supported | Not supported |
1212
| XMP payload decoding | Not yet | Not yet | Not yet |
1313
| IPTC/ICC payload decoding | Not yet | Not yet | Not yet |
14-
| Cleaning and verification | Not yet | Not yet | Not yet |
14+
| Whole-container cleaning | Supported | Not yet | Not yet |
15+
| Structured verification | Supported | Not yet | Not yet |
1516

1617
## TIFF/EXIF subset
1718

@@ -28,3 +29,9 @@ Unknown tags remain structurally represented without speculative meaning or larg
2829
## Remaining container support
2930

3031
JPEG marker and scan traversal remains supported. XMP, ICC, and Photoshop/IPTC signatures are container-detected only. PNG requires its complete signature and WebP requires `RIFF....WEBP`; their chunks and metadata are not parsed yet.
32+
33+
## JPEG cleaning and verification
34+
35+
JPEG Privacy Clean removes recognized EXIF, standard/extended XMP, Photoshop/IPTC, and comment segments. ICC, JFIF/JFXX, Adobe APP14, unknown APP segments, structural markers, all scan data, and trailing bytes are retained. Structurally incomplete JPEGs are rejected; malformed TIFF inside a bounded removable EXIF segment does not block cleaning.
36+
37+
Verification reports observable container presence or absence for EXIF, XMP, IPTC, comments, and ICC. It does not decode XMP/IPTC/ICC payloads or prove preservation from an original input.

‎docs/security-model.md‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ Binary metadata parsing processes attacker-controlled structures, sizes, offsets
1010
4. Core functions make no network requests and access no filesystem or DOM APIs.
1111
5. Image pixel payloads are never decoded.
1212
6. Unknown metadata is not deleted or assigned speculative meaning.
13-
7. ICC and color data will be preserved by default during future cleaning.
14-
8. Cleaner output must eventually be independently inspected and verified.
13+
7. JPEG cleaning preserves ICC, unknown APP, and rendering/container segments by default.
14+
8. Cleaner output is re-inspected before it is returned.
1515
9. Metadata absence never proves an image contains no private information.
1616
10. Steganography detection, malware scanning, visual redaction, and pixel privacy analysis are outside scope.
1717

@@ -40,3 +40,11 @@ Every traversal or decoding loop has a validated finite count or advances a boun
4040
## Environment and dependencies
4141

4242
Core code is local-only and side-effect-free. It has no network, analytics, telemetry, filesystem, DOM, or pixel-codec behavior. The package has zero runtime dependencies.
43+
44+
## JPEG cleaning properties
45+
46+
Cleaning proceeds only after bounded traversal reaches EOI. Truncated lengths, invalid marker structure, unterminated scans, and segment-limit failures produce a typed `IncompleteJpegError`; no partial output is returned. TIFF validity is not required to remove a structurally bounded EXIF APP1.
47+
48+
Removal uses checked, non-overlapping parser ranges. Output length is a safe integer no larger than input length, one output buffer is allocated, and retained ranges are copied in original order. Entropy-coded bytes, restart markers, retained marker fill, structural segments, and bytes after EOI are neither decoded nor regenerated. Exact `Uint8Array` views are honored and caller input is never mutated.
49+
50+
The default policy preserves every ICC and unknown APP segment. Verification proves only the requested observable container state supported by inspection. It does not prove provenance, byte preservation without an original, absence of unknown metadata, or absence of personal information in pixels or unsupported structures.

‎src/core/errors.ts‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import type { Diagnostic } from "./diagnostics.js";
2+
13
export type SecureMetadataErrorCode =
24
| "NOT_IMPLEMENTED"
35
| "INVALID_OFFSET"
46
| "INVALID_LENGTH"
57
| "OUT_OF_BOUNDS"
68
| "INVALID_LIMIT"
7-
| "INPUT_LIMIT_EXCEEDED";
9+
| "INPUT_LIMIT_EXCEEDED"
10+
| "UNSUPPORTED_FORMAT"
11+
| "INCOMPLETE_JPEG"
12+
| "CLEAN_OUTPUT_SIZE_INVALID";
813

914
export class SecureMetadataError extends Error {
1015
override readonly name: string = "SecureMetadataError";
@@ -75,3 +80,31 @@ export class InputLimitExceededError extends SecureMetadataError {
7580
);
7681
}
7782
}
83+
84+
export class UnsupportedFormatError extends SecureMetadataError {
85+
override readonly name: string = "UnsupportedFormatError";
86+
87+
constructor(
88+
readonly operation: "cleanMetadata" | "verifyMetadata",
89+
readonly format: "png" | "webp" | "unknown",
90+
) {
91+
super(
92+
`${operation} does not support ${format} input.`,
93+
"UNSUPPORTED_FORMAT",
94+
);
95+
}
96+
}
97+
98+
export class IncompleteJpegError extends SecureMetadataError {
99+
override readonly name: string = "IncompleteJpegError";
100+
101+
constructor(
102+
readonly operation: "cleanMetadata" | "verifyMetadata",
103+
readonly diagnostics: readonly Diagnostic[],
104+
) {
105+
super(
106+
`${operation} requires a structurally complete JPEG ending at EOI.`,
107+
"INCOMPLETE_JPEG",
108+
);
109+
}
110+
}

‎src/core/limits.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { InvalidParseLimitError } from "./errors.js";
2+
13
export interface ParseLimits {
24
readonly maxInputBytes: number;
35
readonly maxSegments: number;
@@ -21,3 +23,14 @@ export const DEFAULT_PARSE_LIMITS: Readonly<ParseLimits> = Object.freeze({
2123
maxDecompressedBytes: 16 * 1024 * 1024,
2224
maxDiagnostics: 256,
2325
});
26+
27+
export function resolveParseLimit(
28+
name: keyof ParseLimits,
29+
configured: number | undefined,
30+
): number {
31+
const value = configured ?? DEFAULT_PARSE_LIMITS[name];
32+
if (!Number.isSafeInteger(value) || value < 0) {
33+
throw new InvalidParseLimitError(name, value);
34+
}
35+
return value;
36+
}

‎src/core/types.ts‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,23 +93,54 @@ export interface MetadataReport {
9393
}
9494

9595
export interface CleaningPolicy {
96+
readonly removeExif?: boolean;
97+
readonly removeXmp?: boolean;
98+
readonly removeIptc?: boolean;
99+
readonly removeComments?: boolean;
100+
readonly preserveIcc?: boolean;
101+
/** @deprecated Use preserveIcc. */
96102
readonly preserveColorProfiles?: boolean;
97103
readonly limits?: Partial<ParseLimits>;
98104
}
99105

106+
export interface MetadataChange {
107+
readonly namespace: MetadataNamespace;
108+
readonly action: "removed" | "preserved";
109+
readonly name: string;
110+
readonly source: MetadataSource;
111+
}
112+
100113
export interface CleanResult {
101114
readonly output: Uint8Array;
115+
readonly format: "jpeg";
102116
readonly report: MetadataReport;
103-
readonly removedEntryIds: readonly string[];
117+
readonly removed: readonly MetadataChange[];
118+
readonly preserved: readonly MetadataChange[];
119+
readonly diagnostics: readonly Diagnostic[];
104120
}
105121

122+
export type VerificationExpectation = "absent" | "present" | "ignore";
123+
106124
export interface VerificationPolicy {
125+
readonly exif?: VerificationExpectation;
126+
readonly xmp?: VerificationExpectation;
127+
readonly iptc?: VerificationExpectation;
128+
readonly comments?: VerificationExpectation;
129+
readonly icc?: VerificationExpectation;
107130
readonly requireNoPrivacyRelevantMetadata?: boolean;
108131
readonly limits?: Partial<ParseLimits>;
109132
}
110133

134+
export interface VerificationCheck {
135+
readonly namespace: "exif" | "xmp" | "iptc" | "jpeg-comment" | "icc";
136+
readonly expected: Exclude<VerificationExpectation, "ignore">;
137+
readonly actual: "absent" | "present";
138+
readonly passed: boolean;
139+
}
140+
111141
export interface VerificationResult {
112142
readonly valid: boolean;
143+
readonly checks: readonly VerificationCheck[];
113144
readonly report: MetadataReport;
114145
readonly diagnostics: readonly Diagnostic[];
115146
}

‎src/index.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
export { inspectMetadata } from "./inspect.js";
2-
export { cleanMetadata } from "./policy/clean.js";
3-
export { verifyMetadata } from "./verify/verify.js";
2+
export { cleanMetadata, DEFAULT_JPEG_CLEANING_POLICY } from "./policy/clean.js";
3+
export {
4+
DEFAULT_JPEG_VERIFICATION_POLICY,
5+
verifyMetadata,
6+
} from "./verify/verify.js";
47

58
export {
69
BinaryBoundsError,
10+
IncompleteJpegError,
711
InputLimitExceededError,
812
InvalidParseLimitError,
913
NotImplementedError,
1014
SecureMetadataError,
15+
UnsupportedFormatError,
1116
} from "./core/errors.js";
1217
export { DEFAULT_PARSE_LIMITS } from "./core/limits.js";
1318

@@ -29,6 +34,7 @@ export type {
2934
InspectionStatus,
3035
InspectOptions,
3136
MetadataCategory,
37+
MetadataChange,
3238
MetadataContainer,
3339
MetadataEntry,
3440
MetadataNamespace,
@@ -37,6 +43,8 @@ export type {
3743
MetadataValue,
3844
PrivacyRelevance,
3945
RationalValue,
46+
VerificationCheck,
47+
VerificationExpectation,
4048
VerificationPolicy,
4149
VerificationResult,
4250
} from "./core/types.js";

0 commit comments

Comments
 (0)