Created
October 22, 2018 08:47
-
-
Save erikh/640e2c59766bd0a123e9a6dd0c392223 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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