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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
102
cmd/spt/flags.go
Normal 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
115
cmd/spt/hint.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user