diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d98f9d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.* diff --git a/go.mod b/go.mod index 330c197..9fcf153 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,11 @@ module rsc.io/2fa -go 1.16 +go 1.24rc2 -require github.com/atotto/clipboard v0.1.2 +toolchain go1.24.4 + +require ( + github.com/atotto/clipboard v0.1.4 + github.com/urfave/cli/v3 v3.3.8 + golang.org/x/crypto v0.39.0 +) diff --git a/go.sum b/go.sum index 243ea9b..4a085d2 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,14 @@ -github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= -github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/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/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/urfave/cli/v3 v3.3.8 h1:BzolUExliMdet9NlJ/u4m5vHSotJ3PzEqSAZ1oPMa/E= +github.com/urfave/cli/v3 v3.3.8/go.mod h1:FJSKtM/9AiiTOJL4fJ6TbMUkxBXn7GO9guZqoZtpYpo= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go index a8d173c..f0def96 100644 --- a/main.go +++ b/main.go @@ -1,77 +1,22 @@ -// Copyright 2017 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// 2fa is a two-factor authentication agent. -// -// Usage: -// -// 2fa -add [-7] [-8] [-hotp] name -// 2fa -list -// 2fa [-clip] name -// -// “2fa -add name” adds a new key to the 2fa keychain with the given name. -// It prints a prompt to standard error and reads a two-factor key from standard input. -// Two-factor keys are short case-insensitive strings of letters A-Z and digits 2-7. -// -// By default the new key generates time-based (TOTP) authentication codes; -// the -hotp flag makes the new key generate counter-based (HOTP) codes instead. -// -// By default the new key generates 6-digit codes; the -7 and -8 flags select -// 7- and 8-digit codes instead. -// -// “2fa -list” lists the names of all the keys in the keychain. -// -// “2fa name” prints a two-factor authentication code from the key with the -// given name. If “-clip” is specified, 2fa also copies the code to the system -// clipboard. -// -// With no arguments, 2fa prints two-factor authentication codes from all -// known time-based keys. -// -// The default time-based authentication codes are derived from a hash of -// the key and the current time, so it is important that the system clock have -// at least one-minute accuracy. -// -// The keychain is stored unencrypted in the text file $HOME/.2fa. -// -// Example -// -// During GitHub 2FA setup, at the “Scan this barcode with your app” step, -// click the “enter this text code instead” link. A window pops up showing -// “your two-factor secret,” a short string of letters and digits. -// -// Add it to 2fa under the name github, typing the secret at the prompt: -// -// $ 2fa -add github -// 2fa key for github: nzxxiidbebvwk6jb -// $ -// -// Then whenever GitHub prompts for a 2FA code, run 2fa to obtain one: -// -// $ 2fa github -// 268346 -// $ -// -// Or to type less: -// -// $ 2fa -// 268346 github -// $ -// package main import ( "bufio" "bytes" + "context" + "crypto/aes" + "crypto/cipher" "crypto/hmac" + "crypto/rand" "crypto/sha1" + "crypto/sha256" "encoding/base32" "encoding/binary" - "flag" "fmt" - "io/ioutil" + "io" "log" + "net/mail" + "net/url" "os" "path/filepath" "sort" @@ -81,127 +26,334 @@ import ( "unicode" "github.com/atotto/clipboard" + "github.com/urfave/cli/v3" + "golang.org/x/crypto/pbkdf2" ) -var ( - flagAdd = flag.Bool("add", false, "add a key") - flagList = flag.Bool("list", false, "list keys") - flagHotp = flag.Bool("hotp", false, "add key as HOTP (counter-based) key") - flag7 = flag.Bool("7", false, "generate 7-digit code") - flag8 = flag.Bool("8", false, "generate 8-digit code") - flagClip = flag.Bool("clip", false, "copy code to the clipboard") -) +type KeyName string -func usage() { - fmt.Fprintf(os.Stderr, "usage:\n") - fmt.Fprintf(os.Stderr, "\t2fa -add [-7] [-8] [-hotp] keyname\n") - fmt.Fprintf(os.Stderr, "\t2fa -list\n") - fmt.Fprintf(os.Stderr, "\t2fa [-clip] keyname\n") - os.Exit(2) +// validate checks if the KeyName is valid. +// Returns an error if the name contains spaces or is empty. +func (kn KeyName) validate() error { + if len(kn) == 0 { + return fmt.Errorf("key name cannot be empty") + } + if strings.IndexFunc(string(kn), unicode.IsSpace) >= 0 { + return fmt.Errorf("key name must not contain spaces") + } + return nil } func main() { log.SetPrefix("2fa: ") log.SetFlags(0) - flag.Usage = usage - flag.Parse() - - k := readKeychain(filepath.Join(os.Getenv("HOME"), ".2fa")) - if *flagList { - if flag.NArg() != 0 { - usage() - } - k.list() - return - } - if flag.NArg() == 0 && !*flagAdd { - if *flagClip { - usage() - } - k.showAll() - return - } - if flag.NArg() != 1 { - usage() + app := &cli.Command{ + Name: "2fa", + Usage: "Two-factor authentication agent", + Commands: []*cli.Command{ + { + Name: "add", + Usage: "Add a new key to the keychain", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "hotp", + Usage: "Add key as HOTP (counter-based) key", + }, + &cli.BoolFlag{ + Name: "7", + Usage: "Generate 7-digit code", + }, + &cli.BoolFlag{ + Name: "8", + Usage: "Generate 8-digit code", + }, + }, + Action: func(_ context.Context, c *cli.Command) error { + if c.NArg() != 1 { + return fmt.Errorf("usage: 2fa add [-7] [-8] [-hotp] keyname") + } + name := KeyName(c.Args().First()) + configDir, err := os.UserConfigDir() + if err != nil { + return fmt.Errorf("getting user config dir: %v", err) + } + keychainFile := filepath.Join(configDir, "2fa", "2fa") + k := readKeychain(keychainFile) + k.add(name, c.Bool("hotp"), c.Bool("7"), c.Bool("8")) + return nil + }, + }, + { + Name: "list", + Usage: "List all keys in the keychain", + Action: func(_ context.Context, c *cli.Command) error { + if c.NArg() != 0 { + return fmt.Errorf("usage: 2fa list") + } + configDir, err := os.UserConfigDir() + if err != nil { + return fmt.Errorf("getting user config dir: %v", err) + } + keychainFile := filepath.Join(configDir, "2fa", "2fa") + k := readKeychain(keychainFile) + k.list() + return nil + }, + }, + { + Name: "show", + Usage: "Show a two-factor authentication code for a key", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "clip", + Usage: "Copy code to the clipboard", + }, + }, + Action: func(_ context.Context, c *cli.Command) error { + if c.NArg() != 1 { + return fmt.Errorf("usage: 2fa show [--clip] keyname") + } + name := KeyName(c.Args().First()) + configDir, err := os.UserConfigDir() + if err != nil { + return fmt.Errorf("getting user config dir: %v", err) + } + keychainFile := filepath.Join(configDir, "2fa", "2fa") + k := readKeychain(keychainFile) + k.show(name, c.Bool("clip")) + return nil + }, + }, + { + Name: "import", + Usage: "Import keys from a URI-list file", + Action: func(_ context.Context, c *cli.Command) error { + if c.NArg() != 1 { + return fmt.Errorf("usage: 2fa import uri_list_file") + } + configDir, err := os.UserConfigDir() + if err != nil { + return fmt.Errorf("getting user config dir: %v", err) + } + keychainFile := filepath.Join(configDir, "2fa", "2fa") + k := readKeychain(keychainFile) + k.importURIList(c.Args().First()) + return nil + }, + }, + { + Name: "export", + Usage: "Export keys to a URI-list file or stdout", + Action: func(_ context.Context, c *cli.Command) error { + if c.NArg() > 1 { + return fmt.Errorf("usage: 2fa export [uri_list_file | -]") + } + configDir, err := os.UserConfigDir() + if err != nil { + return fmt.Errorf("getting user config dir: %v", err) + } + keychainFile := filepath.Join(configDir, "2fa", "2fa") + k := readKeychain(keychainFile) + if c.NArg() == 0 || c.Args().First() == "-" { + k.exportURIList(os.Stdout) + } else { + f, err := os.OpenFile(c.Args().First(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("opening export file: %v", err) + } + defer f.Close() + k.exportURIList(f) + } + return nil + }, + }, + }, + Action: func(_ context.Context, c *cli.Command) error { + if c.NArg() > 0 { + return fmt.Errorf("usage: 2fa [--clip] [keyname]") + } + configDir, err := os.UserConfigDir() + if err != nil { + return fmt.Errorf("getting user config dir: %v", err) + } + keychainFile := filepath.Join(configDir, "2fa", "2fa") + k := readKeychain(keychainFile) + k.showAll() + return nil + }, } - name := flag.Arg(0) - if strings.IndexFunc(name, unicode.IsSpace) >= 0 { - log.Fatal("name must not contain spaces") + + app.Authors = []any{ + &mail.Address{Name: "xplshn", Address: "anto@xplshn.com.ar"}, + &mail.Address{Name: "rsc", Address: "rsc@swtch.com"}, } - if *flagAdd { - if *flagClip { - usage() - } - k.add(name) - return + + if err := app.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) } - k.show(name) } type Keychain struct { file string data []byte - keys map[string]Key + keys map[KeyName]Key } type Key struct { - raw []byte - digits int - offset int // offset of counter + uri *url.URL // Store the full URI + raw []byte // Decoded secret + digits int // Number of digits + offset int // Offset of counter in data (for HOTP) } const counterLen = 20 +func getEncryptionKey() ([]byte, error) { + uuid, err := os.ReadFile("/sys/class/dmi/id/product_uuid") + if err != nil { + return nil, fmt.Errorf("cannot read machine UUID: %v", err) + } + return pbkdf2.Key(uuid, []byte("2fa-salt"), 100000, 32, sha256.New), nil +} + +func encrypt(data []byte) ([]byte, error) { + key, err := getEncryptionKey() + if err != nil { + return nil, err + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + padding := aes.BlockSize - len(data)%aes.BlockSize + padData := make([]byte, len(data)+padding) + copy(padData, data) + for i := len(data); i < len(padData); i++ { + padData[i] = byte(padding) + } + iv := make([]byte, aes.BlockSize) + if _, err := rand.Read(iv); err != nil { + return nil, err + } + ciphertext := make([]byte, len(padData)) + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ciphertext, padData) + return append(iv, ciphertext...), nil +} + +func decrypt(data []byte) ([]byte, error) { + if len(data) < aes.BlockSize { + return nil, fmt.Errorf("encrypted data too short") + } + key, err := getEncryptionKey() + if err != nil { + return nil, err + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + iv := data[:aes.BlockSize] + ciphertext := data[aes.BlockSize:] + if len(ciphertext)%aes.BlockSize != 0 { + return nil, fmt.Errorf("invalid ciphertext length") + } + plaintext := make([]byte, len(ciphertext)) + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(plaintext, ciphertext) + padding := int(plaintext[len(plaintext)-1]) + if padding > len(plaintext) || padding > aes.BlockSize { + return nil, fmt.Errorf("invalid padding") + } + for i := len(plaintext) - padding; i < len(plaintext); i++ { + if plaintext[i] != byte(padding) { + return nil, fmt.Errorf("invalid padding") + } + } + return plaintext[:len(plaintext)-padding], nil +} + func readKeychain(file string) *Keychain { c := &Keychain{ file: file, - keys: make(map[string]Key), + keys: make(map[KeyName]Key), } - data, err := ioutil.ReadFile(file) + data, err := os.ReadFile(file) if err != nil { if os.IsNotExist(err) { return c } log.Fatal(err) } - c.data = data + c.data, err = decrypt(data) + if err != nil { + log.Fatalf("decrypting keychain: %v", err) + } - lines := bytes.SplitAfter(data, []byte("\n")) + lines := bytes.SplitAfter(c.data, []byte("\n")) offset := 0 for i, line := range lines { lineno := i + 1 - offset += len(line) - f := bytes.Split(bytes.TrimSuffix(line, []byte("\n")), []byte(" ")) - if len(f) == 1 && len(f[0]) == 0 { + line = bytes.TrimSuffix(line, []byte("\n")) + if len(line) == 0 { + offset += len(line) + 1 + continue + } + if !bytes.HasPrefix(line, []byte("otpauth://")) { + log.Printf("%s:%d: invalid URI format", c.file, lineno) + offset += len(line) + 1 continue } - if len(f) >= 3 && len(f[1]) == 1 && '6' <= f[1][0] && f[1][0] <= '8' { - var k Key - name := string(f[0]) - k.digits = int(f[1][0] - '0') - raw, err := decodeKey(string(f[2])) - if err == nil { - k.raw = raw - if len(f) == 3 { - c.keys[name] = k + u, err := url.Parse(string(line)) + if err != nil { + log.Printf("%s:%d: parsing URI: %v", c.file, lineno, err) + offset += len(line) + 1 + continue + } + name := strings.TrimPrefix(u.Path, "/") + keyName := KeyName(name) + if err := keyName.validate(); err != nil { + log.Printf("%s:%d: %v", c.file, lineno, err) + offset += len(line) + 1 + continue + } + secret := u.Query().Get("secret") + if secret == "" { + log.Printf("%s:%d: no secret in URI", c.file, lineno) + offset += len(line) + 1 + continue + } + digits := 6 + if d := u.Query().Get("digits"); d != "" { + n, err := strconv.Atoi(d) + if err == nil && (n == 6 || n == 7 || n == 8) { + digits = n + } + } + var k Key + k.uri = u + k.digits = digits + k.raw, err = decodeKey(secret) + if err != nil { + log.Printf("%s:%d: invalid secret: %v", c.file, lineno, err) + offset += len(line) + 1 + continue + } + if u.Host == "hotp" { + counter := u.Query().Get("counter") + if counter != "" { + _, err := strconv.ParseUint(counter, 10, 64) + if err != nil { + log.Printf("%s:%d: invalid counter: %v", c.file, lineno, err) + offset += len(line) + 1 continue } - if len(f) == 4 && len(f[3]) == counterLen { - _, err := strconv.ParseUint(string(f[3]), 10, 64) - if err == nil { - // Valid counter. - k.offset = offset - counterLen - if line[len(line)-1] == '\n' { - k.offset-- - } - c.keys[name] = k - continue - } - } + k.offset = offset + len(line) - len(counter) } } - log.Printf("%s:%d: malformed key", c.file, lineno) + c.keys[keyName] = k + offset += len(line) + 1 } return c } @@ -209,7 +361,7 @@ func readKeychain(file string) *Keychain { func (c *Keychain) list() { var names []string for name := range c.keys { - names = append(names, name) + names = append(names, string(name)) } sort.Strings(names) for _, name := range names { @@ -224,14 +376,17 @@ func noSpace(r rune) rune { return r } -func (c *Keychain) add(name string) { +func (c *Keychain) add(name KeyName, hotp, flag7, flag8 bool) { + if err := name.validate(); err != nil { + log.Fatalf("invalid key name: %v", err) + } size := 6 - if *flag7 { + if flag7 { size = 7 - if *flag8 { + if flag8 { log.Fatalf("cannot use -7 and -8 together") } - } else if *flag8 { + } else if flag8 { size = 8 } @@ -241,71 +396,164 @@ func (c *Keychain) add(name string) { log.Fatalf("error reading key: %v", err) } text = strings.Map(noSpace, text) - text += strings.Repeat("=", -len(text)&7) // pad to 8 bytes + text += strings.Repeat("=", -len(text)&7) if _, err := decodeKey(text); err != nil { log.Fatalf("invalid key: %v", err) } - line := fmt.Sprintf("%s %d %s", name, size, text) - if *flagHotp { - line += " " + strings.Repeat("0", 20) + query := url.Values{} + query.Set("secret", text) + query.Set("digits", strconv.Itoa(size)) + uriType := "totp" + if hotp { + uriType = "hotp" + query.Set("counter", strings.Repeat("0", counterLen)) + } + uri := &url.URL{ + Scheme: "otpauth", + Host: uriType, + Path: "/" + url.QueryEscape(string(name)), + RawQuery: query.Encode(), + } + line := uri.String() + "\n" + + c.data = append(c.data, []byte(line)...) + + if err := os.MkdirAll(filepath.Dir(c.file), 0700); err != nil { + log.Fatalf("creating config directory: %v", err) + } + + encrypted, err := encrypt(c.data) + if err != nil { + log.Fatalf("encrypting keychain: %v", err) + } + if err := os.WriteFile(c.file, encrypted, 0600); err != nil { + log.Fatalf("writing keychain: %v", err) } - line += "\n" +} - f, err := os.OpenFile(c.file, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0600) +func (c *Keychain) importURIList(file string) { + data, err := os.ReadFile(file) + if err != nil { + log.Fatalf("reading URI list: %v", err) + } + scanner := bufio.NewScanner(bytes.NewReader(data)) + var newData []byte + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "otpauth://") { + log.Printf("skipping non-URI line: %s", line) + continue + } + u, err := url.Parse(line) + if err != nil { + log.Printf("parsing URI: %v", err) + continue + } + name := strings.TrimPrefix(u.Path, "/") + keyName := KeyName(name) + if err := keyName.validate(); err != nil { + log.Printf("invalid key name in URI: %v", err) + continue + } + secret := u.Query().Get("secret") + if secret == "" { + log.Printf("no secret in URI: %s", line) + continue + } + if _, err := decodeKey(secret); err != nil { + log.Printf("invalid secret in URI: %s", line) + continue + } + newData = append(newData, []byte(line+"\n")...) + } + if err := scanner.Err(); err != nil { + log.Fatalf("reading URI list: %v", err) + } + c.data = append(c.data, newData...) + if err := os.MkdirAll(filepath.Dir(c.file), 0700); err != nil { + log.Fatalf("creating config directory: %v", err) + } + encrypted, err := encrypt(c.data) if err != nil { - log.Fatalf("opening keychain: %v", err) + log.Fatalf("encrypting keychain: %v", err) } - f.Chmod(0600) + if err := os.WriteFile(c.file, encrypted, 0600); err != nil { + log.Fatalf("writing keychain: %v", err) + } +} - if _, err := f.Write([]byte(line)); err != nil { - log.Fatalf("adding key: %v", err) +func (c *Keychain) exportURIList(w io.Writer) { + var uriList []string + for _, key := range c.keys { + uriList = append(uriList, key.uri.String()) } - if err := f.Close(); err != nil { - log.Fatalf("adding key: %v", err) + if len(uriList) == 0 { + return + } + sort.Strings(uriList) + _, err := io.WriteString(w, strings.Join(uriList, "\n")+"\n") + if err != nil { + log.Fatalf("writing URI list: %v", err) } } -func (c *Keychain) code(name string) string { +func (c *Keychain) code(name KeyName) string { k, ok := c.keys[name] if !ok { log.Fatalf("no such key %q", name) } var code int - if k.offset != 0 { - n, err := strconv.ParseUint(string(c.data[k.offset:k.offset+counterLen]), 10, 64) + if k.uri.Host == "hotp" { + n, err := strconv.ParseUint(k.uri.Query().Get("counter"), 10, 64) if err != nil { - log.Fatalf("malformed key counter for %q (%q)", name, c.data[k.offset:k.offset+counterLen]) + log.Fatalf("malformed key counter for %q: %v", name, err) } n++ code = hotp(k.raw, n, k.digits) - f, err := os.OpenFile(c.file, os.O_RDWR, 0600) - if err != nil { - log.Fatalf("opening keychain: %v", err) + query := k.uri.Query() + query.Set("counter", fmt.Sprintf("%0*d", counterLen, n)) + k.uri.RawQuery = query.Encode() + // Update URI in memory and file + newURI := k.uri.String() + "\n" + start := k.offset + for start > 0 && c.data[start-1] != '\n' { + start-- + } + end := k.offset + for end < len(c.data) && c.data[end] != '\n' { + end++ } - if _, err := f.WriteAt([]byte(fmt.Sprintf("%0*d", counterLen, n)), int64(k.offset)); err != nil { - log.Fatalf("updating keychain: %v", err) + if end < len(c.data) { + end++ // Include newline } - if err := f.Close(); err != nil { - log.Fatalf("updating keychain: %v", err) + c.data = append(c.data[:start], append([]byte(newURI), c.data[end:]...)...) + encrypted, err := encrypt(c.data) + if err != nil { + log.Fatalf("encrypting keychain: %v", err) + } + if err := os.WriteFile(c.file, encrypted, 0600); err != nil { + log.Fatalf("writing keychain: %v", err) } } else { - // Time-based key. code = totp(k.raw, time.Now(), k.digits) } return fmt.Sprintf("%0*d", k.digits, code) } -func (c *Keychain) show(name string) { +func (c *Keychain) show(name KeyName, clip bool) { + if err := name.validate(); err != nil { + log.Fatalf("invalid key name: %v", err) + } code := c.code(name) - if *flagClip { + if clip { clipboard.WriteAll(code) } fmt.Printf("%s\n", code) } func (c *Keychain) showAll() { - var names []string + var names []KeyName max := 0 for name, k := range c.keys { names = append(names, name) @@ -313,11 +561,11 @@ func (c *Keychain) showAll() { max = k.digits } } - sort.Strings(names) + sort.Slice(names, func(i, j int) bool { return names[i] < names[j] }) for _, name := range names { k := c.keys[name] code := strings.Repeat("-", k.digits) - if k.offset == 0 { + if k.uri.Host == "totp" { code = c.code(name) } fmt.Printf("%-*s\t%s\n", max, code, name) @@ -343,3 +591,5 @@ func hotp(key []byte, counter uint64, digits int) int { func totp(key []byte, t time.Time, digits int) int { return hotp(key, uint64(t.UnixNano())/30e9, digits) } + + diff --git a/vendor/github.com/atotto/clipboard/LICENSE b/vendor/github.com/atotto/clipboard/LICENSE deleted file mode 100644 index dee3257..0000000 --- a/vendor/github.com/atotto/clipboard/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2013 Ato Araki. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of @atotto. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/atotto/clipboard/clipboard.go b/vendor/github.com/atotto/clipboard/clipboard.go deleted file mode 100644 index d7907d3..0000000 --- a/vendor/github.com/atotto/clipboard/clipboard.go +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2013 @atotto. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package clipboard read/write on clipboard -package clipboard - -// ReadAll read string from clipboard -func ReadAll() (string, error) { - return readAll() -} - -// WriteAll write string to clipboard -func WriteAll(text string) error { - return writeAll(text) -} - -// Unsupported might be set true during clipboard init, to help callers decide -// whether or not to offer clipboard options. -var Unsupported bool diff --git a/vendor/github.com/atotto/clipboard/clipboard_darwin.go b/vendor/github.com/atotto/clipboard/clipboard_darwin.go deleted file mode 100644 index 6f33078..0000000 --- a/vendor/github.com/atotto/clipboard/clipboard_darwin.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2013 @atotto. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build darwin - -package clipboard - -import ( - "os/exec" -) - -var ( - pasteCmdArgs = "pbpaste" - copyCmdArgs = "pbcopy" -) - -func getPasteCommand() *exec.Cmd { - return exec.Command(pasteCmdArgs) -} - -func getCopyCommand() *exec.Cmd { - return exec.Command(copyCmdArgs) -} - -func readAll() (string, error) { - pasteCmd := getPasteCommand() - out, err := pasteCmd.Output() - if err != nil { - return "", err - } - return string(out), nil -} - -func writeAll(text string) error { - copyCmd := getCopyCommand() - in, err := copyCmd.StdinPipe() - if err != nil { - return err - } - - if err := copyCmd.Start(); err != nil { - return err - } - if _, err := in.Write([]byte(text)); err != nil { - return err - } - if err := in.Close(); err != nil { - return err - } - return copyCmd.Wait() -} diff --git a/vendor/github.com/atotto/clipboard/clipboard_unix.go b/vendor/github.com/atotto/clipboard/clipboard_unix.go deleted file mode 100644 index 0acd5fa..0000000 --- a/vendor/github.com/atotto/clipboard/clipboard_unix.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2013 @atotto. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build freebsd linux netbsd openbsd solaris dragonfly - -package clipboard - -import ( - "errors" - "os/exec" -) - -const ( - xsel = "xsel" - xclip = "xclip" -) - -var ( - Primary bool - - pasteCmdArgs []string - copyCmdArgs []string - - xselPasteArgs = []string{xsel, "--output", "--clipboard"} - xselCopyArgs = []string{xsel, "--input", "--clipboard"} - - xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"} - xclipCopyArgs = []string{xclip, "-in", "-selection", "clipboard"} - - missingCommands = errors.New("No clipboard utilities available. Please install xsel or xclip.") -) - -func init() { - pasteCmdArgs = xclipPasteArgs - copyCmdArgs = xclipCopyArgs - - if _, err := exec.LookPath(xclip); err == nil { - return - } - - pasteCmdArgs = xselPasteArgs - copyCmdArgs = xselCopyArgs - - if _, err := exec.LookPath(xsel); err == nil { - return - } - - Unsupported = true -} - -func getPasteCommand() *exec.Cmd { - if Primary { - pasteCmdArgs = pasteCmdArgs[:1] - } - return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...) -} - -func getCopyCommand() *exec.Cmd { - if Primary { - copyCmdArgs = copyCmdArgs[:1] - } - return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...) -} - -func readAll() (string, error) { - if Unsupported { - return "", missingCommands - } - pasteCmd := getPasteCommand() - out, err := pasteCmd.Output() - if err != nil { - return "", err - } - return string(out), nil -} - -func writeAll(text string) error { - if Unsupported { - return missingCommands - } - copyCmd := getCopyCommand() - in, err := copyCmd.StdinPipe() - if err != nil { - return err - } - - if err := copyCmd.Start(); err != nil { - return err - } - if _, err := in.Write([]byte(text)); err != nil { - return err - } - if err := in.Close(); err != nil { - return err - } - return copyCmd.Wait() -} diff --git a/vendor/github.com/atotto/clipboard/clipboard_windows.go b/vendor/github.com/atotto/clipboard/clipboard_windows.go deleted file mode 100644 index 5dbb562..0000000 --- a/vendor/github.com/atotto/clipboard/clipboard_windows.go +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2013 @atotto. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build windows - -package clipboard - -import ( - "syscall" - "unsafe" -) - -const ( - cfUnicodetext = 13 - gmemFixed = 0x0000 -) - -var ( - user32 = syscall.MustLoadDLL("user32") - openClipboard = user32.MustFindProc("OpenClipboard") - closeClipboard = user32.MustFindProc("CloseClipboard") - emptyClipboard = user32.MustFindProc("EmptyClipboard") - getClipboardData = user32.MustFindProc("GetClipboardData") - setClipboardData = user32.MustFindProc("SetClipboardData") - - kernel32 = syscall.NewLazyDLL("kernel32") - globalAlloc = kernel32.NewProc("GlobalAlloc") - globalFree = kernel32.NewProc("GlobalFree") - globalLock = kernel32.NewProc("GlobalLock") - globalUnlock = kernel32.NewProc("GlobalUnlock") - lstrcpy = kernel32.NewProc("lstrcpyW") -) - -func readAll() (string, error) { - r, _, err := openClipboard.Call(0) - if r == 0 { - return "", err - } - defer closeClipboard.Call() - - h, _, err := getClipboardData.Call(cfUnicodetext) - if r == 0 { - return "", err - } - - l, _, err := globalLock.Call(h) - if l == 0 { - return "", err - } - - text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:]) - - r, _, err = globalUnlock.Call(h) - if r == 0 { - return "", err - } - - return text, nil -} - -func writeAll(text string) error { - r, _, err := openClipboard.Call(0) - if r == 0 { - return err - } - defer closeClipboard.Call() - - r, _, err = emptyClipboard.Call(0) - if r == 0 { - return err - } - - data := syscall.StringToUTF16(text) - - h, _, err := globalAlloc.Call(gmemFixed, uintptr(len(data)*int(unsafe.Sizeof(data[0])))) - if h == 0 { - return err - } - - l, _, err := globalLock.Call(h) - if l == 0 { - return err - } - - r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0]))) - if r == 0 { - return err - } - - r, _, err = globalUnlock.Call(h) - if r == 0 { - return err - } - - r, _, err = setClipboardData.Call(cfUnicodetext, h) - if r == 0 { - return err - } - return nil -}