Skip to content

Commit 41e2b06

Browse files
author
Steve Coffman
committed
Initial commit
0 parents  commit 41e2b06

File tree

8 files changed

+600
-0
lines changed

8 files changed

+600
-0
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright © 2019 StevenACoffman
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
keyfob is a two-factor authentication agent suitable for AWS and Github. Works pretty much the same as Google Authenticator, but uses your laptop's keychain.
2+
3+
Usage:
4+
5+
go get -u github.com/StevenACoffman/keyfob
6+
7+
keyfob add [name] [key]
8+
keyfob otp [name]
9+
keyfob help
10+
11+
`keyfob add name` adds a new key to the keyfob 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+
The new key generates time-based (TOTP) authentication codes.
17+
18+
`keyfob opt [name]` prints a One Time Password (aka two-factor authentication) code from the key with the
19+
given name. If `--clip` is specified, `keyfob` also copies to the code to the system
20+
clipboard.
21+
22+
With no arguments, `keyfob` prints two-factor authentication codes from all
23+
known time-based keys.
24+
25+
The time-based authentication codes are derived from a hash of the
26+
key and the current time, so it is important that the system clock have at
27+
least one-minute accuracy.
28+
29+
The keychain is stored unencrypted in the text file `$HOME/.keyfob`.
30+
31+
## Example
32+
33+
During GitHub 2FA setup, at the “Scan this barcode with your app” step,
34+
click the “enter this text code instead” link. A window pops up showing
35+
“your two-factor secret,” a short string of letters and digits.
36+
37+
Add it to keyfob under the name github, typing the secret at the prompt:
38+
39+
$ keyfob add github
40+
keyfob key for github: nzxxiidbebvwk6jb
41+
$
42+
43+
Then whenever GitHub prompts for a 2FA code, run keyfob to obtain one:
44+
45+
$ keyfob otp github
46+
268346
47+
$
48+
49+
## Derivation
50+
51+
This is just a little toy cobbled together from [2fa](https://github.com/rsc/2fa/), [cobra](https://github.com/spf13/cobra), and [go-keyring](https://github.com/zalando/go-keyring).
52+
53+
Unlike 2fa, this doesn't support listing all the stored codes, or adding 7 or 8 character long TOTP, or counter-based (HOTP) codes. Pillaging ... ehrm... adapting the 2fa code to do that in here would be easy, but I don't need it.
54+
55+
## Really, does this make sense?
56+
57+
At least to me, it does. My laptop features encrypted storage, a stronger authentication mechanism, and I take good care of its physical integrity.
58+
59+
My phone also runs arbitrary apps, is constantly connected to the Internet, gets forgotten on tables.
60+
61+
Thanks to the convenience of a command line utility, I'm more likely to enable MFA in more places.
62+
63+
Clearly a win for security.
64+
65+
## Dependencies
66+
67+
#### OS X
68+
69+
The OS X implementation depends on the `/usr/bin/security` binary for
70+
interfacing with the OS X keychain. It should be available by default.
71+
72+
#### Linux
73+
74+
The Linux implementation depends on the [Secret Service][SecretService] dbus
75+
interface, which is provided by [GNOME Keyring](https://wiki.gnome.org/Projects/GnomeKeyring).
76+
77+
It's expected that the default collection `login` exists in the keyring, because
78+
it's the default in most distros. If it doesn't exist, you can create it through the
79+
keyring frontend program [Seahorse](https://wiki.gnome.org/Apps/Seahorse):
80+
81+
* Open `seahorse`
82+
* Go to **File > New > Password Keyring**
83+
* Click **Continue**
84+
* When asked for a name, use: **login**
85+

cmd/add.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
Copyright © 2019 NAME HERE <EMAIL ADDRESS>
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package cmd
17+
18+
import (
19+
"bufio"
20+
"fmt"
21+
"github.com/spf13/cobra"
22+
"log"
23+
"os"
24+
"strings"
25+
"github.com/zalando/go-keyring"
26+
"unicode"
27+
)
28+
29+
// addCmd represents the add command
30+
var addCmd = &cobra.Command{
31+
Use: "add [key name] [optional key value]",
32+
Short: "adds a new key to the keychain with the given name",
33+
Long: `adds a new key to the keychain with the given name.
34+
It prints a prompt to standard error and reads a two-factor key from standard input.
35+
Two-factor keys are short case-insensitive strings of letters A-Z and digits 2-7.`,
36+
Args: cobra.RangeArgs(1, 2),
37+
Run: func(cmd *cobra.Command, args []string) {
38+
fmt.Println("add called:" + strings.Join(args, " "))
39+
40+
service := "keyfob"
41+
name := args[0]
42+
43+
var text string
44+
45+
if len(args) == 1 {
46+
fmt.Fprintf(os.Stderr, "2fa key for %s: ", name)
47+
text, err := bufio.NewReader(os.Stdin).ReadString('\n')
48+
if err != nil {
49+
log.Fatalf("error reading key: %v", err)
50+
}
51+
text = strings.Map(noSpace, text)
52+
text += strings.Repeat("=", -len(text)&7) // pad to 8 bytes
53+
54+
} else {
55+
56+
text = args[1]
57+
}
58+
59+
if _, err := decodeKey(text); err != nil {
60+
log.Fatalf("invalid key: %v", err)
61+
}
62+
63+
err := keyring.Set(service, name, text)
64+
if err != nil {
65+
log.Fatalf("Unable to write application password for keyfob: %v", err)
66+
}
67+
},
68+
}
69+
70+
func init() {
71+
rootCmd.AddCommand(addCmd)
72+
73+
// Here you will define your flags and configuration settings.
74+
75+
// Cobra supports Persistent Flags which will work for this command
76+
// and all subcommands, e.g.:
77+
// addCmd.PersistentFlags().String("foo", "", "A help for foo")
78+
79+
// Cobra supports local flags which will only run when this command
80+
// is called directly, e.g.:
81+
// addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
82+
}
83+
84+
func noSpace(r rune) rune {
85+
if unicode.IsSpace(r) {
86+
return -1
87+
}
88+
return r
89+
}

cmd/otp.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
Copyright © 2019 NAME HERE <EMAIL ADDRESS>
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
package cmd
17+
18+
import (
19+
"crypto/hmac"
20+
"crypto/sha1"
21+
"encoding/base32"
22+
"encoding/binary"
23+
"fmt"
24+
"github.com/atotto/clipboard"
25+
"github.com/spf13/cobra"
26+
"github.com/zalando/go-keyring"
27+
"log"
28+
"strings"
29+
"time"
30+
)
31+
32+
// otpCmd represents the otp command
33+
var otpCmd = &cobra.Command{
34+
Use: "otp [key name]",
35+
Short: "Generate a One Time Password",
36+
Long: `otp name prints a two-factor authentication code from the key with the given name.
37+
If -clip is specified, otp also copies to the code to the system clipboard.
38+
With no arguments, otp prints two-factor authentication codes from all known time-based keys.
39+
40+
The default time-based authentication codes are derived from a hash of the key and the current time,
41+
so it is important that the system clock have at least one-minute accuracy.`,
42+
Args: cobra.ExactArgs(1),
43+
Run: func(cmd *cobra.Command, args []string) {
44+
45+
service := "keyfob"
46+
user := args[0]
47+
secret, err := keyring.Get(service, user)
48+
if err != nil {
49+
log.Fatal(err)
50+
}
51+
raw, err := decodeKey(secret)
52+
if err == nil {
53+
code := totp(raw, time.Now(), 6)
54+
codeText := fmt.Sprintf("%0*d", 6, code)
55+
56+
if clip {
57+
clipboard.WriteAll(codeText)
58+
}
59+
60+
fmt.Printf("%s\n", codeText)
61+
return
62+
}
63+
log.Printf("%s: malformed key", secret)
64+
},
65+
}
66+
67+
var clip bool
68+
69+
func init() {
70+
rootCmd.AddCommand(otpCmd)
71+
72+
// Here you will define your flags and configuration settings.
73+
74+
// Cobra supports Persistent Flags which will work for this command
75+
// and all subcommands, e.g.:
76+
// otpCmd.PersistentFlags().String("foo", "", "A help for foo")
77+
78+
// Cobra supports local flags which will only run when this command
79+
// is called directly, e.g.:
80+
otpCmd.Flags().BoolVarP(&clip, "clip", "c", false, "If -clip is specified, also copies the code to the system clipboard.")
81+
}
82+
83+
func decodeKey(key string) ([]byte, error) {
84+
return base32.StdEncoding.DecodeString(strings.ToUpper(key))
85+
}
86+
87+
func hotp(key []byte, counter uint64, digits int) int {
88+
h := hmac.New(sha1.New, key)
89+
binary.Write(h, binary.BigEndian, counter)
90+
sum := h.Sum(nil)
91+
v := binary.BigEndian.Uint32(sum[sum[len(sum)-1]&0x0F:]) & 0x7FFFFFFF
92+
d := uint32(1)
93+
for i := 0; i < digits && i < 8; i++ {
94+
d *= 10
95+
}
96+
return int(v % d)
97+
}
98+
99+
func totp(key []byte, t time.Time, digits int) int {
100+
return hotp(key, uint64(t.UnixNano())/30e9, digits)
101+
}

0 commit comments

Comments
 (0)