From 8427a916ffb1f16d91c5c4e5f074b50fbacd7395 Mon Sep 17 00:00:00 2001 From: Jose Torres Date: Mon, 31 Aug 2020 18:40:14 -0500 Subject: [PATCH 1/7] #5 :: Add method to export the KeyUri as a base64 encoded image containing a QR which allows to register the OTP secrets in authenticator apps. --- authenticator/authenticator.go | 26 +++++++++++++++++++ authenticator/authenticator_test.go | 39 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/authenticator/authenticator.go b/authenticator/authenticator.go index 58c9c3d..a23bb0d 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() } +// Base64QR will encode the value returned by KeyUri.String into a Base64QR code that can be +// displayed and then scanned by the user. The return value is the base64 +// encoded image data. +func (ku *KeyUri) Base64QR() (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..2be1874 100644 --- a/authenticator/authenticator_test.go +++ b/authenticator/authenticator_test.go @@ -71,3 +71,42 @@ func TestKeyUri_String(t *testing.T) { } } } + +func TestKeyUri_Base64QR(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.Base64QR() + + 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) + } +} From 97b113ca64cbf813b8a8e827e3cc777d83d15648 Mon Sep 17 00:00:00 2001 From: Jose Torres Date: Mon, 31 Aug 2020 18:40:42 -0500 Subject: [PATCH 2/7] #5 :: Update go modules with new dependencies. --- go.mod | 2 ++ go.sum | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 go.sum 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= From b73a177f521a6ae0c41972e0b668cbdaccf5ca2b Mon Sep 17 00:00:00 2001 From: Jose Torres Date: Tue, 1 Sep 2020 09:29:30 -0500 Subject: [PATCH 3/7] #5 :: Update the planned functionality. --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 795aae0..4668074 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,6 @@ Implements [RFC 4226][rfc4226] and [RFC 6238][rfc6238]. # Contents - [Supported Operations](#supported-operations) -- [Planned Functionality](#planned-functionality) - [Reading Material](#reading-material) - [Usage](#usage) - [Generating Codes](#generating-codes) @@ -23,8 +22,6 @@ Implements [RFC 4226][rfc4226] and [RFC 6238][rfc6238]. ## Supported Operations - Generate HOTP and TOTP codes. - Verify HOTP an TOTP codes. - -## Planned Functionality - Generate QR code image to register secrets in authenticator apps. ## Reading Material From 8617bd103db2d50940922001613415dc6c5d32aa Mon Sep 17 00:00:00 2001 From: Jose Torres Date: Wed, 2 Sep 2020 10:03:47 -0500 Subject: [PATCH 4/7] #5 :: Use a clearer name for the QR method. --- authenticator/authenticator.go | 8 ++++---- authenticator/authenticator_test.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/authenticator/authenticator.go b/authenticator/authenticator.go index a23bb0d..42b2439 100644 --- a/authenticator/authenticator.go +++ b/authenticator/authenticator.go @@ -29,10 +29,10 @@ func (ku *KeyUri) String() string { return uri.String() } -// Base64QR will encode the value returned by KeyUri.String into a Base64QR code that can be -// displayed and then scanned by the user. The return value is the base64 -// encoded image data. -func (ku *KeyUri) Base64QR() (string, error) { +// 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) diff --git a/authenticator/authenticator_test.go b/authenticator/authenticator_test.go index 2be1874..1e69687 100644 --- a/authenticator/authenticator_test.go +++ b/authenticator/authenticator_test.go @@ -72,7 +72,7 @@ func TestKeyUri_String(t *testing.T) { } } -func TestKeyUri_Base64QR(t *testing.T) { +func TestKeyUri_QRCode(t *testing.T) { ku := KeyUri{ Type: "mock", Label: Label{ @@ -99,7 +99,7 @@ func TestKeyUri_Base64QR(t *testing.T) { "pPZPHUPtqjj5EzA+9VzZPWV1SYccv/7RWxy4Fvfmqr+BQAA///VaqA18lDV5wAAAABJRU5" + "ErkJggg==" - qr, err := ku.Base64QR() + qr, err := ku.QRCode() if err != nil { t.Errorf("unexpected error: %s", err) From 14527f63d7523509b543b0a26905621913283faa Mon Sep 17 00:00:00 2001 From: Jose Torres Date: Wed, 2 Sep 2020 10:04:27 -0500 Subject: [PATCH 5/7] #5 :: Update usage examples with the qr functionality. --- examples/hotp/main.go | 6 ++++-- examples/totp/main.go | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) 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]) } From 99f0bf35abc2e1238747b65da652e69ee90c46c3 Mon Sep 17 00:00:00 2001 From: Jose Torres Date: Wed, 2 Sep 2020 10:23:20 -0500 Subject: [PATCH 6/7] #5 :: Update the main types to return references to the KeyUri. --- hotp.go | 4 ++-- totp.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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, From 56f67df1bcfd0c5ed3c4af1cc99a41425567e3fd Mon Sep 17 00:00:00 2001 From: Jose Torres Date: Wed, 2 Sep 2020 10:23:42 -0500 Subject: [PATCH 7/7] #5 :: Update the readme with the qr usage. --- README.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d46a4cf..c4a03fb 100644 --- a/README.md +++ b/README.md @@ -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