Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions hotp/hotp.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ type GenerateOpts struct {
Digits otp.Digits
// Algorithm to use for HMAC. Defaults to SHA1.
Algorithm otp.Algorithm
// Optional image to display next to the code in supported authenticators.
ImageURL string
// Reader to use for generating HOTP Key.
Rand io.Reader
}
Expand Down Expand Up @@ -226,6 +228,10 @@ func Generate(opts GenerateOpts) (*otp.Key, error) {
v.Set("algorithm", opts.Algorithm.String())
v.Set("digits", opts.Digits.String())

if opts.ImageURL != "" {
v.Set("image", opts.ImageURL)
}

u := url.URL{
Scheme: "otpauth",
Host: "hotp",
Expand Down
10 changes: 10 additions & 0 deletions hotp/hotp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ func TestGenerate(t *testing.T) {
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.")
require.NotContains(t, k.String(), "image=")

k, err = Generate(GenerateOpts{
Issuer: "Snake Oil",
Expand Down Expand Up @@ -199,4 +200,13 @@ func TestGenerate(t *testing.T) {
sec, err := b32NoPadding.DecodeString(k.Secret())
require.NoError(t, err, "Secret was not valid base32")
require.Equal(t, sec, []byte("helloworld"), "Specified Secret was not kept")

k, err = Generate(GenerateOpts{
Issuer: "SnakeOil",
AccountName: "alice@example.com",
ImageURL: "https://example.com/icon.png?size=200",
})
require.NoError(t, err, "generate HOTP with image")
require.Equal(t, "https://example.com/icon.png?size=200", k.ImageURL())
require.Contains(t, k.String(), "image=https:%2F%2Fexample.com%2Ficon.png%3Fsize=200")
}
7 changes: 7 additions & 0 deletions otp.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ func (k *Key) Encoder() Encoder {
}
}

// ImageURL returns an optional image URL for this Key.
// See https://github.com/freeotp/freeotp-android/blob/master/URI.md#image
func (k *Key) ImageURL() string {
q := k.url.Query()
return q.Get("image")
}

// URL returns the OTP URL as a string
func (k *Key) URL() string {
return k.url.String()
Expand Down
12 changes: 12 additions & 0 deletions otp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ func TestKeyNoIssuer(t *testing.T) {
require.Equal(t, "alice@google.com", k.AccountName(), "Extracting Account Name")
}

func TestKeyImageURL(t *testing.T) {
k, err := NewKeyFromURL(`otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example&image=https%3A%2F%2Fexample.com%2Ficon.png%3Fsize%3D200`)
require.NoError(t, err)
require.Equal(t, "https://example.com/icon.png?size=200", k.ImageURL())
}

func TestKeyImageURLMissing(t *testing.T) {
k, err := NewKeyFromURL(`otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example`)
require.NoError(t, err)
require.Equal(t, "", k.ImageURL())
}

func TestKeyWithNewLine(t *testing.T) {
w, err := NewKeyFromURL(`otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP
`)
Expand Down
6 changes: 6 additions & 0 deletions totp/totp.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ type GenerateOpts struct {
Digits otp.Digits
// Algorithm to use for HMAC. Defaults to SHA1.
Algorithm otp.Algorithm
// Optional image to display next to the code in supported authenticators.
ImageURL string
// Reader to use for generating TOTP Key.
Rand io.Reader
}
Expand Down Expand Up @@ -199,6 +201,10 @@ func Generate(opts GenerateOpts) (*otp.Key, error) {
v.Set("algorithm", opts.Algorithm.String())
v.Set("digits", opts.Digits.String())

if opts.ImageURL != "" {
v.Set("image", opts.ImageURL)
}

u := url.URL{
Scheme: "otpauth",
Host: "totp",
Expand Down
10 changes: 10 additions & 0 deletions totp/totp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ func TestGenerate(t *testing.T) {
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.")
require.NotContains(t, k.String(), "image=")

k, err = Generate(GenerateOpts{
Issuer: "Snake Oil",
Expand Down Expand Up @@ -160,6 +161,15 @@ func TestGenerate(t *testing.T) {
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")

k, err = Generate(GenerateOpts{
Issuer: "SnakeOil",
AccountName: "alice@example.com",
ImageURL: "https://example.com/icon.png?size=200",
})
require.NoError(t, err, "generate TOTP with image")
require.Equal(t, "https://example.com/icon.png?size=200", k.ImageURL())
require.Contains(t, k.String(), "image=https:%2F%2Fexample.com%2Ficon.png%3Fsize=200")
}

func TestGoogleLowerCaseSecret(t *testing.T) {
Expand Down