-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Steve Coffman
committed
Jun 22, 2019
0 parents
commit 41e2b06
Showing
8 changed files
with
600 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
The MIT License (MIT) | ||
|
||
Copyright © 2019 StevenACoffman | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
in the Software without restriction, including without limitation the rights | ||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
copies of the Software, and to permit persons to whom the Software is | ||
furnished to do so, subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in | ||
all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | ||
THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
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. | ||
|
||
Usage: | ||
|
||
go get -u github.com/StevenACoffman/keyfob | ||
|
||
keyfob add [name] [key] | ||
keyfob otp [name] | ||
keyfob help | ||
|
||
`keyfob add name` adds a new key to the keyfob 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. | ||
|
||
The new key generates time-based (TOTP) authentication codes. | ||
|
||
`keyfob opt [name]` prints a One Time Password (aka two-factor authentication) code from the key with the | ||
given name. If `--clip` is specified, `keyfob` also copies to the code to the system | ||
clipboard. | ||
|
||
With no arguments, `keyfob` prints two-factor authentication codes from all | ||
known time-based keys. | ||
|
||
The 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/.keyfob`. | ||
|
||
## 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 keyfob under the name github, typing the secret at the prompt: | ||
|
||
$ keyfob add github | ||
keyfob key for github: nzxxiidbebvwk6jb | ||
$ | ||
|
||
Then whenever GitHub prompts for a 2FA code, run keyfob to obtain one: | ||
|
||
$ keyfob otp github | ||
268346 | ||
$ | ||
|
||
## Derivation | ||
|
||
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). | ||
|
||
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. | ||
|
||
## Really, does this make sense? | ||
|
||
At least to me, it does. My laptop features encrypted storage, a stronger authentication mechanism, and I take good care of its physical integrity. | ||
|
||
My phone also runs arbitrary apps, is constantly connected to the Internet, gets forgotten on tables. | ||
|
||
Thanks to the convenience of a command line utility, I'm more likely to enable MFA in more places. | ||
|
||
Clearly a win for security. | ||
|
||
## Dependencies | ||
|
||
#### OS X | ||
|
||
The OS X implementation depends on the `/usr/bin/security` binary for | ||
interfacing with the OS X keychain. It should be available by default. | ||
|
||
#### Linux | ||
|
||
The Linux implementation depends on the [Secret Service][SecretService] dbus | ||
interface, which is provided by [GNOME Keyring](https://wiki.gnome.org/Projects/GnomeKeyring). | ||
|
||
It's expected that the default collection `login` exists in the keyring, because | ||
it's the default in most distros. If it doesn't exist, you can create it through the | ||
keyring frontend program [Seahorse](https://wiki.gnome.org/Apps/Seahorse): | ||
|
||
* Open `seahorse` | ||
* Go to **File > New > Password Keyring** | ||
* Click **Continue** | ||
* When asked for a name, use: **login** | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
/* | ||
Copyright © 2019 NAME HERE <EMAIL ADDRESS> | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
package cmd | ||
|
||
import ( | ||
"bufio" | ||
"fmt" | ||
"github.com/spf13/cobra" | ||
"log" | ||
"os" | ||
"strings" | ||
"github.com/zalando/go-keyring" | ||
"unicode" | ||
) | ||
|
||
// addCmd represents the add command | ||
var addCmd = &cobra.Command{ | ||
Use: "add [key name] [optional key value]", | ||
Short: "adds a new key to the keychain with the given name", | ||
Long: `adds a new key to the 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.`, | ||
Args: cobra.RangeArgs(1, 2), | ||
Run: func(cmd *cobra.Command, args []string) { | ||
fmt.Println("add called:" + strings.Join(args, " ")) | ||
|
||
service := "keyfob" | ||
name := args[0] | ||
|
||
var text string | ||
|
||
if len(args) == 1 { | ||
fmt.Fprintf(os.Stderr, "2fa key for %s: ", name) | ||
text, err := bufio.NewReader(os.Stdin).ReadString('\n') | ||
if err != nil { | ||
log.Fatalf("error reading key: %v", err) | ||
} | ||
text = strings.Map(noSpace, text) | ||
text += strings.Repeat("=", -len(text)&7) // pad to 8 bytes | ||
|
||
} else { | ||
|
||
text = args[1] | ||
} | ||
|
||
if _, err := decodeKey(text); err != nil { | ||
log.Fatalf("invalid key: %v", err) | ||
} | ||
|
||
err := keyring.Set(service, name, text) | ||
if err != nil { | ||
log.Fatalf("Unable to write application password for keyfob: %v", err) | ||
} | ||
}, | ||
} | ||
|
||
func init() { | ||
rootCmd.AddCommand(addCmd) | ||
|
||
// Here you will define your flags and configuration settings. | ||
|
||
// Cobra supports Persistent Flags which will work for this command | ||
// and all subcommands, e.g.: | ||
// addCmd.PersistentFlags().String("foo", "", "A help for foo") | ||
|
||
// Cobra supports local flags which will only run when this command | ||
// is called directly, e.g.: | ||
// addCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") | ||
} | ||
|
||
func noSpace(r rune) rune { | ||
if unicode.IsSpace(r) { | ||
return -1 | ||
} | ||
return r | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
/* | ||
Copyright © 2019 NAME HERE <EMAIL ADDRESS> | ||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
http://www.apache.org/licenses/LICENSE-2.0 | ||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
*/ | ||
package cmd | ||
|
||
import ( | ||
"crypto/hmac" | ||
"crypto/sha1" | ||
"encoding/base32" | ||
"encoding/binary" | ||
"fmt" | ||
"github.com/atotto/clipboard" | ||
"github.com/spf13/cobra" | ||
"github.com/zalando/go-keyring" | ||
"log" | ||
"strings" | ||
"time" | ||
) | ||
|
||
// otpCmd represents the otp command | ||
var otpCmd = &cobra.Command{ | ||
Use: "otp [key name]", | ||
Short: "Generate a One Time Password", | ||
Long: `otp name prints a two-factor authentication code from the key with the given name. | ||
If -clip is specified, otp also copies to the code to the system clipboard. | ||
With no arguments, otp 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.`, | ||
Args: cobra.ExactArgs(1), | ||
Run: func(cmd *cobra.Command, args []string) { | ||
|
||
service := "keyfob" | ||
user := args[0] | ||
secret, err := keyring.Get(service, user) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
raw, err := decodeKey(secret) | ||
if err == nil { | ||
code := totp(raw, time.Now(), 6) | ||
codeText := fmt.Sprintf("%0*d", 6, code) | ||
|
||
if clip { | ||
clipboard.WriteAll(codeText) | ||
} | ||
|
||
fmt.Printf("%s\n", codeText) | ||
return | ||
} | ||
log.Printf("%s: malformed key", secret) | ||
}, | ||
} | ||
|
||
var clip bool | ||
|
||
func init() { | ||
rootCmd.AddCommand(otpCmd) | ||
|
||
// Here you will define your flags and configuration settings. | ||
|
||
// Cobra supports Persistent Flags which will work for this command | ||
// and all subcommands, e.g.: | ||
// otpCmd.PersistentFlags().String("foo", "", "A help for foo") | ||
|
||
// Cobra supports local flags which will only run when this command | ||
// is called directly, e.g.: | ||
otpCmd.Flags().BoolVarP(&clip, "clip", "c", false, "If -clip is specified, also copies the code to the system clipboard.") | ||
} | ||
|
||
func decodeKey(key string) ([]byte, error) { | ||
return base32.StdEncoding.DecodeString(strings.ToUpper(key)) | ||
} | ||
|
||
func hotp(key []byte, counter uint64, digits int) int { | ||
h := hmac.New(sha1.New, key) | ||
binary.Write(h, binary.BigEndian, counter) | ||
sum := h.Sum(nil) | ||
v := binary.BigEndian.Uint32(sum[sum[len(sum)-1]&0x0F:]) & 0x7FFFFFFF | ||
d := uint32(1) | ||
for i := 0; i < digits && i < 8; i++ { | ||
d *= 10 | ||
} | ||
return int(v % d) | ||
} | ||
|
||
func totp(key []byte, t time.Time, digits int) int { | ||
return hotp(key, uint64(t.UnixNano())/30e9, digits) | ||
} |
Oops, something went wrong.