aboutsummaryrefslogtreecommitdiffstats
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/sym/dec.go106
-rw-r--r--internal/sym/enc.go102
-rw-r--r--internal/sym/oae.go203
-rw-r--r--internal/sym/pwhash.go12
-rw-r--r--internal/sym/sym_test.go69
5 files changed, 492 insertions, 0 deletions
diff --git a/internal/sym/dec.go b/internal/sym/dec.go
new file mode 100644
index 0000000..7cbf5d4
--- /dev/null
+++ b/internal/sym/dec.go
@@ -0,0 +1,106 @@
+package sym
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+)
+
+type lineReader struct {
+ r bufio.Scanner
+ line []byte
+}
+
+func (r *lineReader) Read(buf []byte) (int, error) {
+ for len(r.line) == 0 {
+ if !r.r.Scan() {
+ if err := r.r.Err(); err != nil {
+ return 0, err
+ }
+ return 0, io.EOF
+ }
+ line := r.r.Bytes()
+ if bytes.HasPrefix(line, []byte("-")) {
+ continue
+ }
+ r.line = line
+ }
+ n := copy(buf, r.line)
+ r.line = r.line[n:]
+ return n, nil
+}
+
+type PasswordCache map[[SaltSize]byte][]byte
+
+func decryptBinary(w io.Writer, r io.Reader, password string, pwCache PasswordCache) error {
+ header := make([]byte, 1+SaltSize)
+ if _, err := io.ReadFull(r, header); err != nil {
+ return err
+ }
+ salt := header[1:]
+ key, ok := pwCache[[SaltSize]byte(salt)]
+ if !ok {
+ var err error
+ key, err = HashPassword(password, salt)
+ if err != nil {
+ return err
+ }
+ pwCache[[SaltSize]byte(salt)] = key
+ }
+ reader := newDecryptingReader(r, key)
+ _, err := io.Copy(w, reader)
+ return err
+}
+
+func Decrypt(w io.Writer, r io.Reader, password string, pwCache PasswordCache) error {
+ bufReader := bufio.NewReaderSize(r, 1)
+ b, err := bufReader.Peek(1)
+ if err != nil {
+ if err == io.EOF {
+ return fmt.Errorf("no input")
+ }
+ return err
+ }
+ if b[0] == 0 {
+ return decryptBinary(w, bufReader, password, pwCache)
+ }
+ if b[0] != '-' {
+ return errors.New("invalid input")
+ }
+ return decryptBinary(w, base64.NewDecoder(base64.StdEncoding, &lineReader{r: *bufio.NewScanner(bufReader)}), password, pwCache)
+}
+
+func DecryptFile(fileName, password string, pwCache PasswordCache) (err error) {
+ var outFileName string
+ if name, ok := strings.CutSuffix(fileName, ".enc"); ok {
+ outFileName = name
+ } else if name, ok := strings.CutSuffix(fileName, ".enc.txt"); ok {
+ outFileName = name
+ } else {
+ outFileName = fileName + ".dec"
+ }
+ fIn, err := os.Open(fileName)
+ if err != nil {
+ return err
+ }
+ defer fIn.Close()
+ fOut, err := os.Create(outFileName)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ fOut.Close()
+ if err != nil {
+ os.Remove(fOut.Name())
+ }
+ }()
+ if err := Decrypt(fOut, fIn, password, pwCache); err != nil {
+ return err
+ }
+ return fOut.Close()
+}
diff --git a/internal/sym/enc.go b/internal/sym/enc.go
new file mode 100644
index 0000000..32ec307
--- /dev/null
+++ b/internal/sym/enc.go
@@ -0,0 +1,102 @@
+package sym
+
+import (
+ "bufio"
+ "encoding/base64"
+ "encoding/binary"
+ "io"
+ "os"
+)
+
+type newlineWriter struct {
+ w *bufio.Writer
+ n int
+}
+
+func (w *newlineWriter) Write(buf []byte) (int, error) {
+ const lineSize = 80
+ nn := 0
+ for len(buf) > 0 {
+ if w.n == lineSize {
+ if err := w.w.WriteByte('\n'); err != nil {
+ return nn, err
+ }
+ w.n = 0
+ }
+ n, err := w.w.Write(buf[:min(lineSize-w.n, len(buf))])
+ nn += n
+ buf = buf[n:]
+ w.n += n
+ if err != nil {
+ return nn, err
+ }
+ }
+ return nn, nil
+}
+
+func Encrypt(w io.Writer, r io.Reader, key, salt []byte, counter int) error {
+ if _, err := w.Write([]byte{0}); err != nil {
+ return err
+ }
+ if _, err := w.Write(salt); err != nil {
+ return err
+ }
+ var noncePrefix [noncePrefixSize]byte
+ binary.BigEndian.PutUint32(noncePrefix[:], uint32(counter))
+ writer := newEncryptingWriter(w, key, noncePrefix)
+ if _, err := io.Copy(writer, r); err != nil {
+ return err
+ }
+ return writer.Close()
+}
+
+func EncryptBase64(w io.Writer, r io.Reader, key, salt []byte, count int) error {
+ bufWriter := bufio.NewWriter(w)
+ if _, err := bufWriter.WriteString(`-------------------------- Begin encrypted text block --------------------------
+-------------------------- am i cool like gpg? ---------------------------------
+`); err != nil {
+ return err
+ }
+ base64Writer := base64.NewEncoder(base64.StdEncoding, &newlineWriter{w: bufWriter})
+ if err := Encrypt(base64Writer, r, key, salt, count); err != nil {
+ return err
+ }
+ if err := base64Writer.Close(); err != nil {
+ return err
+ }
+ if err := bufWriter.WriteByte('\n'); err != nil {
+ return err
+ }
+ return bufWriter.Flush()
+}
+
+func EncryptFile(fileName string, key, salt []byte, count int, asciiOutput bool) (err error) {
+ f, err := os.Open(fileName)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ ext := ".enc"
+ if asciiOutput {
+ ext = ".enc.txt"
+ }
+ fOut, err := os.Create(fileName + ext)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ fOut.Close()
+ if err != nil {
+ os.Remove(fOut.Name())
+ }
+ }()
+ if asciiOutput {
+ err = EncryptBase64(fOut, f, key, salt, count)
+ } else {
+ err = Encrypt(fOut, f, key, salt, count)
+ }
+ if err != nil {
+ return err
+ }
+ return fOut.Close()
+}
diff --git a/internal/sym/oae.go b/internal/sym/oae.go
new file mode 100644
index 0000000..e71d5c3
--- /dev/null
+++ b/internal/sym/oae.go
@@ -0,0 +1,203 @@
+package sym
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "encoding/binary"
+ "errors"
+ "io"
+ "math"
+)
+
+const (
+ nonceSize = 12
+ aeadOverhead = 16
+ noncePrefixSize = nonceSize - 8
+
+ segmentSize = 4 * 1024 * 1024
+ encryptedSegmentSize = segmentSize + aeadOverhead
+)
+
+type segmentEncrypter struct {
+ key []byte
+ noncePrefix [noncePrefixSize]byte
+
+ aead cipher.AEAD
+ i uint64
+}
+
+func (se *segmentEncrypter) initialize() error {
+ block, err := aes.NewCipher(se.key)
+ if err != nil {
+ return err
+ }
+ se.aead, err = cipher.NewGCM(block)
+ return err
+}
+
+func (se *segmentEncrypter) nonce(lastSegment bool) ([]byte, []byte, error) {
+ if se.i == math.MaxUint64 {
+ return nil, nil, errors.New("counter overflowed")
+ }
+ nonce := make([]byte, nonceSize)
+ copy(nonce, se.noncePrefix[:])
+ binary.BigEndian.PutUint64(nonce[noncePrefixSize:], se.i)
+ ad := make([]byte, 9)
+ binary.BigEndian.PutUint64(ad, se.i)
+ if lastSegment {
+ ad[8] = 1
+ }
+ se.i++
+ return nonce, ad, nil
+}
+
+func (se *segmentEncrypter) encrypt(out, buf []byte, lastSegment bool) ([]byte, error) {
+ nonce, ad, err := se.nonce(lastSegment)
+ if err != nil {
+ return nil, err
+ }
+ return se.aead.Seal(out, nonce, buf, ad), nil
+}
+
+func (se *segmentEncrypter) decrypt(out, buf []byte, lastSegment bool) ([]byte, error) {
+ nonce, ad, err := se.nonce(lastSegment)
+ if err != nil {
+ return nil, err
+ }
+ return se.aead.Open(out, nonce, buf, ad)
+}
+
+type encryptingWriter struct {
+ w io.Writer
+ encrypter segmentEncrypter
+ buf []byte
+ initialized bool
+}
+
+func newEncryptingWriter(w io.Writer, key []byte, noncePrefix [noncePrefixSize]byte) *encryptingWriter {
+ return &encryptingWriter{
+ w: w,
+ encrypter: segmentEncrypter{
+ key: key,
+ noncePrefix: noncePrefix,
+ },
+ }
+}
+
+func (w *encryptingWriter) initialize() error {
+ if w.initialized {
+ return nil
+ }
+ if err := w.encrypter.initialize(); err != nil {
+ return err
+ }
+ if _, err := w.w.Write(w.encrypter.noncePrefix[:]); err != nil {
+ return err
+ }
+ w.buf = make([]byte, 0, encryptedSegmentSize)
+ w.initialized = true
+ return nil
+}
+
+func (w *encryptingWriter) writeBuf(lastSegment bool) error {
+ var err error
+ if w.buf, err = w.encrypter.encrypt(w.buf[:0], w.buf, lastSegment); err != nil {
+ return err
+ }
+ if _, err := w.w.Write(w.buf); err != nil {
+ return err
+ }
+ w.buf = w.buf[:0]
+ return nil
+}
+
+func (w *encryptingWriter) Write(buf []byte) (int, error) {
+ if err := w.initialize(); err != nil {
+ return 0, err
+ }
+ nn := 0
+ for len(buf) > 0 {
+ if len(w.buf) == segmentSize {
+ if err := w.writeBuf(false); err != nil {
+ return nn, err
+ }
+ }
+ n := copy(w.buf[len(w.buf):segmentSize], buf)
+ nn += n
+ w.buf = w.buf[:len(w.buf)+n]
+ buf = buf[n:]
+ }
+ return nn, nil
+}
+
+func (w *encryptingWriter) Close() error {
+ if err := w.initialize(); err != nil {
+ return err
+ }
+ return w.writeBuf(true)
+}
+
+type decryptingReader struct {
+ r *bufio.Reader
+ decrypter segmentEncrypter
+ buf bytes.Buffer
+ initialized bool
+}
+
+func newDecryptingReader(r io.Reader, key []byte) *decryptingReader {
+ return &decryptingReader{
+ r: bufio.NewReaderSize(r, 1),
+ decrypter: segmentEncrypter{
+ key: key,
+ },
+ }
+}
+
+func (r *decryptingReader) initialize() error {
+ if r.initialized {
+ return nil
+ }
+ if _, err := io.ReadFull(r.r, r.decrypter.noncePrefix[:]); err != nil {
+ return err
+ }
+ if err := r.decrypter.initialize(); err != nil {
+ return err
+ }
+ r.buf = *bytes.NewBuffer(make([]byte, 0, encryptedSegmentSize))
+ r.initialized = true
+ return nil
+}
+
+func (r *decryptingReader) fillBuf() error {
+ r.buf.Reset()
+ buf := r.buf.AvailableBuffer()[:encryptedSegmentSize]
+ n, err := io.ReadFull(r.r, buf)
+ if n == 0 {
+ if err == io.ErrUnexpectedEOF {
+ return io.EOF
+ }
+ return err
+ }
+ buf = buf[:n]
+ _, readErr := r.r.Peek(1)
+ buf, err = r.decrypter.decrypt(buf[:0], buf, readErr == io.EOF)
+ if err != nil {
+ return err
+ }
+ r.buf.Write(buf)
+ return nil
+}
+
+func (r *decryptingReader) Read(buf []byte) (int, error) {
+ if err := r.initialize(); err != nil {
+ return 0, err
+ }
+ if r.buf.Len() == 0 {
+ if err := r.fillBuf(); err != nil {
+ return 0, err
+ }
+ }
+ return r.buf.Read(buf)
+}
diff --git a/internal/sym/pwhash.go b/internal/sym/pwhash.go
new file mode 100644
index 0000000..ef17182
--- /dev/null
+++ b/internal/sym/pwhash.go
@@ -0,0 +1,12 @@
+package sym
+
+import (
+ "crypto/pbkdf2"
+ "crypto/sha256"
+)
+
+const SaltSize = 16
+
+func HashPassword(password string, salt []byte) ([]byte, error) {
+ return pbkdf2.Key(sha256.New, password, salt, 35_000_000, 32)
+}
diff --git a/internal/sym/sym_test.go b/internal/sym/sym_test.go
new file mode 100644
index 0000000..0ab0144
--- /dev/null
+++ b/internal/sym/sym_test.go
@@ -0,0 +1,69 @@
+package sym
+
+import (
+ "bytes"
+ "encoding/hex"
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func mustHex(t *testing.T, s string) []byte {
+ t.Helper()
+ b, err := hex.DecodeString(s)
+ if err != nil {
+ t.Fatalf("Bad hex %q: %s", s, err)
+ }
+ return b
+}
+
+func TestEncryptDecrypt(t *testing.T) {
+ t.Parallel()
+
+ const password = "karp cache tidal mars fed rajah uses graze pobox flew"
+ salt := mustHex(t, "9aa7d8bb6d19f794162f4062c789b230")
+ key, err := HashPassword(password, salt)
+ if err != nil {
+ t.Fatalf("HashPassword failed: %s", err)
+ }
+ buf := make([]byte, 10*1024*1024)
+ for i := range buf {
+ buf[i] = byte(i)
+ }
+ for _, tc := range []struct {
+ desc string
+ ascii bool
+ }{{
+ desc: "binary",
+ ascii: false,
+ }, {
+ desc: "ascii",
+ ascii: true,
+ }} {
+ t.Run(tc.desc, func(t *testing.T) {
+ t.Parallel()
+
+ fileName := filepath.Join(t.TempDir(), "file")
+ if err := os.WriteFile(fileName, buf, 0600); err != nil {
+ t.Fatalf("Failed to write test file: %s", err)
+ }
+ if err := EncryptFile(fileName, key, salt, 0, tc.ascii); err != nil {
+ t.Fatalf("EncryptFile failed: %s", err)
+ }
+ ext := ".enc"
+ if tc.ascii {
+ ext = ".enc.txt"
+ }
+ if err := DecryptFile(fileName+ext, password, make(PasswordCache)); err != nil {
+ t.Fatalf("DecryptFile failed: %s", err)
+ }
+ gotContents, err := os.ReadFile(fileName)
+ if err != nil {
+ t.Fatalf("Failed to read file: %s", err)
+ }
+ if !bytes.Equal(gotContents, buf) {
+ t.Errorf("contents differ")
+ }
+ })
+ }
+}