106 lines
2.6 KiB
Go
106 lines
2.6 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
reveal.go
|
|
|
|
The reveal command: extract the envelope from a stego file and unpack the payload
|
|
*/
|
|
|
|
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
|
|
"github.com/CarterPerez-dev/crypha/internal/engine"
|
|
"github.com/CarterPerez-dev/crypha/internal/payload"
|
|
"github.com/CarterPerez-dev/crypha/internal/report"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
func newRevealCmd() *cobra.Command {
|
|
var format, in, out, passphrase string
|
|
|
|
cmd := &cobra.Command{
|
|
Use: cmdReveal + " [-i] STEGO",
|
|
Short: "Reveal a hidden payload from a stego file",
|
|
Args: cobra.MaximumNArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
path, err := pathFromFlagOrArg(in, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if path == "" {
|
|
return errNoStego
|
|
}
|
|
|
|
stego, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
res, err := revealWithPassphrase(format, stego, passphraseFromFlagOrEnv(passphrase))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if jsonEnabled(cmd) {
|
|
if out != "" {
|
|
if err := writeOutputFile(out, res.Data); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return report.RevealJSON(cmd.OutOrStdout(), res, out)
|
|
}
|
|
|
|
outPath, err := writeRevealed(cmd, res.Data, out)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return report.RevealStatus(cmd.ErrOrStderr(), res, outPath)
|
|
},
|
|
}
|
|
|
|
f := cmd.Flags()
|
|
f.StringVar(&format, flagFormat, "", "force a carrier format (auto-detect if omitted)")
|
|
f.StringVarP(&in, flagIn, shortIn, "", "stego input path (or pass as an argument)")
|
|
f.StringVarP(&out, flagOut, shortOut, "", "write the revealed payload here (default stdout)")
|
|
f.StringVarP(&passphrase, flagPassphrase, shortPassphrase, "", "passphrase for an encrypted payload")
|
|
|
|
return cmd
|
|
}
|
|
|
|
func revealWithPassphrase(format string, stego, pass []byte) (engine.RevealResult, error) {
|
|
for attempt := 0; attempt <= maxPassphraseAttempts; attempt++ {
|
|
res, err := engine.Reveal(engine.RevealRequest{Format: format, Stego: stego, Passphrase: pass})
|
|
if err == nil {
|
|
return res, nil
|
|
}
|
|
if !shouldReprompt(err, attempt) {
|
|
return engine.RevealResult{}, err
|
|
}
|
|
entered, perr := promptPassphrase(promptForError(err))
|
|
if perr != nil {
|
|
return engine.RevealResult{}, perr
|
|
}
|
|
pass = entered
|
|
}
|
|
return engine.RevealResult{}, payload.ErrDecrypt
|
|
}
|
|
|
|
func shouldReprompt(err error, attempt int) bool {
|
|
if !interactive() {
|
|
return false
|
|
}
|
|
if errors.Is(err, payload.ErrPassphraseRequired) {
|
|
return true
|
|
}
|
|
return errors.Is(err, payload.ErrDecrypt) && attempt < maxPassphraseAttempts
|
|
}
|
|
|
|
func promptForError(err error) string {
|
|
if errors.Is(err, payload.ErrPassphraseRequired) {
|
|
return promptEnterPassphrase
|
|
}
|
|
return promptRetryPassphrase
|
|
}
|