diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/sym/dec.go | 19 | ||||
| -rw-r--r-- | internal/sym/dec_test.go | 91 | ||||
| -rw-r--r-- | internal/sym/enc.go | 23 | ||||
| -rw-r--r-- | internal/sym/encryptionalg_string.go | 25 | ||||
| -rw-r--r-- | internal/sym/magic.go | 3 | ||||
| -rw-r--r-- | internal/sym/metadata.go | 21 | ||||
| -rw-r--r-- | internal/sym/oae.go | 68 | ||||
| -rw-r--r-- | internal/sym/pwhash.go | 34 | ||||
| -rw-r--r-- | internal/sym/pwhash_string.go | 25 |
9 files changed, 269 insertions, 40 deletions
diff --git a/internal/sym/dec.go b/internal/sym/dec.go index 71da93c..ed051fd 100644 --- a/internal/sym/dec.go +++ b/internal/sym/dec.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "encoding/base64" + "encoding/binary" "errors" "flag" "fmt" @@ -34,14 +35,21 @@ func (r *lineReader) Read(buf []byte) (int, error) { } func decryptBinary(w io.Writer, r io.Reader, password string) error { - header := make([]byte, len(magic)) - if _, err := io.ReadFull(r, header); err != nil { + fileFormat := make([]byte, 4) + if _, err := io.ReadFull(r, fileFormat); err != nil { return err } - if !bytes.Equal(header, []byte(magic)) { + if string(fileFormat) != magic { return fmt.Errorf("bad file format") } - _, err := io.Copy(w, newDecryptingReader(r, password)) + header := new(fileMetadata) + if err := binary.Read(r, binary.BigEndian, header); err != nil { + return err + } + if err := header.validate(); err != nil { + return err + } + _, err := io.Copy(w, header.EncryptionMetadata.newDecryptingReader(r, password, &header.HashMetadata)) return err } @@ -60,8 +68,7 @@ func decrypt(w io.Writer, r io.Reader, password string) error { if b[0] != '-' { return errors.New("invalid input") } - _, err = io.Copy(w, newDecryptingReader(base64.NewDecoder(base64.StdEncoding, &lineReader{r: bufReader}), password)) - return err + return decryptBinary(w, base64.NewDecoder(base64.StdEncoding, &lineReader{r: bufReader}), password) } type decryptFlags struct { diff --git a/internal/sym/dec_test.go b/internal/sym/dec_test.go index b70d325..727fa51 100644 --- a/internal/sym/dec_test.go +++ b/internal/sym/dec_test.go @@ -2,6 +2,7 @@ package sym import ( "bytes" + "encoding/binary" "errors" "flag" "path/filepath" @@ -45,6 +46,31 @@ func TestDecryptFile_Force(t *testing.T) { } } +func encodeHeader(t *testing.T, f *fileMetadata) []byte { + t.Helper() + + if f.HashMetadata.PasswordHashType == pwHashInvalid { + f.HashMetadata.PasswordHashType = pwHashPBKDF2_HMAC_SHA256 + } + if f.HashMetadata.Iterations == 0 { + f.HashMetadata.Iterations = defaultPBKDF2Iters + } + if f.HashMetadata.SaltSize == 0 { + f.HashMetadata.SaltSize = defaultSaltSize + } + if f.EncryptionMetadata.EncryptionType == encryptionAlgInvalid { + f.EncryptionMetadata.EncryptionType = encryptionAlgAES256_GCM + } + if f.EncryptionMetadata.SegmentSize == 0 { + f.EncryptionMetadata.SegmentSize = defaultSegmentSize + } + b, err := binary.Append([]byte(magic), binary.BigEndian, f) + if err != nil { + t.Fatalf("Bad file metadata: %s", err) + } + return b +} + func TestDecrypt_BadFileFormat(t *testing.T) { t.Parallel() @@ -58,17 +84,68 @@ func TestDecrypt_BadFileFormat(t *testing.T) { desc: "Short", fileContent: []byte{0x80}, }, { - desc: "BadHeader", - fileContent: []byte{0x80, 'a', 's', 'd', 'f'}, - }, { desc: "BadFormat", fileContent: []byte("bad file format"), }, { - desc: "BadContent", - fileContent: []byte("\x80symasdfasdf"), + desc: "BadMagic", + fileContent: []byte("\x80asdf"), + }, { + desc: "BadHeader", + fileContent: []byte("\x80symasdf"), + }, { + desc: "BadVersion", + fileContent: encodeHeader(t, &fileMetadata{ + Version: -1, + }), + }, { + desc: "BadEncryptionAlg", + fileContent: encodeHeader(t, &fileMetadata{ + EncryptionMetadata: encryptionMetadata{ + EncryptionType: -1, + }, + }), + }, { + desc: "BadSegmentSize", + fileContent: encodeHeader(t, &fileMetadata{ + EncryptionMetadata: encryptionMetadata{ + SegmentSize: -1, + }, + }), + }, { + desc: "BadSaltSize", + fileContent: encodeHeader(t, &fileMetadata{ + HashMetadata: hashMetadata{ + SaltSize: -1, + }, + }), + }, { + desc: "BadPasswordHashType", + fileContent: encodeHeader(t, &fileMetadata{ + HashMetadata: hashMetadata{ + PasswordHashType: -1, + }, + }), + }, { + desc: "BadIterations", + fileContent: encodeHeader(t, &fileMetadata{ + HashMetadata: hashMetadata{ + Iterations: -1, + }, + }), + }, { + desc: "NoSalt", + fileContent: encodeHeader(t, &fileMetadata{}), + }, { + desc: "ShortSalt", + fileContent: slices.Concat( + encodeHeader(t, &fileMetadata{}), + []byte("asdf")), }, { - desc: "BadContentLong", - fileContent: slices.Concat([]byte("\x80sym"), bytes.Repeat([]byte("asdf"), 100)), + desc: "BadContent", + fileContent: slices.Concat( + encodeHeader(t, &fileMetadata{}), + bytes.Repeat([]byte{0}, defaultSaltSize), + []byte("bad content")), }} { t.Run(tc.desc, func(t *testing.T) { t.Parallel() diff --git a/internal/sym/enc.go b/internal/sym/enc.go index be05e68..274954a 100644 --- a/internal/sym/enc.go +++ b/internal/sym/enc.go @@ -45,7 +45,22 @@ func encryptBinary(w io.Writer, r io.Reader, password string) error { if _, err := io.WriteString(w, magic); err != nil { return err } - writer := newEncryptingWriter(w, password) + header := &fileMetadata{ + Version: 0, + HashMetadata: hashMetadata{ + PasswordHashType: pwHashPBKDF2_HMAC_SHA256, + Iterations: defaultPBKDF2Iters, + SaltSize: defaultSaltSize, + }, + EncryptionMetadata: encryptionMetadata{ + EncryptionType: encryptionAlgAES256_GCM, + SegmentSize: defaultSegmentSize, + }, + } + if err := binary.Write(w, binary.BigEndian, header); err != nil { + return err + } + writer := header.EncryptionMetadata.newEncryptingWriter(w, password, &header.HashMetadata) if _, err := io.Copy(writer, r); err != nil { return err } @@ -60,11 +75,7 @@ func encryptBase64(w io.Writer, r io.Reader, password string) error { return err } base64Writer := base64.NewEncoder(base64.StdEncoding, &newlineWriter{w: bufWriter}) - encryptingWriter := newEncryptingWriter(base64Writer, password) - if _, err := io.Copy(encryptingWriter, r); err != nil { - return err - } - if err := encryptingWriter.Close(); err != nil { + if err := encryptBinary(base64Writer, r, password); err != nil { return err } if err := base64Writer.Close(); err != nil { diff --git a/internal/sym/encryptionalg_string.go b/internal/sym/encryptionalg_string.go new file mode 100644 index 0000000..b7384ee --- /dev/null +++ b/internal/sym/encryptionalg_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type=encryptionAlg -linecomment"; DO NOT EDIT. + +package sym + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[encryptionAlgInvalid-0] + _ = x[encryptionAlgAES256_GCM-1] +} + +const _encryptionAlg_name = "encryptionAlgInvalidAES-256-GCM" + +var _encryptionAlg_index = [...]uint8{0, 20, 31} + +func (i encryptionAlg) String() string { + idx := int(i) - 0 + if i < 0 || idx >= len(_encryptionAlg_index)-1 { + return "encryptionAlg(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _encryptionAlg_name[_encryptionAlg_index[idx]:_encryptionAlg_index[idx+1]] +} diff --git a/internal/sym/magic.go b/internal/sym/magic.go deleted file mode 100644 index 198fe86..0000000 --- a/internal/sym/magic.go +++ /dev/null @@ -1,3 +0,0 @@ -package sym - -const magic = "\x80sym" diff --git a/internal/sym/metadata.go b/internal/sym/metadata.go new file mode 100644 index 0000000..3875a63 --- /dev/null +++ b/internal/sym/metadata.go @@ -0,0 +1,21 @@ +package sym + +import "fmt" + +const magic = "\x80sym" + +type fileMetadata struct { + Version int8 + HashMetadata hashMetadata + EncryptionMetadata encryptionMetadata +} + +func (f *fileMetadata) validate() error { + if f.Version != 0 { + return fmt.Errorf("bad version") + } + if err := f.HashMetadata.validate(); err != nil { + return err + } + return f.EncryptionMetadata.validate() +} diff --git a/internal/sym/oae.go b/internal/sym/oae.go index 72fbf64..3b17957 100644 --- a/internal/sym/oae.go +++ b/internal/sym/oae.go @@ -7,6 +7,7 @@ import ( "crypto/cipher" "crypto/rand" "errors" + "fmt" "io" ) @@ -14,21 +15,49 @@ const ( nonceSize = 12 aeadOverhead = 16 - encryptedSegmentSize = 1024 * 1024 - plaintextSegmentSize = encryptedSegmentSize - aeadOverhead + defaultSegmentSize = 1024 * 1024 - saltSize = 32 + defaultSaltSize = 32 ) +//go:generate go tool stringer -type=encryptionAlg -linecomment +type encryptionAlg int8 + +const ( + encryptionAlgInvalid encryptionAlg = iota + encryptionAlgAES256_GCM // AES-256-GCM +) + +type encryptionMetadata struct { + EncryptionType encryptionAlg + SegmentSize int32 +} + +func (e *encryptionMetadata) validate() error { + if e.EncryptionType != encryptionAlgAES256_GCM { + return fmt.Errorf("invalid encryption alg %q", e.EncryptionType) + } + if e.SegmentSize <= 0 || e.SegmentSize > defaultSegmentSize { + return fmt.Errorf("segment size too long") + } + return nil +} + +func (e *encryptionMetadata) plaintextSegmentSize() int { + return int(e.SegmentSize) - aeadOverhead +} + type segmentEncrypter struct { - password string + hashMetadata hashMetadata + encryptionMetadata encryptionMetadata + password string aead cipher.AEAD nonce [nonceSize]byte } func (se *segmentEncrypter) initialize(salt []byte) error { - key, err := hashPassword(se.password, salt) + key, err := se.hashMetadata.hashPassword(se.password, salt) if err != nil { return err } @@ -82,11 +111,13 @@ type encryptingWriter struct { initialized bool } -func newEncryptingWriter(w io.Writer, password string) *encryptingWriter { +func (e *encryptionMetadata) newEncryptingWriter(w io.Writer, password string, passwordMetadata *hashMetadata) *encryptingWriter { return &encryptingWriter{ w: w, encrypter: segmentEncrypter{ - password: password, + hashMetadata: *passwordMetadata, + encryptionMetadata: *e, + password: password, }, } } @@ -95,7 +126,7 @@ func (w *encryptingWriter) initialize() error { if w.initialized { return nil } - header := make([]byte, saltSize) + header := make([]byte, w.encrypter.hashMetadata.SaltSize) rand.Read(header) if err := w.encrypter.initialize(header); err != nil { return err @@ -103,7 +134,7 @@ func (w *encryptingWriter) initialize() error { if _, err := w.w.Write(header); err != nil { return err } - w.buf = make([]byte, 0, encryptedSegmentSize) + w.buf = make([]byte, 0, w.encrypter.encryptionMetadata.SegmentSize) w.initialized = true return nil } @@ -126,12 +157,12 @@ func (w *encryptingWriter) Write(buf []byte) (int, error) { } nn := 0 for len(buf) > 0 { - if len(w.buf) == plaintextSegmentSize { + if len(w.buf) == w.encrypter.encryptionMetadata.plaintextSegmentSize() { if err := w.writeBuf(false); err != nil { return nn, err } } - n := copy(w.buf[len(w.buf):plaintextSegmentSize], buf) + n := copy(w.buf[len(w.buf):w.encrypter.encryptionMetadata.plaintextSegmentSize()], buf) nn += n w.buf = w.buf[:len(w.buf)+n] buf = buf[n:] @@ -153,11 +184,13 @@ type decryptingReader struct { initialized bool } -func newDecryptingReader(r io.Reader, password string) *decryptingReader { +func (e *encryptionMetadata) newDecryptingReader(r io.Reader, password string, passwordMetadata *hashMetadata) *decryptingReader { return &decryptingReader{ r: bufio.NewReaderSize(r, 1), decrypter: segmentEncrypter{ - password: password, + hashMetadata: *passwordMetadata, + encryptionMetadata: *e, + password: password, }, } } @@ -166,21 +199,24 @@ func (r *decryptingReader) initialize() error { if r.initialized { return nil } - header := make([]byte, saltSize) + header := make([]byte, r.decrypter.hashMetadata.SaltSize) if _, err := io.ReadFull(r.r, header); err != nil { + if err == io.EOF { + return io.ErrUnexpectedEOF + } return err } if err := r.decrypter.initialize(header); err != nil { return err } - r.buf = *bytes.NewBuffer(make([]byte, 0, encryptedSegmentSize)) + r.buf = *bytes.NewBuffer(make([]byte, 0, r.decrypter.encryptionMetadata.SegmentSize)) r.initialized = true return nil } func (r *decryptingReader) fillBuf() error { r.buf.Reset() - buf := r.buf.AvailableBuffer()[:encryptedSegmentSize] + buf := r.buf.AvailableBuffer()[:r.decrypter.encryptionMetadata.SegmentSize] n, err := io.ReadFull(r.r, buf) if n == 0 { return err diff --git a/internal/sym/pwhash.go b/internal/sym/pwhash.go index 86539a0..d05f6b5 100644 --- a/internal/sym/pwhash.go +++ b/internal/sym/pwhash.go @@ -3,13 +3,43 @@ package sym import ( "crypto/pbkdf2" "crypto/sha256" + "fmt" "os" "golang.org/x/term" ) -func hashPassword(password string, salt []byte) ([]byte, error) { - return pbkdf2.Key(sha256.New, password, salt, 35_000_000, 32) +const defaultPBKDF2Iters = 35_000_000 + +//go:generate go tool stringer -type=pwHash -linecomment +type pwHash int8 + +const ( + pwHashInvalid pwHash = iota + pwHashPBKDF2_HMAC_SHA256 // PBKDF2-HMAC-SHA256 +) + +type hashMetadata struct { + PasswordHashType pwHash + Iterations int32 + SaltSize int8 +} + +func (h *hashMetadata) validate() error { + if h.PasswordHashType != pwHashPBKDF2_HMAC_SHA256 { + return fmt.Errorf("invalid hash type %q", h.PasswordHashType) + } + if h.Iterations <= 0 || h.Iterations > defaultPBKDF2Iters { + return fmt.Errorf("too many iterations") + } + if h.SaltSize <= 0 || h.SaltSize > defaultSaltSize { + return fmt.Errorf("salt size too long") + } + return nil +} + +func (h *hashMetadata) hashPassword(password string, salt []byte) ([]byte, error) { + return pbkdf2.Key(sha256.New, password, salt, int(h.Iterations), 32) } func termReadPassword() (string, error) { diff --git a/internal/sym/pwhash_string.go b/internal/sym/pwhash_string.go new file mode 100644 index 0000000..ceb4d1f --- /dev/null +++ b/internal/sym/pwhash_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type=pwHash -linecomment"; DO NOT EDIT. + +package sym + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[pwHashInvalid-0] + _ = x[pwHashPBKDF2_HMAC_SHA256-1] +} + +const _pwHash_name = "pwHashInvalidPBKDF2-HMAC-SHA256" + +var _pwHash_index = [...]uint8{0, 13, 31} + +func (i pwHash) String() string { + idx := int(i) - 0 + if i < 0 || idx >= len(_pwHash_index)-1 { + return "pwHash(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _pwHash_name[_pwHash_index[idx]:_pwHash_index[idx+1]] +} |
