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
This commit is contained in:
Suyono 2023-07-25 11:10:22 +10:00
parent 2508b4c822
commit 64e4ca400f
9 changed files with 395 additions and 124 deletions

4
.gitignore vendored
View File

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

View File

@ -1,18 +1,20 @@
# 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.
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
### 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`
@ -36,3 +38,18 @@ 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
```

View File

@ -1,10 +1,10 @@
package spt
import (
"encoding/base64"
"fmt"
"gitea.suyono.dev/suyono/simple-privacy-tool/privacy"
"io"
"os"
)
type decryptApp struct {
@ -12,16 +12,6 @@ type decryptApp struct {
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
@ -37,8 +27,25 @@ func (d *decryptApp) GetPassphrase() (err error) {
}
func (d *decryptApp) ProcessFiles() (err error) {
d.r = privacy.NewPrivacyReader(d.srcFile)
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
}
@ -46,17 +53,8 @@ func (d *decryptApp) ProcessFiles() (err error) {
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 = io.Copy(d.dstFile, d.r); err != nil {
return
}
if err = d.dstFile.Close(); err != nil {
@ -69,45 +67,3 @@ func (d *decryptApp) ProcessFiles() (err error) {
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)
}

View File

@ -1,6 +1,7 @@
package spt
import (
"encoding/base64"
"gitea.suyono.dev/suyono/simple-privacy-tool/privacy"
"io"
)
@ -10,12 +11,6 @@ type encryptApp struct {
wc *privacy.WriteCloser
}
func newEncryptApp(a *cApp) *encryptApp {
return &encryptApp{
cApp: *a,
}
}
func (e *encryptApp) GetPassphrase() (err error) {
var (
passphrase string
@ -40,7 +35,21 @@ func (e *encryptApp) GetPassphrase() (err error) {
}
func (e *encryptApp) ProcessFiles() (err error) {
e.wc = privacy.NewPrivacyWriteCloser(e.dstFile, privacy.DefaultCipherMethod) //TODO: need to handle when custom keygen accepted
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
}

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
}

View File

@ -2,9 +2,11 @@ package spt
import (
"errors"
"os"
tw "gitea.suyono.dev/suyono/terminal_wrapper"
"github.com/spf13/cobra"
"os"
"log"
)
type cApp struct {
@ -21,20 +23,39 @@ var (
ErrPassphraseMismatch = errors.New("mismatch passphrase")
rootCmd = &cobra.Command{
Use: "spt",
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",
Use: "encrypt srcFile dstFile",
Args: validatePositionalArgs,
RunE: encrypt,
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",
RunE: decrypt,
Args: validatePositionalArgs,
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",
}
)
@ -43,7 +64,8 @@ func Execute() error {
}
func init() {
rootCmd.AddCommand(encryptCmd, decryptCmd)
initFlags()
rootCmd.AddCommand(encryptCmd, decryptCmd, hintCmd)
}
func validatePositionalArgs(cmd *cobra.Command, args []string) error {
@ -65,11 +87,13 @@ func encrypt(cmd *cobra.Command, args []string) (err error) {
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{
@ -80,14 +104,14 @@ func encrypt(cmd *cobra.Command, args []string) (err error) {
return
}
eApp = newEncryptApp(app)
eApp = &encryptApp{
cApp: *app,
}
if err = eApp.GetPassphrase(); err != nil {
return
}
//TODO: process additional flags
if err = eApp.ProcessFiles(); err != nil {
return
}
@ -105,11 +129,13 @@ func decrypt(cmd *cobra.Command, args []string) (err error) {
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{
@ -120,14 +146,14 @@ func decrypt(cmd *cobra.Command, args []string) (err error) {
return
}
dApp = newDecryptApp(app)
dApp = &decryptApp{
cApp: *app,
}
if err = dApp.GetPassphrase(); err != nil {
return
}
//TODO: process additional flags
if err = dApp.ProcessFiles(); err != nil {
return
}

View File

@ -1,14 +1,9 @@
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)
}
_ = spt.Execute()
}

View File

@ -1,11 +1,13 @@
package privacy
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"golang.org/x/crypto/chacha20poly1305"
"io"
)
@ -20,9 +22,9 @@ type KeyGen interface {
const (
segmentSizeBytesLen int = 4
Uninitialised CipherMethodType = 0
XChaCha20Simple CipherMethodType = 1
AES256GCMSimple CipherMethodType = 2
Uninitialised CipherMethodType = 0x00
XChaCha20Simple CipherMethodType = 0x01
AES256GCMSimple CipherMethodType = 0x02
DefaultCipherMethod = XChaCha20Simple
)
@ -31,7 +33,6 @@ 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")
@ -39,6 +40,12 @@ var (
segmentLenBytes = make([]byte, segmentSizeBytesLen)
)
type InvalidCipherMethod []byte
func (i InvalidCipherMethod) Error() string {
return "invalid cipher method type"
}
type Reader struct {
*Privacy
reader io.Reader
@ -158,12 +165,19 @@ func (p *Privacy) GenerateKey(passphrase string) error {
key = p.keygen.GenerateKey([]byte(passphrase), p.salt)
switch p.cmType {
case XChaCha20Simple:
p.aead, err = chacha20poly1305.NewX(key)
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 ErrInvalidCipherMethod
}
if err != nil {
return err
return InvalidCipherMethod([]byte{})
}
return nil
@ -187,7 +201,7 @@ func (wc *WriteCloser) Write(b []byte) (n int, err error) {
}
if !wc.magicWritten {
n, err = wc.writeCloser.Write(wc.salt)
n, err = wc.writeUp(wc.salt)
if err != nil {
return
}
@ -229,7 +243,7 @@ func (wc *WriteCloser) writeSegment() (n int, err error) {
written = len(wc.bufSlice)
binary.LittleEndian.PutUint32(segmentLenBytes, uint32(written))
n, err = wc.writeCloser.Write(segmentLenBytes)
n, err = wc.writeUp(segmentLenBytes)
if err != nil {
return
}
@ -243,7 +257,7 @@ func (wc *WriteCloser) writeSegment() (n int, err error) {
ciphertext = plaintext[:0]
wc.aead.Seal(ciphertext, nonce, plaintext, segmentLenBytes)
n, err = wc.writeCloser.Write(wc.buf[:written+wc.aead.NonceSize()+wc.aead.Overhead()])
n, err = wc.writeUp(wc.buf[:written+wc.aead.NonceSize()+wc.aead.Overhead()])
if err != nil {
return
}
@ -252,6 +266,25 @@ func (wc *WriteCloser) writeSegment() (n int, err error) {
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()
@ -265,32 +298,46 @@ func (wc *WriteCloser) Close() (err error) {
func (r *Reader) ReadMagic() (err error) {
if r.cmType == Uninitialised {
magic := make([]byte, 16)
_, err = r.reader.Read(magic[:1])
if err != nil {
if _, err = r.readUp(magic); 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
}
case AES256GCMSimple:
r.cmType = AES256GCMSimple
default:
return ErrInvalidCipherMethod
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
@ -316,10 +363,11 @@ func (r *Reader) Read(b []byte) (n int, err error) {
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.reader.Read(segmentLenBytes)
n, err = r.readUp(segmentLenBytes)
if err != nil {
if err == io.EOF {
if copied > 0 {
@ -338,7 +386,7 @@ func (r *Reader) Read(b []byte) (n int, err error) {
return 0, ErrInvalidSegmentLength
}
n, err = r.reader.Read(r.buf[:int(segmentLen)+r.aead.Overhead()+r.aead.NonceSize()])
n, err = r.readUp(r.buf[:int(segmentLen)+r.aead.Overhead()+r.aead.NonceSize()])
if err != nil {
return
}
@ -348,7 +396,7 @@ func (r *Reader) Read(b []byte) (n int, err error) {
plaintext = ciphertext[:0]
if _, err = r.aead.Open(plaintext, nonce, ciphertext, segmentLenBytes); err != nil {
return
return copied, fmt.Errorf("decrypt Read: %w", err)
}
plaintext = plaintext[:int(segmentLen)]
r.bufSlice = plaintext
@ -364,5 +412,6 @@ func (r *Reader) Read(b []byte) (n int, err error) {
}
}
return copied, nil
n = copied
return
}