diff --git a/README.md b/README.md index 90afd31..c4a03fb 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,9 @@ Implements [RFC 4226][rfc4226] and [RFC 6238][rfc6238]. - Generate HOTP and TOTP codes. - Verify HOTP an TOTP codes. - Export OTP config as a [Google Authenticator URI][googleURI]. +- Export OTP config as a QR code image (used to register secrets in authenticator apps). ## Planned Functionality -- Export OTP config as a QR code image (used to register secrets in authenticator apps). - Export OTP config as a JSON. ## Reading Material @@ -118,7 +118,16 @@ The former being the preferred way because of the ease of use and the avoidance of human error. #### QR Code -TODO +To generate the QR code just get the `KeyUri` and call the `QRCode` method: +```go +otp := otpgo.TOTP{} +base64EncodedQRImage, _ := otp. + KeyUri("john.doe@example.org", "A Company"). + QRCode() + +// Then use base64EncodedQRImage however you like +// e.g.: send it to the client to display as an image +``` #### Manual registration TODO diff --git a/authenticator/authenticator.go b/authenticator/authenticator.go index 58c9c3d..42b2439 100644 --- a/authenticator/authenticator.go +++ b/authenticator/authenticator.go @@ -1,8 +1,15 @@ package authenticator import ( + "encoding/base64" "fmt" "net/url" + + "github.com/skip2/go-qrcode" +) + +const ( + QRSize = 256 ) // The KeyUri type holds all the config necessary to generate a valid @@ -22,6 +29,25 @@ func (ku *KeyUri) String() string { return uri.String() } +// QRCode will encode the value returned by KeyUri.String into a base64 +// encoded image containing a QR code that can be displayed and then scanned by +// the user. The return value is the base64 encoded image data. +func (ku *KeyUri) QRCode() (string, error) { + uri := ku.String() + + qr, err := qrcode.New(uri, qrcode.Medium) + if err != nil { + return "", err + } + + bytes, err := qr.PNG(QRSize) + if err != nil { + return "", err + } + + return "data:image/png;base64," + base64.StdEncoding.EncodeToString(bytes), nil +} + // The Label is used to identify which account a key is associated with. type Label struct { AccountName string // Should be a username, email, etc. diff --git a/authenticator/authenticator_test.go b/authenticator/authenticator_test.go index 818f990..1e69687 100644 --- a/authenticator/authenticator_test.go +++ b/authenticator/authenticator_test.go @@ -71,3 +71,42 @@ func TestKeyUri_String(t *testing.T) { } } } + +func TestKeyUri_QRCode(t *testing.T) { + ku := KeyUri{ + Type: "mock", + Label: Label{ + AccountName: "J0hn@example.com", + Issuer: "Example Co.", + }, + Parameters: mockFormatter("$p3c!al C#4rs"), + } + + expected := "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEAAQMAAAB" + + "mvDolAAAABlBMVEX///8AAABVwtN+AAACtklEQVR42uyZzY3sIBCEC3HoIyGQiUlstNiax" + + "JhMCIEjB+R66p4f7+oFYCxN35b9DgNuV1VjfOtbFywhyQfJNazwRJQiO5yutgsBFZBHQ9i" + + "arzcgAmR3DfAzAaQ82hJ2ZJKeSABcy7xPBhSk51HXAaAvUwLaDxvJgRFZeurXAwApWMIa1" + + "nivvnZ066A/HXU2YK9eW8LWbvFeR5TSF9sW5wGeFR7tJ4w4MNDRXf9P684FpNo+CnLLZPU" + + "0fVjwEz4iNgFAEokqYysyfRUKWcLnoC8BIGoT88E1sAKsSNxlD2y3eQD9ydYwgK++et0DO" + + "wLb8eqdDgAd2jBh48ANCgCqzEdfzwCYW+xh134wRZNdF5DbdQCpHaL/fluz2sdfvzgfQNR" + + "Mo7uASjGrlO7I9hGQOQAkzYctP/0CPWGRAnfow/mAVCQkdbDAeovKY5EdP2GdB1CFMMfYW" + + "uaAryRcX7Q52pWAnjqCSvHQ8QE9yS7lcL0ZAKE2TChha9rgiMrDcXvr3AyAmpo2DNXFbmD" + + "tSdiBH2AqQHfxaLl5rtHXnmwyy+1agD6N9px7NE9KYTE9eSMTAJYnNZmTrLlaw7jumuPAR" + + "ABST/bDEe8WF2182MMxiJ0OaMiRouMD1HWHnXxXbfYTAbqCoJFxjevrT9nD9lHaKwDCrsO" + + "D44qXNcP8wh3JfAZANOQocrdkbpFXo9m7oyYBNMQ21zzv1DlHeW7HfDEH0JfwkVZNj2prC" + + "AOXAgrVL/RZACO+rkV+Z6DTASuN4mpqGnNeofdXnjwfeN0mAVlTjTmvGUjmTIBdJlsbI2q" + + "m7UmKFOSPPkwB2A1nsVvaWxzRdkE7+6sBOgJxxDWyknx+XhiYCyiv8SHbtR2A5ZcUTwG8L" + + "pPZPHUPtqjj5EzA+9VzZPWV1SYccv/7RWxy4Fvfmqr+BQAA///VaqA18lDV5wAAAABJRU5" + + "ErkJggg==" + + qr, err := ku.QRCode() + + if err != nil { + t.Errorf("unexpected error: %s", err) + t.FailNow() + } + + if expected != qr { + t.Errorf("unexpected qr\nexpected: %s\n actual: %s", expected, qr) + } +} diff --git a/examples/hotp/main.go b/examples/hotp/main.go index fada151..4b1e06b 100644 --- a/examples/hotp/main.go +++ b/examples/hotp/main.go @@ -54,6 +54,8 @@ func main() { ku := h.KeyUri(aUsername, anIssuer) // From here you can get the plain text uri. - msg = "Exporting config for \"%s\" at \"%s\":\n\t- Plain URI --> %s\n" - fmt.Printf(msg, aUsername, anIssuer, ku.String()) + msg = "Exporting config for \"%s\" at \"%s\":\n\t- Plain URI --> %s\n\t- QR code image, base 64 encoded (" + + "truncated to save space) --> %s...\n" + qr, _ := ku.QRCode() + fmt.Printf(msg, aUsername, anIssuer, ku.String(), qr[0:200]) } diff --git a/examples/totp/main.go b/examples/totp/main.go index 7b70cae..0be08ee 100644 --- a/examples/totp/main.go +++ b/examples/totp/main.go @@ -46,6 +46,8 @@ func main() { ku := t.KeyUri(aUsername, anIssuer) // From here you can get the plain text uri. - msg = "Exporting config for \"%s\" at \"%s\":\n\t- Plain URI --> %s\n" - fmt.Printf(msg, aUsername, anIssuer, ku.String()) + msg = "Exporting config for \"%s\" at \"%s\":\n\t- Plain URI --> %s\n\t- QR code image, base 64 encoded (" + + "truncated to save space) --> %s...\n" + qr, _ := ku.QRCode() + fmt.Printf(msg, aUsername, anIssuer, ku.String(), qr[0:200]) } diff --git a/go.mod b/go.mod index 30c85c5..f2f21cb 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/jltorresm/otpgo go 1.14 + +require github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e99b5b9 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= +github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= diff --git a/hotp.go b/hotp.go index 97e1e4d..685d136 100644 --- a/hotp.go +++ b/hotp.go @@ -86,8 +86,8 @@ func (h *HOTP) Validate(token string) (bool, error) { // KeyUri return an authenticator.KeyUri configured with the current HOTP params. // - accountName is the username or email of the account // - issuer is the site or org -func (h *HOTP) KeyUri(accountName, issuer string) authenticator.KeyUri { - return authenticator.KeyUri{ +func (h *HOTP) KeyUri(accountName, issuer string) *authenticator.KeyUri { + return &authenticator.KeyUri{ Type: "hotp", Label: authenticator.Label{ AccountName: accountName, diff --git a/totp.go b/totp.go index 28c2923..f0526b3 100644 --- a/totp.go +++ b/totp.go @@ -91,8 +91,8 @@ func (t *TOTP) Validate(token string) (bool, error) { // KeyUri return an authenticator.KeyUri configured with the current TOTP params. // - accountName is the username or email of the account // - issuer is the site or org -func (t *TOTP) KeyUri(accountName, issuer string) authenticator.KeyUri { - return authenticator.KeyUri{ +func (t *TOTP) KeyUri(accountName, issuer string) *authenticator.KeyUri { + return &authenticator.KeyUri{ Type: "totp", Label: authenticator.Label{ AccountName: accountName,