├── .gitignore ├── README.md ├── LICENSE └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | two-factor-auth 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple CLI app that generates tokens compatible with Google Authenticator. I implemented this mainly to understand how it works, you should probably not use this. 2 | 3 | Example output: 4 | 5 | ```sh 6 | $ go run main.go "" 7 | 934523 (17 second(s) remaining) 8 | ``` 9 | 10 | Relevant RFCs: 11 | 12 | * http://tools.ietf.org/html/rfc4226 13 | * http://tools.ietf.org/html/rfc6238 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Robbie Vanbrabant 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/base32" 7 | "fmt" 8 | "os" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | func toBytes(value int64) []byte { 14 | var result []byte 15 | mask := int64(0xFF) 16 | shifts := [8]uint16{56, 48, 40, 32, 24, 16, 8, 0} 17 | for _, shift := range shifts { 18 | result = append(result, byte((value>>shift)&mask)) 19 | } 20 | return result 21 | } 22 | 23 | func toUint32(bytes []byte) uint32 { 24 | return (uint32(bytes[0]) << 24) + (uint32(bytes[1]) << 16) + 25 | (uint32(bytes[2]) << 8) + uint32(bytes[3]) 26 | } 27 | 28 | func oneTimePassword(key []byte, value []byte) uint32 { 29 | // sign the value using HMAC-SHA1 30 | hmacSha1 := hmac.New(sha1.New, key) 31 | hmacSha1.Write(value) 32 | hash := hmacSha1.Sum(nil) 33 | 34 | // We're going to use a subset of the generated hash. 35 | // Using the last nibble (half-byte) to choose the index to start from. 36 | // This number is always appropriate as it's maximum decimal 15, the hash will 37 | // have the maximum index 19 (20 bytes of SHA1) and we need 4 bytes. 38 | offset := hash[len(hash)-1] & 0x0F 39 | 40 | // get a 32-bit (4-byte) chunk from the hash starting at offset 41 | hashParts := hash[offset : offset+4] 42 | 43 | // ignore the most significant bit as per RFC 4226 44 | hashParts[0] = hashParts[0] & 0x7F 45 | 46 | number := toUint32(hashParts) 47 | 48 | // size to 6 digits 49 | // one million is the first number with 7 digits so the remainder 50 | // of the division will always return < 7 digits 51 | pwd := number % 1000000 52 | 53 | return pwd 54 | } 55 | 56 | // all []byte in this program are treated as Big Endian 57 | func main() { 58 | if len(os.Args) < 2 { 59 | fmt.Fprintln(os.Stderr, "must specify key to use") 60 | os.Exit(1) 61 | } 62 | 63 | input := os.Args[1] 64 | 65 | // decode the key from the first argument 66 | inputNoSpaces := strings.Replace(input, " ", "", -1) 67 | inputNoSpacesUpper := strings.ToUpper(inputNoSpaces) 68 | key, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(inputNoSpacesUpper) 69 | if err != nil { 70 | fmt.Fprintln(os.Stderr, err.Error()) 71 | os.Exit(1) 72 | } 73 | 74 | // generate a one-time password using the time at 30-second intervals 75 | epochSeconds := time.Now().Unix() 76 | pwd := oneTimePassword(key, toBytes(epochSeconds/30)) 77 | 78 | secondsRemaining := 30 - (epochSeconds % 30) 79 | fmt.Printf("%06d (%d second(s) remaining)\n", pwd, secondsRemaining) 80 | } 81 | --------------------------------------------------------------------------------