feat: add the possibility to generate otps

This commit is contained in:
Dawid Wysokiński 2022-05-17 07:10:36 +02:00
parent b9f1696b14
commit 08f3d4665a
Signed by: Kichiyaki
GPG Key ID: 1ECC5DE481BE5184
9 changed files with 264 additions and 149 deletions

View File

@ -1,67 +0,0 @@
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
}

View File

@ -1,24 +0,0 @@
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)
}
}

2
go.mod
View File

@ -3,6 +3,7 @@ module github.com/Kichiyaki/gootp
go 1.18
require (
github.com/pquerna/otp v1.3.0
github.com/stretchr/testify v1.7.1
github.com/urfave/cli/v2 v2.6.0
golang.org/x/crypto v0.0.0-20220511200225-c6db032c6c88
@ -10,6 +11,7 @@ require (
)
require (
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
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

5
go.sum
View File

@ -1,12 +1,17 @@
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
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/pquerna/otp v1.3.0 h1:oJV/SkzR33anKXwQU3Of42rL4wbrffP4uvUf1SvS5Xs=
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
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.6.0 h1:yj2Drkflh8X/zUrkWlWlUjZYHyWN7WMmpVxyxXIUyv8=

View File

@ -1,4 +1,4 @@
package andotp
package internal
import (
"crypto/aes"
@ -6,6 +6,7 @@ import (
"crypto/rand"
"crypto/sha1"
"encoding/binary"
"encoding/json"
"fmt"
"math/big"
@ -98,3 +99,31 @@ func Decrypt(password, text []byte) ([]byte, error) {
return plaintext, nil
}
type Entry struct {
Algorithm string `json:"algorithm"`
Digits uint `json:"digits"`
Issuer string `json:"issuer"`
Label string `json:"label"`
LastUsed uint `json:"last_used"`
Period uint `json:"period"`
Secret string `json:"secret"`
Tags []string `json:"tags"`
Thumbnail string `json:"thumbnail"`
Type string `json:"type"`
UsedFrequency uint `json:"usedFrequency"`
}
func DecryptAsEntries(password, text []byte) ([]Entry, error) {
result, err := Decrypt(password, text)
if err != nil {
return nil, fmt.Errorf("decrypt: %w", err)
}
var entries []Entry
if err := json.Unmarshal(result, &entries); err != nil {
return nil, fmt.Errorf("json.Unmarshal: %w", err)
}
return entries, nil
}

View File

@ -1,57 +0,0 @@
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)
}

46
internal/andotp_test.go Normal file
View File

@ -0,0 +1,46 @@
package internal_test
import (
"encoding/json"
"github.com/Kichiyaki/gootp/internal"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestEncryptDecrypt(t *testing.T) {
t.Parallel()
entries := []internal.Entry{
{
Algorithm: "SHA1",
Digits: 6,
Issuer: "TestIssuer",
Label: "TestLabel",
LastUsed: uint(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 := internal.Encrypt(password, entriesJSON)
assert.Nil(t, err)
decrypted, err := internal.Decrypt(password, encrypted)
assert.Nil(t, err)
var result []internal.Entry
err = json.Unmarshal(decrypted, &result)
assert.Nil(t, err)
assert.Equal(t, entries, result)
decryptedEntries, err := internal.DecryptAsEntries(password, encrypted)
assert.Nil(t, err)
assert.Equal(t, entries, decryptedEntries)
}

62
internal/otp.go Normal file
View File

@ -0,0 +1,62 @@
package internal
import (
"fmt"
"github.com/pquerna/otp"
"github.com/pquerna/otp/totp"
"strings"
"time"
)
func GenerateOTP(entry Entry) (string, error) {
algorithm, err := parseAlgorithm(entry.Algorithm)
if err != nil {
return "", fmt.Errorf("parseAlgorithm: %w", err)
}
digits, err := parseDigits(entry.Digits)
if err != nil {
return "", fmt.Errorf("parseDigits: %w", err)
}
switch strings.ToUpper(entry.Type) {
case "TOTP":
code, err := totp.GenerateCodeCustom(entry.Secret, time.Now(), totp.ValidateOpts{
Algorithm: algorithm,
Period: entry.Period,
Digits: digits,
})
if err != nil {
return "", fmt.Errorf("something went wrong while generating totp: %w", err)
}
return code, nil
default:
return "", fmt.Errorf("unsupported entry type: %s", entry.Type)
}
}
func parseAlgorithm(algorithm string) (otp.Algorithm, error) {
switch strings.ToUpper(algorithm) {
case "SHA1":
return otp.AlgorithmSHA1, nil
case "SHA256":
return otp.AlgorithmSHA256, nil
case "SHA512":
return otp.AlgorithmSHA512, nil
case "MD5":
return otp.AlgorithmMD5, nil
default:
return 0, fmt.Errorf("unsupported algorithm: %s", algorithm)
}
}
func parseDigits(digits uint) (otp.Digits, error) {
switch digits {
case 6:
return otp.DigitsSix, nil
case 8:
return otp.DigitsEight, nil
default:
return 0, fmt.Errorf("unsupported digits: %d", digits)
}
}

119
main.go Normal file
View File

@ -0,0 +1,119 @@
package main
import (
"fmt"
"github.com/Kichiyaki/gootp/internal"
"github.com/urfave/cli/v2"
"golang.org/x/term"
"log"
"os"
"syscall"
)
var Version = "development"
func main() {
app := newApp()
if err := app.Run(os.Args); err != nil {
log.Fatalln("app.Run:", err)
}
}
func newApp() *cli.App {
return &cli.App{
Name: "gootp",
Version: Version,
Action: func(c *cli.Context) error {
var err error
password := []byte(c.String("password"))
if len(password) == 0 {
password, err = readPasswordFromStdin()
}
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)
}
entries, err := internal.DecryptAsEntries(password, b)
if err != nil {
return fmt.Errorf("something went wrong while decrypting file: %w", err)
}
for _, entry := range entries {
otp, err := internal.GenerateOTP(entry)
if err != nil {
log.Printf("%s - %s: %s", entry.Issuer, entry.Label, err)
continue
}
log.Printf("%s - %s: %s", entry.Issuer, entry.Label, otp)
}
return nil
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "path",
Usage: "path to encrypted andotp file",
Required: true,
},
&cli.StringFlag{
Name: "password",
Usage: "encryption password",
Required: false,
},
},
EnableBashCompletion: true,
Commands: []*cli.Command{
newDecryptCommand(),
},
}
}
func newDecryptCommand() *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 = readPasswordFromStdin()
}
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 := internal.Decrypt(password, b)
if err != nil {
return fmt.Errorf("something went wrong while decrypting file: %w", err)
}
fmt.Print(string(result))
return nil
},
}
}
func readPasswordFromStdin() ([]byte, error) {
fmt.Print("Password: ")
pass, err := term.ReadPassword(syscall.Stdin)
if err != nil {
return nil, fmt.Errorf("term.ReadPassword: %w", err)
}
fmt.Print("\n")
return pass, nil
}