Skip to content

Instantly share code, notes, and snippets.

@erikh
Created October 22, 2018 08:47
Show Gist options
  • Save erikh/640e2c59766bd0a123e9a6dd0c392223 to your computer and use it in GitHub Desktop.
Save erikh/640e2c59766bd0a123e9a6dd0c392223 to your computer and use it in GitHub Desktop.
// Package aestar implements an encrypted tar file that can be extracted by any
// tar program. The filenames and contents are obfuscated; an encrypted index
// is formed to map the obfuscated names to real names.
//
// aestar does not implement all tar features at this time, but as it matures
// it will extend to support most common options.
package aestar
import (
"archive/tar"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"time"
"github.com/pkg/errors"
)
func randInt(index index) (string, error) {
retryNum:
buf := make([]byte, 64)
if _, err := rand.Read(buf); err != nil {
return "", errors.Wrap(err, "reading from random device")
}
i := 0
for x := uint(0); x < 8; x++ {
i <<= 8 * x
i |= int(buf[x])
}
key := fmt.Sprintf("%d", uint64(i))
if _, ok := index[key]; ok {
goto retryNum
}
return key, nil
}
func getLink(p string, fi os.FileInfo, diagChan chan error) (string, error) {
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err := filepath.EvalSymlinks(p)
if err != nil {
// this is not an error unless the caller specifies it
diagChan <- errors.Wrapf(err, "while evaluating symlinks for %q", p)
} else {
return link, nil
}
}
return "", nil
}
type index map[string]indexEntry
type indexEntry struct {
target string
link bool
}
type archive struct {
tw *tar.Writer
encryptionKey []byte
writer io.WriteCloser
path string
index, revIndex index
pathChan chan string
errChan, diagChan chan error
}
func (a *archive) doArchive() {
defer a.writer.Close()
defer a.tw.Close()
// walk to create the index first.
err := filepath.Walk(a.path, func(p string, fi os.FileInfo, err error) error {
a.pathChan <- p
key, err := randInt(a.index)
if err != nil {
return err
}
a.revIndex[p] = indexEntry{key, false}
a.index[key] = indexEntry{p, false}
link, err := getLink(p, fi, a.diagChan)
if err != nil {
return err
}
if link != "" {
if _, ok := a.revIndex[link]; !ok {
key, err := randInt(a.index)
if err != nil {
return err
}
a.index[key] = indexEntry{link, true}
a.revIndex[link] = indexEntry{key, true}
fmt.Println("LINK:", link, key)
link = key
} else {
link = a.revIndex[link].target
}
}
return nil
})
if err != nil {
a.errChan <- err
return
}
content, err := json.Marshal(a.index)
if err != nil {
a.errChan <- errors.Wrap(err, "generating index")
return
}
header := &tar.Header{
Name: "aestar-index",
Mode: 0777,
Uid: os.Getuid(),
Gid: os.Getgid(),
ModTime: time.Now(),
AccessTime: time.Now(),
ChangeTime: time.Now(),
}
pr, pw := io.Pipe()
go func() {
if _, err := pw.Write(content); err != nil {
pw.CloseWithError(err)
} else {
pw.Close()
}
}()
if err := writeEncryptedFile(a.tw, header, a.encryptionKey, pr, int64(len(content))); err != nil {
a.errChan <- err
return
}
// walk the index, bringing in the necessary files
for key, p := range a.index {
if !p.link {
if err := a.writeRecord(key, p.target); err != nil {
a.errChan <- err
return
}
}
}
}
// Archive creates an aestar file. A reader which corresponds to the tar file
// being generated, and channels for propagating paths (for output or other
// processing), diagnostics (warnings), and errors is returned. The end-user is
// responsible for both consuming and closing the channels when the tar
// operation completes.
func Archive(path string, encryptionKey []byte) (io.Reader, chan string, chan error, chan error) {
r, w := io.Pipe()
tw := tar.NewWriter(w)
a := &archive{
index: index{},
revIndex: index{},
errChan: make(chan error, 1),
pathChan: make(chan string, 1),
diagChan: make(chan error, 1),
tw: tw,
writer: w,
path: path,
encryptionKey: encryptionKey,
}
go a.doArchive()
return r, a.pathChan, a.diagChan, a.errChan
}
func (a *archive) writeRecord(key, p string) error {
fmt.Println(p)
fi, err := os.Stat(p)
if err != nil {
a.diagChan <- errors.Wrapf(err, "while performing stat on %q", p)
return nil
}
link, err := getLink(p, fi, a.diagChan)
if err != nil {
return err
}
if link != "" {
link = a.revIndex[link].target
}
// FIXME symlink targets need to be obfuscated too.
header, err := tar.FileInfoHeader(fi, link)
if err != nil {
return errors.Wrapf(err, "constructing tar header for %q (link: %q)", p, link)
}
header.Name = key
if fi.Mode()&os.ModeType == 0 {
f, err := os.Open(p)
if err != nil {
return errors.Wrapf(err, "opening %q for read", p)
}
defer f.Close()
return writeEncryptedFile(a.tw, header, a.encryptionKey, f, fi.Size())
}
if err := a.tw.WriteHeader(header); err != nil {
return errors.Wrapf(err, "writing header for %q", p)
}
return nil
}
func writeEncryptedFile(tw *tar.Writer, header *tar.Header, encryptionKey []byte, reader io.ReadCloser, sizeLen int64) error {
block, err := aes.NewCipher(encryptionKey)
if err != nil {
return errors.Wrap(err, "configuring encryption with key")
}
iv := make([]byte, block.BlockSize())
if _, err := rand.Read(iv); err != nil {
return errors.Wrap(err, "reading from random device")
}
stream := cipher.NewCFBEncrypter(block, iv)
sw := cipher.StreamReader{S: stream, R: reader}
tf, err := ioutil.TempFile("", "aestar-")
if err != nil {
return errors.Wrap(err, "could not create temporary file for writing")
}
defer os.Remove(tf.Name())
n, err := io.Copy(tf, sw)
if err != nil {
return errors.Wrapf(err, "during copy of %q", header.Name)
}
header.Size = n
tf.Close()
reader.Close()
if err := tw.WriteHeader(header); err != nil {
return errors.Wrapf(err, "writing header for %q", header.Name)
}
tf, err = os.Open(tf.Name())
if err != nil {
return errors.Wrap(err, "temporary file could not be re-opened")
}
if _, err := io.Copy(tw, tf); err != nil {
return errors.Wrapf(err, "copying encrypted content for %q", header.Name)
}
return tf.Close()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment