diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..272ae4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.idea/ +/simple-privacy-tool +/spt \ No newline at end of file diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..e8249f4 --- /dev/null +++ b/.tool-versions @@ -0,0 +1 @@ +golang 1.20.5 diff --git a/README.md b/README.md index c374ae3..caa276c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,38 @@ # simple-privacy-tool +Simple Privacy Tool is a simple tool to encrypt and decrypt files. It uses the symmetric algorithm XChaCha-Poly1305 and +Argon2id for the key derivation function. + +Since this tool uses a symmetric algorithm, the level of privacy hinges solely on the password's strength. So, make sure +to choose your password carefully. + +### Build + +```shell +go build +``` + +### Usage + +#### Encrypt +Encrypting `plainfile` to `cryptedfile` +```shell +simple-privacy-tool encrypt plainfile cryptedfile +``` + +#### Decrypt +Decrypting `cryptedfile` back to `plainfile` +```shell +simple-privacy-tool decrypt cryptedfile plainfile +``` + +#### Using STDIN/STDOUT +`simple-privacy-tool` can operate on `STDIN` or `STDOUT`. Just replace the file path with `-` +```shell +tar -zcf - dir | simple-privacy-tool encrypt - - | another-command +``` + +Special usage, just omit both file paths to use `STDIN` and `STDOUT`. +```shell +tar -zcf - dir | simple-privacy-tool encrypt | another-command +``` diff --git a/cmd/spt/decrypt.go b/cmd/spt/decrypt.go new file mode 100644 index 0000000..a9a0b18 --- /dev/null +++ b/cmd/spt/decrypt.go @@ -0,0 +1,113 @@ +package spt + +import ( + "fmt" + "gitea.suyono.dev/suyono/simple-privacy-tool/privacy" + "io" + "os" +) + +type decryptApp struct { + cApp + r *privacy.Reader +} + +type stdoutWrapper struct { + file *os.File +} + +func newDecryptApp(a *cApp) *decryptApp { + return &decryptApp{ + cApp: *a, + } +} + +func (d *decryptApp) GetPassphrase() (err error) { + var ( + passphrase string + ) + + if passphrase, err = d.term.ReadPassword("input passphrase: "); err != nil { + return + } + + d.passphrase = passphrase + + return +} + +func (d *decryptApp) ProcessFiles() (err error) { + d.r = privacy.NewPrivacyReader(d.srcFile) + if err = d.r.ReadMagic(); err != nil { + return + } + + if err = d.r.GenerateKey(d.passphrase); err != nil { + return + } + + if d.dstPath == "-" || d.srcPath == "-" { + w := stdoutWrapper{ + file: d.dstFile, + } + if _, err = io.Copy(w, d.r); err != nil { + return + } + } else { + if _, err = io.Copy(d.dstFile, d.r); err != nil { + return + } + } + + if err = d.dstFile.Close(); err != nil { + return + } + + if err = d.srcFile.Close(); err != nil { + return + } + + return +} + +func (sw stdoutWrapper) ReadFrom(reader io.Reader) (n int64, err error) { + var ( + nr int + rErr error + ) + buf := make([]byte, 32768) + + for { + if nr, err = reader.Read(buf); err != nil { + break + } + + n += int64(nr) + if _, err = sw.file.Write(buf[:nr]); err != nil { + return n, fmt.Errorf("readfrom internal write: %w", err) + } + } + + rErr = err + if nr > 0 { + n += int64(nr) + if nr > 32768 { + return n, fmt.Errorf("last piece length %d: %w", nr, err) + } + if _, err = sw.file.Write(buf[:nr]); err != nil { + return n, fmt.Errorf("readfrom internal write: %w", err) + } + } + + if rErr == io.EOF || rErr == nil { + err = nil + } else { + err = rErr + } + + return +} + +func (sw stdoutWrapper) Write(b []byte) (n int, err error) { + return sw.file.Write(b) +} diff --git a/cmd/spt/encrypt.go b/cmd/spt/encrypt.go new file mode 100644 index 0000000..b0488c0 --- /dev/null +++ b/cmd/spt/encrypt.go @@ -0,0 +1,65 @@ +package spt + +import ( + "gitea.suyono.dev/suyono/simple-privacy-tool/privacy" + "io" +) + +type encryptApp struct { + cApp + wc *privacy.WriteCloser +} + +func newEncryptApp(a *cApp) *encryptApp { + return &encryptApp{ + cApp: *a, + } +} + +func (e *encryptApp) GetPassphrase() (err error) { + var ( + passphrase string + verify string + ) + + if passphrase, err = e.term.ReadPassword("input passphrase: "); err != nil { + return + } + + if verify, err = e.term.ReadPassword("verify - input passphrase: "); err != nil { + return + } + + if passphrase != verify { + return ErrPassphraseMismatch + } + + e.passphrase = passphrase + + return nil +} + +func (e *encryptApp) ProcessFiles() (err error) { + e.wc = privacy.NewPrivacyWriteCloser(e.dstFile, privacy.DefaultCipherMethod) //TODO: need to handle when custom keygen accepted + if err = e.wc.NewSalt(); err != nil { + return + } + + if err = e.wc.GenerateKey(e.passphrase); err != nil { + return + } + + if _, err = io.Copy(e.wc, e.srcFile); err != nil { + return + } + + if err = e.wc.Close(); err != nil { + return + } + + if err = e.srcFile.Close(); err != nil { + return + } + + return +} diff --git a/cmd/spt/spt.go b/cmd/spt/spt.go new file mode 100644 index 0000000..679997b --- /dev/null +++ b/cmd/spt/spt.go @@ -0,0 +1,162 @@ +package spt + +import ( + "errors" + tw "gitea.suyono.dev/suyono/terminal_wrapper" + "github.com/spf13/cobra" + "os" +) + +type cApp struct { + term *tw.Terminal + srcPath string + dstPath string + srcFile *os.File + dstFile *os.File + passphrase string +} + +var ( + //ErrFatalError = errors.New("fatal error occurred") + ErrPassphraseMismatch = errors.New("mismatch passphrase") + + rootCmd = &cobra.Command{ + Use: "spt", + Short: "a simple tool to encrypt and decrypt file", + } + + encryptCmd = &cobra.Command{ + Use: "encrypt", + Args: validatePositionalArgs, + RunE: encrypt, + } + + decryptCmd = &cobra.Command{ + Use: "decrypt", + RunE: decrypt, + Args: validatePositionalArgs, + } +) + +func Execute() error { + return rootCmd.Execute() +} + +func init() { + rootCmd.AddCommand(encryptCmd, decryptCmd) +} + +func validatePositionalArgs(cmd *cobra.Command, args []string) error { + if len(args) != 0 && len(args) != 2 { + //TODO: improve the error message + return errors.New("invalid arguments") + } + + return nil +} + +func encrypt(cmd *cobra.Command, args []string) (err error) { + var ( + terminal *tw.Terminal + app *cApp + eApp *encryptApp + ) + + if terminal, err = tw.MakeTerminal(os.Stderr); err != nil { + return + } + defer func() { + existingErr := err + if err = terminal.Restore(); err == nil && existingErr != nil { + err = existingErr + } + }() + + app = &cApp{ + term: terminal, + } + + if err = app.ProcessArgs(args); err != nil { + return + } + + eApp = newEncryptApp(app) + + if err = eApp.GetPassphrase(); err != nil { + return + } + + //TODO: process additional flags + + if err = eApp.ProcessFiles(); err != nil { + return + } + + return nil +} + +func decrypt(cmd *cobra.Command, args []string) (err error) { + var ( + terminal *tw.Terminal + app *cApp + dApp *decryptApp + ) + + if terminal, err = tw.MakeTerminal(os.Stderr); err != nil { + return + } + defer func() { + existingErr := err + if err = terminal.Restore(); err == nil && existingErr != nil { + err = existingErr + } + }() + + app = &cApp{ + term: terminal, + } + + if err = app.ProcessArgs(args); err != nil { + return + } + + dApp = newDecryptApp(app) + + if err = dApp.GetPassphrase(); err != nil { + return + } + + //TODO: process additional flags + + if err = dApp.ProcessFiles(); err != nil { + return + } + + return nil +} + +func (a *cApp) ProcessArgs(args []string) (err error) { + if len(args) == 0 { + a.srcFile = os.Stdin + a.dstFile = os.Stdout + } else { + a.srcPath = args[0] + a.dstPath = args[1] + if a.srcPath == "-" { + a.srcFile = os.Stdin + } else { + if a.srcFile, err = os.Open(a.srcPath); err != nil { + return + } + } + + if a.dstPath == "-" { + a.dstFile = os.Stdout + } else { + if a.dstFile, err = os.OpenFile(a.dstPath, os.O_CREATE|os.O_WRONLY, 0640); err != nil { //TODO: allow user to define the destination file permission + return + } + } + } + return +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..597cf56 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module gitea.suyono.dev/suyono/simple-privacy-tool + +go 1.20 + +require ( + gitea.suyono.dev/suyono/terminal_wrapper v0.0.0-20230722101024-a3e50949f40f + github.com/spf13/cobra v1.7.0 + golang.org/x/crypto v0.11.0 +) + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/term v0.10.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d17d662 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +gitea.suyono.dev/suyono/terminal_wrapper v0.0.0-20230722101024-a3e50949f40f h1:yPac52dWHSutQH99FUhqz9rt2FHKaHr9srdJuDI9pkg= +gitea.suyono.dev/suyono/terminal_wrapper v0.0.0-20230722101024-a3e50949f40f/go.mod h1:I8qBJ8oaj+RwbLggXWNRrvRCDD+/bnRoQne0V4xsBNU= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..4cdca53 --- /dev/null +++ b/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + "gitea.suyono.dev/suyono/simple-privacy-tool/cmd/spt" +) + +func main() { + err := spt.Execute() + if err != nil { + fmt.Printf("error: %v\n", err) + //panic(err) + } +} diff --git a/privacy/argon2.go b/privacy/argon2.go new file mode 100644 index 0000000..70e99c2 --- /dev/null +++ b/privacy/argon2.go @@ -0,0 +1,54 @@ +package privacy + +import ( + "encoding/json" + "errors" + "golang.org/x/crypto/argon2" +) + +type argon2Params struct { + Time uint32 + Memory uint32 + Threads uint8 + Name string +} + +var ErrInvalidParameter = errors.New("invalid parameter") + +const argon2KeyGenName = "argon2" + +func (a argon2Params) GenerateKey(password, salt []byte) []byte { + return argon2.IDKey(password, salt, a.Time, a.Memory, a.Threads, 32) +} + +func NewArgon2() KeyGen { + return argon2Params{ + Time: 1, + Memory: 64 * 1024, + Threads: 4, + Name: argon2KeyGenName, + } +} + +func NewArgon2WithParams(time, memory uint32, threads uint8) (k KeyGen, err error) { + if time == 0 || memory == 0 || threads == 0 { + return nil, ErrInvalidParameter + } + + return argon2Params{ + Time: time, + Memory: memory, + Threads: threads, + Name: argon2KeyGenName, + }, nil +} + +func (a argon2Params) MarshalJSON() ([]byte, error) { + m := map[string]any{ + "name": a.Name, + "memory": a.Memory, + "threads": a.Threads, + "time": a.Time, + } + return json.Marshal(&m) +} diff --git a/privacy/argon2_test.go b/privacy/argon2_test.go new file mode 100644 index 0000000..d508458 --- /dev/null +++ b/privacy/argon2_test.go @@ -0,0 +1,169 @@ +package privacy + +import ( + "crypto/rand" + "encoding/json" + "reflect" + "testing" +) + +func TestNewArgon2(t *testing.T) { + k := NewArgon2() + if _, ok := k.(argon2Params); !ok { + t.Fatal("unexpected") + } +} + +func TestNewArgon2WithParams(t *testing.T) { + type args struct { + time uint32 + memory uint32 + threads uint8 + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "positive", + args: args{ + time: 1, + memory: 4 * 1024, + threads: 4, + }, + wantErr: false, + }, + { + name: "negative: zero time", + args: args{ + time: 0, + memory: 1 * 1024, + threads: 4, + }, + wantErr: true, + }, + { + name: "negative: zero memory", + args: args{ + time: 1, + memory: 0, + threads: 4, + }, + wantErr: true, + }, + { + name: "negative: zero threads", + args: args{ + time: 1, + memory: 1 * 1024, + threads: 0, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewArgon2WithParams(tt.args.time, tt.args.memory, tt.args.threads) + if (err != nil) != tt.wantErr { + t.Errorf("NewArgon2WithParams() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} + +func TestArgon2Params_MarshalJSON(t *testing.T) { + var ( + b []byte + err error + str string + ok bool + a any + ) + + if b, err = NewArgon2().MarshalJSON(); err != nil { + t.Fatal("unexpected", err) + } + + m := make(map[string]any) + if err = json.Unmarshal(b, &m); err != nil { + t.Fatal("unexpected", err) + } + + if a, ok = m["name"]; !ok { + t.Fatal("unexpected: no field name") + } + + if str, ok = a.(string); !ok { + t.Fatal("unexpected: name field is not a string") + } + + if str != argon2KeyGenName { + t.Fatal("unexpected: value of the name") + } +} + +func Test_argon2Params_GenerateKey(t *testing.T) { + type fields struct { + Time uint32 + Memory uint32 + Threads uint8 + Name string + } + type args struct { + password []byte + salt []byte + } + + var ( + prepKG KeyGen + prepErr error + ) + + passphrase := "some passphrase" + salt := make([]byte, 16) + if _, prepErr = rand.Read(salt); prepErr != nil { + t.Fatal("test preparation failure:", prepErr) + } + + if prepKG, prepErr = NewArgon2WithParams(1, 1*1024, 2); prepErr != nil { + t.Fatal("test preparation failure:", prepErr) + } + prepBytes := prepKG.GenerateKey([]byte(passphrase), salt) + + tests := []struct { + name string + fields fields + args args + want []byte + }{ + { + name: "positive", + fields: fields{ + Time: 1, + Memory: 1 * 1024, + Threads: 2, + Name: argon2KeyGenName, + }, + args: args{ + password: []byte(passphrase), + salt: salt, + }, + want: prepBytes, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := argon2Params{ + Time: tt.fields.Time, + Memory: tt.fields.Memory, + Threads: tt.fields.Threads, + Name: tt.fields.Name, + } + if got := a.GenerateKey(tt.args.password, tt.args.salt); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GenerateKey() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/privacy/privacy.go b/privacy/privacy.go new file mode 100644 index 0000000..7692f95 --- /dev/null +++ b/privacy/privacy.go @@ -0,0 +1,368 @@ +package privacy + +import ( + "crypto/cipher" + "crypto/rand" + "encoding/binary" + "encoding/json" + "errors" + "golang.org/x/crypto/chacha20poly1305" + "io" +) + +type CipherMethodType byte + +type KeyGen interface { + json.Marshaler + GenerateKey(password, salt []byte) []byte +} + +const ( + segmentSizeBytesLen int = 4 + + Uninitialised CipherMethodType = 0 + XChaCha20Simple CipherMethodType = 1 + AES256GCMSimple CipherMethodType = 2 + + DefaultCipherMethod = XChaCha20Simple +) + +var ( + ErrInvalidSaltLen = errors.New("invalid salt length") + ErrUninitialisedSalt = errors.New("uninitialised salt") + ErrUninitialisedMethod = errors.New("cipher method type uninitialised") + ErrInvalidCipherMethod = errors.New("invalid cipher method type") + //ErrCannotReadMagicBytes = errors.New("cannot read magic bytes") //no usage for now + ErrInvalidReadFlow = errors.New("func ReadMagic should be called before calling Read") + ErrInvalidKeyState = errors.New("func GenerateKey should be called first") + ErrInvalidSegmentLength = errors.New("segment length is too long") + segmentLenBytes = make([]byte, segmentSizeBytesLen) +) + +type Reader struct { + *Privacy + reader io.Reader + buf []byte + bufSlice []byte + isEOF bool +} + +type WriteCloser struct { + *Privacy + writeCloser io.WriteCloser + buf []byte + bufSlice []byte + magicWritten bool +} + +type Privacy struct { + salt []byte + segmentSize uint32 + cmType CipherMethodType + aead cipher.AEAD + keygen KeyGen +} + +func newPrivacy(k KeyGen) *Privacy { + return &Privacy{ + segmentSize: 64 * 1024 * 1024, + cmType: Uninitialised, + keygen: k, + } +} + +func NewPrivacyReader(reader io.Reader) *Reader { + return NewPrivacyReaderWithKeyGen(reader, NewArgon2()) +} + +func NewPrivacyReaderWithKeyGen(reader io.Reader, keygen KeyGen) *Reader { + return &Reader{ + Privacy: newPrivacy(keygen), + reader: reader, + isEOF: false, + } +} + +func NewPrivacyWriterCloserDefault(wc io.WriteCloser) *WriteCloser { + return NewPrivacyWriteCloser(wc, DefaultCipherMethod) +} + +func NewPrivacyWriteCloser(wc io.WriteCloser, cmType CipherMethodType) *WriteCloser { + return NewPrivacyWriteCloserWithKeyGen(wc, cmType, NewArgon2()) +} + +func NewPrivacyWriteCloserWithKeyGen(wc io.WriteCloser, cmType CipherMethodType, keygen KeyGen) *WriteCloser { + privacy := newPrivacy(keygen) + privacy.cmType = cmType + return &WriteCloser{ + Privacy: privacy, + writeCloser: wc, + magicWritten: false, + } +} + +func (p *Privacy) SetSalt(salt []byte) error { + if len(salt) != 16 { + return ErrInvalidSaltLen + } + + if len(p.salt) != 16 { + p.salt = make([]byte, 16) + } + + copy(p.salt, salt) + + return nil +} + +func (p *Privacy) GetSegmentSize() uint32 { + return p.segmentSize +} + +func (p *Privacy) SetSegmentSize(size uint32) { + p.segmentSize = size +} + +func (p *Privacy) NewSalt() error { + if len(p.salt) != 16 { + p.salt = make([]byte, 16) + } + + if p.cmType == Uninitialised { + return ErrUninitialisedMethod + } + + p.salt[0] = byte(p.cmType) + _, err := rand.Read(p.salt[1:]) + if err != nil { + return err + } + + return nil +} + +func (p *Privacy) GenerateKey(passphrase string) error { + var ( + key []byte + err error + ) + + if p.cmType == Uninitialised { + return ErrUninitialisedMethod + } + + if len(p.salt) != 16 { + return ErrUninitialisedSalt + } + + key = p.keygen.GenerateKey([]byte(passphrase), p.salt) + switch p.cmType { + case XChaCha20Simple: + p.aead, err = chacha20poly1305.NewX(key) + default: + return ErrInvalidCipherMethod + } + if err != nil { + return err + } + + return nil +} + +func (wc *WriteCloser) Write(b []byte) (n int, err error) { + var ( + copied int + nonceSize int + lastMarker int + plaintext []byte + ) + + if wc.aead == nil { + return 0, ErrInvalidKeyState + } + + if cap(wc.buf) != int(wc.segmentSize)+wc.aead.NonceSize()+wc.aead.Overhead() { + wc.buf = make([]byte, int(wc.segmentSize)+wc.aead.NonceSize()+wc.aead.Overhead()) + wc.bufSlice = wc.buf[wc.aead.NonceSize():wc.aead.NonceSize()] + } + + if !wc.magicWritten { + n, err = wc.writeCloser.Write(wc.salt) + if err != nil { + return + } + wc.magicWritten = true + } + + nonceSize = wc.aead.NonceSize() + copied = 0 + for copied < len(b) { + if len(wc.bufSlice) == int(wc.segmentSize) { + n, err = wc.writeSegment() + if err != nil { + return + } + } else { + lastMarker = len(wc.bufSlice) + plaintext = wc.buf[nonceSize : nonceSize+len(wc.bufSlice)] + if len(b[copied:]) <= int(wc.segmentSize)-len(wc.bufSlice) { + plaintext = plaintext[:len(plaintext)+len(b[copied:])] + copied += copy(plaintext[lastMarker:], b[copied:]) + } else { + plaintext = plaintext[:int(wc.segmentSize)] + copied += copy(plaintext[lastMarker:], b[copied:]) + } + wc.bufSlice = plaintext + } + } + + return copied, nil +} + +func (wc *WriteCloser) writeSegment() (n int, err error) { + var ( + nonce []byte + ciphertext []byte + plaintext []byte + written int + ) + + written = len(wc.bufSlice) + binary.LittleEndian.PutUint32(segmentLenBytes, uint32(written)) + n, err = wc.writeCloser.Write(segmentLenBytes) + if err != nil { + return + } + + nonce = wc.buf[:wc.aead.NonceSize()] + _, err = rand.Read(nonce) + if err != nil { + return + } + plaintext = wc.buf[wc.aead.NonceSize() : wc.aead.NonceSize()+written] + ciphertext = plaintext[:0] + + wc.aead.Seal(ciphertext, nonce, plaintext, segmentLenBytes) + n, err = wc.writeCloser.Write(wc.buf[:written+wc.aead.NonceSize()+wc.aead.Overhead()]) + if err != nil { + return + } + wc.bufSlice = wc.buf[wc.aead.NonceSize():wc.aead.NonceSize()] + + return written, nil +} + +func (wc *WriteCloser) Close() (err error) { + if len(wc.bufSlice) > 0 { + _, err = wc.writeSegment() + if err != nil { + return + } + } + return wc.writeCloser.Close() +} + +func (r *Reader) ReadMagic() (err error) { + if r.cmType == Uninitialised { + magic := make([]byte, 16) + _, err = r.reader.Read(magic[:1]) + if err != nil { + return + } + + switch CipherMethodType(magic[0]) { + case XChaCha20Simple: + r.cmType = XChaCha20Simple + _, err = r.reader.Read(magic[1:]) + if err != nil { + return + } + + err = r.SetSalt(magic) + if err != nil { + return + } + default: + return ErrInvalidCipherMethod + } + + } + + return nil +} + +func (r *Reader) Read(b []byte) (n int, err error) { + var ( + segmentLen uint32 + nonce []byte + ciphertext []byte + plaintext []byte + copied int + ) + + if r.cmType == Uninitialised { + return 0, ErrInvalidReadFlow + } + + if r.aead == nil { + return 0, ErrInvalidKeyState + } + + if r.isEOF { + return 0, io.EOF + } + + if cap(r.buf) != int(r.segmentSize)+r.aead.Overhead()+r.aead.NonceSize() { + r.buf = make([]byte, int(r.segmentSize)+r.aead.Overhead()+r.aead.NonceSize()) + } + + copied = 0 + for copied < len(b) { + if len(r.bufSlice) == 0 { + n, err = r.reader.Read(segmentLenBytes) + if err != nil { + if err == io.EOF { + if copied > 0 { + r.isEOF = true + return copied, nil + } else { + r.isEOF = true + return 0, err + } + } + return + } + + segmentLen = binary.LittleEndian.Uint32(segmentLenBytes) + if segmentLen > r.segmentSize { + return 0, ErrInvalidSegmentLength + } + + n, err = r.reader.Read(r.buf[:int(segmentLen)+r.aead.Overhead()+r.aead.NonceSize()]) + if err != nil { + return + } + + nonce = r.buf[:r.aead.NonceSize()] + ciphertext = r.buf[r.aead.NonceSize() : r.aead.NonceSize()+int(segmentLen)+r.aead.Overhead()] + plaintext = ciphertext[:0] + + if _, err = r.aead.Open(plaintext, nonce, ciphertext, segmentLenBytes); err != nil { + return + } + plaintext = plaintext[:int(segmentLen)] + r.bufSlice = plaintext + } else { + if len(b[copied:]) <= len(r.bufSlice) { + cp := copy(b[copied:], r.bufSlice) + r.bufSlice = r.bufSlice[cp:] + copied += cp + } else { + copied += copy(b[copied:], r.bufSlice) + r.bufSlice = r.buf[r.aead.NonceSize():r.aead.NonceSize()] + } + } + } + + return copied, nil +} diff --git a/privacy/privacy_test.go b/privacy/privacy_test.go new file mode 100644 index 0000000..d7f921e --- /dev/null +++ b/privacy/privacy_test.go @@ -0,0 +1,241 @@ +package privacy + +import ( + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "errors" + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/chacha20poly1305" + "io" + mr "math/rand" + "testing" +) + +type tBuffer struct { + buf []byte + rOff int +} + +func newTBuf(size int) *tBuffer { + return &tBuffer{ + buf: make([]byte, 0, size), + } +} + +func (tb *tBuffer) Read(b []byte) (n int, err error) { + if tb.rOff == len(tb.buf) { + return 0, io.EOF + } + + if len(b)+tb.rOff <= len(tb.buf) { + copy(b, tb.buf[tb.rOff:]) + tb.rOff += len(b) + return len(b), nil + } else { + copy(b[:len(tb.buf)-tb.rOff], tb.buf[tb.rOff:]) + n = len(tb.buf) - tb.rOff + err = nil + tb.rOff = len(tb.buf) + return + } +} + +func (tb *tBuffer) Write(b []byte) (n int, err error) { + if len(tb.buf)+len(b) > cap(tb.buf) { + return 0, errors.New("insufficient space") + } + + wOff := len(tb.buf) + tb.buf = tb.buf[:wOff+len(b)] + copy(tb.buf[wOff:], b) + return len(b), nil +} + +func (tb *tBuffer) Close() error { + return nil +} + +func TestReadWriteClose(t *testing.T) { + tb := newTBuf(70 * 1024) + keygen, err := NewArgon2WithParams(1, 4*1024, 2) + if err != nil { + t.Fatal("test preparation failure:", err) + } + + passphrase := "some passphrase" + + writer := NewPrivacyWriteCloserWithKeyGen(tb, DefaultCipherMethod, keygen) + + t.Run("uninitialised salt", func(t *testing.T) { + err = writer.GenerateKey(passphrase) + if err == nil { + t.Fatal("unexpected: it should error") + } + if err != ErrUninitialisedSalt { + t.Fatal("unexpected error result:", err) + } + }) + + writer.SetSegmentSize(uint32(16 * 1024)) + + if err = writer.NewSalt(); err != nil { + t.Fatal("unexpected: NewSalt failed", err) + } + if err = writer.GenerateKey(passphrase); err != nil { + t.Fatal("unexpected: failed to generate key", err) + } + + sha := sha256.New() + ur := mr.New(mr.NewSource(1)) + bb := make([]byte, 1048) + + var ( + //bar int + n int + wl int + rl int + ) + for i := 0; i < 63; i++ { + //bar = 1000 + ur.Intn(49) + //ur.Read(bb[:bar]) + //sha.Write(bb[:bar]) + //if n, err = writer.Write(bb[:bar]); err != nil { + // t.Fatal("unexpected: Write failed", err) + //} + ur.Read(bb) + sha.Write(bb) + if n, err = writer.Write(bb); err != nil { + t.Fatal("unexpected: Write failed", err) + } + wl += n + } + + if err = writer.Close(); err != nil { + t.Fatal("unexpected: Close failed", err) + } + + writeHash := sha.Sum(nil) + t.Log("write hash:", hex.EncodeToString(writeHash)) + + reader := NewPrivacyReaderWithKeyGen(tb, keygen) + reader.SetSegmentSize(uint32(16 * 1024)) + if err = reader.ReadMagic(); err != nil { + t.Fatal("unexpected: ReadMagic failed", err) + } + + if err = reader.GenerateKey(passphrase); err != nil { + t.Fatal("unexpected: GenerateKey failed", err) + } + + sha.Reset() + err = nil + n = 0 + for err == nil { + if n, err = reader.Read(bb); err != nil { + if err == io.EOF { + continue + } else { + t.Fatal("unexpected: Read failed", err) + } + } + rl += n + sha.Write(bb) + } + + readHash := sha.Sum(nil) + t.Log("read hash:", hex.EncodeToString(readHash)) + + t.Log("wl", wl) + t.Log("rl", rl) + for i := range writeHash { + if readHash[i] != writeHash[i] { + t.Fatal("unexpected: mismatch hash") + } + } +} + +func TestTrial(t *testing.T) { + x := make([]byte, 20) + for i := range x { + x[i] = byte(i) + } + + t.Log("len x:", len(x)) + t.Log("cap x:", cap(x)) + t.Log("x:", x) + + y := x[5:13] + t.Log("len y:", len(y)) + t.Log("cap y:", cap(y)) + t.Log("y:", y) + + z := y[10:15] + t.Log("len z:", len(z)) + t.Log("cap z:", cap(z)) + t.Log("z:", z) +} + +func TestKeyGen(t *testing.T) { + salt := make([]byte, 16) + _, err := rand.Read(salt) + if err != nil { + t.Fatal("unexpected error:", err) + } + passphrase := []byte("some passphrase") + + key := argon2.IDKey(passphrase, salt, 1000, 64*1024, 8, 32) + + _ = key + //keyCompare := argon2Params.IDKey(passphrase, salt, 100, 64*1024, 4, 32) + // + //for i := range key { + // if key[i] != keyCompare[i] { + // t.Fatal("unexpected result") + // } + //} +} + +func TestExample(t *testing.T) { + passphrase := []byte("some passphrase") + salt := make([]byte, 16) + _, err := rand.Read(salt) + if err != nil { + t.Fatal("error prepare salt", err) + } + + key := argon2.IDKey(passphrase, salt, 1, 4*1024, 2, 32) + + var aead cipher.AEAD + if aead, err = chacha20poly1305.NewX(key); err != nil { + t.Fatal("chacha", err) + } + + ur := mr.New(mr.NewSource(1)) + bb := make([]byte, 1024+aead.NonceSize()+aead.Overhead()) + + if _, err = rand.Read(bb[:aead.NonceSize()]); err != nil { + t.Fatal("fill up nonce", err) + } + + additional := []byte("some additional data") + ur.Read(bb[aead.NonceSize() : aead.NonceSize()+1024]) + before := sha256.Sum256(bb[aead.NonceSize() : aead.NonceSize()+1024]) + + aead.Seal(bb[aead.NonceSize():aead.NonceSize()], bb[:aead.NonceSize()], bb[aead.NonceSize():aead.NonceSize()+1024], additional) + + if _, err = aead.Open(bb[aead.NonceSize():aead.NonceSize()], + bb[:aead.NonceSize()], bb[aead.NonceSize():aead.NonceSize()+1024+aead.Overhead()], + additional); err != nil { + t.Fatal("decrypt error", err) + } + + after := sha256.Sum256(bb[aead.NonceSize() : aead.NonceSize()+1024]) + + for i := range before { + if before[i] != after[i] { + t.Fatal("data corruption?") + } + } +}