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

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
}