commit 40f5816dc0082c16f2a7b328de1c49b04178043d Author: Dawid WysokiƄski Date: Sun Mar 27 18:42:27 2022 +0200 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/cmd/gootp/decrypt/decrypt.go b/cmd/gootp/decrypt/decrypt.go new file mode 100644 index 0000000..c0cc5a0 --- /dev/null +++ b/cmd/gootp/decrypt/decrypt.go @@ -0,0 +1,67 @@ +package decrypt + +import ( + "fmt" + "os" + "syscall" + + "github.com/Kichiyaki/gootp/internal/andotp" + "golang.org/x/term" + + "github.com/urfave/cli/v2" +) + +func NewCommand() *cli.Command { + return &cli.Command{ + Name: "decrypt", + Usage: "decrypt backup file generated by andotp", + Action: func(c *cli.Context) error { + var err error + password := []byte(c.String("password")) + if len(password) == 0 { + password, err = getPassword() + } + if err != nil { + return err + } + + b, err := os.ReadFile(c.String("path")) + if err != nil { + return fmt.Errorf("something went wrong while reading file: %w", err) + } + + result, err := andotp.Decrypt(password, b) + if err != nil { + return fmt.Errorf("something went wrong while decrypting file: %w", err) + } + + fmt.Print("\n") + fmt.Print(string(result)) + + return nil + }, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "path", + Usage: "path to backup file", + Required: true, + }, + &cli.StringFlag{ + Name: "password", + Usage: "encryption password", + Required: false, + }, + }, + } +} + +func getPassword() ([]byte, error) { + fmt.Print("Password: ") + + pass, err := term.ReadPassword(syscall.Stdin) + if err != nil { + return nil, fmt.Errorf("term.ReadPassword: %w", err) + } + + return pass, nil +} diff --git a/cmd/gootp/main.go b/cmd/gootp/main.go new file mode 100644 index 0000000..fb5a34e --- /dev/null +++ b/cmd/gootp/main.go @@ -0,0 +1,24 @@ +package main + +import ( + "log" + "os" + + "github.com/Kichiyaki/gootp/cmd/gootp/decrypt" + + "github.com/urfave/cli/v2" +) + +func main() { + app := &cli.App{ + Name: "gootp", + EnableBashCompletion: true, + Commands: []*cli.Command{ + decrypt.NewCommand(), + }, + } + + if err := app.Run(os.Args); err != nil { + log.Fatalln("app.Run:", err) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9637af4 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module github.com/Kichiyaki/gootp + +go 1.17 + +require golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 + +require ( + github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/stretchr/testify v1.7.1 // indirect + github.com/urfave/cli/v2 v2.4.0 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f15dd91 --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I= +github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg= +golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s= +golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/andotp/andotp.go b/internal/andotp/andotp.go new file mode 100644 index 0000000..e4f83fa --- /dev/null +++ b/internal/andotp/andotp.go @@ -0,0 +1,95 @@ +package andotp + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha1" + "encoding/binary" + "fmt" + "math/big" + + "golang.org/x/crypto/pbkdf2" +) + +const ( + ivLen = 12 + keyLen = 32 + iterationLen = 4 + saltLen = 12 + maxIterations = 160000 + minIterations = 140000 +) + +func Encrypt(password, plaintext []byte) ([]byte, error) { + iter := make([]byte, iterationLen) + iv := make([]byte, ivLen) + salt := make([]byte, saltLen) + + maxMinIterationsSubtracted, err := rand.Int(rand.Reader, big.NewInt(int64(maxIterations-minIterations))) + if err != nil { + return nil, fmt.Errorf("rand.Int: %w", err) + } + + iterations := int(maxMinIterationsSubtracted.Int64() + minIterations) + binary.BigEndian.PutUint32(iter, uint32(iterations)) + + _, err = rand.Read(iv) + if err != nil { + return nil, fmt.Errorf("rand.Read: %w", err) + } + + _, err = rand.Read(salt) + if err != nil { + return nil, fmt.Errorf("rand.Read: %w", err) + } + + secretKey := pbkdf2.Key(password, salt, iterations, keyLen, sha1.New) + + block, err := aes.NewCipher(secretKey) + if err != nil { + return nil, fmt.Errorf("aes.NewCipher: %w", err) + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("cipher.NewGCM: %w", err) + } + + cipherText := aesGCM.Seal(nil, iv, plaintext, nil) + + finalCipher := make([]byte, 0, len(iter)+len(salt)+len(iv)+len(cipherText)) + finalCipher = append(finalCipher, iter...) + finalCipher = append(finalCipher, salt...) + finalCipher = append(finalCipher, iv...) + finalCipher = append(finalCipher, cipherText...) + + return finalCipher, nil + +} + +func Decrypt(password, text []byte) ([]byte, error) { + iterations := text[:iterationLen] + salt := text[iterationLen : iterationLen+saltLen] + iv := text[iterationLen+saltLen : iterationLen+saltLen+ivLen] + cipherText := text[iterationLen+saltLen+ivLen:] + iter := int(binary.BigEndian.Uint32(iterations)) + secretKey := pbkdf2.Key(password, salt, iter, keyLen, sha1.New) + + block, err := aes.NewCipher(secretKey) + if err != nil { + return nil, fmt.Errorf("aes.NewCipher: %w", err) + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("cipher.NewGCM: %w", err) + } + + plaintext, err := aesGCM.Open(nil, iv, cipherText, nil) + if err != nil { + return nil, fmt.Errorf("aesGCM.Open: %w", err) + } + + return plaintext, nil +} diff --git a/internal/andotp/andotp_test.go b/internal/andotp/andotp_test.go new file mode 100644 index 0000000..efb9866 --- /dev/null +++ b/internal/andotp/andotp_test.go @@ -0,0 +1,57 @@ +package andotp_test + +import ( + "encoding/json" + "testing" + "time" + + "github.com/Kichiyaki/gootp/internal/andotp" + "github.com/stretchr/testify/assert" +) + +type entry struct { + Algorithm string `json:"algorithm"` + Digits uint8 `json:"digits"` + Issuer string `json:"issuer"` + Label string `json:"label"` + LastUsed uint64 `json:"last_used"` + Period uint32 `json:"period"` + Secret string `json:"secret"` + Tags []string `json:"tags"` + Thumbnail string `json:"thumbnail"` + Type string `json:"type"` + UsedFrequency uint64 `json:"usedFrequency"` +} + +func TestEncryptDecrypt(t *testing.T) { + t.Parallel() + + entries := []entry{ + { + Algorithm: "SHA1", + Digits: 6, + Issuer: "TestIssuer", + Label: "TestLabel", + LastUsed: uint64(time.Now().Unix()), + Period: 30, + Secret: "secret", + Thumbnail: "Default", + Type: "TOTP", + UsedFrequency: 0, + }, + } + entriesJSON, err := json.Marshal(entries) + assert.Nil(t, err) + password := []byte("password22231") + + encrypted, err := andotp.Encrypt(password, entriesJSON) + assert.Nil(t, err) + + decrypted, err := andotp.Decrypt(password, encrypted) + assert.Nil(t, err) + + var result []entry + err = json.Unmarshal(decrypted, &result) + assert.Nil(t, err) + assert.Equal(t, entries, result) +}