aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli.go13
-rw-r--r--cli_test.go293
-rw-r--r--config.go40
-rw-r--r--go.mod10
-rw-r--r--go.sum6
-rw-r--r--gotp.go11
-rw-r--r--gotp_test.go34
-rw-r--r--issuer.go3
-rw-r--r--testdata/cli_with_passphrase_test.txt92
9 files changed, 453 insertions, 49 deletions
diff --git a/cli.go b/cli.go
index c07dcad..e3b6d77 100644
--- a/cli.go
+++ b/cli.go
@@ -110,7 +110,7 @@ func (cli *Cli) Generate(label string, n int) (listOtp []string, err error) {
proto = totp.New(cryptoHash, issuer.Digits, issuer.TimeStep)
- listOtp, err = proto.GenerateN(secret, n)
+ listOtp, err = proto.GenerateNWithTime(timeNow(), secret, n)
if err != nil {
return nil, fmt.Errorf(`%s: %w`, logp, err)
}
@@ -193,14 +193,13 @@ func (cli *Cli) List() (labels []string) {
// Remove a TOTP configuration by its label.
func (cli *Cli) Remove(label string) (err error) {
- var (
- logp = `Remove`
-
- ok bool
- )
+ var logp = `Remove`
+ label = strings.TrimSpace(label)
label = strings.ToLower(label)
+ var ok bool
+
_, ok = cli.cfg.Issuers[label]
if !ok {
return fmt.Errorf(`%s: %q not exist`, logp, label)
@@ -286,12 +285,14 @@ func (cli *Cli) Rename(label, newLabel string) (err error) {
ok bool
)
+ label = strings.TrimSpace(label)
label = strings.ToLower(label)
rawValue, ok = cli.cfg.Issuers[label]
if !ok {
return fmt.Errorf(`%s: %q not exist`, logp, label)
}
+ newLabel = strings.TrimSpace(newLabel)
newLabel = strings.ToLower(newLabel)
_, ok = cli.cfg.Issuers[newLabel]
if ok {
diff --git a/cli_test.go b/cli_test.go
index 2ae9f8e..9b24196 100644
--- a/cli_test.go
+++ b/cli_test.go
@@ -5,7 +5,9 @@ package gotp
import (
"bytes"
+ "errors"
"fmt"
+ "io/fs"
"os"
"path/filepath"
"testing"
@@ -228,3 +230,294 @@ func TestCli_ViewEncrypted(t *testing.T) {
issA.raw = nil
test.Assert(t, `Get: testA`, issA, gotIssA)
}
+
+func TestCli_withPassphrase(t *testing.T) {
+ var (
+ tdata *test.Data
+ err error
+ )
+
+ tdata, err = test.LoadData(`testdata/cli_with_passphrase_test.txt`)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Prepare directory with private key.
+
+ var (
+ dirConfig = t.TempDir()
+ filePrivateKey = filepath.Join(dirConfig, privateKeyFile)
+ )
+
+ err = os.WriteFile(filePrivateKey, tdata.Input[`gotp.key`], 0600)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var cli *Cli
+
+ cli, err = NewCli(dirConfig)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ t.Run(`Add`, func(t *testing.T) {
+ testAddWithPassphrase(t, tdata, cli)
+ })
+
+ t.Run(`Generate`, func(t *testing.T) {
+ testGenerateWithPassphrase(t, tdata, cli)
+ })
+
+ t.Run(`Get`, func(t *testing.T) {
+ testGetWithPassphrase(t, tdata, cli)
+ })
+
+ t.Run(`List`, func(t *testing.T) {
+ var (
+ expListLabel = []string{`test-sha1`, `test-sha256`, `test-sha512`}
+ gotListLabel = cli.List()
+ )
+ test.Assert(t, `List`, expListLabel, gotListLabel)
+ })
+
+ t.Run(`Remove`, func(t *testing.T) {
+ testRemoveWithPassphrase(t, tdata, cli)
+ })
+
+ t.Run(`Rename`, func(t *testing.T) {
+ testRenameWithPassphrase(t, tdata, cli)
+ })
+
+ t.Run(`RemovePrivateKey`, func(t *testing.T) {
+ testRemovePrivateKeyWithPassphrase(t, tdata, cli)
+ })
+
+ t.Run(`SetPrivateKey`, func(t *testing.T) {
+ testSetPrivateKeyWithPassphrase(t, tdata, cli)
+ })
+}
+
+func testAddWithPassphrase(t *testing.T, tdata *test.Data, cli *Cli) {
+ var (
+ pass = string(tdata.Input[`gotp.pass`]) + "\r\n"
+ lines = bytes.Split(tdata.Input[`list_raw_issuer`], []byte{'\n'})
+
+ line []byte
+ labelIssuer [][]byte
+ issuer *Issuer
+ err error
+ )
+
+ for _, line = range lines {
+ labelIssuer = bytes.Split(line, []byte{'='})
+
+ issuer, err = NewIssuer(string(labelIssuer[0]), string(labelIssuer[1]), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ mockTermrw.BufRead.WriteString(pass)
+
+ err = cli.Add(issuer)
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ assertGotpConf(t, cli, string(tdata.Output[`gotp.conf:encrypted`]))
+
+ mockTermrw.BufRead.Reset()
+}
+
+func testGenerateWithPassphrase(t *testing.T, tdata *test.Data, cli *Cli) {
+ type testCase struct {
+ label string
+ pass string
+ expListOTP []string
+ n int
+ }
+
+ var validPass = string(tdata.Input[`gotp.pass`]) + "\r\n"
+
+ var listCase = []testCase{{
+ label: `test-sha1`,
+ n: 3,
+ pass: validPass,
+ expListOTP: []string{`002561`, `439480`, `508390`},
+ }, {
+ label: `test-sha256`,
+ n: 3,
+ pass: validPass,
+ expListOTP: []string{`182691`, `322218`, `699844`},
+ }, {
+ label: `test-sha512`,
+ n: 3,
+ pass: validPass,
+ expListOTP: []string{`595992`, `757602`, `224726`},
+ }}
+
+ var (
+ c testCase
+ gotListOTP []string
+ err error
+ )
+
+ for _, c = range listCase {
+ mockTermrw.BufRead.WriteString(c.pass)
+
+ gotListOTP, err = cli.Generate(c.label, c.n)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ test.Assert(t, c.label, c.expListOTP, gotListOTP)
+ }
+ mockTermrw.BufRead.Reset()
+}
+
+func testGetWithPassphrase(t *testing.T, tdata *test.Data, cli *Cli) {
+ type testCase struct {
+ label string
+ expIssuer string
+ }
+
+ var listCase = []testCase{{
+ label: `test-sha1`,
+ expIssuer: string(tdata.Output[`get:test-sha1`]),
+ }, {
+ label: `test-sha256`,
+ expIssuer: string(tdata.Output[`get:test-sha256`]),
+ }, {
+ label: `test-sha512`,
+ expIssuer: string(tdata.Output[`get:test-sha512`]),
+ }}
+
+ var (
+ pass = string(tdata.Input[`gotp.pass`]) + "\r\n"
+
+ c testCase
+ issuer *Issuer
+ err error
+ )
+
+ for _, c = range listCase {
+ mockTermrw.BufRead.WriteString(pass)
+
+ issuer, err = cli.Get(c.label)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ test.Assert(t, c.label, c.expIssuer, issuer.String())
+ }
+ mockTermrw.BufRead.Reset()
+}
+
+func testRemoveWithPassphrase(t *testing.T, tdata *test.Data, cli *Cli) {
+ var err = cli.Remove(`test-sha512`)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertGotpConf(t, cli, string(tdata.Output[`gotp.conf:remove:encrypted`]))
+
+ mockTermrw.BufRead.Reset()
+}
+
+// The Rename method does not require private key.
+func testRenameWithPassphrase(t *testing.T, tdata *test.Data, cli *Cli) {
+ var err = cli.Rename(`test-sha1`, `renamed-sha1`)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertGotpConf(t, cli, string(tdata.Output[`gotp.conf:rename:encrypted`]))
+
+ mockTermrw.BufRead.Reset()
+}
+
+func testRemovePrivateKeyWithPassphrase(t *testing.T, tdata *test.Data, cli *Cli) {
+ var pass = string(tdata.Input[`gotp.pass`]) + "\r\n"
+
+ mockTermrw.BufRead.WriteString(pass)
+
+ var err = cli.RemovePrivateKey()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ assertGotpConf(t, cli, string(tdata.Output[`gotp.conf`]))
+
+ var fileGotpKey = filepath.Join(cli.cfg.dir, privateKeyFile)
+
+ _, err = os.Stat(fileGotpKey)
+ if !errors.Is(err, fs.ErrNotExist) {
+ t.Fatalf(`expecting gotp.key to be removed, but still exists`)
+ }
+
+ mockTermrw.BufRead.Reset()
+}
+
+func testSetPrivateKeyWithPassphrase(t *testing.T, tdata *test.Data, cli *Cli) {
+ // Write the private key file.
+ var newPrivateKeyFile = filepath.Join(cli.cfg.dir, `new.key`)
+
+ var err = os.WriteFile(newPrivateKeyFile, tdata.Input[`gotp.key`], 0600)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var pass = string(tdata.Input[`gotp.pass`]) + "\r\n"
+ mockTermrw.BufRead.WriteString(pass)
+
+ err = cli.SetPrivateKey(newPrivateKeyFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = os.Stat(cli.cfg.privateKeyFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // When SetPrivateKey success, the output is somehow not
+ // deterministic.
+ // So we need to compare them with two possible outcomes.
+
+ var gotpConf = filepath.Join(cli.cfg.dir, configFile)
+ var rawconf []byte
+
+ rawconf, err = os.ReadFile(gotpConf)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rawconf = bytes.TrimSpace(rawconf)
+
+ var gotConf = string(rawconf)
+ var exp = string(tdata.Output[`gotp.conf:set-private-key:encrypted`])
+
+ if exp != gotConf {
+ exp = string(tdata.Output[`gotp.conf:set-private-key:encrypted:alt`])
+ test.Assert(t, `assertGotpConf`, exp, string(rawconf))
+ }
+
+ mockTermrw.BufRead.Reset()
+}
+
+func assertGotpConf(t *testing.T, cli *Cli, exp string) {
+ var (
+ gotpConf = filepath.Join(cli.cfg.dir, configFile)
+
+ rawconf []byte
+ err error
+ )
+
+ rawconf, err = os.ReadFile(gotpConf)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ test.Assert(t, `assertGotpConf`, exp, string(rawconf))
+}
diff --git a/config.go b/config.go
index bf23c89..d07a8ea 100644
--- a/config.go
+++ b/config.go
@@ -4,6 +4,7 @@
package gotp
import (
+ "crypto"
"crypto/rsa"
"errors"
"fmt"
@@ -12,9 +13,8 @@ import (
"path/filepath"
"strings"
+ libcrypto "github.com/shuLhan/share/lib/crypto"
"github.com/shuLhan/share/lib/ini"
- "golang.org/x/crypto/ssh"
- "golang.org/x/term"
)
const (
@@ -142,13 +142,9 @@ func (cfg *config) get(name string) (issuer *Issuer, err error) {
// loadPrivateKey parse the RSA private key with optional passphrase.
// It will return nil if private key file does not exist.
func (cfg *config) loadPrivateKey() (err error) {
- var (
- logp = `loadPrivateKey`
-
- rawPem []byte
- )
+ var logp = `loadPrivateKey`
- rawPem, err = os.ReadFile(cfg.privateKeyFile)
+ _, err = os.Stat(cfg.privateKeyFile)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return nil
@@ -156,33 +152,11 @@ func (cfg *config) loadPrivateKey() (err error) {
return fmt.Errorf(`%s: %w`, logp, err)
}
- var privateKey interface{}
+ var privateKey crypto.PrivateKey
- privateKey, err = ssh.ParseRawPrivateKey(rawPem)
+ privateKey, err = libcrypto.LoadPrivateKeyInteractive(termrw, cfg.privateKeyFile)
if err != nil {
- var errPassMissing = &ssh.PassphraseMissingError{}
-
- if !errors.As(err, &errPassMissing) {
- return fmt.Errorf(`%s %q: %w`, logp, cfg.privateKeyFile, err)
- }
-
- fmt.Printf(`Enter passphrase for %s: `, cfg.privateKeyFile)
-
- var (
- stdin = int(os.Stdin.Fd())
- pass []byte
- )
-
- pass, err = term.ReadPassword(stdin)
- fmt.Println()
- if err != nil {
- return fmt.Errorf(`%s %q: %w`, logp, cfg.privateKeyFile, err)
- }
-
- privateKey, err = ssh.ParseRawPrivateKeyWithPassphrase(rawPem, pass)
- if err != nil {
- return fmt.Errorf(`%s %q: %w`, logp, cfg.privateKeyFile, err)
- }
+ return fmt.Errorf(`%s %q: %w`, logp, cfg.privateKeyFile, err)
}
var ok bool
diff --git a/go.mod b/go.mod
index f7d6947..ab0ee37 100644
--- a/go.mod
+++ b/go.mod
@@ -5,12 +5,12 @@ module git.sr.ht/~shulhan/gotp
go 1.20
+require github.com/shuLhan/share v0.52.1-0.20240129164925-ec2b46e7780a
+
require (
- github.com/shuLhan/share v0.52.0
- golang.org/x/crypto v0.18.0
- golang.org/x/term v0.16.0
+ golang.org/x/crypto v0.18.0 // indirect
+ golang.org/x/sys v0.16.0 // indirect
+ golang.org/x/term v0.16.0 // indirect
)
-require golang.org/x/sys v0.16.0 // indirect
-
//replace github.com/shuLhan/share => ../share
diff --git a/go.sum b/go.sum
index c56572d..99be57d 100644
--- a/go.sum
+++ b/go.sum
@@ -1,8 +1,8 @@
-github.com/shuLhan/share v0.52.0 h1:xbHez3JUYlt7WsUV8JOBLJOkd65P6dpp6zIq1EFXVvI=
-github.com/shuLhan/share v0.52.0/go.mod h1:aNDs/SjnVYXaLEEJzjmfrUFeLD6u0YHWy6pI8o8iqYw=
+github.com/shuLhan/share v0.52.1-0.20240129164925-ec2b46e7780a h1:eZTrZmz8KS7CNZbQCQQZ5YJDEv6AyqZSHytbLCZyqG0=
+github.com/shuLhan/share v0.52.1-0.20240129164925-ec2b46e7780a/go.mod h1:97/BcWdLau8i+xeFvPHdyqph1HgxVBSVhQEUIyCmgRc=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
-golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
diff --git a/gotp.go b/gotp.go
index 52000a8..e268872 100644
--- a/gotp.go
+++ b/gotp.go
@@ -5,7 +5,9 @@
package gotp
import (
+ "io"
"strings"
+ "time"
"unicode"
)
@@ -30,6 +32,15 @@ const (
// Version define the latest version of this module and gotp CLI.
var Version = `0.4.0`
+// termrw define terminal for reading passphrase.
+// It is defined to mock parameter termrw in
+// [libcrypto.LoadPrivateKeyInteractive].
+var termrw io.ReadWriter
+
+// timeNow return the current time in UTC.
+// It is defined to mock current time for testing Generate.
+var timeNow = func() time.Time { return time.Now().UTC() }
+
// normalizeLabel convert non alpha number, hyphen, underscore, or period
// characters into `-`.
func normalizeLabel(in string) (out string) {
diff --git a/gotp_test.go b/gotp_test.go
new file mode 100644
index 0000000..7743867
--- /dev/null
+++ b/gotp_test.go
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: 2024 Shulhan <ms@kilabit.info>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package gotp
+
+import (
+ "crypto/rand"
+ "os"
+ "testing"
+ "time"
+
+ "github.com/shuLhan/share/lib/test/mock"
+)
+
+// Mock the termrw for reading passphrase.
+var mockTermrw = &mock.ReadWriter{}
+
+// mockRandReader mock the reader for crypto [rand.Reader] which is used to
+// provides predictable result in [libcrypto.EncryptOaep].
+var mockRandReader = mock.NewRandReader([]byte(`gotptest`))
+
+func TestMain(m *testing.M) {
+ termrw = mockTermrw
+
+ // Overwrite the random reader to provide predictable result.
+ rand.Reader = mockRandReader
+
+ // Overwrite current time for predictable OTP.
+ timeNow = func() time.Time {
+ return time.Date(2024, time.January, 30, 00, 03, 00, 00, time.UTC)
+ }
+
+ os.Exit(m.Run())
+}
diff --git a/issuer.go b/issuer.go
index ed18919..1c69f07 100644
--- a/issuer.go
+++ b/issuer.go
@@ -109,7 +109,6 @@ func (issuer *Issuer) pack(privateKey *rsa.PrivateKey) (value string, err error)
var (
logp = `pack`
plainText = issuer.String()
- rng = rand.Reader
)
issuer.raw = []byte(plainText)
@@ -117,7 +116,7 @@ func (issuer *Issuer) pack(privateKey *rsa.PrivateKey) (value string, err error)
return string(issuer.raw), nil
}
- issuer.raw, err = libcrypto.EncryptOaep(sha256.New(), rng, &privateKey.PublicKey, issuer.raw, nil)
+ issuer.raw, err = libcrypto.EncryptOaep(sha256.New(), rand.Reader, &privateKey.PublicKey, issuer.raw, nil)
if err != nil {
return ``, fmt.Errorf(`%s: %w`, logp, err)
}
diff --git a/testdata/cli_with_passphrase_test.txt b/testdata/cli_with_passphrase_test.txt
new file mode 100644
index 0000000..6eae769
--- /dev/null
+++ b/testdata/cli_with_passphrase_test.txt
@@ -0,0 +1,92 @@
+# vim: set tw=0:
+
+Test CLI with private key protected with passphrase 'gotptest'.
+
+>>> gotp.pass
+gotptest
+
+>>> gotp.key
+-----BEGIN OPENSSH PRIVATE KEY-----
+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
+3CcPeTEmGrC25pqwXei+8qR10ZM=
+-----END OPENSSH PRIVATE KEY-----
+
+>>> list_raw_issuer
+test-sha1 = SHA1:GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
+test-sha256 = SHA256:GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
+test-sha512 = SHA512:GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ
+
+<<< gotp.conf:encrypted
+[gotp "issuer"]
+test-sha1 = GKhXh+9sSkfRjpVfu8o1dIAPTyahjHpGZxfSTqFJIUnAJj3HpQGa0UO3QNQ4qN66CYRaYchzS491REMdC+9mC1BoEFN0BQWVshF4Cy8fz/6geM8oPqUIQ0+pGKdnW9LWZGUB5NP72FGtBeDdQ5QHtMIKtexSHpO855mosY6wjik6l2oaJ+wbK2X4JNb4PyJIQksQjYWepMvx0m6b9NjkTJA+srUYmNu47VXf5JWtjXZY6uYd4ol2rkQJUNLcYY+B+VhqXJVvr/NaR15WCUjW+Q8JTGK0m3AsUYRnE5MJSwvjUSZUPOc842zlXtGaOUbKAQk3x2T69chK8+EaWGUS/hoVU7UT4ivIXJZr1kEcqNlLzLBMdOEI2S4mQJat98BDSucGcCbo3hghmny6KLaN5IUdUZ06Ra2JUdoJFAOmiaI/+bK6f6wr1J3prqbdQcsoLPsy6SVKIQv4rYoRIUHVPA5SLlYNcnNSpsnc4IjgaQb/sD07TM+9LYvy1KnCG8IF
+test-sha256 = e6BCkqDD0Su15P1PPyZz6PXiYhLQ+imlhpy99+r5xM+hbL6MfZ71JaXiXw5+1JJ5oM+yqV0ejzg6pW2U9yYu2M2QfDv7BxY8rV5+8fchKgWoZ7t3cfX9VX2OXKbjnZLMrO5vYFvk8jUfeemFzy9UvoCUfYdPq3V9/2IUStBgyWNRwOmHq6ImVc5/4YoMCqXvQ/rxUl/NCujF3qQvQPLCL2Abm/lQRdWiQDzEB8+tn40iax1XoGK4dYTeuJJX7tYwv2cvQctbjYJcb+9cA+AroHW0TuyBWt37iII1rCvIA9pBb45U17Aj74Xj1vH9/WamLWLAX9bfLZwgzl2/Qa7c86jgw5jlfPcVUvxtFOJSIS/2cudxD9j8EOg4cAzySry8WP+ZPxnVqI+I4ZqSVFOtV5uSXuTiVPXCv1gtVl44ChwJw38LVBztADffM6Iqp1WjeSbASFwzDvEZmR/7qqeSgPeem/k9oPKfAJwi251oXdj2gJxG1R+JaxVjNVs1qf75
+test-sha512 = XDHGL4xQp3VCE4oIxKlBvKs9xwStDPaAe52RTXL3uiXU+RKH+w2Pgh9hl6O1mjyL4oSRJ872po9jLKxAk2OkOMINRbb601GaiuLY4Xhb/lOMQek0jCK0NDweMtodt5EAoMhSJ0styclEucHQWFLIFGwotfYTTjJYjombRFfG5CqsMB6XFQBvL5uvXpe2axJ+vyP3t0RuW/Rroovyn2lckZhyJGsHObyScC1sgdVoZhAeFDiihD6Cn1oLeiHrN9RviA8vPBZ1PV5+To2TLafJu+3InheeyIQWtBLVY8+dfVurYzpAHPi3rXc86FXIYaH9bI7muWWRIpN7lIb3RAZcYoXFdiXlvq07cd90FhsuCh9UuUzSEs1RtlsF2NhNobpP2xjMhdO+4LAaTqfew6snUUN/+G6lUDOBUeNp9HPzzMAlZG/eZ7y0u1dsd9vYwEr24ivDO+i66R4d9vmbCIohzHqc5HbSqZxi5k4H4mVnJTadC2fMrsp/nQaFtKjjX/VM
+
+<<< get:test-sha1
+SHA1:GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ:6:30:
+
+<<< get:test-sha256
+SHA256:GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ:6:30:
+
+<<< get:test-sha512
+SHA512:GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ:6:30:
+
+<<< gotp.conf:remove:encrypted
+[gotp "issuer"]
+test-sha1 = GKhXh+9sSkfRjpVfu8o1dIAPTyahjHpGZxfSTqFJIUnAJj3HpQGa0UO3QNQ4qN66CYRaYchzS491REMdC+9mC1BoEFN0BQWVshF4Cy8fz/6geM8oPqUIQ0+pGKdnW9LWZGUB5NP72FGtBeDdQ5QHtMIKtexSHpO855mosY6wjik6l2oaJ+wbK2X4JNb4PyJIQksQjYWepMvx0m6b9NjkTJA+srUYmNu47VXf5JWtjXZY6uYd4ol2rkQJUNLcYY+B+VhqXJVvr/NaR15WCUjW+Q8JTGK0m3AsUYRnE5MJSwvjUSZUPOc842zlXtGaOUbKAQk3x2T69chK8+EaWGUS/hoVU7UT4ivIXJZr1kEcqNlLzLBMdOEI2S4mQJat98BDSucGcCbo3hghmny6KLaN5IUdUZ06Ra2JUdoJFAOmiaI/+bK6f6wr1J3prqbdQcsoLPsy6SVKIQv4rYoRIUHVPA5SLlYNcnNSpsnc4IjgaQb/sD07TM+9LYvy1KnCG8IF
+test-sha256 = e6BCkqDD0Su15P1PPyZz6PXiYhLQ+imlhpy99+r5xM+hbL6MfZ71JaXiXw5+1JJ5oM+yqV0ejzg6pW2U9yYu2M2QfDv7BxY8rV5+8fchKgWoZ7t3cfX9VX2OXKbjnZLMrO5vYFvk8jUfeemFzy9UvoCUfYdPq3V9/2IUStBgyWNRwOmHq6ImVc5/4YoMCqXvQ/rxUl/NCujF3qQvQPLCL2Abm/lQRdWiQDzEB8+tn40iax1XoGK4dYTeuJJX7tYwv2cvQctbjYJcb+9cA+AroHW0TuyBWt37iII1rCvIA9pBb45U17Aj74Xj1vH9/WamLWLAX9bfLZwgzl2/Qa7c86jgw5jlfPcVUvxtFOJSIS/2cudxD9j8EOg4cAzySry8WP+ZPxnVqI+I4ZqSVFOtV5uSXuTiVPXCv1gtVl44ChwJw38LVBztADffM6Iqp1WjeSbASFwzDvEZmR/7qqeSgPeem/k9oPKfAJwi251oXdj2gJxG1R+JaxVjNVs1qf75
+
+<<< gotp.conf:rename:encrypted
+[gotp "issuer"]
+renamed-sha1 = GKhXh+9sSkfRjpVfu8o1dIAPTyahjHpGZxfSTqFJIUnAJj3HpQGa0UO3QNQ4qN66CYRaYchzS491REMdC+9mC1BoEFN0BQWVshF4Cy8fz/6geM8oPqUIQ0+pGKdnW9LWZGUB5NP72FGtBeDdQ5QHtMIKtexSHpO855mosY6wjik6l2oaJ+wbK2X4JNb4PyJIQksQjYWepMvx0m6b9NjkTJA+srUYmNu47VXf5JWtjXZY6uYd4ol2rkQJUNLcYY+B+VhqXJVvr/NaR15WCUjW+Q8JTGK0m3AsUYRnE5MJSwvjUSZUPOc842zlXtGaOUbKAQk3x2T69chK8+EaWGUS/hoVU7UT4ivIXJZr1kEcqNlLzLBMdOEI2S4mQJat98BDSucGcCbo3hghmny6KLaN5IUdUZ06Ra2JUdoJFAOmiaI/+bK6f6wr1J3prqbdQcsoLPsy6SVKIQv4rYoRIUHVPA5SLlYNcnNSpsnc4IjgaQb/sD07TM+9LYvy1KnCG8IF
+test-sha256 = e6BCkqDD0Su15P1PPyZz6PXiYhLQ+imlhpy99+r5xM+hbL6MfZ71JaXiXw5+1JJ5oM+yqV0ejzg6pW2U9yYu2M2QfDv7BxY8rV5+8fchKgWoZ7t3cfX9VX2OXKbjnZLMrO5vYFvk8jUfeemFzy9UvoCUfYdPq3V9/2IUStBgyWNRwOmHq6ImVc5/4YoMCqXvQ/rxUl/NCujF3qQvQPLCL2Abm/lQRdWiQDzEB8+tn40iax1XoGK4dYTeuJJX7tYwv2cvQctbjYJcb+9cA+AroHW0TuyBWt37iII1rCvIA9pBb45U17Aj74Xj1vH9/WamLWLAX9bfLZwgzl2/Qa7c86jgw5jlfPcVUvxtFOJSIS/2cudxD9j8EOg4cAzySry8WP+ZPxnVqI+I4ZqSVFOtV5uSXuTiVPXCv1gtVl44ChwJw38LVBztADffM6Iqp1WjeSbASFwzDvEZmR/7qqeSgPeem/k9oPKfAJwi251oXdj2gJxG1R+JaxVjNVs1qf75
+
+<<< gotp.conf
+[gotp "issuer"]
+renamed-sha1 = SHA1:GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ:6:30:
+test-sha256 = SHA256:GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ:6:30:
+
+<<< gotp.conf:set-private-key:encrypted
+[gotp "issuer"]
+renamed-sha1 = QwG2OcN4EwDXemN5cFPpRaxy6Elen/uNNMai9I3Zu05DX6t+k/DL7ZS/ppI3wV7TXT94SHOgzvAPlMiAZ8N8sRbXwsNMUr3aYZ+sV25PWAzP0VWYyQ6Z+nS66QY0xWJ+cdUVFRDSbwEc6Jw20/8na6pktd6TY3E0GS+AJNkamlGFlmQw2t4ZoCM2zytshsK1hV2S98xpxnugpTfwo6qUFEzLQ4ugb96qSiPPHLixj4QrfHg2bL5iSJuI2QGyWKLtWihQa2xFOZdLfCQ5ZOabgyiwhSAEgVju+JnzGvlPQVkkb0/h+t6NrySkY1k0gBidXJsvYjUt2GdUZAlG105iv3fZV7N0I4Zs9DNKhObG4MNBSBKWjNbC0lgOw0XeaeL/GERQI2V3Y5fUCCcnRVzSmMnVc6hW4YY4bsVbtWwWCCQFVYMN036rY5dA8+Y3z1gg7Ysdmx2dQA9DIGI1E61hHElvJCsYxd2Ol2mXXKUnN8o2/IYYBHEZd83jUSyLVbXl
+test-sha256 = uVVVi4dPsfnBl22KmsIFpvFn61/uztcCd95BYTfhwaEHs+bhLx2ttbg7P8J92OxSpuu9SRZutI4VZgTshQfJjr6EsdFobx0Ej+7J3i1GnhZ3TjzQ/uIDYzHUOMPpFW3ma6igk6RAawLnI2xnwLh4ODBP8sp2cGbRHnOEO/86BzS5FoA/AcfkFV+7tynk/t1OxPusr+yQ1bhlJTO/VQTuMRZXkali5WolyW9OHVfOERwn4ocTcSIlp0PcxkaAyi3ZW8Us62DeZCb85yEMa6HieKcmHMuSc+SpULYpalZ7juVk2609hmO9VNA30fUV1ZQWgvDToWdonhb1061rxoP5hvNxllNCvflMv9uVWyW0vVdrLhtzj5Fm5ds3k9A2GrXMAljk2mTbi0i1HLd5UIWob4V99nvEo1zOVRmJlFqzXJDlpE+urweXWq3QGwDNhGVIsMkka4LlHhxDhSH63FJZVK4Q57+/33Id4/15Po+eaCfd/HoyYED9y/YmSHhTE0QF
+
+<<< gotp.conf:set-private-key:encrypted:alt
+[gotp "issuer"]
+renamed-sha1 = cRLJ96VngTdoDWDfDNpojzwym1XSURnWSmMSDUyP5HPwpbMXKjKMt02C84enSOldIU4jqdKU4Xh2qqmHvE2TiSO43Mv/bxyDCaEUJs3TNknv4g8aJInymVAwVJ+8F3ZgdyR/TBGzBl2OH2wcLhqcaCOXeRzt+wZ1/RUvs6VaZrjLlRz6J/07gfnWJFxSHoZO9lVPyEn5ZcGpIOycm+VWc5KUSQ+OutL1LD/k7ZjssYo7QBJ5bM6i0VPVdNgIGIF7Fjsv4LYSkJUof6muBj6WmUvPliQaX3cdMitMf3Lz0HBJ9t0biHYqmX/mRZ71yBPEd3dCCt8Se8l3jOIM6Z1StcutxV2W488ISOdZUdcOTO3jycVkNzPqKrgY7ADZd+mcSMyTeAskpDDPp5w5UnjlKF88M+m/IY+2aaBLvoDatgkb4cE0jCzMV27sjRo/Mvvltkysz4ESYsnbkerOuYqPKLb84LqwjJ14k+hht+GSfR2XT+gQbh+YHGYIH2Vz67XL
+test-sha256 = gWIH03IceDf4zlBI7TBBexxvxP3FDfoGz80GJ958O6KuIB/ZazGl3AqwwT3hHe6fkEAgZ1GDZjtd6stKzo7GNSbT6nr8f8gxpwRKutoE714CdCutFFazZS90sPK9fcurM/CqkWcQfGlEMe3r3ntQi+iHc+8z4bRnwGdUqJ3fgT1MADMEbN6d8AZ2SsDLHmTPNbElvZ3QPTeo16fOmdyzIAm21YMrFaLKmw5B1xHUVRiDCINzEELzoZRAgb6ooisHb0tZZ+LCMkH0MYZUX+dYzXzzLL2yZF8gi5XWt///YO1nkDV3rp5qtflrNM6IphOO12g4gtO+7t2jTfZzYSdfSOGWaPXRDZxsVchG3lIhUPEYr5ns494PdoGO+8IFmRNpYh/XikL0vA4mPiMjqRMnR5wRoLnoOxLzaIV82ywGihvB3ioA1LI6ehhRPAYb45trVEqRkPNot1LSg7empJbda1DRVZ71lYo7OHgpTslfQDGT/hizfwF13gVRUxrhiGU0