12 Commits

Author SHA1 Message Date
3fc43f3046 Merge pull request 'v1.0.0' (#2) from gitea.suyono.dev/suyono/simple-privacy-tool/dev into main
Reviewed-on: #2
2023-07-27 05:26:44 +00:00
64e4ca400f feat(argon2id cost param): enable user to modify the cost param
feat(hint): embed argon2 custom param as hint
feat(aes): add AES-GCM as algo
fix(stdin/stdout): fixed STDIN/STDOUT issue
2023-07-27 15:19:02 +10:00
2508b4c822 chore: merge back to dev 2023-07-24 15:32:37 +10:00
c942a33e2e Merge pull request 'early release preparation' (#1) from gitea.suyono.dev/suyono/simple-privacy-tool/alpha-preparation into main
Reviewed-on: #1
2023-07-23 12:19:06 +00:00
1d91861915 chore: remove unrelated information from READMe.md 2023-07-23 22:09:50 +10:00
fe749b2e42 basic functionality 2023-07-23 22:09:50 +10:00
6c4b48873b wip: debugging issue 2023-07-23 22:00:52 +10:00
7b29d8f6cc feat: encrypt & decrypt tested (simple) 2023-07-21 20:06:04 +10:00
dd0524d680 WIP 2023-07-20 10:55:29 +10:00
bd976b1533 change workstation 2023-07-20 08:32:38 +10:00
3dba047793 WIP 2023-07-11 16:15:49 +10:00
91f7cbbd06 change workstation 2023-07-11 10:57:17 +10:00
15 changed files with 1531 additions and 0 deletions

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
/.idea/
/simple-privacy-tool
/spt
/vendor
/test-payload*

1
.tool-versions Normal file
View File

@@ -0,0 +1 @@
golang 1.20.5

View File

@@ -1,2 +1,55 @@
# simple-privacy-tool
Simple Privacy Tool is a simple tool to encrypt and decrypt files. It uses the symmetric algorithm XChaCha20-Poly1305 and
AES-GCM, 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
```
or install with `go install gitea.suyono.dev/suyono/simple-privacy-tool`
### Usage
By default simple-privacy-tool uses XChaCha20-Poly1305.
#### 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
```
#### Customize Argon2id parameter
The simple-privacy-tool accepts several flags to tweak Argon2id parameters. There are three parameters that user can
adjust: time, memory, and threads. Example
```shell
simple-privacy-tool encrypt --kdf argon2 --argon2id-time 2 --argon2id-mem 65536 --argon2id-thread 4 --hint inputFile outputFile
```
The user has to include `--kdf` flag to be able to customize the parameter. Optionally, user can add `--hint` flag to embed
the custom parameter in the encrypted file as a hint. Warning: the hint in the encrypted file is not protected (authenticated)
and the decryption process doesn't use the hint.
The purpose of the hint is as human reminder. User can print the embedded hint by using command
```shell
simple-privacy-tool hint encryptedFile
```

69
cmd/spt/decrypt.go Normal file
View File

@@ -0,0 +1,69 @@
package spt
import (
"encoding/base64"
"fmt"
"gitea.suyono.dev/suyono/simple-privacy-tool/privacy"
"io"
)
type decryptApp struct {
cApp
r *privacy.Reader
}
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) {
var (
src io.Reader
)
if f.IsBase64() {
src = base64.NewDecoder(base64.StdEncoding, d.srcFile)
} else {
src = d.srcFile
}
d.r = privacy.NewPrivacyReaderWithKeyGen(src, f.KeyGen())
redo:
if err = d.r.ReadMagic(); err != nil {
if h, ok := err.(privacy.InvalidCipherMethod); ok {
if err = SkipHint(h, src); err != nil {
return fmt.Errorf("reading magic bytes: %w", err)
}
goto redo
}
return
}
if err = d.r.GenerateKey(d.passphrase); err != nil {
return
}
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
}

74
cmd/spt/encrypt.go Normal file
View File

@@ -0,0 +1,74 @@
package spt
import (
"encoding/base64"
"gitea.suyono.dev/suyono/simple-privacy-tool/privacy"
"io"
)
type encryptApp struct {
cApp
wc *privacy.WriteCloser
}
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) {
var dst io.WriteCloser
if f.IsBase64() {
dst = base64.NewEncoder(base64.StdEncoding, e.dstFile)
} else {
dst = e.dstFile
}
if f.IncludeHint() {
if err = WriteHint(f.KeyGen(), dst); err != nil {
return
}
}
e.wc = privacy.NewPrivacyWriteCloserWithKeyGen(dst, f.CipherMethod(), f.KeyGen())
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
}

102
cmd/spt/flags.go Normal file
View File

