aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--go.mod9
-rw-r--r--go.sum8
-rw-r--r--internal/sym/dec.go19
-rw-r--r--internal/sym/dec_test.go91
-rw-r--r--internal/sym/enc.go23
-rw-r--r--internal/sym/encryptionalg_string.go25
-rw-r--r--internal/sym/magic.go3
-rw-r--r--internal/sym/metadata.go21
-rw-r--r--internal/sym/oae.go68
-rw-r--r--internal/sym/pwhash.go34
-rw-r--r--internal/sym/pwhash_string.go25
11 files changed, 285 insertions, 41 deletions
diff --git a/go.mod b/go.mod
index c6758cf..a1f7c9f 100644
--- a/go.mod
+++ b/go.mod
@@ -7,4 +7,11 @@ require (
roseh.moe/pkg/wordlist v1.0.2
)
-require golang.org/x/sys v0.37.0 // indirect
+require (
+ golang.org/x/mod v0.29.0 // indirect
+ golang.org/x/sync v0.17.0 // indirect
+ golang.org/x/sys v0.37.0 // indirect
+ golang.org/x/tools v0.38.0 // indirect
+)
+
+tool golang.org/x/tools/cmd/stringer
diff --git a/go.sum b/go.sum
index 0a889e3..2b38e1d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,6 +1,14 @@
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
+golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
roseh.moe/pkg/wordlist v1.0.2 h1:riB2RqCU5zXfCQjaPHYTXV/688FV9/Bp6Hnb0IAkP9c=
roseh.moe/pkg/wordlist v1.0.2/go.mod h1:2Jd7j6Qy5SElcrITJJNUcbxH9TPKYn0k+BJL3tsJdiY=
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]]
+}