diff --git a/src/main/java/com/j256/twofactorauth/TimeBasedOneTimePasswordUtil.java b/src/main/java/com/j256/twofactorauth/TimeBasedOneTimePasswordUtil.java index 10f78e6..e67cf47 100644 --- a/src/main/java/com/j256/twofactorauth/TimeBasedOneTimePasswordUtil.java +++ b/src/main/java/com/j256/twofactorauth/TimeBasedOneTimePasswordUtil.java @@ -1,5 +1,7 @@ package com.j256.twofactorauth; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; import java.security.GeneralSecurityException; import java.security.SecureRandom; import java.util.Arrays; @@ -472,11 +474,13 @@ public static String qrImageUrl(String keyId, String secret, int numDigits) { * The dimension of the image, width and height. Can be set to {@link #DEFAULT_QR_DIMENTION}. */ public static String qrImageUrl(String keyId, String secret, int numDigits, int imageDimension) { - StringBuilder sb = new StringBuilder(128); - sb.append("https://chart.googleapis.com/chart?chs=" + imageDimension + "x" + imageDimension + "&cht=qr&chl=" - + imageDimension + "x" + imageDimension + "&chld=M|0&cht=qr&chl="); - addOtpAuthPart(keyId, secret, sb, numDigits); - return sb.toString(); + return new StringBuilder(128) + .append("https://chart.googleapis.com/chart?chs=") + .append(imageDimension).append('x').append(imageDimension) + .append("&cht=qr&chl=").append(imageDimension).append('x').append(imageDimension) + .append("&chld=M|0&cht=qr&chl=") + .append(urlEncodeSegmentOrQuery(generateOtpAuthUrl(keyId, secret, numDigits))) + .toString(); } /** @@ -506,18 +510,23 @@ public static String generateOtpAuthUrl(String keyId, String secret) { * The number of digits" of the OTP. */ public static String generateOtpAuthUrl(String keyId, String secret, int numDigits) { - StringBuilder sb = new StringBuilder(128); - addOtpAuthPart(keyId, secret, sb, numDigits); - return sb.toString(); - } - - private static void addOtpAuthPart(String keyId, String secret, StringBuilder sb, int numDigits) { - sb.append("otpauth://totp/") - .append(keyId) - .append("%3Fsecret%3D") - .append(secret) - .append("%26digits%3D") - .append(numDigits); + return new StringBuilder(128) + .append("otpauth://totp/") + .append(urlEncodeSegmentOrQuery(keyId)) + .append("?secret=") + .append(urlEncodeSegmentOrQuery(secret)) + .append("&digits=") + .append(numDigits) + .toString(); + } + + private static String urlEncodeSegmentOrQuery(String raw) + { + try { + return URLEncoder.encode(raw, "UTF-8").replace("+", "%20"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Could not url-encode as UTF-8 is missing", e); + } } private static boolean validateCurrentNumber(byte[] key, int authNumber, long windowMillis, long timeMillis, diff --git a/src/test/java/com/j256/twofactorauth/TimeBasedOneTimePasswordUtilTest.java b/src/test/java/com/j256/twofactorauth/TimeBasedOneTimePasswordUtilTest.java index 0b492b3..06c5901 100644 --- a/src/test/java/com/j256/twofactorauth/TimeBasedOneTimePasswordUtilTest.java +++ b/src/test/java/com/j256/twofactorauth/TimeBasedOneTimePasswordUtilTest.java @@ -245,6 +245,17 @@ public void testHexWindow() throws GeneralSecurityException { } } + @Test + public void testQrImageUrl() { + String keyId = "Schl\u00FCssel"; + String secret = "ny4A5CPJZ46LXZCP"; + assertEquals("otpauth://totp/Schl%C3%BCssel?secret=ny4A5CPJZ46LXZCP&digits=6", TimeBasedOneTimePasswordUtil.generateOtpAuthUrl(keyId, secret)); + assertEquals("otpauth://totp/Schl%C3%BCssel?secret=ny4A5CPJZ46LXZCP&digits=8", TimeBasedOneTimePasswordUtil.generateOtpAuthUrl(keyId, secret, 8)); + assertEquals("https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=200x200&chld=M|0&cht=qr&chl=otpauth%3A%2F%2Ftotp%2FSchl%25C3%25BCssel%3Fsecret%3Dny4A5CPJZ46LXZCP%26digits%3D6", TimeBasedOneTimePasswordUtil.qrImageUrl(keyId, secret)); + assertEquals("https://chart.googleapis.com/chart?chs=200x200&cht=qr&chl=200x200&chld=M|0&cht=qr&chl=otpauth%3A%2F%2Ftotp%2FSchl%25C3%25BCssel%3Fsecret%3Dny4A5CPJZ46LXZCP%26digits%3D3", TimeBasedOneTimePasswordUtil.qrImageUrl(keyId, secret, 3)); + assertEquals("https://chart.googleapis.com/chart?chs=500x500&cht=qr&chl=500x500&chld=M|0&cht=qr&chl=otpauth%3A%2F%2Ftotp%2FSchl%25C3%25BCssel%3Fsecret%3Dny4A5CPJZ46LXZCP%26digits%3D3", TimeBasedOneTimePasswordUtil.qrImageUrl(keyId, secret, 3, 500)); + } + @Test public void testCoverage() throws GeneralSecurityException { String secret = "ny4A5CPJZ46LXZCP"; @@ -262,11 +273,6 @@ public void testCoverage() throws GeneralSecurityException { assertTrue(num >= 0 && num < 1000000); num = TimeBasedOneTimePasswordUtil.generateCurrentNumber(secret, 3); assertTrue(num >= 0 && num < 1000); - assertNotNull(TimeBasedOneTimePasswordUtil.generateOtpAuthUrl("key", secret)); - assertNotNull(TimeBasedOneTimePasswordUtil.generateOtpAuthUrl("key", secret, 8)); - assertNotNull(TimeBasedOneTimePasswordUtil.qrImageUrl("key", secret)); - assertNotNull(TimeBasedOneTimePasswordUtil.qrImageUrl("key", secret, 3)); - assertNotNull(TimeBasedOneTimePasswordUtil.qrImageUrl("key", secret, 3, 500)); String hexSecret = "0123456789abcdefABCDEF"; num = TimeBasedOneTimePasswordUtil.generateCurrentNumberHex(hexSecret);