@@ -0,0 +1,102 @@
package spt
import (
"errors"
"gitea.suyono.dev/suyono/simple-privacy-tool/privacy"
)
type flags struct {
base64Encoding bool
algo string
cmType privacy.CipherMethodType
kdf string
argon2idTime int
argon2idMemory int
argon2idThreads int
keygen privacy.KeyGen
hint bool
}
const (
defaultAlgo = ""
chacha20Algo = "chacha"
aesAlgo = "aes"
defaultKdf = defaultAlgo
argon2idKdf = "argon2"
argon2idTime = 1
argon2idMemory = 64 * 1024
argon2idThreads = 4
)
var (
f flags
)
func initFlags() {
encryptCmd.PersistentFlags().BoolVar(&f.hint, "hint", false, "include hint in the output file")
encryptCmd.PersistentFlags().StringVar(&f.algo, "algo", defaultAlgo, "encryption algorithm, valid values: chacha and aes. Default algo is chacha")
rootCmd.PersistentFlags().StringVar(&f.kdf, "kdf", defaultKdf, "Key Derivation Function, valid values: argon2")
rootCmd.PersistentFlags().IntVar(&f.argon2idTime, "argon2id-time", argon2idTime, "sets argon2id time cost-parameter")
rootCmd.PersistentFlags().IntVar(&f.argon2idMemory, "argon2id-mem", argon2idMemory, "sets argon2id memory cost-parameter (in KB)")
rootCmd.PersistentFlags().IntVar(&f.argon2idThreads, "argon2id-thread", argon2idThreads, "sets argon2id thread cost-parameter")
rootCmd.PersistentFlags().BoolVar(&f.base64Encoding, "base64", false, "the file is encoded in Base64")
}
func processFlags() (err error) {
if err = processAlgoFlags(); err != nil {
return
}
if err = processKeyGenFlags(); err != nil {
return
}
return
}
func processAlgoFlags() (err error) {
switch f.algo {
case defaultAlgo, chacha20Algo:
f.cmType = privacy.XChaCha20Simple
case aesAlgo:
f.cmType = privacy.AES256GCMSimple
default:
return errors.New("invalid algo")
}
return
}
func processKeyGenFlags() (err error) {
switch f.kdf {
case defaultKdf:
f.keygen = privacy.NewArgon2()
case argon2idKdf:
if f.argon2idTime < 0 || f.argon2idThreads < 0 || f.argon2idMemory < 0 {
return errors.New("invalid argon2id parameter")
}
f.keygen, err = privacy.NewArgon2WithParams(uint32(f.argon2idTime), uint32(f.argon2idMemory), uint8(argon2idThreads))
default:
return errors.New("invalid KDF")
}
return
}
func (f flags) IsBase64() bool {
return f.base64Encoding
}
func (f flags) CipherMethod() privacy.CipherMethodType {
return f.cmType
}
func (f flags) KeyGen() privacy.KeyGen {
return f.keygen
}
func (f flags) IncludeHint() bool {
return f.hint
}

115
cmd/spt/hint.go Normal file
View File

@@ -0,0 +1,115 @@
package spt
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"gitea.suyono.dev/suyono/simple-privacy-tool/privacy"
"github.com/spf13/cobra"
"io"
"os"
)
var (
ErrNotHint = errors.New("not a hint")
)
func SkipHint(b []byte, r io.Reader) (err error) {
if b[0] != 0xFF {
return ErrNotHint
}
hintLen := int(binary.LittleEndian.Uint16(b[1:3])) - 13 // 13 is hint minimum length, prevent overlapping with magic
if hintLen > 0 {
skip := make([]byte, hintLen)
var (
n, total int
)
for total < hintLen {
if n, err = r.Read(skip); err != nil {
return
}
total += n
}
}
return
}
func WriteHint(k privacy.KeyGen, w io.Writer) (err error) {
var (
b []byte
bb *bytes.Buffer
total int
n int
)
if b, err = k.MarshalJSON(); err != nil {
return
}
wb := make([]byte, 3)
wb[0] = 0xFF
if len(b) >= 13 {
binary.LittleEndian.PutUint16(wb[1:], uint16(len(b)))
} else {
binary.LittleEndian.PutUint16(wb[1:], uint16(13))
}
bb = bytes.NewBuffer(make([]byte, 0))
bb.Write(wb)
bb.Write(b)
if len(b) < 13 {
bb.Write(make([]byte, 13-len(b)))
}
b = bb.Bytes()
for total < len(b) {
if n, err = w.Write(b[total:]); err != nil {
return
}
total += n
}
return
}
func CmdReadHint(cmd *cobra.Command, args []string) (err error) {
var (
f *os.File
tb []byte
hLen int
)
if f, err = os.Open(args[0]); err != nil {
return
}
tb = make([]byte, 3)
if _, err = f.Read(tb); err != nil {
return
}
if tb[0] != 0xFF {
return ErrNotHint
}
hLen = int(binary.LittleEndian.Uint16(tb[1:]))
tb = make([]byte, hLen)
if _, err = f.Read(tb); err != nil {
return
}
if hLen == 13 {
for hLen > 0 {
if tb[hLen-1] != 0 {
break
}
hLen--
}
}
fmt.Println("hint:", string(tb[:hLen]))
return
}

188
cmd/spt/spt.go Normal file
View File

