Skip to content

Commit 28337f3

Browse files
authored
feat: add sha256 and sha512 support (#135)
* feat: add sha256 and sha512 support As the original implementation followed only RFC-4226, it had only SHA1 support. This covers a lot of tokens, but secure tokens are not SHA1, but SHA2 based as described in RFC-6238. Until now, I though only hardware tokens use SHA2 (SHA256, SHA512) and maybe some edge cases, but not common. That was probably true almost 10 years ago when I originally created this tool. Today, even tho I didn't see 256 or 512 tokens yet, I think it's more common, and soon they will be used in a lot of places. Resolves #133 References: - #133 - https://www.ietf.org/rfc/rfc4226.txt - https://www.ietf.org/rfc/rfc6238.txt Signed-off-by: Victoria Nadasdi <[email protected]>
1 parent 5db5147 commit 28337f3

File tree

18 files changed

+257
-75
lines changed

18 files changed

+257
-75
lines changed

internal/cmd/add_token.go

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,9 @@ func AddTokenCommand() *cli.Command {
1717
Usage: "Add new token.",
1818
ArgsUsage: "[namespace] [account]",
1919
Flags: []cli.Flag{
20-
&cli.UintFlag{
21-
Name: "length",
22-
Value: s.DefaultTokenLength,
23-
Usage: "Length of the generated token.",
24-
},
25-
&cli.StringFlag{
26-
Name: "prefix",
27-
Value: "",
28-
Usage: "Prefix for the token.",
29-
},
20+
flagLength(),
21+
flagPrefix(),
22+
flagAlgorithm(),
3023
},
3124
Action: func(ctx *cli.Context) error {
3225
var (
@@ -54,7 +47,13 @@ func AddTokenCommand() *cli.Command {
5447
}
5548
}
5649

57-
account = &s.Account{Name: accName, Token: token, Prefix: ctx.String("prefix"), Length: ctx.Uint("length")}
50+
account = &s.Account{
51+
Name: accName,
52+
Token: token,
53+
Prefix: ctx.String("prefix"),
54+
Length: ctx.Uint("length"),
55+
Algorithm: ctx.String("algorithm"),
56+
}
5857
namespace.Accounts = append(namespace.Accounts, account)
5958

6059
err = storage.Save()

internal/cmd/dump.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,8 @@ func DumpCommand() *cli.Command {
1818
Usage: "Dump all available accounts under all namespaces.",
1919
ArgsUsage: " ",
2020
Flags: []cli.Flag{
21-
&cli.BoolFlag{
22-
Name: "yes-please",
23-
Value: false,
24-
Usage: warningMsg,
25-
},
26-
&cli.StringFlag{
27-
Name: "output",
28-
Usage: "Output file. (REQUIRED)",
29-
Required: true,
30-
},
21+
flagYesPlease(warningMsg),
22+
flagOutput(),
3123
},
3224
Action: func(ctx *cli.Context) error {
3325
if !ctx.Bool("yes-please") {

internal/cmd/error.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,16 @@ func (e CommandError) Error() string {
2121
func resourceNotFoundError(name string) CommandError {
2222
return CommandError{Message: name + " does not exist"}
2323
}
24+
25+
// FlagError is an error during flag parsing.
26+
type FlagError struct {
27+
Message string
28+
}
29+
30+
func (e FlagError) Error() string {
31+
return "flag error: %s" + e.Message
32+
}
33+
34+
func invalidAlgorithmError(value string) FlagError {
35+
return FlagError{Message: "Invalid algorithm: " + value}
36+
}

internal/cmd/flags.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package cmd
2+
3+
import (
4+
"github.com/urfave/cli/v2"
5+
"github.com/yitsushi/totp-cli/internal/storage"
6+
)
7+
8+
func flagAlgorithm() *cli.StringFlag {
9+
return &cli.StringFlag{
10+
Name: "algorithm",
11+
Value: "sha1",
12+
Usage: "Algorithm to use for HMAC (sha1, sha256, sha512).",
13+
Action: func(_ *cli.Context, value string) error {
14+
if value != "sha1" && value != "sha256" && value != "sha512" {
15+
return invalidAlgorithmError(value)
16+
}
17+
18+
return nil
19+
},
20+
}
21+
}
22+
23+
func flagShowRemaining() *cli.BoolFlag {
24+
return &cli.BoolFlag{
25+
Name: "show-remaining",
26+
Value: false,
27+
Usage: "Show how much time left until the code will be invalid.",
28+
}
29+
}
30+
31+
func flagLength() *cli.UintFlag {
32+
return &cli.UintFlag{
33+
Name: "length",
34+
Value: storage.DefaultTokenLength,
35+
Usage: "Length of the generated token.",
36+
}
37+
}
38+
39+
func flagPrefix() *cli.StringFlag {
40+
return &cli.StringFlag{
41+
Name: "prefix",
42+
Value: "",
43+
Usage: "Prefix for the token.",
44+
}
45+
}
46+
47+
func flagYesPlease(usage string) *cli.BoolFlag {
48+
return &cli.BoolFlag{
49+
Name: "yes-please",
50+
Value: false,
51+
Usage: usage,
52+
}
53+
}
54+
55+
func flagOutput() *cli.StringFlag {
56+
return &cli.StringFlag{
57+
Name: "output",
58+
Usage: "Output file. (REQUIRED)",
59+
Required: true,
60+
}
61+
}
62+
63+
func flagInput() *cli.StringFlag {
64+
return &cli.StringFlag{
65+
Name: "input",
66+
Usage: "Input YAML file. (REQUIRED)",
67+
Required: true,
68+
}
69+
}
70+
71+
func flagFollow() *cli.BoolFlag {
72+
return &cli.BoolFlag{
73+
Name: "follow",
74+
Value: false,
75+
Usage: "Generate codes continuously.",
76+
}
77+
}
78+
79+
func flagClearPrefix() *cli.BoolFlag {
80+
return &cli.BoolFlag{
81+
Name: "clear",
82+
Value: false,
83+
Usage: "Clear prefix from account.",
84+
}
85+
}

internal/cmd/generate.go

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,8 @@ func GenerateCommand() *cli.Command {
1414
Name: "generate",
1515
Aliases: []string{"g"},
1616
Flags: []cli.Flag{
17-
&cli.BoolFlag{
18-
Name: "follow",
19-
Value: false,
20-
Usage: "Generate codes continuously.",
21-
},
22-
&cli.BoolFlag{
23-
Name: "show-remaining",
24-
Value: false,
25-
Usage: "Show how much time left until the code will be invalid.",
26-
},
17+
flagFollow(),
18+
flagShowRemaining(),
2719
},
2820
Usage: "Generate a specific OTP",
2921
ArgsUsage: "<namespace> <account>",

internal/cmd/generatehelper.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"time"
66

77
"github.com/yitsushi/totp-cli/internal/security"
8+
"github.com/yitsushi/totp-cli/internal/security/algo"
89
s "github.com/yitsushi/totp-cli/internal/storage"
910
)
1011

@@ -17,7 +18,20 @@ func formatCode(code string, remaining int64, showRemaining bool) string {
1718
}
1819

1920
func generateCode(account *s.Account) (string, int64) {
20-
code, remaining, err := security.GenerateOTPCode(account.Token, time.Now(), account.Length)
21+
var algorithm algo.Algorithm
22+
23+
switch account.Algorithm {
24+
case "sha1":
25+
algorithm = algo.SHA1{}
26+
case "sha256":
27+
algorithm = algo.SHA256{}
28+
case "sha512":
29+
algorithm = algo.SHA512{}
30+
default:
31+
algorithm = algo.Default{}
32+
}
33+
34+
code, remaining, err := security.GenerateOTPCode(account.Token, time.Now(), account.Length, algorithm)
2135
if err != nil {
2236
fmt.Printf("Error: %s\n", err.Error())
2337

internal/cmd/import.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ func ImportCommand() *cli.Command {
1616
Name: "import",
1717
Usage: "Import tokens from a yaml file.",
1818
Flags: []cli.Flag{
19-
&cli.StringFlag{
20-
Name: "input",
21-
Usage: "Input YAML file. (REQUIRED)",
22-
Required: true,
23-
},
19+
flagInput(),
2420
},
2521
Action: func(ctx *cli.Context) (err error) {
2622
var file []byte

internal/cmd/instant.go

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ package cmd
33
import (
44
"fmt"
55
"os"
6-
"time"
76

87
"github.com/urfave/cli/v2"
9-
"github.com/yitsushi/totp-cli/internal/security"
108
"github.com/yitsushi/totp-cli/internal/storage"
119
"github.com/yitsushi/totp-cli/internal/terminal"
1210
)
@@ -18,31 +16,25 @@ func InstantCommand() *cli.Command {
1816
Usage: "Generate an OTP from TOTP_TOKEN or stdin without the Storage backend.",
1917
ArgsUsage: " ",
2018
Flags: []cli.Flag{
21-
&cli.UintFlag{
22-
Name: "length",
23-
Value: storage.DefaultTokenLength,
24-
Usage: "Length of the generated token.",
25-
},
26-
&cli.BoolFlag{
27-
Name: "show-remaining",
28-
Value: false,
29-
Usage: "Show how much time left until the code will be invalid.",
30-
},
19+
flagLength(),
20+
flagShowRemaining(),
21+
flagAlgorithm(),
3122
},
3223
Action: func(ctx *cli.Context) error {
33-
token := os.Getenv("TOTP_TOKEN")
34-
if token == "" {
35-
term := terminal.New(os.Stdin, os.Stdout, os.Stderr)
36-
token, _ = term.Read("")
24+
account := storage.Account{
25+
Name: "instant",
26+
Token: os.Getenv("TOTP_TOKEN"),
27+
Length: ctx.Uint("length"),
28+
Algorithm: ctx.String("algorithm"),
3729
}
3830

39-
length := ctx.Uint("length")
40-
41-
code, remaining, err := security.GenerateOTPCode(token, time.Now(), length)
42-
if err != nil {
43-
return err
31+
if account.Token == "" {
32+
term := terminal.New(os.Stdin, os.Stdout, os.Stderr)
33+
account.Token, _ = term.Read("")
4434
}
4535

36+
code, remaining := generateCode(&account)
37+
4638
fmt.Println(formatCode(code, remaining, ctx.Bool("show-remaining")))
4739

4840
return nil

internal/cmd/set_prefix.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,7 @@ func SetPrefixCommand() *cli.Command {
1616
Usage: "Set prefix for a token.",
1717
ArgsUsage: "[namespace] [account] [prefix]",
1818
Flags: []cli.Flag{
19-
&cli.BoolFlag{
20-
Name: "clear",
21-
Value: false,
22-
Usage: "Clear prefix from account.",
23-
},
19+
flagClearPrefix(),
2420
},
2521
Action: func(ctx *cli.Context) (err error) {
2622
var (

internal/security/algo/default.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package algo
2+
3+
// Default is the default algorithm to use.
4+
type Default = SHA1

0 commit comments

Comments
 (0)