├── go.mod ├── go.sum ├── main_test.go ├── vendor └── github.com │ └── atotto │ └── clipboard │ ├── clipboard.go │ ├── clipboard_darwin.go │ ├── LICENSE │ ├── clipboard_unix.go │ └── clipboard_windows.go ├── LICENSE ├── README.md └── main.go /go.mod: -------------------------------------------------------------------------------- 1 | module rsc.io/2fa 2 | 3 | go 1.16 4 | 5 | require github.com/atotto/clipboard v0.1.2 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= 2 | github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 3 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import "testing" 8 | 9 | func Test(t *testing.T) {} 10 | -------------------------------------------------------------------------------- /vendor/github.com/atotto/clipboard/clipboard.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 @atotto. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package clipboard read/write on clipboard 6 | package clipboard 7 | 8 | // ReadAll read string from clipboard 9 | func ReadAll() (string, error) { 10 | return readAll() 11 | } 12 | 13 | // WriteAll write string to clipboard 14 | func WriteAll(text string) error { 15 | return writeAll(text) 16 | } 17 | 18 | // Unsupported might be set true during clipboard init, to help callers decide 19 | // whether or not to offer clipboard options. 20 | var Unsupported bool 21 | -------------------------------------------------------------------------------- /vendor/github.com/atotto/clipboard/clipboard_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 @atotto. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build darwin 6 | 7 | package clipboard 8 | 9 | import ( 10 | "os/exec" 11 | ) 12 | 13 | var ( 14 | pasteCmdArgs = "pbpaste" 15 | copyCmdArgs = "pbcopy" 16 | ) 17 | 18 | func getPasteCommand() *exec.Cmd { 19 | return exec.Command(pasteCmdArgs) 20 | } 21 | 22 | func getCopyCommand() *exec.Cmd { 23 | return exec.Command(copyCmdArgs) 24 | } 25 | 26 | func readAll() (string, error) { 27 | pasteCmd := getPasteCommand() 28 | out, err := pasteCmd.Output() 29 | if err != nil { 30 | return "", err 31 | } 32 | return string(out), nil 33 | } 34 | 35 | func writeAll(text string) error { 36 | copyCmd := getCopyCommand() 37 | in, err := copyCmd.StdinPipe() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | if err := copyCmd.Start(); err != nil { 43 | return err 44 | } 45 | if _, err := in.Write([]byte(text)); err != nil { 46 | return err 47 | } 48 | if err := in.Close(); err != nil { 49 | return err 50 | } 51 | return copyCmd.Wait() 52 | } 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /vendor/github.com/atotto/clipboard/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Ato Araki. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of @atotto. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2fa is a two-factor authentication agent. 2 | 3 | Usage: 4 | 5 | go install rsc.io/2fa@latest 6 | 7 | 2fa -add [-7] [-8] [-hotp] name 8 | 2fa -list 9 | 2fa name 10 | 11 | `2fa -add name` adds a new key to the 2fa keychain with the given name. It 12 | prints a prompt to standard error and reads a two-factor key from standard 13 | input. Two-factor keys are short case-insensitive strings of letters A-Z and 14 | digits 2-7. 15 | 16 | By default the new key generates time-based (TOTP) authentication codes; the 17 | `-hotp` flag makes the new key generate counter-based (HOTP) codes instead. 18 | 19 | By default the new key generates 6-digit codes; the `-7` and `-8` flags select 20 | 7- and 8-digit codes instead. 21 | 22 | `2fa -list` lists the names of all the keys in the keychain. 23 | 24 | `2fa name` prints a two-factor authentication code from the key with the 25 | given name. If `-clip` is specified, `2fa` also copies to the code to the system 26 | clipboard. 27 | 28 | With no arguments, `2fa` prints two-factor authentication codes from all 29 | known time-based keys. 30 | 31 | The default time-based authentication codes are derived from a hash of the 32 | key and the current time, so it is important that the system clock have at 33 | least one-minute accuracy. 34 | 35 | The keychain is stored unencrypted in the text file `$HOME/.2fa`. 36 | 37 | ## Example 38 | 39 | During GitHub 2FA setup, at the “Scan this barcode with your app” step, 40 | click the “enter this text code instead” link. A window pops up showing 41 | “your two-factor secret,” a short string of letters and digits. 42 | 43 | Add it to 2fa under the name github, typing the secret at the prompt: 44 | 45 | $ 2fa -add github 46 | 2fa key for github: nzxxiidbebvwk6jb 47 | $ 48 | 49 | Then whenever GitHub prompts for a 2FA code, run 2fa to obtain one: 50 | 51 | $ 2fa github 52 | 268346 53 | $ 54 | 55 | Or to type less: 56 | 57 | $ 2fa 58 | 268346 github 59 | $ 60 | -------------------------------------------------------------------------------- /vendor/github.com/atotto/clipboard/clipboard_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 @atotto. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build freebsd linux netbsd openbsd solaris dragonfly 6 | 7 | package clipboard 8 | 9 | import ( 10 | "errors" 11 | "os/exec" 12 | ) 13 | 14 | const ( 15 | xsel = "xsel" 16 | xclip = "xclip" 17 | ) 18 | 19 | var ( 20 | Primary bool 21 | 22 | pasteCmdArgs []string 23 | copyCmdArgs []string 24 | 25 | xselPasteArgs = []string{xsel, "--output", "--clipboard"} 26 | xselCopyArgs = []string{xsel, "--input", "--clipboard"} 27 | 28 | xclipPasteArgs = []string{xclip, "-out", "-selection", "clipboard"} 29 | xclipCopyArgs = []string{xclip, "-in", "-selection", "clipboard"} 30 | 31 | missingCommands = errors.New("No clipboard utilities available. Please install xsel or xclip.") 32 | ) 33 | 34 | func init() { 35 | pasteCmdArgs = xclipPasteArgs 36 | copyCmdArgs = xclipCopyArgs 37 | 38 | if _, err := exec.LookPath(xclip); err == nil { 39 | return 40 | } 41 | 42 | pasteCmdArgs = xselPasteArgs 43 | copyCmdArgs = xselCopyArgs 44 | 45 | if _, err := exec.LookPath(xsel); err == nil { 46 | return 47 | } 48 | 49 | Unsupported = true 50 | } 51 | 52 | func getPasteCommand() *exec.Cmd { 53 | if Primary { 54 | pasteCmdArgs = pasteCmdArgs[:1] 55 | } 56 | return exec.Command(pasteCmdArgs[0], pasteCmdArgs[1:]...) 57 | } 58 | 59 | func getCopyCommand() *exec.Cmd { 60 | if Primary { 61 | copyCmdArgs = copyCmdArgs[:1] 62 | } 63 | return exec.Command(copyCmdArgs[0], copyCmdArgs[1:]...) 64 | } 65 | 66 | func readAll() (string, error) { 67 | if Unsupported { 68 | return "", missingCommands 69 | } 70 | pasteCmd := getPasteCommand() 71 | out, err := pasteCmd.Output() 72 | if err != nil { 73 | return "", err 74 | } 75 | return string(out), nil 76 | } 77 | 78 | func writeAll(text string) error { 79 | if Unsupported { 80 | return missingCommands 81 | } 82 | copyCmd := getCopyCommand() 83 | in, err := copyCmd.StdinPipe() 84 | if err != nil { 85 | return err 86 | } 87 | 88 | if err := copyCmd.Start(); err != nil { 89 | return err 90 | } 91 | if _, err := in.Write([]byte(text)); err != nil { 92 | return err 93 | } 94 | if err := in.Close(); err != nil { 95 | return err 96 | } 97 | return copyCmd.Wait() 98 | } 99 | -------------------------------------------------------------------------------- /vendor/github.com/atotto/clipboard/clipboard_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 @atotto. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build windows 6 | 7 | package clipboard 8 | 9 | import ( 10 | "syscall" 11 | "unsafe" 12 | ) 13 | 14 | const ( 15 | cfUnicodetext = 13 16 | gmemFixed = 0x0000 17 | ) 18 | 19 | var ( 20 | user32 = syscall.MustLoadDLL("user32") 21 | openClipboard = user32.MustFindProc("OpenClipboard") 22 | closeClipboard = user32.MustFindProc("CloseClipboard") 23 | emptyClipboard = user32.MustFindProc("EmptyClipboard") 24 | getClipboardData = user32.MustFindProc("GetClipboardData") 25 | setClipboardData = user32.MustFindProc("SetClipboardData") 26 | 27 | kernel32 = syscall.NewLazyDLL("kernel32") 28 | globalAlloc = kernel32.NewProc("GlobalAlloc") 29 | globalFree = kernel32.NewProc("GlobalFree") 30 | globalLock = kernel32.NewProc("GlobalLock") 31 | globalUnlock = kernel32.NewProc("GlobalUnlock") 32 | lstrcpy = kernel32.NewProc("lstrcpyW") 33 | ) 34 | 35 | func readAll() (string, error) { 36 | r, _, err := openClipboard.Call(0) 37 | if r == 0 { 38 | return "", err 39 | } 40 | defer closeClipboard.Call() 41 | 42 | h, _, err := getClipboardData.Call(cfUnicodetext) 43 | if r == 0 { 44 | return "", err 45 | } 46 | 47 | l, _, err := globalLock.Call(h) 48 | if l == 0 { 49 | return "", err 50 | } 51 | 52 | text := syscall.UTF16ToString((*[1 << 20]uint16)(unsafe.Pointer(l))[:]) 53 | 54 | r, _, err = globalUnlock.Call(h) 55 | if r == 0 { 56 | return "", err 57 | } 58 | 59 | return text, nil 60 | } 61 | 62 | func writeAll(text string) error { 63 | r, _, err := openClipboard.Call(0) 64 | if r == 0 { 65 | return err 66 | } 67 | defer closeClipboard.Call() 68 | 69 | r, _, err = emptyClipboard.Call(0) 70 | if r == 0 { 71 | return err 72 | } 73 | 74 | data := syscall.StringToUTF16(text) 75 | 76 | h, _, err := globalAlloc.Call(gmemFixed, uintptr(len(data)*int(unsafe.Sizeof(data[0])))) 77 | if h == 0 { 78 | return err 79 | } 80 | 81 | l, _, err := globalLock.Call(h) 82 | if l == 0 { 83 | return err 84 | } 85 | 86 | r, _, err = lstrcpy.Call(l, uintptr(unsafe.Pointer(&data[0]))) 87 | if r == 0 { 88 | return err 89 | } 90 | 91 | r, _, err = globalUnlock.Call(h) 92 | if r == 0 { 93 | return err 94 | } 95 | 96 | r, _, err = setClipboardData.Call(cfUnicodetext, h) 97 | if r == 0 { 98 | return err 99 | } 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // 2fa is a two-factor authentication agent. 6 | // 7 | // Usage: 8 | // 9 | // 2fa -add [-7] [-8] [-hotp] name 10 | // 2fa -list 11 | // 2fa [-clip] name 12 | // 13 | // “2fa -add name” adds a new key to the 2fa keychain with the given name. 14 | // It prints a prompt to standard error and reads a two-factor key from standard input. 15 | // Two-factor keys are short case-insensitive strings of letters A-Z and digits 2-7. 16 | // 17 | // By default the new key generates time-based (TOTP) authentication codes; 18 | // the -hotp flag makes the new key generate counter-based (HOTP) codes instead. 19 | // 20 | // By default the new key generates 6-digit codes; the -7 and -8 flags select 21 | // 7- and 8-digit codes instead. 22 | // 23 | // “2fa -list” lists the names of all the keys in the keychain. 24 | // 25 | // “2fa name” prints a two-factor authentication code from the key with the 26 | // given name. If “-clip” is specified, 2fa also copies the code to the system 27 | // clipboard. 28 | // 29 | // With no arguments, 2fa prints two-factor authentication codes from all 30 | // known time-based keys. 31 | // 32 | // The default time-based authentication codes are derived from a hash of 33 | // the key and the current time, so it is important that the system clock have 34 | // at least one-minute accuracy. 35 | // 36 | // The keychain is stored unencrypted in the text file $HOME/.2fa. 37 | // 38 | // Example 39 | // 40 | // During GitHub 2FA setup, at the “Scan this barcode with your app” step, 41 | // click the “enter this text code instead” link. A window pops up showing 42 | // “your two-factor secret,” a short string of letters and digits. 43 | // 44 | // Add it to 2fa under the name github, typing the secret at the prompt: 45 | // 46 | // $ 2fa -add github 47 | // 2fa key for github: nzxxiidbebvwk6jb 48 | // $ 49 | // 50 | // Then whenever GitHub prompts for a 2FA code, run 2fa to obtain one: 51 | // 52 | // $ 2fa github 53 | // 268346 54 | // $ 55 | // 56 | // Or to type less: 57 | // 58 | // $ 2fa 59 | // 268346 github 60 | // $ 61 | // 62 | package main 63 | 64 | import ( 65 | "bufio" 66 | "bytes" 67 | "crypto/hmac" 68 | "crypto/sha1" 69 | "encoding/base32" 70 | "encoding/binary" 71 | "flag" 72 | "fmt" 73 | "io/ioutil" 74 | "log" 75 | "os" 76 | "path/filepath" 77 | "sort" 78 | "strconv" 79 | "strings" 80 | "time" 81 | "unicode" 82 | 83 | "github.com/atotto/clipboard" 84 | ) 85 | 86 | var ( 87 | flagAdd = flag.Bool("add", false, "add a key") 88 | flagList = flag.Bool("list", false, "list keys") 89 | flagHotp = flag.Bool("hotp", false, "add key as HOTP (counter-based) key") 90 | flag7 = flag.Bool("7", false, "generate 7-digit code") 91 | flag8 = flag.Bool("8", false, "generate 8-digit code") 92 | flagClip = flag.Bool("clip", false, "copy code to the clipboard") 93 | ) 94 | 95 | func usage() { 96 | fmt.Fprintf(os.Stderr, "usage:\n") 97 | fmt.Fprintf(os.Stderr, "\t2fa -add [-7] [-8] [-hotp] keyname\n") 98 | fmt.Fprintf(os.Stderr, "\t2fa -list\n") 99 | fmt.Fprintf(os.Stderr, "\t2fa [-clip] keyname\n") 100 | os.Exit(2) 101 | } 102 | 103 | func main() { 104 | log.SetPrefix("2fa: ") 105 | log.SetFlags(0) 106 | flag.Usage = usage 107 | flag.Parse() 108 | 109 | k := readKeychain(filepath.Join(os.Getenv("HOME"), ".2fa")) 110 | 111 | if *flagList { 112 | if flag.NArg() != 0 { 113 | usage() 114 | } 115 | k.list() 116 | return 117 | } 118 | if flag.NArg() == 0 && !*flagAdd { 119 | if *flagClip { 120 | usage() 121 | } 122 | k.showAll() 123 | return 124 | } 125 | if flag.NArg() != 1 { 126 | usage() 127 | } 128 | name := flag.Arg(0) 129 | if strings.IndexFunc(name, unicode.IsSpace) >= 0 { 130 | log.Fatal("name must not contain spaces") 131 | } 132 | if *flagAdd { 133 | if *flagClip { 134 | usage() 135 | } 136 | k.add(name) 137 | return 138 | } 139 | k.show(name) 140 | } 141 | 142 | type Keychain struct { 143 | file string 144 | data []byte 145 | keys map[string]Key 146 | } 147 | 148 | type Key struct { 149 | raw []byte 150 | digits int 151 | offset int // offset of counter 152 | } 153 | 154 | const counterLen = 20 155 | 156 | func readKeychain(file string) *Keychain { 157 | c := &Keychain{ 158 | file: file, 159 | keys: make(map[string]Key), 160 | } 161 | data, err := ioutil.ReadFile(file) 162 | if err != nil { 163 | if os.IsNotExist(err) { 164 | return c 165 | } 166 | log.Fatal(err) 167 | } 168 | c.data = data 169 | 170 | lines := bytes.SplitAfter(data, []byte("\n")) 171 | offset := 0 172 | for i, line := range lines { 173 | lineno := i + 1 174 | offset += len(line) 175 | f := bytes.Split(bytes.TrimSuffix(line, []byte("\n")), []byte(" ")) 176 | if len(f) == 1 && len(f[0]) == 0 { 177 | continue 178 | } 179 | if len(f) >= 3 && len(f[1]) == 1 && '6' <= f[1][0] && f[1][0] <= '8' { 180 | var k Key 181 | name := string(f[0]) 182 | k.digits = int(f[1][0] - '0') 183 | raw, err := decodeKey(string(f[2])) 184 | if err == nil { 185 | k.raw = raw 186 | if len(f) == 3 { 187 | c.keys[name] = k 188 | continue 189 | } 190 | if len(f) == 4 && len(f[3]) == counterLen { 191 | _, err := strconv.ParseUint(string(f[3]), 10, 64) 192 | if err == nil { 193 | // Valid counter. 194 | k.offset = offset - counterLen 195 | if line[len(line)-1] == '\n' { 196 | k.offset-- 197 | } 198 | c.keys[name] = k 199 | continue 200 | } 201 | } 202 | } 203 | } 204 | log.Printf("%s:%d: malformed key", c.file, lineno) 205 | } 206 | return c 207 | } 208 | 209 | func (c *Keychain) list() { 210 | var names []string 211 | for name := range c.keys { 212 | names = append(names, name) 213 | } 214 | sort.Strings(names) 215 | for _, name := range names { 216 | fmt.Println(name) 217 | } 218 | } 219 | 220 | func noSpace(r rune) rune { 221 | if unicode.IsSpace(r) { 222 | return -1 223 | } 224 | return r 225 | } 226 | 227 | func (c *Keychain) add(name string) { 228 | size := 6 229 | if *flag7 { 230 | size = 7 231 | if *flag8 { 232 | log.Fatalf("cannot use -7 and -8 together") 233 | } 234 | } else if *flag8 { 235 | size = 8 236 | } 237 | 238 | fmt.Fprintf(os.Stderr, "2fa key for %s: ", name) 239 | text, err := bufio.NewReader(os.Stdin).ReadString('\n') 240 | if err != nil { 241 | log.Fatalf("error reading key: %v", err) 242 | } 243 | text = strings.Map(noSpace, text) 244 | text += strings.Repeat("=", -len(text)&7) // pad to 8 bytes 245 | if _, err := decodeKey(text); err != nil { 246 | log.Fatalf("invalid key: %v", err) 247 | } 248 | 249 | line := fmt.Sprintf("%s %d %s", name, size, text) 250 | if *flagHotp { 251 | line += " " + strings.Repeat("0", 20) 252 | } 253 | line += "\n" 254 | 255 | f, err := os.OpenFile(c.file, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0600) 256 | if err != nil { 257 | log.Fatalf("opening keychain: %v", err) 258 | } 259 | f.Chmod(0600) 260 | 261 | if _, err := f.Write([]byte(line)); err != nil { 262 | log.Fatalf("adding key: %v", err) 263 | } 264 | if err := f.Close(); err != nil { 265 | log.Fatalf("adding key: %v", err) 266 | } 267 | } 268 | 269 | func (c *Keychain) code(name string) string { 270 | k, ok := c.keys[name] 271 | if !ok { 272 | log.Fatalf("no such key %q", name) 273 | } 274 | var code int 275 | if k.offset != 0 { 276 | n, err := strconv.ParseUint(string(c.data[k.offset:k.offset+counterLen]), 10, 64) 277 | if err != nil { 278 | log.Fatalf("malformed key counter for %q (%q)", name, c.data[k.offset:k.offset+counterLen]) 279 | } 280 | n++ 281 | code = hotp(k.raw, n, k.digits) 282 | f, err := os.OpenFile(c.file, os.O_RDWR, 0600) 283 | if err != nil { 284 | log.Fatalf("opening keychain: %v", err) 285 | } 286 | if _, err := f.WriteAt([]byte(fmt.Sprintf("%0*d", counterLen, n)), int64(k.offset)); err != nil { 287 | log.Fatalf("updating keychain: %v", err) 288 | } 289 | if err := f.Close(); err != nil { 290 | log.Fatalf("updating keychain: %v", err) 291 | } 292 | } else { 293 | // Time-based key. 294 | code = totp(k.raw, time.Now(), k.digits) 295 | } 296 | return fmt.Sprintf("%0*d", k.digits, code) 297 | } 298 | 299 | func (c *Keychain) show(name string) { 300 | code := c.code(name) 301 | if *flagClip { 302 | clipboard.WriteAll(code) 303 | } 304 | fmt.Printf("%s\n", code) 305 | } 306 | 307 | func (c *Keychain) showAll() { 308 | var names []string 309 | max := 0 310 | for name, k := range c.keys { 311 | names = append(names, name) 312 | if max < k.digits { 313 | max = k.digits 314 | } 315 | } 316 | sort.Strings(names) 317 | for _, name := range names { 318 | k := c.keys[name] 319 | code := strings.Repeat("-", k.digits) 320 | if k.offset == 0 { 321 | code = c.code(name) 322 | } 323 | fmt.Printf("%-*s\t%s\n", max, code, name) 324 | } 325 | } 326 | 327 | func decodeKey(key string) ([]byte, error) { 328 | return base32.StdEncoding.DecodeString(strings.ToUpper(key)) 329 | } 330 | 331 | func hotp(key []byte, counter uint64, digits int) int { 332 | h := hmac.New(sha1.New, key) 333 | binary.Write(h, binary.BigEndian, counter) 334 | sum := h.Sum(nil) 335 | v := binary.BigEndian.Uint32(sum[sum[len(sum)-1]&0x0F:]) & 0x7FFFFFFF 336 | d := uint32(1) 337 | for i := 0; i < digits && i < 8; i++ { 338 | d *= 10 339 | } 340 | return int(v % d) 341 | } 342 | 343 | func totp(key []byte, t time.Time, digits int) int { 344 | return hotp(key, uint64(t.UnixNano())/30e9, digits) 345 | } 346 | --------------------------------------------------------------------------------