355 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|