pax_global_header00006660000000000000000000000064137526520050014517gustar00rootroot0000000000000052 comment=bd63651f7d7bdae4ec15a49eda5d9e83720d3442 otp-1.3.0/000077500000000000000000000000001375265200500123225ustar00rootroot00000000000000otp-1.3.0/.travis.yml000066400000000000000000000001221375265200500144260ustar00rootroot00000000000000arch: - amd64 - ppc64le language: go env: - GO111MODULE=on go: - "1.15" otp-1.3.0/LICENSE000066400000000000000000000261361375265200500133370ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. otp-1.3.0/NOTICE000066400000000000000000000001711375265200500132250ustar00rootroot00000000000000otp Copyright (c) 2014, Paul Querna This product includes software developed by Paul Querna (http://paul.querna.org/). otp-1.3.0/README.md000066400000000000000000000067711375265200500136140ustar00rootroot00000000000000# otp: One Time Password utilities Go / Golang [![PkgGoDev](https://pkg.go.dev/badge/github.com/pquerna/otp)](https://pkg.go.dev/github.com/pquerna/otp) [![Build Status](https://travis-ci.org/pquerna/otp.svg?branch=master)](https://travis-ci.org/pquerna/otp) # Why One Time Passwords? One Time Passwords (OTPs) are an mechanism to improve security over passwords alone. When a Time-based OTP (TOTP) is stored on a user's phone, and combined with something the user knows (Password), you have an easy on-ramp to [Multi-factor authentication](http://en.wikipedia.org/wiki/Multi-factor_authentication) without adding a dependency on a SMS provider. This Password and TOTP combination is used by many popular websites including Google, Github, Facebook, Salesforce and many others. The `otp` library enables you to easily add TOTPs to your own application, increasing your user's security against mass-password breaches and malware. Because TOTP is standardized and widely deployed, there are many [mobile clients and software implementations](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm#Client_implementations). ## `otp` Supports: * Generating QR Code images for easy user enrollment. * Time-based One-time Password Algorithm (TOTP) (RFC 6238): Time based OTP, the most commonly used method. * HMAC-based One-time Password Algorithm (HOTP) (RFC 4226): Counter based OTP, which TOTP is based upon. * Generation and Validation of codes for either algorithm. ## Implementing TOTP in your application: ### User Enrollment For an example of a working enrollment work flow, [Github has documented theirs](https://help.github.com/articles/configuring-two-factor-authentication-via-a-totp-mobile-app/ ), but the basics are: 1. Generate new TOTP Key for a User. `key,_ := totp.Generate(...)`. 1. Display the Key's Secret and QR-Code for the User. `key.Secret()` and `key.Image(...)`. 1. Test that the user can successfully use their TOTP. `totp.Validate(...)`. 1. Store TOTP Secret for the User in your backend. `key.Secret()` 1. Provide the user with "recovery codes". (See Recovery Codes bellow) ### Code Generation * In either TOTP or HOTP cases, use the `GenerateCode` function and a counter or `time.Time` struct to generate a valid code compatible with most implementations. * For uncommon or custom settings, or to catch unlikely errors, use `GenerateCodeCustom` in either module. ### Validation 1. Prompt and validate User's password as normal. 1. If the user has TOTP enabled, prompt for TOTP passcode. 1. Retrieve the User's TOTP Secret from your backend. 1. Validate the user's passcode. `totp.Validate(...)` ### Recovery Codes When a user loses access to their TOTP device, they would no longer have access to their account. Because TOTPs are often configured on mobile devices that can be lost, stolen or damaged, this is a common problem. For this reason many providers give their users "backup codes" or "recovery codes". These are a set of one time use codes that can be used instead of the TOTP. These can simply be randomly generated strings that you store in your backend. [Github's documentation provides an overview of the user experience]( https://help.github.com/articles/downloading-your-two-factor-authentication-recovery-codes/). ## Improvements, bugs, adding feature, etc: Please [open issues in Github](https://github.com/pquerna/otp/issues) for ideas, bugs, and general thoughts. Pull requests are of course preferred :) ## License `otp` is licensed under the [Apache License, Version 2.0](./LICENSE) otp-1.3.0/doc.go000066400000000000000000000043201375265200500134150ustar00rootroot00000000000000/** * Copyright 2014 Paul Querna * * 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 otp implements both HOTP and TOTP based // one time passcodes in a Google Authenticator compatible manner. // // When adding a TOTP for a user, you must store the "secret" value // persistently. It is recommend to store the secret in an encrypted field in your // datastore. Due to how TOTP works, it is not possible to store a hash // for the secret value like you would a password. // // To enroll a user, you must first generate an OTP for them. Google // Authenticator supports using a QR code as an enrollment method: // // import ( // "github.com/pquerna/otp/totp" // // "bytes" // "image/png" // ) // // key, err := totp.Generate(totp.GenerateOpts{ // Issuer: "Example.com", // AccountName: "alice@example.com", // }) // // // Convert TOTP key into a QR code encoded as a PNG image. // var buf bytes.Buffer // img, err := key.Image(200, 200) // png.Encode(&buf, img) // // // display the QR code to the user. // display(buf.Bytes()) // // // Now Validate that the user's successfully added the passcode. // passcode := promptForPasscode() // valid := totp.Validate(passcode, key.Secret()) // // if valid { // // User successfully used their TOTP, save it to your backend! // storeSecret("alice@example.com", key.Secret()) // } // // Validating a TOTP passcode is very easy, just prompt the user for a passcode // and retrieve the associated user's previously stored secret. // import "github.com/pquerna/otp/totp" // // passcode := promptForPasscode() // secret := getSecret("alice@example.com") // // valid := totp.Validate(passcode, secret) // // if valid { // // Success! continue login process. // } package otp otp-1.3.0/example/000077500000000000000000000000001375265200500137555ustar00rootroot00000000000000otp-1.3.0/example/main.go000066400000000000000000000036321375265200500152340ustar00rootroot00000000000000package main import ( "github.com/pquerna/otp" "github.com/pquerna/otp/totp" "bufio" "bytes" "encoding/base32" "fmt" "image/png" "io/ioutil" "os" "time" ) func display(key *otp.Key, data []byte) { fmt.Printf("Issuer: %s\n", key.Issuer()) fmt.Printf("Account Name: %s\n", key.AccountName()) fmt.Printf("Secret: %s\n", key.Secret()) fmt.Println("Writing PNG to qr-code.png....") ioutil.WriteFile("qr-code.png", data, 0644) fmt.Println("") fmt.Println("Please add your TOTP to your OTP Application now!") fmt.Println("") } func promptForPasscode() string { reader := bufio.NewReader(os.Stdin) fmt.Print("Enter Passcode: ") text, _ := reader.ReadString('\n') return text } // Demo function, not used in main // Generates Passcode using a UTF-8 (not base32) secret and custom paramters func GeneratePassCode(utf8string string) string{ secret := base32.StdEncoding.EncodeToString([]byte(utf8string)) passcode, err := totp.GenerateCodeCustom(secret, time.Now(), totp.ValidateOpts{ Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA512, }) if err != nil { panic(err) } return passcode } func main() { key, err := totp.Generate(totp.GenerateOpts{ Issuer: "Example.com", AccountName: "alice@example.com", }) if err != nil { panic(err) } // Convert TOTP key into a PNG var buf bytes.Buffer img, err := key.Image(200, 200) if err != nil { panic(err) } png.Encode(&buf, img) // display the QR code to the user. display(key, buf.Bytes()) // Now Validate that the user's successfully added the passcode. fmt.Println("Validating TOTP...") passcode := promptForPasscode() valid := totp.Validate(passcode, key.Secret()) if valid { println("Valid passcode!") os.Exit(0) } else { println("Invalid passcode!") os.Exit(1) } } otp-1.3.0/go.mod000066400000000000000000000002331375265200500134260ustar00rootroot00000000000000module github.com/pquerna/otp go 1.12 require ( github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc github.com/stretchr/testify v1.3.0 ) otp-1.3.0/go.sum000066400000000000000000000017721375265200500134640ustar00rootroot00000000000000github.com/boombuler/barcode v1.0.0 h1:s1TvRnXwL2xJRaccrdcBQMZxq6X7DvsMogtmJeHDdrc= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= otp-1.3.0/hotp/000077500000000000000000000000001375265200500132745ustar00rootroot00000000000000otp-1.3.0/hotp/hotp.go000066400000000000000000000130511375265200500145750ustar00rootroot00000000000000/** * Copyright 2014 Paul Querna * * 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 hotp import ( "github.com/pquerna/otp" "io" "crypto/hmac" "crypto/rand" "crypto/subtle" "encoding/base32" "encoding/binary" "fmt" "math" "net/url" "strings" ) const debug = false // Validate a HOTP passcode given a counter and secret. // This is a shortcut for ValidateCustom, with parameters that // are compataible with Google-Authenticator. func Validate(passcode string, counter uint64, secret string) bool { rv, _ := ValidateCustom( passcode, counter, secret, ValidateOpts{ Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }, ) return rv } // ValidateOpts provides options for ValidateCustom(). type ValidateOpts struct { // Digits as part of the input. Defaults to 6. Digits otp.Digits // Algorithm to use for HMAC. Defaults to SHA1. Algorithm otp.Algorithm } // GenerateCode creates a HOTP passcode given a counter and secret. // This is a shortcut for GenerateCodeCustom, with parameters that // are compataible with Google-Authenticator. func GenerateCode(secret string, counter uint64) (string, error) { return GenerateCodeCustom(secret, counter, ValidateOpts{ Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }) } // GenerateCodeCustom uses a counter and secret value and options struct to // create a passcode. func GenerateCodeCustom(secret string, counter uint64, opts ValidateOpts) (passcode string, err error) { // As noted in issue #10 and #17 this adds support for TOTP secrets that are // missing their padding. secret = strings.TrimSpace(secret) if n := len(secret) % 8; n != 0 { secret = secret + strings.Repeat("=", 8-n) } // As noted in issue #24 Google has started producing base32 in lower case, // but the StdEncoding (and the RFC), expect a dictionary of only upper case letters. secret = strings.ToUpper(secret) secretBytes, err := base32.StdEncoding.DecodeString(secret) if err != nil { return "", otp.ErrValidateSecretInvalidBase32 } buf := make([]byte, 8) mac := hmac.New(opts.Algorithm.Hash, secretBytes) binary.BigEndian.PutUint64(buf, counter) if debug { fmt.Printf("counter=%v\n", counter) fmt.Printf("buf=%v\n", buf) } mac.Write(buf) sum := mac.Sum(nil) // "Dynamic truncation" in RFC 4226 // http://tools.ietf.org/html/rfc4226#section-5.4 offset := sum[len(sum)-1] & 0xf value := int64(((int(sum[offset]) & 0x7f) << 24) | ((int(sum[offset+1] & 0xff)) << 16) | ((int(sum[offset+2] & 0xff)) << 8) | (int(sum[offset+3]) & 0xff)) l := opts.Digits.Length() mod := int32(value % int64(math.Pow10(l))) if debug { fmt.Printf("offset=%v\n", offset) fmt.Printf("value=%v\n", value) fmt.Printf("mod'ed=%v\n", mod) } return opts.Digits.Format(mod), nil } // ValidateCustom validates an HOTP with customizable options. Most users should // use Validate(). func ValidateCustom(passcode string, counter uint64, secret string, opts ValidateOpts) (bool, error) { passcode = strings.TrimSpace(passcode) if len(passcode) != opts.Digits.Length() { return false, otp.ErrValidateInputInvalidLength } otpstr, err := GenerateCodeCustom(secret, counter, opts) if err != nil { return false, err } if subtle.ConstantTimeCompare([]byte(otpstr), []byte(passcode)) == 1 { return true, nil } return false, nil } // GenerateOpts provides options for .Generate() type GenerateOpts struct { // Name of the issuing Organization/Company. Issuer string // Name of the User's Account (eg, email address) AccountName string // Size in size of the generated Secret. Defaults to 10 bytes. SecretSize uint // Secret to store. Defaults to a randomly generated secret of SecretSize. You should generally leave this empty. Secret []byte // Digits to request. Defaults to 6. Digits otp.Digits // Algorithm to use for HMAC. Defaults to SHA1. Algorithm otp.Algorithm // Reader to use for generating HOTP Key. Rand io.Reader } var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding) // Generate creates a new HOTP Key. func Generate(opts GenerateOpts) (*otp.Key, error) { // url encode the Issuer/AccountName if opts.Issuer == "" { return nil, otp.ErrGenerateMissingIssuer } if opts.AccountName == "" { return nil, otp.ErrGenerateMissingAccountName } if opts.SecretSize == 0 { opts.SecretSize = 10 } if opts.Digits == 0 { opts.Digits = otp.DigitsSix } if opts.Rand == nil { opts.Rand = rand.Reader } // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example v := url.Values{} if len(opts.Secret) != 0 { v.Set("secret", b32NoPadding.EncodeToString(opts.Secret)) } else { secret := make([]byte, opts.SecretSize) _, err := opts.Rand.Read(secret) if err != nil { return nil, err } v.Set("secret", b32NoPadding.EncodeToString(secret)) } v.Set("issuer", opts.Issuer) v.Set("algorithm", opts.Algorithm.String()) v.Set("digits", opts.Digits.String()) u := url.URL{ Scheme: "otpauth", Host: "hotp", Path: "/" + opts.Issuer + ":" + opts.AccountName, RawQuery: v.Encode(), } return otp.NewKeyFromURL(u.String()) } otp-1.3.0/hotp/hotp_test.go000066400000000000000000000131551375265200500156410ustar00rootroot00000000000000/** * Copyright 2014 Paul Querna * * 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 hotp import ( "github.com/pquerna/otp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "encoding/base32" "testing" ) type tc struct { Counter uint64 TOTP string Mode otp.Algorithm Secret string } var ( secSha1 = base32.StdEncoding.EncodeToString([]byte("12345678901234567890")) rfcMatrixTCs = []tc{ {0, "755224", otp.AlgorithmSHA1, secSha1}, {1, "287082", otp.AlgorithmSHA1, secSha1}, {2, "359152", otp.AlgorithmSHA1, secSha1}, {3, "969429", otp.AlgorithmSHA1, secSha1}, {4, "338314", otp.AlgorithmSHA1, secSha1}, {5, "254676", otp.AlgorithmSHA1, secSha1}, {6, "287922", otp.AlgorithmSHA1, secSha1}, {7, "162583", otp.AlgorithmSHA1, secSha1}, {8, "399871", otp.AlgorithmSHA1, secSha1}, {9, "520489", otp.AlgorithmSHA1, secSha1}, } ) // Test values from http://tools.ietf.org/html/rfc4226#appendix-D func TestValidateRFCMatrix(t *testing.T) { for _, tx := range rfcMatrixTCs { valid, err := ValidateCustom(tx.TOTP, tx.Counter, tx.Secret, ValidateOpts{ Digits: otp.DigitsSix, Algorithm: tx.Mode, }) require.NoError(t, err, "unexpected error totp=%s mode=%v counter=%v", tx.TOTP, tx.Mode, tx.Counter) require.True(t, valid, "unexpected totp failure totp=%s mode=%v counter=%v", tx.TOTP, tx.Mode, tx.Counter) } } func TestGenerateRFCMatrix(t *testing.T) { for _, tx := range rfcMatrixTCs { passcode, err := GenerateCodeCustom(tx.Secret, tx.Counter, ValidateOpts{ Digits: otp.DigitsSix, Algorithm: tx.Mode, }) assert.Nil(t, err) assert.Equal(t, tx.TOTP, passcode) } } func TestValidateInvalid(t *testing.T) { secSha1 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890")) valid, err := ValidateCustom("foo", 11, secSha1, ValidateOpts{ Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }) require.Equal(t, otp.ErrValidateInputInvalidLength, err, "Expected Invalid length error.") require.Equal(t, false, valid, "Valid should be false when we have an error.") valid, err = ValidateCustom("foo", 11, secSha1, ValidateOpts{ Digits: otp.DigitsEight, Algorithm: otp.AlgorithmSHA1, }) require.Equal(t, otp.ErrValidateInputInvalidLength, err, "Expected Invalid length error.") require.Equal(t, false, valid, "Valid should be false when we have an error.") valid, err = ValidateCustom("000000", 11, secSha1, ValidateOpts{ Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }) require.NoError(t, err, "Expected no error.") require.Equal(t, false, valid, "Valid should be false.") valid = Validate("000000", 11, secSha1) require.Equal(t, false, valid, "Valid should be false.") } // This tests for issue #10 - secrets without padding func TestValidatePadding(t *testing.T) { valid, err := ValidateCustom("831097", 0, "JBSWY3DPEHPK3PX", ValidateOpts{ Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }) require.NoError(t, err, "Expected no error.") require.Equal(t, true, valid, "Valid should be true.") } func TestValidateLowerCaseSecret(t *testing.T) { valid, err := ValidateCustom("831097", 0, "jbswy3dpehpk3px", ValidateOpts{ Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }) require.NoError(t, err, "Expected no error.") require.Equal(t, true, valid, "Valid should be true.") } func TestGenerate(t *testing.T) { k, err := Generate(GenerateOpts{ Issuer: "SnakeOil", AccountName: "alice@example.com", }) require.NoError(t, err, "generate basic TOTP") require.Equal(t, "SnakeOil", k.Issuer(), "Extracting Issuer") require.Equal(t, "alice@example.com", k.AccountName(), "Extracting Account Name") require.Equal(t, 16, len(k.Secret()), "Secret is 16 bytes long as base32.") k, err = Generate(GenerateOpts{ Issuer: "SnakeOil", AccountName: "alice@example.com", SecretSize: 20, }) require.NoError(t, err, "generate larger TOTP") require.Equal(t, 32, len(k.Secret()), "Secret is 32 bytes long as base32.") k, err = Generate(GenerateOpts{ Issuer: "", AccountName: "alice@example.com", }) require.Equal(t, otp.ErrGenerateMissingIssuer, err, "generate missing issuer") require.Nil(t, k, "key should be nil on error.") k, err = Generate(GenerateOpts{ Issuer: "Foobar, Inc", AccountName: "", }) require.Equal(t, otp.ErrGenerateMissingAccountName, err, "generate missing account name.") require.Nil(t, k, "key should be nil on error.") k, err = Generate(GenerateOpts{ Issuer: "SnakeOil", AccountName: "alice@example.com", SecretSize: 17, // anything that is not divisable by 5, really }) require.NoError(t, err, "Secret size is valid when length not divisable by 5.") require.NotContains(t, k.Secret(), "=", "Secret has no escaped characters.") k, err = Generate(GenerateOpts{ Issuer: "SnakeOil", AccountName: "alice@example.com", Secret: []byte("helloworld"), }) require.NoError(t, err, "Secret generation failed") sec, err := b32NoPadding.DecodeString(k.Secret()) require.NoError(t, err, "Secret wa not valid base32") require.Equal(t, sec, []byte("helloworld"), "Specified Secret was not kept") } otp-1.3.0/interop/000077500000000000000000000000001375265200500140025ustar00rootroot00000000000000otp-1.3.0/interop/go.mod000066400000000000000000000000571375265200500151120ustar00rootroot00000000000000module github.com/pquerna/otp/interop go 1.12 otp-1.3.0/interop/go.sum000066400000000000000000000000001375265200500151230ustar00rootroot00000000000000otp-1.3.0/interop/interop.go000066400000000000000000000000201375265200500160010ustar00rootroot00000000000000package interop otp-1.3.0/interop/twofactor_test.go000066400000000000000000000027131375265200500174030ustar00rootroot00000000000000/** * Copyright 2018 Paul Querna * * 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 interop_test import ( "testing" "time" "github.com/gokyle/twofactor" "github.com/pquerna/otp" "github.com/pquerna/otp/totp" "github.com/stretchr/testify/require" ) func TestTwoFactor(t *testing.T) { key, err := totp.Generate(totp.GenerateOpts{ Issuer: "Example.com", AccountName: "alice@example.com", Algorithm: otp.AlgorithmSHA512, }) require.NoError(t, err) require.NotNil(t, key) tf, label, err := twofactor.FromURL(key.URL()) require.NoError(t, err) require.NotNil(t, tf) require.Equal(t, "Example.com:alice@example.com", label) code := tf.OTP() require.NotEmpty(t, code) valid, err := totp.ValidateCustom(code, key.Secret(), time.Now().UTC(), totp.ValidateOpts{ Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA512, }, ) require.NoError(t, err) require.True(t, valid) } otp-1.3.0/otp.go000066400000000000000000000112631375265200500134560ustar00rootroot00000000000000/** * Copyright 2014 Paul Querna * * 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 otp import ( "github.com/boombuler/barcode" "github.com/boombuler/barcode/qr" "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/sha512" "errors" "fmt" "hash" "image" "net/url" "strings" "strconv" ) // Error when attempting to convert the secret from base32 to raw bytes. var ErrValidateSecretInvalidBase32 = errors.New("Decoding of secret as base32 failed.") // The user provided passcode length was not expected. var ErrValidateInputInvalidLength = errors.New("Input length unexpected") // When generating a Key, the Issuer must be set. var ErrGenerateMissingIssuer = errors.New("Issuer must be set") // When generating a Key, the Account Name must be set. var ErrGenerateMissingAccountName = errors.New("AccountName must be set") // Key represents an TOTP or HTOP key. type Key struct { orig string url *url.URL } // NewKeyFromURL creates a new Key from an TOTP or HOTP url. // // The URL format is documented here: // https://github.com/google/google-authenticator/wiki/Key-Uri-Format // func NewKeyFromURL(orig string) (*Key, error) { s := strings.TrimSpace(orig) u, err := url.Parse(s) if err != nil { return nil, err } return &Key{ orig: s, url: u, }, nil } func (k *Key) String() string { return k.orig } // Image returns an QR-Code image of the specified width and height, // suitable for use by many clients like Google-Authenricator // to enroll a user's TOTP/HOTP key. func (k *Key) Image(width int, height int) (image.Image, error) { b, err := qr.Encode(k.orig, qr.M, qr.Auto) if err != nil { return nil, err } b, err = barcode.Scale(b, width, height) if err != nil { return nil, err } return b, nil } // Type returns "hotp" or "totp". func (k *Key) Type() string { return k.url.Host } // Issuer returns the name of the issuing organization. func (k *Key) Issuer() string { q := k.url.Query() issuer := q.Get("issuer") if issuer != "" { return issuer } p := strings.TrimPrefix(k.url.Path, "/") i := strings.Index(p, ":") if i == -1 { return "" } return p[:i] } // AccountName returns the name of the user's account. func (k *Key) AccountName() string { p := strings.TrimPrefix(k.url.Path, "/") i := strings.Index(p, ":") if i == -1 { return p } return p[i+1:] } // Secret returns the opaque secret for this Key. func (k *Key) Secret() string { q := k.url.Query() return q.Get("secret") } // Period returns a tiny int representing the rotation time in seconds. func (k *Key) Period() uint64 { q := k.url.Query() if u, err := strconv.ParseUint(q.Get("period"), 10, 64); err == nil { return u } // If no period is defined 30 seconds is the default per (rfc6238) return 30 } // URL returns the OTP URL as a string func (k *Key) URL() string { return k.url.String() } // Algorithm represents the hashing function to use in the HMAC // operation needed for OTPs. type Algorithm int const ( // AlgorithmSHA1 should be used for compatibility with Google Authenticator. // // See https://github.com/pquerna/otp/issues/55 for additional details. AlgorithmSHA1 Algorithm = iota AlgorithmSHA256 AlgorithmSHA512 AlgorithmMD5 ) func (a Algorithm) String() string { switch a { case AlgorithmSHA1: return "SHA1" case AlgorithmSHA256: return "SHA256" case AlgorithmSHA512: return "SHA512" case AlgorithmMD5: return "MD5" } panic("unreached") } func (a Algorithm) Hash() hash.Hash { switch a { case AlgorithmSHA1: return sha1.New() case AlgorithmSHA256: return sha256.New() case AlgorithmSHA512: return sha512.New() case AlgorithmMD5: return md5.New() } panic("unreached") } // Digits represents the number of digits present in the // user's OTP passcode. Six and Eight are the most common values. type Digits int const ( DigitsSix Digits = 6 DigitsEight Digits = 8 ) // Format converts an integer into the zero-filled size for this Digits. func (d Digits) Format(in int32) string { f := fmt.Sprintf("%%0%dd", d) return fmt.Sprintf(f, in) } // Length returns the number of characters for this Digits. func (d Digits) Length() int { return int(d) } func (d Digits) String() string { return fmt.Sprintf("%d", d) } otp-1.3.0/otp_test.go000066400000000000000000000037361375265200500145230ustar00rootroot00000000000000/** * Copyright 2014 Paul Querna * * 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 otp import ( "github.com/stretchr/testify/require" "testing" ) func TestKeyAllThere(t *testing.T) { k, err := NewKeyFromURL(`otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example`) require.NoError(t, err, "failed to parse url") require.Equal(t, "totp", k.Type(), "Extracting Type") require.Equal(t, "Example", k.Issuer(), "Extracting Issuer") require.Equal(t, "alice@google.com", k.AccountName(), "Extracting Account Name") require.Equal(t, "JBSWY3DPEHPK3PXP", k.Secret(), "Extracting Secret") } func TestKeyIssuerOnlyInPath(t *testing.T) { k, err := NewKeyFromURL(`otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP`) require.NoError(t, err, "failed to parse url") require.Equal(t, "Example", k.Issuer(), "Extracting Issuer") require.Equal(t, "alice@google.com", k.AccountName(), "Extracting Account Name") } func TestKeyNoIssuer(t *testing.T) { k, err := NewKeyFromURL(`otpauth://totp/alice@google.com?secret=JBSWY3DPEHPK3PXP`) require.NoError(t, err, "failed to parse url") require.Equal(t, "", k.Issuer(), "Extracting Issuer") require.Equal(t, "alice@google.com", k.AccountName(), "Extracting Account Name") } func TestKeyWithNewLine(t *testing.T) { w, err := NewKeyFromURL(`otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP `) require.NoError(t, err) sec := w.Secret() require.Equal(t, "JBSWY3DPEHPK3PXP", sec) } otp-1.3.0/totp/000077500000000000000000000000001375265200500133105ustar00rootroot00000000000000otp-1.3.0/totp/totp.go000066400000000000000000000131001375265200500146200ustar00rootroot00000000000000/** * Copyright 2014 Paul Querna * * 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 totp import ( "github.com/pquerna/otp" "github.com/pquerna/otp/hotp" "io" "crypto/rand" "encoding/base32" "math" "net/url" "strconv" "time" ) // Validate a TOTP using the current time. // A shortcut for ValidateCustom, Validate uses a configuration // that is compatible with Google-Authenticator and most clients. func Validate(passcode string, secret string) bool { rv, _ := ValidateCustom( passcode, secret, time.Now().UTC(), ValidateOpts{ Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }, ) return rv } // GenerateCode creates a TOTP token using the current time. // A shortcut for GenerateCodeCustom, GenerateCode uses a configuration // that is compatible with Google-Authenticator and most clients. func GenerateCode(secret string, t time.Time) (string, error) { return GenerateCodeCustom(secret, t, ValidateOpts{ Period: 30, Skew: 1, Digits: otp.DigitsSix, Algorithm: otp.AlgorithmSHA1, }) } // ValidateOpts provides options for ValidateCustom(). type ValidateOpts struct { // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds. Period uint // Periods before or after the current time to allow. Value of 1 allows up to Period // of either side of the specified time. Defaults to 0 allowed skews. Values greater // than 1 are likely sketchy. Skew uint // Digits as part of the input. Defaults to 6. Digits otp.Digits // Algorithm to use for HMAC. Defaults to SHA1. Algorithm otp.Algorithm } // GenerateCodeCustom takes a timepoint and produces a passcode using a // secret and the provided opts. (Under the hood, this is making an adapted // call to hotp.GenerateCodeCustom) func GenerateCodeCustom(secret string, t time.Time, opts ValidateOpts) (passcode string, err error) { if opts.Period == 0 { opts.Period = 30 } counter := uint64(math.Floor(float64(t.Unix()) / float64(opts.Period))) passcode, err = hotp.GenerateCodeCustom(secret, counter, hotp.ValidateOpts{ Digits: opts.Digits, Algorithm: opts.Algorithm, }) if err != nil { return "", err } return passcode, nil } // ValidateCustom validates a TOTP given a user specified time and custom options. // Most users should use Validate() to provide an interpolatable TOTP experience. func ValidateCustom(passcode string, secret string, t time.Time, opts ValidateOpts) (bool, error) { if opts.Period == 0 { opts.Period = 30 } counters := []uint64{} counter := int64(math.Floor(float64(t.Unix()) / float64(opts.Period))) counters = append(counters, uint64(counter)) for i := 1; i <= int(opts.Skew); i++ { counters = append(counters, uint64(counter+int64(i))) counters = append(counters, uint64(counter-int64(i))) } for _, counter := range counters { rv, err := hotp.ValidateCustom(passcode, counter, secret, hotp.ValidateOpts{ Digits: opts.Digits, Algorithm: opts.Algorithm, }) if err != nil { return false, err } if rv == true { return true, nil } } return false, nil } // GenerateOpts provides options for Generate(). The default values // are compatible with Google-Authenticator. type GenerateOpts struct { // Name of the issuing Organization/Company. Issuer string // Name of the User's Account (eg, email address) AccountName string // Number of seconds a TOTP hash is valid for. Defaults to 30 seconds. Period uint // Size in size of the generated Secret. Defaults to 20 bytes. SecretSize uint // Secret to store. Defaults to a randomly generated secret of SecretSize. You should generally leave this empty. Secret []byte // Digits to request. Defaults to 6. Digits otp.Digits // Algorithm to use for HMAC. Defaults to SHA1. Algorithm otp.Algorithm // Reader to use for generating TOTP Key. Rand io.Reader } var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding) // Generate a new TOTP Key. func Generate(opts GenerateOpts) (*otp.Key, error) { // url encode the Issuer/AccountName if opts.Issuer == "" { return nil, otp.ErrGenerateMissingIssuer } if opts.AccountName == "" { return nil, otp.ErrGenerateMissingAccountName } if opts.Period == 0 { opts.Period = 30 } if opts.SecretSize == 0 { opts.SecretSize = 20 } if opts.Digits == 0 { opts.Digits = otp.DigitsSix } if opts.Rand == nil { opts.Rand = rand.Reader } // otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example v := url.Values{} if len(opts.Secret) != 0 { v.Set("secret", b32NoPadding.EncodeToString(opts.Secret)) } else { secret := make([]byte, opts.SecretSize) _, err := opts.Rand.Read(secret) if err != nil { return nil, err } v.Set("secret", b32NoPadding.EncodeToString(secret)) } v.Set("issuer", opts.Issuer) v.Set("period", strconv.FormatUint(uint64(opts.Period), 10)) v.Set("algorithm", opts.Algorithm.String()) v.Set("digits", opts.Digits.String()) u := url.URL{ Scheme: "otpauth", Host: "totp", Path: "/" + opts.Issuer + ":" + opts.AccountName, RawQuery: v.Encode(), } return otp.NewKeyFromURL(u.String()) } otp-1.3.0/totp/totp_test.go000066400000000000000000000132441375265200500156700ustar00rootroot00000000000000/** * Copyright 2014 Paul Querna * * 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 totp import ( "github.com/pquerna/otp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "encoding/base32" "testing" "time" ) type tc struct { TS int64 TOTP string Mode otp.Algorithm Secret string } var ( secSha1 = base32.StdEncoding.EncodeToString([]byte("12345678901234567890")) secSha256 = base32.StdEncoding.EncodeToString([]byte("12345678901234567890123456789012")) secSha512 = base32.StdEncoding.EncodeToString([]byte("1234567890123456789012345678901234567890123456789012345678901234")) rfcMatrixTCs = []tc{ {59, "94287082", otp.AlgorithmSHA1, secSha1}, {59, "46119246", otp.AlgorithmSHA256, secSha256}, {59, "90693936", otp.AlgorithmSHA512, secSha512}, {1111111109, "07081804", otp.AlgorithmSHA1, secSha1}, {1111111109, "68084774", otp.AlgorithmSHA256, secSha256}, {1111111109, "25091201", otp.AlgorithmSHA512, secSha512}, {1111111111, "14050471", otp.AlgorithmSHA1, secSha1}, {1111111111, "67062674", otp.AlgorithmSHA256, secSha256}, {1111111111, "99943326", otp.AlgorithmSHA512, secSha512}, {1234567890, "89005924", otp.AlgorithmSHA1, secSha1}, {1234567890, "91819424", otp.AlgorithmSHA256, secSha256}, {1234567890, "93441116", otp.AlgorithmSHA512, secSha512}, {2000000000, "69279037", otp.AlgorithmSHA1, secSha1}, {2000000000, "90698825", otp.AlgorithmSHA256, secSha256}, {2000000000, "38618901", otp.AlgorithmSHA512, secSha512}, {20000000000, "65353130", otp.AlgorithmSHA1, secSha1}, {20000000000, "77737706", otp.AlgorithmSHA256, secSha256}, {20000000000, "47863826", otp.AlgorithmSHA512, secSha512}, } ) // // Test vectors from http://tools.ietf.org/html/rfc6238#appendix-B // NOTE -- the test vectors are documented as having the SAME // secret -- this is WRONG -- they have a variable secret // depending upon the hmac algorithm: // http://www.rfc-editor.org/errata_search.php?rfc=6238 // this only took a few hours of head/desk interaction to figure out. // func TestValidateRFCMatrix(t *testing.T) { for _, tx := range rfcMatrixTCs { valid, err := ValidateCustom(tx.TOTP, tx.Secret, time.Unix(tx.TS, 0).UTC(), ValidateOpts{ Digits: otp.DigitsEight, Algorithm: tx.Mode, }) require.NoError(t, err, "unexpected error totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS) require.True(t, valid, "unexpected totp failure totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS) } } func TestGenerateRFCTCs(t *testing.T) { for _, tx := range rfcMatrixTCs { passcode, err := GenerateCodeCustom(tx.Secret, time.Unix(tx.TS, 0).UTC(), ValidateOpts{ Digits: otp.DigitsEight, Algorithm: tx.Mode, }) assert.Nil(t, err) assert.Equal(t, tx.TOTP, passcode) } } func TestValidateSkew(t *testing.T) { secSha1 := base32.StdEncoding.EncodeToString([]byte("12345678901234567890")) tests := []tc{ {29, "94287082", otp.AlgorithmSHA1, secSha1}, {59, "94287082", otp.AlgorithmSHA1, secSha1}, {61, "94287082", otp.AlgorithmSHA1, secSha1}, } for _, tx := range tests { valid, err := ValidateCustom(tx.TOTP, tx.Secret, time.Unix(tx.TS, 0).UTC(), ValidateOpts{ Digits: otp.DigitsEight, Algorithm: tx.Mode, Skew: 1, }) require.NoError(t, err, "unexpected error totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS) require.True(t, valid, "unexpected totp failure totp=%s mode=%v ts=%v", tx.TOTP, tx.Mode, tx.TS) } } func TestGenerate(t *testing.T) { k, err := Generate(GenerateOpts{ Issuer: "SnakeOil", AccountName: "alice@example.com", }) require.NoError(t, err, "generate basic TOTP") require.Equal(t, "SnakeOil", k.Issuer(), "Extracting Issuer") require.Equal(t, "alice@example.com", k.AccountName(), "Extracting Account Name") require.Equal(t, 32, len(k.Secret()), "Secret is 32 bytes long as base32.") k, err = Generate(GenerateOpts{ Issuer: "SnakeOil", AccountName: "alice@example.com", SecretSize: 20, }) require.NoError(t, err, "generate larger TOTP") require.Equal(t, 32, len(k.Secret()), "Secret is 32 bytes long as base32.") k, err = Generate(GenerateOpts{ Issuer: "SnakeOil", AccountName: "alice@example.com", SecretSize: 13, // anything that is not divisable by 5, really }) require.NoError(t, err, "Secret size is valid when length not divisable by 5.") require.NotContains(t, k.Secret(), "=", "Secret has no escaped characters.") k, err = Generate(GenerateOpts{ Issuer: "SnakeOil", AccountName: "alice@example.com", Secret: []byte("helloworld"), }) require.NoError(t, err, "Secret generation failed") sec, err := b32NoPadding.DecodeString(k.Secret()) require.NoError(t, err, "Secret wa not valid base32") require.Equal(t, sec, []byte("helloworld"), "Specified Secret was not kept") } func TestGoogleLowerCaseSecret(t *testing.T) { w, err := otp.NewKeyFromURL(`otpauth://totp/Google%3Afoo%40example.com?secret=qlt6vmy6svfx4bt4rpmisaiyol6hihca&issuer=Google`) require.NoError(t, err) sec := w.Secret() require.Equal(t, "qlt6vmy6svfx4bt4rpmisaiyol6hihca", sec) n := time.Now().UTC() code, err := GenerateCode(w.Secret(), n) require.NoError(t, err) valid := Validate(code, w.Secret()) require.True(t, valid) }