stego/stego.go
2024-04-15 00:47:09 +02:00

355 lines
7.3 KiB
Go

// Copyright (C) 2018 Marius Schellenberger
package main
import (
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"errors"
"flag"
"fmt"
"image"
"image/color"
_ "image/gif"
_ "image/jpeg"
"image/png"
"io"
"math"
"os"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/hkdf"
)
const (
KeySize = chacha20poly1305.KeySize
NonceSize = chacha20poly1305.NonceSize
Overhead = chacha20poly1305.Overhead
ChunkSize = 512
FullSize = ChunkSize + Overhead
)
func deriveKey(key, nonce []byte, info string, length int) (k []byte) {
h := hkdf.New(sha256.New, key, nonce, []byte(info))
k = make([]byte, length)
if _, err := io.ReadFull(h, k); err != nil {
panic("crypto: internal error: failed to read from HKDF: " + err.Error())
}
return
}
const encodedSizeLen = 4
func encodeLen(l int) []byte {
b := make([]byte, encodedSizeLen)
binary.LittleEndian.PutUint32(b, uint32(l))
return b
}
func extract(p image.Image, i, j, size int) (int, int, []byte) {
b := make([]byte, size)
rect := p.Bounds()
c := 0
for ; i < rect.Max.Y; i++ {
for ; j < rect.Max.X; j++ {
if c == size {
return i, j, b
}
pix := color.NRGBAModel.Convert(p.At(j, i)).(color.NRGBA)
z := (pix.R << 6)
z |= (pix.G << 6) >> 2
z |= (pix.B << 6) >> 4
z |= (pix.A << 6) >> 6
b[c] = z
c++
}
j = 0
}
return i, j, b
}
func bytesToLen(b []byte) int {
return int(binary.LittleEndian.Uint32(b))
}
func Size(s int) int {
return s + int(math.Ceil(float64(s)/ChunkSize)*Overhead)
}
func unseal(p image.Image, key []byte) (b []byte) {
if key != nil {
i, j, nonce := extract(p, 0, 0, NonceSize)
k := deriveKey(key, nonce, infoEncryption, KeySize)
s := NewAEADStream(k).Decrypt()
i, j, firstBlock := extract(p, i, j, FullSize)
dec := make([]byte, ChunkSize)
s.XORKeyStream(dec, firstBlock)
l := bytesToLen(dec[:encodedSizeLen]) + encodedSizeLen - ChunkSize
_, _, data := extract(p, i, j, Size(l))
b = make([]byte, l)
s.XORKeyStream(b, data)
return append(dec[encodedSizeLen:], b...)
}
i, j, lbuf := extract(p, 0, 0, encodedSizeLen)
_, _, b = extract(p, i, j, bytesToLen(lbuf))
return
}
const (
infoEncryption = "ChaCha20-Poly1305 encryption key"
infoFiller = "ChaCha20-Poly1305 randomness filler"
infoFillerNonce = "ChaCha20-Poly1305 filler nonce"
infoFillerStart = "RandomnessFiller"
)
func filler(key, nonce []byte, l int) (r []byte) {
k := deriveKey(key, nonce, infoFiller, KeySize)
n := deriveKey(key, nonce, infoFillerNonce, NonceSize)
c, err := chacha20.NewUnauthenticatedCipher(k, n)
if err != nil {
panic(err)
}
var a, b [ChunkSize]byte
copy(a[:], []byte(infoFillerStart))
for len(r) < l {
c.XORKeyStream(b[:], a[:])
r = append(r, b[:]...)
a = b
}
return
}
type AEADStream struct {
c cipher.AEAD
nonce [NonceSize]byte
counter uint64
}
func NewAEADStream(key []byte) *AEADStream {
c, err := chacha20poly1305.New(key)
if err != nil {
panic(err)
}
return &AEADStream{c: c}
}
func (aead *AEADStream) Encrypt() cipher.Stream {
return &Encrypt{aead}
}
func (aead *AEADStream) Decrypt() cipher.Stream {
return &Decrypt{aead}
}
func (aead *AEADStream) NonceSize() int {
return aead.c.NonceSize()
}
func (aead *AEADStream) Overhead() int {
return aead.c.Overhead()
}
func (aead *AEADStream) Seal(dst, _, plaintext, additionalData []byte) []byte {
aead.inc()
b := aead.c.Seal(dst, aead.nonce[:], plaintext, additionalData)
return b
}
func (aead *AEADStream) Open(dst, _, ciphertext, additionalData []byte) ([]byte, error) {
aead.inc()
b, err := aead.c.Open(dst, aead.nonce[:], ciphertext, additionalData)
return b, err
}
func (aead *AEADStream) inc() {
put(&aead.nonce, aead.counter)
aead.counter++
}
func put(nonce *[NonceSize]byte, n uint64) {
binary.BigEndian.PutUint64((*nonce)[3:11], n)
}
type Encrypt struct {
cipher.AEAD
}
func h(b []byte, v ...any) {
fmt.Print(hex.EncodeToString(b) + " ")
fmt.Println(v...)
}
func (e *Encrypt) XORKeyStream(dst, src []byte) {
size := ChunkSize
var buffer []byte
var a [FullSize]byte
for len(src) > 0 {
if len(src) < size {
size = len(src)
}
b := src[:size]
src = src[size:]
buf := e.Seal(a[:0], nil, b, nil)
buffer = append(buffer, buf...)
}
copy(dst, buffer)
}
type Decrypt struct {
cipher.AEAD
}
func (d *Decrypt) XORKeyStream(dst, src []byte) {
size := FullSize
var buffer []byte
var a [ChunkSize]byte
for len(src) > 0 {
if len(src) < size {
size = len(src)
}
b := src[:size]
src = src[size:]
buf, err := d.Open(a[:0], nil, b, nil)
if err != nil {
panic(err)
}
buffer = append(buffer, buf...)
}
copy(dst, buffer)
}
func encrypt(data, key []byte, max int) (enc []byte, err error) {
nonce := make([]byte, NonceSize)
_, err = io.ReadFull(rand.Reader, nonce)
if err != nil {
return
}
k := deriveKey(key, nonce, infoEncryption, KeySize)
s := NewAEADStream(k).Encrypt()
enc = make([]byte, Size(len(data)))
s.XORKeyStream(enc, data)
enc = append(append(nonce, enc...), filler(key, nonce, max)...)[:max]
return
}
func seal(p image.Image, s, key []byte) (img image.Image, err error) {
rect := p.Bounds()
n := image.NewNRGBA(rect)
c := 0
arr := append(encodeLen(len(s)), s...)
l := len(arr)
max := (rect.Max.Y * rect.Max.X)
if l > max {
return nil, errors.New("not enough space in image")
}
if key != nil {
arr, err = encrypt(arr, key, max)
if err != nil {
return
}
l = max
}
for i := 0; i < rect.Max.Y; i++ {
for j := 0; j < rect.Max.X; j++ {
pix := color.NRGBAModel.Convert(p.At(j, i)).(color.NRGBA)
if c < l {
z := arr[c]
r := (pix.R &^ 3) | (z >> 6)
g := (pix.G &^ 3) | (z<<2)>>6
b := (pix.B &^ 3) | (z<<4)>>6
a := (pix.A &^ 3) | (z<<6)>>6
n.Set(j, i, color.NRGBA{r, g, b, a})
c++
} else {
n.Set(j, i, pix)
}
}
}
return n, nil
}
var (
inFile string
outFile string
keyFile string
secret string
)
func readKey() (b []byte, err error) {
if keyFile != "" {
if keyFile == "-" {
b, err = io.ReadAll(os.Stdin)
} else {
b, err = os.ReadFile(keyFile)
}
} else {
if k := os.Getenv(EnvKey); k != "" {
b = []byte(k)
} else {
return nil, nil
}
}
if err != nil {
return
}
return hex.DecodeString(string(b))
}
func errf(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
const EnvKey = "STEGO_KEY"
func main() {
flag.StringVar(&inFile, "in", "-", "input file (default: stdin)")
flag.StringVar(&keyFile, "key", "", "key file; used for encryption")
flag.StringVar(&outFile, "out", "-", "output file, always png (default: stdout)")
flag.StringVar(&secret, "secret", "", "secret file to hide")
flag.Parse()
if inFile == "-" && keyFile == "-" {
errf(errors.New("can not read input and key files from stdin"))
}
key, err := readKey()
if err != nil {
errf(err)
}
var (
in = os.Stdin
out = os.Stdout
)
if inFile != "-" {
in, err = os.Open(inFile)
if err != nil {
errf(err)
}
}
if outFile != "-" {
out, err = os.OpenFile(outFile, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
errf(err)
}
}
p, _, err := image.Decode(in)
if err != nil {
errf(err)
}
if secret != "" {
b, err := os.ReadFile(secret)
if err != nil {
errf(err)
}
n, err := seal(p, b, key)
if err != nil {
errf(err)
}
err = png.Encode(out, n)
if err != nil {
errf(err)
}
return
}
_, err = out.Write(unseal(p, key))
if err != nil {
errf(err)
}
}