@@ -0,0 +1,188 @@
package spt
import (
"errors"
"os"
tw "gitea.suyono.dev/suyono/terminal_wrapper"
"github.com/spf13/cobra"
"log"
)
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: "simple-privacy-tool",
Short: "a simple tool to encrypt and decrypt file",
}
hintCmd = &cobra.Command{
Use: "hint file",
Args: cobra.ExactArgs(1),
RunE: CmdReadHint,
Short: "extract and print hint from encrypted file",
}
encryptCmd = &cobra.Command{
Use: "encrypt srcFile dstFile",
Args: validatePositionalArgs,
RunE: func(cmd *cobra.Command, args []string) error {
if err := processFlags(); err != nil {
return err
}
return encrypt(cmd, args)
},
Short: "encrypt srcFile, output to dstFile",
}
decryptCmd = &cobra.Command{
Use: "decrypt srcFile dstFile",
RunE: func(cmd *cobra.Command, args []string) error {
if err := processFlags(); err != nil {
return err
}
return decrypt(cmd, args)
},
Args: validatePositionalArgs,
Short: "decrypt srcFile, output to dstFile",
}
)
func Execute() error {
return rootCmd.Execute()
}
func init() {
initFlags()
rootCmd.AddCommand(encryptCmd, decryptCmd, hintCmd)
}
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
}
log.SetOutput(terminal)
defer func() {
existingErr := err
if err = terminal.Restore(); err == nil && existingErr != nil {
err = existingErr
}
log.SetOutput(os.Stderr)
}()
app = &cApp{
term: terminal,
}
if err = app.ProcessArgs(args); err != nil {
return
}
eApp = &encryptApp{
cApp: *app,
}
if err = eApp.GetPassphrase(); err != nil {
return
}
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
}
log.SetOutput(terminal)
defer func() {
existingErr := err
if err = terminal.Restore(); err == nil && existingErr != nil {
err = existingErr
}
log.SetOutput(os.Stderr)
}()
app = &cApp{
term: terminal,
}
if err = app.ProcessArgs(args); err != nil {
return
}
dApp = &decryptApp{
cApp: *app,
}
if err = dApp.GetPassphrase(); err != nil {
return
}
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
}

16
go.mod Normal file
View File

@@ -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
)

18
go.sum Normal file
View File

@@ -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=

9
main.go Normal file
View File

@@ -0,0 +1,9 @@
package main
import (
"gitea.suyono.dev/suyono/simple-privacy-tool/cmd/spt"
)
func main() {
_ = spt.Execute()
}

54
privacy/argon2.go Normal file
View File

@@ -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)
}

169
privacy/argon2_test.go Normal file
View File

@@ -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)
}
})
}
}

417
privacy/privacy.go Normal file
View File

@@ -0,0 +1,417 @@
package privacy
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"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 = 0x00
XChaCha20Simple CipherMethodType = 0x01
AES256GCMSimple CipherMethodType = 0x02
DefaultCipherMethod = XChaCha20Simple
)
var (
ErrInvalidSaltLen = errors.New("invalid salt length")
ErrUninitialisedSalt = errors.New("uninitialised salt")
ErrUninitialisedMethod = errors.New("cipher method type uninitialised")
//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 InvalidCipherMethod []byte
func (i InvalidCipherMethod) Error() string {
return "invalid cipher method type"
}
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:
if p.aead, err = chacha20poly1305.NewX(key); err != nil {
return err
}
case AES256GCMSimple:
var block cipher.Block
if block, err = aes.NewCipher(key); err != nil {
return err
}
if p.aead, err = cipher.NewGCM(block); err != nil {
return err
}
default:
return InvalidCipherMethod([]byte{})
}
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.writeUp(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.writeUp(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.writeUp(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) writeUp(b []byte) (n int, err error) {
var (
total int
)
for {
if n, err = wc.writeCloser.Write(b[total:]); err != nil {
return n + total, err
}
total += n
if total == len(b) {
break
}
}
return total, 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)
if _, err = r.readUp(magic); err != nil {
return
}
switch CipherMethodType(magic[0]) {
case XChaCha20Simple:
r.cmType = XChaCha20Simple
case AES256GCMSimple:
r.cmType = AES256GCMSimple
default:
return InvalidCipherMethod(magic)
}
if err = r.SetSalt(magic); err != nil {
return
}
}
return nil
}
func (r *Reader) readUp(b []byte) (n int, err error) {
var (
total int
)
for {
if n, err = r.reader.Read(b[total:]); err != nil {
return n + total, err
}
total += n
if total == len(b) {
break
}
}
return total, 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())
}
//log.Printf("Read for %d bytes\n", len(b))
copied = 0
for copied < len(b) {
if len(r.bufSlice) == 0 {
n, err = r.readUp(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.readUp(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 copied, fmt.Errorf("decrypt Read: %w", err)
}
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()]
}
}
}
n = copied
return
}

241
privacy/privacy_test.go Normal file
View File

@@ -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?")
}
}
}