diff options
| -rw-r--r-- | cli.go | 13 | ||||
| -rw-r--r-- | cli_test.go | 293 | ||||
| -rw-r--r-- | config.go | 40 | ||||
| -rw-r--r-- | go.mod | 10 | ||||
| -rw-r--r-- | go.sum | 6 | ||||
| -rw-r--r-- | gotp.go | 11 | ||||
| -rw-r--r-- | gotp_test.go | 34 | ||||
| -rw-r--r-- | issuer.go | 3 | ||||
| -rw-r--r-- | testdata/cli_with_passphrase_test.txt | 92 |
9 files changed, 453 insertions, 49 deletions
@@ -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)) +} @@ -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 @@ -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 @@ -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= @@ -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()) +} @@ -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 |
