feat: add the possibility to generate otps
This commit is contained in:
parent
b9f1696b14
commit
08f3d4665a
|
@ -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
|
||||
}
|
|
@ -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
2
go.mod
|
@ -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
5
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
Reference in New Issue