From 8723d1c85c623617a870ad0e127c7188062c56ae Mon Sep 17 00:00:00 2001 From: NRSrivastava Date: Sun, 13 Jun 2021 12:34:52 +0530 Subject: [PATCH 1/4] Accepts user-defined recovery code's length. Differentiated between readable and actual code --- .gitignore | 3 + .../samstevens/totp/recovery/CodePack.java | 11 +++ .../totp/recovery/InsecureCodeException.java | 10 +++ .../totp/recovery/RecoveryCodeGenerator.java | 64 ++++++++++++++--- .../recovery/RecoveryCodeGeneratorTest.java | 71 ++++++++++++++++--- 5 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 totp/src/main/java/dev/samstevens/totp/recovery/CodePack.java create mode 100644 totp/src/main/java/dev/samstevens/totp/recovery/InsecureCodeException.java diff --git a/.gitignore b/.gitignore index 1f7d008..5855832 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ target .project .classpath bin + +# NetBeans +nb-configuration.xml diff --git a/totp/src/main/java/dev/samstevens/totp/recovery/CodePack.java b/totp/src/main/java/dev/samstevens/totp/recovery/CodePack.java new file mode 100644 index 0000000..0007465 --- /dev/null +++ b/totp/src/main/java/dev/samstevens/totp/recovery/CodePack.java @@ -0,0 +1,11 @@ +package dev.samstevens.totp.recovery; + +public class CodePack { + public String readableCode=null; + public String actualCode=null; + + public CodePack(String readableCode, String actualCode) { + this.readableCode = readableCode; + this.actualCode = actualCode; + } +} diff --git a/totp/src/main/java/dev/samstevens/totp/recovery/InsecureCodeException.java b/totp/src/main/java/dev/samstevens/totp/recovery/InsecureCodeException.java new file mode 100644 index 0000000..b43110b --- /dev/null +++ b/totp/src/main/java/dev/samstevens/totp/recovery/InsecureCodeException.java @@ -0,0 +1,10 @@ +package dev.samstevens.totp.recovery; + +import java.security.InvalidParameterException; + +public class InsecureCodeException extends InvalidParameterException { + public InsecureCodeException(){} + public InsecureCodeException(String s){ + super(s); + } +} diff --git a/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java b/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java index 3100dff..7233127 100644 --- a/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java +++ b/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java @@ -9,45 +9,87 @@ public class RecoveryCodeGenerator { // Recovery code must reach a minimum entropy to be secured // code entropy = log( {characters-count} ^ {code-length} ) / log(2) - // the settings used below allows the code to reach an entropy of 82 bits : + // the default settings used below allows the code to reach an entropy of 82 bits : // log(36^16) / log(2) == 82.7... + // the minimum allowed settings used below allows the code to reach a minimum entropy of 36 bits : + // log(36^7) / log(2) == 36.18.. which is considered reasonable; fairly secure passwords + // Recovery code must be simple to read and enter by end user when needed : // - generate a code composed of numbers and lower case characters from latin alphabet (36 possible characters) // - split code in groups separated with dash for better readability, for example 4ckn-xspn-et8t-xgr0 private static final char[] CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); - private static final int CODE_LENGTH = 16; - private static final int GROUPS_NBR = 4; + private static int CODE_LENGTH; + private static int GROUPS_NBR; private Random random = new SecureRandom(); - public String[] generateCodes(int amount) { + public RecoveryCodeGenerator(){ + CODE_LENGTH = 16; + GROUPS_NBR = 4; + } + + public RecoveryCodeGenerator(int codeLength,int groupNumber){ + if (groupNumber < 1) { + throw new InvalidParameterException("Group Number must be at least 1."); + } + if(codeLength < 7){ + throw new InsecureCodeException("Code Length must be at least 7 to be secure"); + } + + CODE_LENGTH=codeLength; + GROUPS_NBR=groupNumber; + } + + public static class Builder{ + private int codeLength=16; + private int groupNumber=4; + + public Builder setCodeLength(int codeLength) { + this.codeLength = codeLength; + return this; + } + + public Builder setGroupNumber(int groupNumber) { + this.groupNumber = groupNumber; + return this; + } + + public RecoveryCodeGenerator build(){ + return new RecoveryCodeGenerator(codeLength,groupNumber); + } + } + + public CodePack[] generateCodes(int amount) { // Must generate at least one code if (amount < 1) { throw new InvalidParameterException("Amount must be at least 1."); } // Create an array and fill with generated codes - String[] codes = new String[amount]; + CodePack[] codes = new CodePack[amount]; Arrays.setAll(codes, i -> generateCode()); return codes; } - private String generateCode() { - final StringBuilder code = new StringBuilder(CODE_LENGTH + (CODE_LENGTH/GROUPS_NBR) - 1); - + private CodePack generateCode() { + final StringBuilder code_readable = new StringBuilder(CODE_LENGTH + (CODE_LENGTH/GROUPS_NBR) - 1); + final StringBuilder code_actual = new StringBuilder(CODE_LENGTH); + for (int i = 0; i < CODE_LENGTH; i++) { // Append random character from authorized ones - code.append(CHARACTERS[random.nextInt(CHARACTERS.length)]); + char c =CHARACTERS[random.nextInt(CHARACTERS.length)]; + code_actual.append(c); + code_readable.append(c); // Split code into groups for increased readability if ((i+1) % GROUPS_NBR == 0 && (i+1) != CODE_LENGTH) { - code.append("-"); + code_readable.append("-"); } } - return code.toString(); + return new CodePack(code_readable.toString(),code_actual.toString()); } } \ No newline at end of file diff --git a/totp/src/test/java/dev/samstevens/totp/recovery/RecoveryCodeGeneratorTest.java b/totp/src/test/java/dev/samstevens/totp/recovery/RecoveryCodeGeneratorTest.java index c2c27b2..0850d10 100644 --- a/totp/src/test/java/dev/samstevens/totp/recovery/RecoveryCodeGeneratorTest.java +++ b/totp/src/test/java/dev/samstevens/totp/recovery/RecoveryCodeGeneratorTest.java @@ -1,10 +1,12 @@ package dev.samstevens.totp.recovery; import org.junit.jupiter.api.Test; + import java.security.InvalidParameterException; import java.util.Arrays; -import java.util.HashSet; import java.util.Set; +import java.util.stream.Collectors; + import static org.junit.jupiter.api.Assertions.*; public class RecoveryCodeGeneratorTest { @@ -12,32 +14,43 @@ public class RecoveryCodeGeneratorTest { @Test public void testCorrectAmountGenerated() { RecoveryCodeGenerator generator = new RecoveryCodeGenerator(); - String[] codes = generator.generateCodes(16); + CodePack[] codes = generator.generateCodes(16); // Assert 16 non null codes generated assertEquals(16, codes.length); - for (String code : codes) { + for (CodePack code : codes) { assertNotNull(code); } } @Test - public void testCodesMatchFormat() { + public void testCodesMatchReadableFormat() { + RecoveryCodeGenerator generator = new RecoveryCodeGenerator(); + CodePack[] codes = generator.generateCodes(16); + + // Assert each one is the correct format + for (CodePack code : codes) { + assertTrue(code.readableCode.matches("[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}"), code.readableCode); + } + } + + @Test + public void testCodesMatchActualFormat() { RecoveryCodeGenerator generator = new RecoveryCodeGenerator(); - String[] codes = generator.generateCodes(16); + CodePack[] codes = generator.generateCodes(16); // Assert each one is the correct format - for (String code : codes) { - assertTrue(code.matches("[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}"), code); + for (CodePack code : codes) { + assertTrue(code.actualCode.length()==16,code.actualCode); } } @Test public void testCodesAreUnique() { RecoveryCodeGenerator generator = new RecoveryCodeGenerator(); - String[] codes = generator.generateCodes(25); + CodePack[] codes = generator.generateCodes(25); - Set uniqueCodes = new HashSet<>(Arrays.asList(codes)); + Set uniqueCodes = Arrays.stream(codes).map(map->map.actualCode).collect(Collectors.toSet()); assertEquals(25, uniqueCodes.size()); } @@ -50,4 +63,44 @@ public void testInvalidNumberThrowsException() { generator.generateCodes(-1); }); } + + @Test + public void testReadableCodeGrouping(){ + RecoveryCodeGenerator generator = + new RecoveryCodeGenerator + .Builder() + .setCodeLength(13) + .setGroupNumber(3) + .build(); + + CodePack codes[] = generator.generateCodes(14); + for (CodePack code : codes) { + assertTrue(code.readableCode.matches("[a-z0-9]{3}-[a-z0-9]{3}-[a-z0-9]{3}-[a-z0-9]{3}-[a-z0-9]{1}"), code.readableCode); + } + } + + @Test + public void testInvalidGroupSize(){ + assertThrows(InvalidParameterException.class, + ()->{ + new RecoveryCodeGenerator.Builder() + .setCodeLength(12) + .setGroupNumber(0) + .build(); + } + ); + } + + @Test + public void testInsecureCodeLength(){ + assertThrows(InsecureCodeException.class, + ()->{ + new RecoveryCodeGenerator + .Builder() + .setGroupNumber(5) + .setCodeLength(6) + .build(); + } + ); + } } From f3bc337013b50304507b1521d6fc8661d72b9c49 Mon Sep 17 00:00:00 2001 From: NRSrivastava Date: Sun, 13 Jun 2021 13:33:04 +0530 Subject: [PATCH 2/4] README.md updated --- .gitignore | 4 +++- README.md | 30 +++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 5855832..59c3298 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ -.idea target + +#IntelliJ +.idea *.iml # Eclipse diff --git a/README.md b/README.md index 1716c3c..0a46264 100644 --- a/README.md +++ b/README.md @@ -247,13 +247,37 @@ Thoses settings guarantees recovery codes security (with an entropy of 82 bits) ```java import dev.samstevens.totp.recovery.RecoveryCodeGenerator; +import dev.samstevens.totp.recovery.CodePack; ... // Generate 16 random recovery codes RecoveryCodeGenerator recoveryCodes = new RecoveryCodeGenerator(); -String[] codes = recoveryCodes.generateCodes(16); -// codes = ["tf8i-exmo-3lcb-slkm", "boyv-yq75-z99k-r308", "w045-mq6w-mg1i-q12o", ...] +CodePack[] codes = recoveryCodes.generateCodes(16); +// codes = [CodePack Object1, CodePack Object2, CodePack Object3, ...] +// Object1.actualCode ="tf8iexmo3lcbslkm" +// Object1.readableCode="tf8i-exmo-3lcb-slkm" +``` + + +The user can even define custom length for the Recovery Codes and the Grouping size for the Recovery readable code. +The minimum length of the Recovery Code is 7, therefore keeping the minimun entropy of 36 bits, hence generating a fairly secure code. + + +```java +import dev.samstevens.totp.recovery.RecoveryCodeGenerator; +import dev.samstevens.totp.recovery.CodePack; +... +// Generate 15 random recovery codes of length 13 and grouping size of 3 for readable code +RecoveryCodeGenerator generator = + new RecoveryCodeGenerator + .Builder() + .setCodeLength(13) + .setGroupNumber(3) + .build(); +CodePack[] codes = recoveryCodes.generateCodes(15); +// codes = [CodePack Object1, CodePack Object2, CodePack Object3, ...] +// Object1.actualCode ="tf8xmo3lcslkm" +// Object1.readableCode="tf8-xmo-3lc-slk-m" ``` - ## Running Tests From f97987e9db1a8828fc6179ac970b27f5ad9c869c Mon Sep 17 00:00:00 2001 From: NRSrivastava Date: Mon, 14 Jun 2021 01:26:48 +0530 Subject: [PATCH 3/4] Backwards Compatibility added. --- totp/pom.xml | 5 ++ .../totp/recovery/RecoveryCodeGenerator.java | 35 +++++++++++-- .../recovery/RecoveryCodeGeneratorTest.java | 50 ++++++++++++++++--- 3 files changed, 79 insertions(+), 11 deletions(-) diff --git a/totp/pom.xml b/totp/pom.xml index 3f3b915..13edb72 100644 --- a/totp/pom.xml +++ b/totp/pom.xml @@ -59,5 +59,10 @@ javase 3.4.0 + + commons-codec + commons-codec + 1.15 + diff --git a/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java b/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java index 7233127..d5a5fee 100644 --- a/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java +++ b/totp/src/main/java/dev/samstevens/totp/recovery/RecoveryCodeGenerator.java @@ -60,20 +60,49 @@ public RecoveryCodeGenerator build(){ } } - public CodePack[] generateCodes(int amount) { + public String[] generateCodes(int amount) { // Must generate at least one code if (amount < 1) { throw new InvalidParameterException("Amount must be at least 1."); } // Create an array and fill with generated codes - CodePack[] codes = new CodePack[amount]; + String[] codes = new String[amount]; Arrays.setAll(codes, i -> generateCode()); return codes; } - private CodePack generateCode() { + private String generateCode() { + final StringBuilder code = new StringBuilder(CODE_LENGTH + (CODE_LENGTH/GROUPS_NBR) - 1); + + for (int i = 0; i < CODE_LENGTH; i++) { + // Append random character from authorized ones + code.append(CHARACTERS[random.nextInt(CHARACTERS.length)]); + + // Split code into groups for increased readability + if ((i+1) % GROUPS_NBR == 0 && (i+1) != CODE_LENGTH) { + code.append("-"); + } + } + + return code.toString(); + } + + public CodePack[] generateDifferentiatedCodes(int amount) { + // Must generate at least one code + if (amount < 1) { + throw new InvalidParameterException("Amount must be at least 1."); + } + + // Create an array and fill with generated codes + CodePack[] codes = new CodePack[amount]; + Arrays.setAll(codes, i -> generateDifferentiatedCode()); + + return codes; + } + + private CodePack generateDifferentiatedCode() { final StringBuilder code_readable = new StringBuilder(CODE_LENGTH + (CODE_LENGTH/GROUPS_NBR) - 1); final StringBuilder code_actual = new StringBuilder(CODE_LENGTH); diff --git a/totp/src/test/java/dev/samstevens/totp/recovery/RecoveryCodeGeneratorTest.java b/totp/src/test/java/dev/samstevens/totp/recovery/RecoveryCodeGeneratorTest.java index 0850d10..2e402a9 100644 --- a/totp/src/test/java/dev/samstevens/totp/recovery/RecoveryCodeGeneratorTest.java +++ b/totp/src/test/java/dev/samstevens/totp/recovery/RecoveryCodeGeneratorTest.java @@ -4,6 +4,7 @@ import java.security.InvalidParameterException; import java.util.Arrays; +import java.util.HashSet; import java.util.Set; import java.util.stream.Collectors; @@ -14,7 +15,19 @@ public class RecoveryCodeGeneratorTest { @Test public void testCorrectAmountGenerated() { RecoveryCodeGenerator generator = new RecoveryCodeGenerator(); - CodePack[] codes = generator.generateCodes(16); + String[] codes = generator.generateCodes(16); + + // Assert 16 non null codes generated + assertEquals(16, codes.length); + for (String code : codes) { + assertNotNull(code); + } + } + + @Test + public void testCorrectAmountGeneratedDifferentiated() { + RecoveryCodeGenerator generator = new RecoveryCodeGenerator(); + CodePack[] codes = generator.generateDifferentiatedCodes(16); // Assert 16 non null codes generated assertEquals(16, codes.length); @@ -24,9 +37,20 @@ public void testCorrectAmountGenerated() { } @Test - public void testCodesMatchReadableFormat() { + public void testCodesMatchFormat() { RecoveryCodeGenerator generator = new RecoveryCodeGenerator(); - CodePack[] codes = generator.generateCodes(16); + String[] codes = generator.generateCodes(16); + + // Assert each one is the correct format + for (String code : codes) { + assertTrue(code.matches("[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}"), code); + } + } + + @Test + public void testDifferentiatedCodesMatchReadableFormat() { + RecoveryCodeGenerator generator = new RecoveryCodeGenerator(); + CodePack[] codes = generator.generateDifferentiatedCodes(16); // Assert each one is the correct format for (CodePack code : codes) { @@ -35,9 +59,9 @@ public void testCodesMatchReadableFormat() { } @Test - public void testCodesMatchActualFormat() { + public void testDifferentiatedCodesMatchActualFormat() { RecoveryCodeGenerator generator = new RecoveryCodeGenerator(); - CodePack[] codes = generator.generateCodes(16); + CodePack[] codes = generator.generateDifferentiatedCodes(16); // Assert each one is the correct format for (CodePack code : codes) { @@ -48,7 +72,17 @@ public void testCodesMatchActualFormat() { @Test public void testCodesAreUnique() { RecoveryCodeGenerator generator = new RecoveryCodeGenerator(); - CodePack[] codes = generator.generateCodes(25); + String[] codes = generator.generateCodes(25); + + Set uniqueCodes = new HashSet<>(Arrays.asList(codes)); + + assertEquals(25, uniqueCodes.size()); + } + + @Test + public void testDifferentiatedCodesAreUnique() { + RecoveryCodeGenerator generator = new RecoveryCodeGenerator(); + CodePack[] codes = generator.generateDifferentiatedCodes(25); Set uniqueCodes = Arrays.stream(codes).map(map->map.actualCode).collect(Collectors.toSet()); @@ -65,7 +99,7 @@ public void testInvalidNumberThrowsException() { } @Test - public void testReadableCodeGrouping(){ + public void testDifferentiatedReadableCodeGrouping(){ RecoveryCodeGenerator generator = new RecoveryCodeGenerator .Builder() @@ -73,7 +107,7 @@ public void testReadableCodeGrouping(){ .setGroupNumber(3) .build(); - CodePack codes[] = generator.generateCodes(14); + CodePack codes[] = generator.generateDifferentiatedCodes(14); for (CodePack code : codes) { assertTrue(code.readableCode.matches("[a-z0-9]{3}-[a-z0-9]{3}-[a-z0-9]{3}-[a-z0-9]{3}-[a-z0-9]{1}"), code.readableCode); } From b60e6f0a7de6420fb45ee11a337eae08593fdc75 Mon Sep 17 00:00:00 2001 From: Neelesh Ranjan Srivastava <46863299+NRSrivastava@users.noreply.github.com> Date: Mon, 14 Jun 2021 01:37:00 +0530 Subject: [PATCH 4/4] Updated README.md --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0a46264..aa3dd25 100644 --- a/README.md +++ b/README.md @@ -244,14 +244,24 @@ The default implementation provided in this library generates recovery codes : Thoses settings guarantees recovery codes security (with an entropy of 82 bits) while keeping codes simple to read and enter by end user when needed. - +```java +import dev.samstevens.totp.recovery.RecoveryCodeGenerator; +... +// Generate 16 random recovery codes +RecoveryCodeGenerator recoveryCodes = new RecoveryCodeGenerator(); +String[] codes = recoveryCodes.generateCodes(16); +// codes = ["tf8i-exmo-3lcb-slkm", "boyv-yq75-z99k-r308", "w045-mq6w-mg1i-q12o", ...] +``` + +OR with Differentiated Recovery Codes + ```java import dev.samstevens.totp.recovery.RecoveryCodeGenerator; import dev.samstevens.totp.recovery.CodePack; ... // Generate 16 random recovery codes RecoveryCodeGenerator recoveryCodes = new RecoveryCodeGenerator(); -CodePack[] codes = recoveryCodes.generateCodes(16); +CodePack[] codes = recoveryCodes.generateDifferentiatedCodes(16); // codes = [CodePack Object1, CodePack Object2, CodePack Object3, ...] // Object1.actualCode ="tf8iexmo3lcbslkm" // Object1.readableCode="tf8i-exmo-3lcb-slkm" @@ -273,7 +283,7 @@ RecoveryCodeGenerator generator = .setCodeLength(13) .setGroupNumber(3) .build(); -CodePack[] codes = recoveryCodes.generateCodes(15); +CodePack[] codes = recoveryCodes.generateDifferentiatedCodes(15); // codes = [CodePack Object1, CodePack Object2, CodePack Object3, ...] // Object1.actualCode ="tf8xmo3lcslkm" // Object1.readableCode="tf8-xmo-3lc-slk-m"