Technique interfaces
Techniques covers what each technique does on disk: byte layouts, capacity, and detection. This page covers only the code shape.
type Technique interface {
Name() string
Hide(filesystem.Entry, Request) (Result, error)
Detect(filesystem.Entry, Request) ([]Finding, error)
Clear(filesystem.Entry, Request) (Result, error)
Extract(filesystem.Entry, Request) ([]byte, error)
}
One interface, three techniques
There is one Technique interface, not one per capability.
Each concrete technique takes a bare filesystem.Entry and checks for the one capability it needs: NamedStreamCapable, SlackSpaceCapable, or TimestompCapable.
If the check fails, it returns the ErrUnsupported sentinel, wrapped as technique %q: unsupported on this filesystem. Match it with errors.Is, not by string.
Get(name) maps a --technique string to the concrete technique.
Request is the shared input. Most of its fields are optional, and an operation reads only the ones it needs:
Slack framing
New payloads use a 28-byte NEM2 header with a random per-hide nonce. Older 12-byte NEMO frames stay readable.
The header tells a payload apart from leftover noise. The nonce stops identical payloads from reusing stale backup digests. Frame parsing checks the versioned header and payload bounds before slicing. The code is in slackframe.go.
The exact byte layout is in nemo’s payload frame.
Restoration and backup contract
For slack-space operations, Request.Backup func(Backup) error is called before any destructive write, when it is set. It receives the pre-write state: Backup{Technique, Target, Location, Original []byte, Timestamp}. If it returns an error, the operation aborts.
- Slack-space
HideandClearpass the overwritten bytes this way. clearwrites back the caller’s original bytes (Request.Restore, separate from the hide payload inRequest.Data) when it has them. Otherwise it zero-fills the frame.Result.Restoredsays which happened.- A
Request.Restorewhose length does not match the frame is rejected, not zero-filled. - A nil
Backupmeans the caller opted out of reversibility.
Persistence
The saving side is in manifest.go:
AppendManifest(path, Backup)LoadManifest(path)LatestImageBackup(records, technique, target, location, identity)
The file is nemo-manifest.jsonl by default, and --manifest moves it. It is JSON Lines with one Backup per line. Original is base64 and Timestamp is RFC 3339.
hidepasses a closure that callsAppendManifest. If that write fails, the hide aborts before any byte is overwritten.clearreads it back withLoadManifestandLatestImageBackup. It checks the image identity and exact frame digest before changing anything.- Later matching records win. Re-hiding a target and then clearing it restores the last hide’s bytes.
See slack-space hiding for what gets recorded.
Extract
extract reads a hidden payload without changing anything.
- named-stream: calls
filesystem.NamedStreamCapable.ReadStream. - slack-space: walks the entry’s
SlackRegionsand returns the first valid framed payload. It uses the same CRC-checking frame parser asHideandClear. If none decodes, it errors with"no framed slack payload found to extract". - timestomp: has no payload to retrieve and always errors.
First-frame limitation
A target’s slack space can hold more than one valid frame. Stale frames from an earlier, uncleared hide are one cause.
Extract returns only the first frame. Detect lists them all. So the two can disagree about which payload is the payload. Clear also uses the first frame, so at least extract and clear agree.
There is no --index flag to pick a specific frame.
Timestomp limitation
filesystem.TimestompCapable exposes Timestamp, SupportsTimestamp, and SetTimestamp.
Reading a current value does not show whether it was altered. There is no historical baseline and no anomaly detector, so timestomp.Detect returns no findings.
timestomp.Hide does not save the original timestamp to the manifest. timestomp.Clear needs the caller to supply the original, and it rejects a zero value. See Timestomping.
Finding and Result
Finding and Result are shared value types that do not depend on the technique:
type Finding struct {
Technique string // "named-stream" | "slack-space" | "timestomp"
Location string // stream name, slack region offset range, or timestamp field
Size int64 // bytes of hidden data recovered; 0 for timestomp findings
}
type Result struct {
Technique string
Target string // path of the entry acted on
Detail string // stream name written, slack offset range, or "field=value" for timestomp
Bytes int64 // bytes written/removed; 0 for timestomp
}
Finding is what detect prints, one per line (technique, location, size). It matches the output spec in the CLI reference.
Result is what hide and clear return on success. It carries no hash or timestamp, because internal/custody already captures both for every WriteAt at the Image layer. See Image and custody.
The command builds the custody-log line from Result (which technique, which target, what happened) plus the hash and timestamp the custody decorator recorded. Neither layer duplicates the other.
Why commands stay simple
Commands depend only on these interfaces, never on a filesystem-specific technique type.
Hide, Detect, and Clear only touch the capability interfaces. So the technique logic works on any filesystem. There is one implementation per technique, all in internal/technique: namedStreamTechnique, slackSpaceTechnique, and timestompTechnique. Each works with any Entry that has the capability it needs.