├── .gitignore ├── go.mod ├── LICENSE ├── README.md ├── go.sum ├── pngUtils.go └── cryptpng.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.png 2 | *.jpg 3 | *.txt 4 | *.pdf 5 | .idea 6 | cryptpng -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/trivernis/cryptpng 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/cheggaaa/pb/v3 v3.0.4 7 | golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 8 | ) 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Trivernis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cryptpng ![](https://img.shields.io/discord/729250668162056313) 2 | 3 | A way to store encrypted data inside a png without altering the image itself. 4 | 5 | ## Usage 6 | 7 | ```shell script 8 | # encrypt 9 | cryptpng encrypt --image --in --out 10 | 11 | # decrypt 12 | cryptpng decrypt --image --out 13 | ``` 14 | 15 | ## Technical Information 16 | 17 | It should be possible to store data with a size up to ~ 4GB, but in reality most image viewers have 18 | problems with chunks that are bigger than several Megabytes. 19 | The data itself is stored in a [png chunk](http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html) 20 | and encrypted via aes. The encryption chunk is stored right before the `IDAT` chunk that contains the 21 | image data. The steps for encrypting are: 22 | 23 | ### Encrypt 24 | 25 | 1. Parse the png file and split it into chunks. 26 | 2. Prompt for a password and use the scrypt 32byte value with a generated salt. 27 | 3. Store the salt in the `saLt` chunk. 28 | 4. Encrypt the data using aes and the provided hashed key. 29 | 5. Split the data into parts of 1 MiB of size. 30 | 6. Store every data part into a separate `crPt` chunk. 31 | 7. Write the png header and chunks to the output file. 32 | 33 | ### Decrypt 34 | 35 | 1. Parse the png file and split it into chunks. 36 | 2. Get the `saLt` chunk. 37 | 3. Get the `crPt` chunks and and concat the data. 38 | 4. Prompt for the password and create the scrypt 32byte hash with the salt. 39 | 5. Decrypt the data using aes and the provided hash key. 40 | 6. Write the data to the specified output file. 41 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= 2 | github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= 3 | github.com/cheggaaa/pb/v3 v3.0.4 h1:QZEPYOj2ix6d5oEg63fbHmpolrnNiwjUsk+h74Yt4bM= 4 | github.com/cheggaaa/pb/v3 v3.0.4/go.mod h1:7rgWxLrAUcFMkvJuv09+DYi7mMUYi8nO9iOWcvGJPfw= 5 | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= 6 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 7 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 8 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 9 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 10 | github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= 11 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 12 | github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= 13 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 14 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 15 | golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg= 16 | golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 17 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 18 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 19 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 20 | golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= 21 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 22 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU= 24 | golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 25 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 26 | -------------------------------------------------------------------------------- /pngUtils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "hash/crc32" 7 | "io" 8 | "os" 9 | ) 10 | 11 | 12 | type ChunkData struct { 13 | length uint32 14 | name string 15 | data []byte 16 | crc uint32 17 | } 18 | 19 | func (c *ChunkData) GetRaw() []byte { 20 | var raw []byte 21 | lengthRaw := make([]byte, 4) 22 | crcRaw := make([]byte, 4) 23 | binary.BigEndian.PutUint32(lengthRaw, c.length) 24 | binary.BigEndian.PutUint32(crcRaw, c.crc) 25 | nameRaw := []byte(c.name) 26 | raw = append(raw, lengthRaw...) 27 | raw = append(raw, nameRaw...) 28 | raw = append(raw, c.data...) 29 | raw = append(raw, crcRaw...) 30 | return raw 31 | } 32 | 33 | // verifies the integrity of the chunks data using crc 34 | func (c *ChunkData) Verify() bool { 35 | var data []byte 36 | data = append(data, []byte(c.name)...) 37 | data = append(data, c.data...) 38 | crc := crc32.ChecksumIEEE(data) 39 | return c.crc == crc 40 | } 41 | 42 | type PngData struct { 43 | header []byte 44 | chunks []ChunkData 45 | } 46 | 47 | // Reads the png data from a file into the struct 48 | func (p *PngData) Read(f *os.File) error { 49 | valid, header := ValidatePng(f) 50 | if valid { 51 | p.header = header 52 | err := p.readChunks(f) 53 | if err != io.EOF { 54 | return err 55 | } 56 | } else { 57 | return errors.New("invalid png") 58 | } 59 | return nil 60 | } 61 | 62 | // writes all the data of the png into a new file 63 | func (p *PngData) Write(f *os.File) error { 64 | _, err := f.Write(p.header) 65 | if err != nil { 66 | return err 67 | } 68 | err = p.writeChunks(f) 69 | return err 70 | } 71 | 72 | // reads all chunks from a png file. 73 | // must be called after reading the header 74 | func (p *PngData) readChunks(f *os.File) error { 75 | chunk, err := ReadChunk(f) 76 | for err == nil { 77 | p.chunks = append(p.chunks, chunk) 78 | chunk, err = ReadChunk(f) 79 | } 80 | p.chunks = append(p.chunks, chunk) 81 | return err 82 | } 83 | 84 | // writes all chunks to the given file 85 | func (p *PngData) writeChunks(f *os.File) error { 86 | for _, chunk := range p.chunks { 87 | _, err := f.Write(chunk.GetRaw()) 88 | if err != nil { 89 | return err 90 | } 91 | } 92 | return nil 93 | } 94 | 95 | // adds a meta chunk to the chunk data before the IDAT chunk. 96 | func (p *PngData) AddMetaChunk(metaChunk ChunkData) { 97 | var newChunks []ChunkData 98 | appended := false 99 | for _, chunk := range p.chunks { 100 | if chunk.name == "IDAT" && !appended { 101 | newChunks = append(newChunks, metaChunk) 102 | newChunks = append(newChunks, chunk) 103 | appended = true 104 | } else { 105 | newChunks = append(newChunks, chunk) 106 | } 107 | } 108 | p.chunks = newChunks 109 | } 110 | 111 | // Returns the reference of a chunk by name 112 | func (p *PngData) GetChunk(name string) *ChunkData { 113 | for _, chunk := range p.chunks { 114 | if chunk.name == name { 115 | return &chunk 116 | } 117 | } 118 | return nil 119 | } 120 | 121 | // returns all chunks with a given name 122 | func (p *PngData) GetChunksByName(name string) []ChunkData { 123 | var chunks []ChunkData 124 | for _, chunk := range p.chunks { 125 | if chunk.name == name { 126 | chunks = append(chunks, chunk) 127 | } 128 | } 129 | return chunks 130 | } 131 | 132 | // validates the png by reading the header of the file 133 | func ValidatePng(f *os.File) (bool, []byte) { 134 | headerBytes := make([]byte, 8) 135 | _, err := f.Read(headerBytes) 136 | check(err) 137 | firstByteMatch := headerBytes[0] == 0x89 138 | pngAsciiMatch := string(headerBytes[1:4]) == "PNG" 139 | dosCRLF := headerBytes[4] == 0x0d && headerBytes[5] == 0x0a 140 | dosEof := headerBytes[6] == 0x1a 141 | unixLF := headerBytes[7] == 0x0a 142 | return firstByteMatch && pngAsciiMatch && dosCRLF && dosEof && unixLF, headerBytes 143 | } 144 | 145 | // reads the data of one chunk 146 | // it is assumed that the file reader is at the beginning of the chunk when reading 147 | func ReadChunk(f *os.File) (ChunkData, error) { 148 | lengthRaw := make([]byte, 4) 149 | _, err := f.Read(lengthRaw) 150 | length := binary.BigEndian.Uint32(lengthRaw) 151 | crcRaw := make([]byte, 4) 152 | nameRaw := make([]byte, 4) 153 | _, _ = f.Read(nameRaw) 154 | name := string(nameRaw) 155 | data := make([]byte, length) 156 | _, err = f.Read(data) 157 | _, err = f.Read(crcRaw) 158 | crc := binary.BigEndian.Uint32(crcRaw) 159 | return ChunkData{ 160 | length: length, 161 | name: name, 162 | data: data, 163 | crc: crc, 164 | }, err 165 | } 166 | 167 | // creates a chunk with the given data and name 168 | func CreateChunk(data []byte, name string) ChunkData { 169 | rawLength := make([]byte, 4) 170 | binary.BigEndian.PutUint32(rawLength, uint32(len(data))) 171 | rawName := []byte(name) 172 | var dataAndName []byte 173 | dataAndName = append(dataAndName, rawName...) 174 | dataAndName = append(dataAndName, data...) 175 | crc := crc32.ChecksumIEEE(dataAndName) 176 | rawCrc := make([]byte, 4) 177 | binary.BigEndian.PutUint32(rawCrc, crc) 178 | return ChunkData{ 179 | length: uint32(len(data)), 180 | name: name, 181 | data: data, 182 | crc: crc, 183 | } 184 | } 185 | 186 | -------------------------------------------------------------------------------- /cryptpng.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "errors" 9 | "flag" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "log" 14 | "os" 15 | "syscall" 16 | "math" 17 | "strings" 18 | 19 | "golang.org/x/crypto/scrypt" 20 | "golang.org/x/crypto/ssh/terminal" 21 | "github.com/cheggaaa/pb/v3" 22 | ) 23 | 24 | func check(err error) { 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | } 29 | 30 | const saltChunkName = "saLt" 31 | const chunkName = "crPt" 32 | const chunkSize = 0x100000 33 | const scrN = 32768 34 | const scrR = 8 35 | const scrP = 1 36 | const scrKeyLength = 32 37 | 38 | var inputFile string 39 | var outputFile string 40 | var imageFile string 41 | var decryptImage bool 42 | func main() { 43 | encryptFlags := flag.NewFlagSet("encrypt", flag.ContinueOnError) 44 | decryptFlags := flag.NewFlagSet("decrypt", flag.ContinueOnError) 45 | encryptFlags.StringVar(&imageFile, "image", "image.png", "The path of the png file.") 46 | decryptFlags.StringVar(&imageFile, "image", "image.png", "The path of the png file.") 47 | encryptFlags.StringVar(&inputFile, "in", "input.txt","The file with the input data.") 48 | encryptFlags.StringVar(&outputFile, "out", "out.png", "The output filename for the image.") 49 | decryptFlags.StringVar(&outputFile, "out", "out.png", "The output file for the decrypted data.") 50 | flag.Parse() 51 | switch os.Args[1] { 52 | case "encrypt": 53 | err := encryptFlags.Parse(os.Args[2:]) 54 | check(err) 55 | f, err := os.Open(imageFile) 56 | check(err) 57 | defer f.Close() 58 | check(err) 59 | if _, err := os.Stat(outputFile); err == nil { 60 | reader := bufio.NewReader(os.Stdin) 61 | fmt.Printf("The output file %s exists and will be overwritten. Continue? [Y/n] ", outputFile) 62 | if ans, _ := reader.ReadString('\n'); strings.ToLower(ans) != "y\n" { 63 | log.Fatal("Aborting...") 64 | } 65 | } 66 | fout, err := os.Create(outputFile) 67 | check(err) 68 | defer fout.Close() 69 | fin, err := os.Open(inputFile) 70 | check(err) 71 | defer fin.Close() 72 | EncryptDataPng(f, fin, fout) 73 | case "decrypt": 74 | err := decryptFlags.Parse(os.Args[2:]) 75 | check(err) 76 | f, err := os.Open(imageFile) 77 | check(err) 78 | defer f.Close() 79 | check(err) 80 | fout, err := os.Create(outputFile) 81 | check(err) 82 | defer fout.Close() 83 | DecryptDataPng(f, fout) 84 | } 85 | } 86 | 87 | // encrypts the data of fin inside the png (f) and writes it to fout 88 | func EncryptDataPng(f *os.File, fin *os.File, fout *os.File) { 89 | png := PngData{} 90 | log.Println("Reading image file...") 91 | err := png.Read(f) 92 | check(err) 93 | log.Println("Reading input file...") 94 | inputData, err := ioutil.ReadAll(fin) 95 | check(err) 96 | log.Println("Encrypting data...") 97 | inputData, salt := encryptData(inputData) 98 | check(err) 99 | log.Println("Creating salt chunk...") 100 | saltChunk := CreateChunk(salt, saltChunkName) 101 | png.AddMetaChunk(saltChunk) 102 | chunkCount := int(math.Ceil(float64(len(inputData)) / chunkSize)) 103 | bar := pb.StartNew(chunkCount) 104 | log.Printf("Creating %d chunks to store the data...\n", chunkCount) 105 | for i := 0; i < chunkCount; i++ { 106 | dataStart := i * chunkSize 107 | dataEnd := dataStart + int(math.Min(chunkSize, float64(len(inputData[dataStart:])))) 108 | cryptChunk := CreateChunk(inputData[dataStart:dataEnd], chunkName) 109 | png.AddMetaChunk(cryptChunk) 110 | bar.Increment() 111 | } 112 | bar.Finish() 113 | log.Println("Writing output file...") 114 | err = png.Write(fout) 115 | log.Println("Finished!") 116 | check(err) 117 | } 118 | 119 | // Decrypts the data from a png file 120 | func DecryptDataPng(f *os.File, fout *os.File) { 121 | png := PngData{} 122 | log.Println("Reading image file...") 123 | err := png.Read(f) 124 | check(err) 125 | salt := make([]byte, 0) 126 | log.Println("Getting salt chunk...") 127 | saltChunk := png.GetChunk(saltChunkName) 128 | if saltChunk != nil { 129 | salt = append(salt, saltChunk.data...) 130 | } 131 | var data []byte 132 | cryptChunks := png.GetChunksByName(chunkName) 133 | chunkCount := len(cryptChunks) 134 | log.Printf("Reading %d crypt chunks...", chunkCount) 135 | bar := pb.StartNew(chunkCount) 136 | for i, cryptChunk := range cryptChunks { 137 | if !cryptChunk.Verify() { 138 | log.Fatalf("Corrupted chunk data, chunk #%d", i) 139 | } 140 | data = append(data, cryptChunk.data...) 141 | bar.Increment() 142 | } 143 | bar.Finish() 144 | if len(data) > 0 { 145 | log.Println("Decrypting data...") 146 | data, err = decryptData(data, salt) 147 | check(err) 148 | log.Println("Writing output file...") 149 | _, err = fout.Write(data) 150 | check(err) 151 | log.Println("Finished!") 152 | } else { 153 | log.Fatal("no encrypted data inside the input image") 154 | } 155 | } 156 | 157 | // creates an encrypted png chunk 158 | func encryptData(data []byte) ([]byte, []byte) { 159 | key, salt := readPassword(nil) 160 | encData, err := encrypt(key, data) 161 | check(err) 162 | return encData, salt 163 | } 164 | 165 | // decrypts the data of a png chunk 166 | func decryptData(data []byte, salt []byte) ([]byte, error) { 167 | key, _ := readPassword(&salt) 168 | return decrypt(key, data) 169 | } 170 | 171 | // reads a password from the terminal 172 | // turns off the input for the typing of the password 173 | func readPassword(passwordSalt *[]byte) ([]byte, []byte) { 174 | fmt.Print("Password: ") 175 | bytePw, err := terminal.ReadPassword(int(syscall.Stdin)) 176 | check(err) 177 | if passwordSalt != nil { 178 | key, err := scrypt.Key(bytePw, *passwordSalt, scrN, scrR, scrP, scrKeyLength) 179 | check(err) 180 | return key, *passwordSalt 181 | } else { 182 | salt := make([]byte, 32) 183 | _, err = io.ReadFull(rand.Reader, salt) 184 | check(err) 185 | key, err := scrypt.Key(bytePw, salt, scrN, scrR, scrP, scrKeyLength) 186 | check(err) 187 | return key, salt 188 | } 189 | } 190 | 191 | // function to encrypt the data 192 | func encrypt(key, data []byte) ([]byte, error) { 193 | block, err := aes.NewCipher(key) 194 | if err != nil { 195 | return nil, err 196 | } 197 | cipherText := make([]byte, aes.BlockSize+len(data)) 198 | iv := cipherText[:aes.BlockSize] 199 | if _, err := io.ReadFull(rand.Reader, iv); err != nil { 200 | return nil, err 201 | } 202 | cfb := cipher.NewCFBEncrypter(block, iv) 203 | cfb.XORKeyStream(cipherText[aes.BlockSize:], data) 204 | return cipherText, nil 205 | } 206 | 207 | // function to decrypt the data 208 | func decrypt(key, data []byte) ([]byte, error) { 209 | block, err := aes.NewCipher(key) 210 | if err != nil { 211 | return nil, err 212 | } 213 | if len(data) < aes.BlockSize { 214 | return nil, errors.New("data too short") 215 | } 216 | iv := data[:aes.BlockSize] 217 | data = data[aes.BlockSize:] 218 | cfb := cipher.NewCFBDecrypter(block, iv) 219 | cfb.XORKeyStream(data, data) 220 | return data, nil 221 | } --------------------------------------------------------------------------------