From 8954a0f55f0887af4b0d44eb1de848fd13ef801e Mon Sep 17 00:00:00 2001 From: Ivan Pizhenko Date: Sun, 7 Mar 2021 12:01:42 +0200 Subject: [PATCH 1/7] issue #1061 Port zxcvbnStrengthBar --- .idea/modules.xml | 4 +- .../com/flowcrypt/email/security/PgpPwd.kt | 175 ++++++++++++++++++ .../flowcrypt/email/security/PgpPwdTest.kt | 32 ++++ 3 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt create mode 100644 FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt diff --git a/.idea/modules.xml b/.idea/modules.xml index 8a0128ffb3..257bb210ba 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,8 +2,8 @@ - - + + \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt new file mode 100644 index 0000000000..df891e3989 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt @@ -0,0 +1,175 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: Ivan Pizhenko + */ + +package com.flowcrypt.email.security + +import java.lang.IllegalArgumentException +import java.math.BigDecimal +import java.math.BigInteger +import java.math.RoundingMode +import java.security.SecureRandom + +object PgpPwd { + data class Word( + val match: String, + val word: String, + val bar: Long, + var color: String, + val pass: Boolean + ) + + data class PwdStrengthResult ( + val word: Word, + val seconds: BigInteger, + val time: String + ) + + enum class PwdType { + PASSPHRASE, + PASSWORD + } + + fun estimateStrength(guesses: BigInteger, type: PwdType = PwdType.PASSPHRASE): + PwdStrengthResult { + val timeToCrack = guesses.divideAndRemainder(CRACK_GUESSES_PER_SECOND) + if (timeToCrack[1] >= HALF_CRACK_GUESSES_PER_SECOND) { + timeToCrack[0] = timeToCrack[0].inc() + } + val readableTime = readableCrackTime(timeToCrack[0]) + val words = when (type) { + PwdType.PASSPHRASE -> CRACK_TIME_WORDS_PASSPHRASE + PwdType.PASSWORD -> CRACK_TIME_WORDS_PASSWORD + } + for (word in words) { + if (readableTime.contains(word.match)) { + return PwdStrengthResult(word, timeToCrack[0], readableTime) + } + } + throw IllegalArgumentException("Can't estimate strength for the number of guesses $guesses") + } + + // Generates random password using digits and uppercase English letters, + // for example: TDW6-DU5M-TANI-LJXY + fun random(): String { + val bytes = ByteArray(16) + val rnd = SecureRandom() + rnd.nextBytes(bytes) + val s = StringBuilder() + bytes.forEachIndexed { i, b0 -> + if (i > 0 && i % 4 == 0) s.append('-') + val b = b0 % 36 + s.append(if (b < 10) '0' + b else 'A' + (b - 10)) + } + return s.toString() + } + + + // https://stackoverflow.com/questions/8211744/convert-time-interval-given-in-seconds-into-more-human-readable-form + private fun readableCrackTime(totalSeconds: BigInteger): String { + val n = BigDecimal(totalSeconds) + val millennia = n.div(SECONDS_PER_MILLENIUM).setScale(0, RoundingMode.HALF_UP) + if (millennia > BigDecimal.ZERO) { + return if (millennia == BigDecimal.ONE) "a millennium" else "millennia" + } + + val centuries = n.div(SECONDS_PER_CENTURY).setScale(0, RoundingMode.HALF_UP) + if (centuries > BigDecimal.ZERO) { + return if (centuries == BigInteger.ONE) "a century" else "centuries" + } + + val years = n.div(SECONDS_PER_YEAR).setScale(0, RoundingMode.HALF_UP) + if (years > BigDecimal.ZERO) { + return "$years year${numberWordEnding(years)}" + } + + val months = n.div(SECONDS_PER_MONTH).setScale(0, RoundingMode.HALF_UP) + if (months > BigDecimal.ZERO) { + return "$months month${numberWordEnding(months)}" + } + + val weeks = n.div(SECONDS_PER_WEEK).setScale(0, RoundingMode.HALF_UP) + if (weeks > BigDecimal.ZERO) { + return "$weeks week${numberWordEnding(weeks)}" + } + + val days = n.div(SECONDS_PER_DAY).setScale(0, RoundingMode.HALF_UP) + if (days > BigDecimal.ZERO) { + return "$days day${numberWordEnding(days)}" + } + + val hours = n.div(SECONDS_PER_HOUR).setScale(0, RoundingMode.HALF_UP) + if (hours > BigDecimal.ZERO) { + return "$hours hour${numberWordEnding(hours)}" + } + + val minutes = n.div(SECONDS_PER_MINUTE).setScale(0, RoundingMode.HALF_UP) + if (minutes > BigDecimal.ZERO) { + return "$minutes minute${numberWordEnding(minutes)}" + } + + if (n > BigDecimal.ZERO) { + return "$n second${numberWordEnding(n)}" + } + + return "less than a second" + } + + private fun numberWordEnding(n: BigDecimal): String { + return if (n > BigDecimal.ONE) "s" else "" + } + + // (10k pc)*(2 core p/pc)*(4k guess p/core) + // https://www.abuse.ch/?p=3294 + // https://threatpost.com/how-much-does-botnet-cost-022813/77573/ + // https://www.abuse.ch/?p=3294 + private val CRACK_GUESSES_PER_SECOND = BigInteger.valueOf(10000 * 2 * 4000) + private val HALF_CRACK_GUESSES_PER_SECOND = CRACK_GUESSES_PER_SECOND.div(BigInteger.valueOf(2)) + + private const val DAYS_PER_MONTH = 30 + private const val DAYS_PER_YEAR = 12 * DAYS_PER_MONTH + private val SECONDS_PER_MILLENIUM = BigDecimal(86400L * DAYS_PER_YEAR * 100 * 1000) + private val SECONDS_PER_CENTURY = BigDecimal(86400L * DAYS_PER_YEAR * 100) + private val SECONDS_PER_YEAR = BigDecimal(86400L * DAYS_PER_YEAR) + private val SECONDS_PER_MONTH = BigDecimal(86400 * 30) + private val SECONDS_PER_WEEK = BigDecimal(86400 * 7) + private val SECONDS_PER_DAY = BigDecimal(86400) + private val SECONDS_PER_HOUR = BigDecimal(3600) + private val SECONDS_PER_MINUTE = BigDecimal(60) + + private val CRACK_TIME_WORDS_PASSWORD = arrayOf( + // the requirements for a one-time password are less strict + Word(match = "millenni", word = "perfect", bar = 100, color = "green", pass = true), + Word(match = "centu", word = "perfect", bar = 95, color = "green", pass = true), + Word(match = "year", word = "great", bar = 80, color = "orange", pass = true), + Word(match = "month", word = "good", bar = 70, color = "darkorange", pass = true), + Word(match = "week", word = "good", bar = 30, color = "darkred", pass = true), + Word(match = "day", word = "reasonable", bar = 40, color = "darkorange", pass = true), + Word(match = "hour", word = "bare minimum", bar = 20, color = "darkred", pass = true), + Word(match = "minute", word = "poor", bar = 15, color = "red", pass = false), + Word(match = "", word = "weak", bar = 10, color = "red", pass = false) + ) + + private val CRACK_TIME_WORDS_PASSPHRASE = arrayOf( + // the requirements for a pass phrase are meant to be strict + Word(match = "millenni", word = "perfect", bar = 100, color = "green", pass = true), + Word(match = "centu", word = "great", bar = 80, color = "green", pass = true), + Word(match = "year", word = "good", bar = 60, color = "orange", pass = true), + Word(match = "month", word = "reasonable", bar = 40, color = "darkorange", pass = true), + Word(match = "week", word = "poor", bar = 30, color = "darkred", pass = false), + Word(match = "day", word = "poor", bar = 20, color = "darkred", pass = false), + Word(match = "", word = "weak", bar = 10, color = "red", pass = false) + ) + + @Suppress("unused") + val weakWords = listOf ( + "crypt", "up", "cryptup", "flow", "flowcrypt", "encryption", "pgp", "email", "set", + "backup", "passphrase", "best", "pass", "phrases", "are", "long", "and", "have", + "several", "words", "in", "them", "Best pass phrases are long", "have several words", + "in them", "bestpassphrasesarelong", "haveseveralwords", "inthem", + "Loss of this pass phrase", "cannot be recovered", "Note it down", "on a paper", + "lossofthispassphrase", "cannotberecovered", "noteitdown", "onapaper", "setpassword", + "set password", "set pass word", "setpassphrase", "set pass phrase", "set passphrase" + ) +} diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt new file mode 100644 index 0000000000..ed7d18a137 --- /dev/null +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt @@ -0,0 +1,32 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: Ivan Pizhenko + */ + +package com.flowcrypt.email.security + +import org.junit.Test +import org.junit.Assert.assertEquals +import java.math.BigInteger + +class PgpPwdTest { + @Test + fun testEstimateStrength() { + val result = PgpPwd.estimateStrength( + BigInteger("88946283684264"), PgpPwd.PwdType.PASSPHRASE) + assertEquals( + PgpPwd.PwdStrengthResult( + word = PgpPwd.Word( + match = "week", + word = "poor", + bar = 30, + color = "darkred", + pass = false + ), + seconds = BigInteger.valueOf(1111829), + time = "2 weeks" + ), + result + ) + } +} From 4d932dad79077757a21de2e210cd024703737857 Mon Sep 17 00:00:00 2001 From: Ivan Pizhenko Date: Sun, 7 Mar 2021 12:08:45 +0200 Subject: [PATCH 2/7] Restore modules.xml --- .idea/modules.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.idea/modules.xml b/.idea/modules.xml index 257bb210ba..8a0128ffb3 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,8 +2,8 @@ - - + + \ No newline at end of file From 778988403154755c1eacc61fb9f56bda8b874a71 Mon Sep 17 00:00:00 2001 From: Ivan Pizhenko Date: Sun, 7 Mar 2021 13:00:50 +0200 Subject: [PATCH 3/7] Tests + fix for random() --- .idea/modules.xml | 2 +- .../com/flowcrypt/email/security/PgpPwd.kt | 17 +++++- .../flowcrypt/email/security/PgpPwdTest.kt | 55 ++++++++++++++----- 3 files changed, 57 insertions(+), 17 deletions(-) diff --git a/.idea/modules.xml b/.idea/modules.xml index 8a0128ffb3..c7f4e06668 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,7 +2,7 @@ - + diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt index df891e3989..f0fe69efed 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt @@ -56,16 +56,27 @@ object PgpPwd { val bytes = ByteArray(16) val rnd = SecureRandom() rnd.nextBytes(bytes) + return bytesToPassword(bytes) + } + + fun bytesToPassword(bytes: ByteArray): String { + val minLength = 16 + if (bytes.size < minLength) { + throw IllegalArgumentException( + "Source byte array is too short: required minimum length is $minLength, " + + "but the actual length is ${bytes.size}" + ) + } val s = StringBuilder() bytes.forEachIndexed { i, b0 -> if (i > 0 && i % 4 == 0) s.append('-') - val b = b0 % 36 - s.append(if (b < 10) '0' + b else 'A' + (b - 10)) + var b = b0 % 36 + if (b < 0) b += 36 + s.append(if (b < 10) '0' + b else 'A' + ((b as Int) - 10)) } return s.toString() } - // https://stackoverflow.com/questions/8211744/convert-time-interval-given-in-seconds-into-more-human-readable-form private fun readableCrackTime(totalSeconds: BigInteger): String { val n = BigDecimal(totalSeconds) diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt index ed7d18a137..77b4624fb8 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt @@ -7,26 +7,55 @@ package com.flowcrypt.email.security import org.junit.Test import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import java.lang.IllegalArgumentException import java.math.BigInteger class PgpPwdTest { @Test fun testEstimateStrength() { - val result = PgpPwd.estimateStrength( + val actualResult = PgpPwd.estimateStrength( BigInteger("88946283684264"), PgpPwd.PwdType.PASSPHRASE) - assertEquals( - PgpPwd.PwdStrengthResult( - word = PgpPwd.Word( - match = "week", - word = "poor", - bar = 30, - color = "darkred", - pass = false - ), - seconds = BigInteger.valueOf(1111829), - time = "2 weeks" + val expectedResult = PgpPwd.PwdStrengthResult( + word = PgpPwd.Word( + match = "week", + word = "poor", + bar = 30, + color = "darkred", + pass = false ), - result + seconds = BigInteger.valueOf(1111829), + time = "2 weeks" ) + assertEquals(expectedResult, actualResult) + } + + @Test + fun testBytesToPassword() { + val bytes = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 10, 11, 12, 13, 14, 15) + assertEquals("1234-5678-90AB-CDEF", PgpPwd.bytesToPassword(bytes)) + } + + @Test + fun testBytesToPasswordRejectsTooShortByteArray() { + val bytes = byteArrayOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) + + // I'd better use assertThrows(), but the strange thing happens: + // in the IntelliJ it resolves fine, but during compilation it says + // something like "Unresolved symbol assertThrows" + try { + PgpPwd.bytesToPassword(bytes) + throw Exception("IllegalArgumentException not thrown") + } catch (ex: IllegalArgumentException) { + // this is expected + } + } + + private val passwordRegex = Regex("[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4}") + + @Test + fun testRandom() { + val password = PgpPwd.random() + assertTrue("Password structure mismatch", passwordRegex.matches(password)); } } From 107570c93db80c7eb7527b723b959a18c1ba4b1e Mon Sep 17 00:00:00 2001 From: Ivan Pizhenko Date: Sun, 7 Mar 2021 13:11:32 +0200 Subject: [PATCH 4/7] remove redundant typecast --- FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt index f0fe69efed..dabe79235e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt @@ -72,7 +72,7 @@ object PgpPwd { if (i > 0 && i % 4 == 0) s.append('-') var b = b0 % 36 if (b < 0) b += 36 - s.append(if (b < 10) '0' + b else 'A' + ((b as Int) - 10)) + s.append(if (b < 10) '0' + b else 'A' + (b - 10)) } return s.toString() } From fc37949e0e80f6135bd30e252b5e2e658796d1f1 Mon Sep 17 00:00:00 2001 From: Ivan Pizhenko Date: Sun, 7 Mar 2021 13:12:56 +0200 Subject: [PATCH 5/7] remove extra semicolon --- .../src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt index 77b4624fb8..980781a3ed 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt @@ -56,6 +56,6 @@ class PgpPwdTest { @Test fun testRandom() { val password = PgpPwd.random() - assertTrue("Password structure mismatch", passwordRegex.matches(password)); + assertTrue("Password structure mismatch", passwordRegex.matches(password)) } } From 7d6cca06363b2fb3c0ccc7b9e8ac4d3441474606 Mon Sep 17 00:00:00 2001 From: Ivan Pizhenko Date: Sun, 7 Mar 2021 21:06:35 +0200 Subject: [PATCH 6/7] more proper package --- .../java/com/flowcrypt/email/security/{ => pgp}/PgpPwd.kt | 4 ++-- .../java/com/flowcrypt/email/security/{ => pgp}/PgpPwdTest.kt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename FlowCrypt/src/main/java/com/flowcrypt/email/security/{ => pgp}/PgpPwd.kt (99%) rename FlowCrypt/src/test/java/com/flowcrypt/email/security/{ => pgp}/PgpPwdTest.kt (96%) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpPwd.kt similarity index 99% rename from FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt rename to FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpPwd.kt index dabe79235e..f849a0130e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/PgpPwd.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpPwd.kt @@ -1,9 +1,9 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: Ivan Pizhenko + * Contributors: ivan */ -package com.flowcrypt.email.security +package com.flowcrypt.email.security.pgp import java.lang.IllegalArgumentException import java.math.BigDecimal diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpPwdTest.kt similarity index 96% rename from FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt rename to FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpPwdTest.kt index 980781a3ed..bce93d22c6 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/PgpPwdTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpPwdTest.kt @@ -1,9 +1,9 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: Ivan Pizhenko + * Contributors: ivan */ -package com.flowcrypt.email.security +package com.flowcrypt.email.security.pgp import org.junit.Test import org.junit.Assert.assertEquals From d7acdb5c015080ad4385cb875fe804fa246ef750 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 8 Mar 2021 20:54:10 +0200 Subject: [PATCH 7/7] Fixed indents. Refactored code.| #1061 --- .idea/modules.xml | 4 +- .../flowcrypt/email/security/pgp/PgpPwd.kt | 321 +++++++++--------- .../email/security/pgp/PgpPwdTest.kt | 23 +- 3 files changed, 174 insertions(+), 174 deletions(-) diff --git a/.idea/modules.xml b/.idea/modules.xml index c7f4e06668..3a8542cc2f 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -2,8 +2,8 @@ - + - \ No newline at end of file + diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpPwd.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpPwd.kt index f849a0130e..7cdab47214 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpPwd.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/security/pgp/PgpPwd.kt @@ -5,182 +5,183 @@ package com.flowcrypt.email.security.pgp -import java.lang.IllegalArgumentException import java.math.BigDecimal import java.math.BigInteger import java.math.RoundingMode import java.security.SecureRandom +import java.util.concurrent.TimeUnit.DAYS +import java.util.concurrent.TimeUnit.HOURS +import java.util.concurrent.TimeUnit.MINUTES object PgpPwd { - data class Word( - val match: String, - val word: String, - val bar: Long, - var color: String, - val pass: Boolean - ) - - data class PwdStrengthResult ( - val word: Word, - val seconds: BigInteger, - val time: String - ) - - enum class PwdType { - PASSPHRASE, - PASSWORD + data class Word( + val match: String, + val word: String, + val bar: Long, + var color: String, + val pass: Boolean + ) + + data class PwdStrengthResult( + val word: Word, + val seconds: BigInteger, + val time: String + ) + + enum class PwdType { + PASSPHRASE, + PASSWORD + } + + fun estimateStrength(guesses: BigInteger, type: PwdType = PwdType.PASSPHRASE): PwdStrengthResult { + val timeToCrack = guesses.divideAndRemainder(CRACK_GUESSES_PER_SECOND) + if (timeToCrack[1] >= HALF_CRACK_GUESSES_PER_SECOND) { + timeToCrack[0] = timeToCrack[0].inc() + } + val readableTime = readableCrackTime(timeToCrack[0]) + val words = when (type) { + PwdType.PASSPHRASE -> CRACK_TIME_WORDS_PASSPHRASE + PwdType.PASSWORD -> CRACK_TIME_WORDS_PASSWORD + } + for (word in words) { + if (readableTime.contains(word.match)) { + return PwdStrengthResult(word, timeToCrack[0], readableTime) + } + } + throw IllegalArgumentException("Can't estimate strength for the number of guesses $guesses") + } + + /** + * Generates random password using digits and uppercase English letters, for example: + * TDW6-DU5M-TANI-LJXY + */ + fun random(): String { + val bytes = ByteArray(16) + val rnd = SecureRandom() + rnd.nextBytes(bytes) + return bytesToPassword(bytes) + } + + fun bytesToPassword(bytes: ByteArray): String { + val minLength = 16 + if (bytes.size < minLength) { + throw IllegalArgumentException( + "Source byte array is too short: required minimum length is $minLength, " + + "but the actual length is ${bytes.size}" + ) + } + val s = StringBuilder() + bytes.forEachIndexed { i, b0 -> + if (i > 0 && i % 4 == 0) s.append('-') + var b = b0 % 36 + if (b < 0) b += 36 + s.append(if (b < 10) '0' + b else 'A' + (b - 10)) + } + return s.toString() + } + + // https://stackoverflow.com/questions/8211744/convert-time-interval-given-in-seconds-into-more-human-readable-form + private fun readableCrackTime(totalSeconds: BigInteger): String { + val n = BigDecimal(totalSeconds) + val millennia = n.div(SECONDS_PER_MILLENNIUM).setScale(0, RoundingMode.HALF_UP) + if (millennia > BigDecimal.ZERO) { + return if (millennia == BigDecimal.ONE) "a millennium" else "millennia" + } + + val centuries = n.div(SECONDS_PER_CENTURY).setScale(0, RoundingMode.HALF_UP) + if (centuries > BigDecimal.ZERO) { + return if (centuries == BigInteger.ONE) "a century" else "centuries" + } + + val years = n.div(SECONDS_PER_YEAR).setScale(0, RoundingMode.HALF_UP) + if (years > BigDecimal.ZERO) { + return "$years year${numberWordEnding(years)}" + } + + val months = n.div(SECONDS_PER_MONTH).setScale(0, RoundingMode.HALF_UP) + if (months > BigDecimal.ZERO) { + return "$months month${numberWordEnding(months)}" } - fun estimateStrength(guesses: BigInteger, type: PwdType = PwdType.PASSPHRASE): - PwdStrengthResult { - val timeToCrack = guesses.divideAndRemainder(CRACK_GUESSES_PER_SECOND) - if (timeToCrack[1] >= HALF_CRACK_GUESSES_PER_SECOND) { - timeToCrack[0] = timeToCrack[0].inc() - } - val readableTime = readableCrackTime(timeToCrack[0]) - val words = when (type) { - PwdType.PASSPHRASE -> CRACK_TIME_WORDS_PASSPHRASE - PwdType.PASSWORD -> CRACK_TIME_WORDS_PASSWORD - } - for (word in words) { - if (readableTime.contains(word.match)) { - return PwdStrengthResult(word, timeToCrack[0], readableTime) - } - } - throw IllegalArgumentException("Can't estimate strength for the number of guesses $guesses") + val weeks = n.div(SECONDS_PER_WEEK).setScale(0, RoundingMode.HALF_UP) + if (weeks > BigDecimal.ZERO) { + return "$weeks week${numberWordEnding(weeks)}" } - // Generates random password using digits and uppercase English letters, - // for example: TDW6-DU5M-TANI-LJXY - fun random(): String { - val bytes = ByteArray(16) - val rnd = SecureRandom() - rnd.nextBytes(bytes) - return bytesToPassword(bytes) + val days = n.div(SECONDS_PER_DAY).setScale(0, RoundingMode.HALF_UP) + if (days > BigDecimal.ZERO) { + return "$days day${numberWordEnding(days)}" } - fun bytesToPassword(bytes: ByteArray): String { - val minLength = 16 - if (bytes.size < minLength) { - throw IllegalArgumentException( - "Source byte array is too short: required minimum length is $minLength, " + - "but the actual length is ${bytes.size}" - ) - } - val s = StringBuilder() - bytes.forEachIndexed { i, b0 -> - if (i > 0 && i % 4 == 0) s.append('-') - var b = b0 % 36 - if (b < 0) b += 36 - s.append(if (b < 10) '0' + b else 'A' + (b - 10)) - } - return s.toString() + val hours = n.div(SECONDS_PER_HOUR).setScale(0, RoundingMode.HALF_UP) + if (hours > BigDecimal.ZERO) { + return "$hours hour${numberWordEnding(hours)}" } - // https://stackoverflow.com/questions/8211744/convert-time-interval-given-in-seconds-into-more-human-readable-form - private fun readableCrackTime(totalSeconds: BigInteger): String { - val n = BigDecimal(totalSeconds) - val millennia = n.div(SECONDS_PER_MILLENIUM).setScale(0, RoundingMode.HALF_UP) - if (millennia > BigDecimal.ZERO) { - return if (millennia == BigDecimal.ONE) "a millennium" else "millennia" - } - - val centuries = n.div(SECONDS_PER_CENTURY).setScale(0, RoundingMode.HALF_UP) - if (centuries > BigDecimal.ZERO) { - return if (centuries == BigInteger.ONE) "a century" else "centuries" - } - - val years = n.div(SECONDS_PER_YEAR).setScale(0, RoundingMode.HALF_UP) - if (years > BigDecimal.ZERO) { - return "$years year${numberWordEnding(years)}" - } - - val months = n.div(SECONDS_PER_MONTH).setScale(0, RoundingMode.HALF_UP) - if (months > BigDecimal.ZERO) { - return "$months month${numberWordEnding(months)}" - } - - val weeks = n.div(SECONDS_PER_WEEK).setScale(0, RoundingMode.HALF_UP) - if (weeks > BigDecimal.ZERO) { - return "$weeks week${numberWordEnding(weeks)}" - } - - val days = n.div(SECONDS_PER_DAY).setScale(0, RoundingMode.HALF_UP) - if (days > BigDecimal.ZERO) { - return "$days day${numberWordEnding(days)}" - } - - val hours = n.div(SECONDS_PER_HOUR).setScale(0, RoundingMode.HALF_UP) - if (hours > BigDecimal.ZERO) { - return "$hours hour${numberWordEnding(hours)}" - } - - val minutes = n.div(SECONDS_PER_MINUTE).setScale(0, RoundingMode.HALF_UP) - if (minutes > BigDecimal.ZERO) { - return "$minutes minute${numberWordEnding(minutes)}" - } - - if (n > BigDecimal.ZERO) { - return "$n second${numberWordEnding(n)}" - } - - return "less than a second" + val minutes = n.div(SECONDS_PER_MINUTE).setScale(0, RoundingMode.HALF_UP) + if (minutes > BigDecimal.ZERO) { + return "$minutes minute${numberWordEnding(minutes)}" } - private fun numberWordEnding(n: BigDecimal): String { - return if (n > BigDecimal.ONE) "s" else "" + if (n > BigDecimal.ZERO) { + return "$n second${numberWordEnding(n)}" } - // (10k pc)*(2 core p/pc)*(4k guess p/core) - // https://www.abuse.ch/?p=3294 - // https://threatpost.com/how-much-does-botnet-cost-022813/77573/ - // https://www.abuse.ch/?p=3294 - private val CRACK_GUESSES_PER_SECOND = BigInteger.valueOf(10000 * 2 * 4000) - private val HALF_CRACK_GUESSES_PER_SECOND = CRACK_GUESSES_PER_SECOND.div(BigInteger.valueOf(2)) - - private const val DAYS_PER_MONTH = 30 - private const val DAYS_PER_YEAR = 12 * DAYS_PER_MONTH - private val SECONDS_PER_MILLENIUM = BigDecimal(86400L * DAYS_PER_YEAR * 100 * 1000) - private val SECONDS_PER_CENTURY = BigDecimal(86400L * DAYS_PER_YEAR * 100) - private val SECONDS_PER_YEAR = BigDecimal(86400L * DAYS_PER_YEAR) - private val SECONDS_PER_MONTH = BigDecimal(86400 * 30) - private val SECONDS_PER_WEEK = BigDecimal(86400 * 7) - private val SECONDS_PER_DAY = BigDecimal(86400) - private val SECONDS_PER_HOUR = BigDecimal(3600) - private val SECONDS_PER_MINUTE = BigDecimal(60) - - private val CRACK_TIME_WORDS_PASSWORD = arrayOf( - // the requirements for a one-time password are less strict - Word(match = "millenni", word = "perfect", bar = 100, color = "green", pass = true), - Word(match = "centu", word = "perfect", bar = 95, color = "green", pass = true), - Word(match = "year", word = "great", bar = 80, color = "orange", pass = true), - Word(match = "month", word = "good", bar = 70, color = "darkorange", pass = true), - Word(match = "week", word = "good", bar = 30, color = "darkred", pass = true), - Word(match = "day", word = "reasonable", bar = 40, color = "darkorange", pass = true), - Word(match = "hour", word = "bare minimum", bar = 20, color = "darkred", pass = true), - Word(match = "minute", word = "poor", bar = 15, color = "red", pass = false), - Word(match = "", word = "weak", bar = 10, color = "red", pass = false) - ) - - private val CRACK_TIME_WORDS_PASSPHRASE = arrayOf( - // the requirements for a pass phrase are meant to be strict - Word(match = "millenni", word = "perfect", bar = 100, color = "green", pass = true), - Word(match = "centu", word = "great", bar = 80, color = "green", pass = true), - Word(match = "year", word = "good", bar = 60, color = "orange", pass = true), - Word(match = "month", word = "reasonable", bar = 40, color = "darkorange", pass = true), - Word(match = "week", word = "poor", bar = 30, color = "darkred", pass = false), - Word(match = "day", word = "poor", bar = 20, color = "darkred", pass = false), - Word(match = "", word = "weak", bar = 10, color = "red", pass = false) - ) - - @Suppress("unused") - val weakWords = listOf ( - "crypt", "up", "cryptup", "flow", "flowcrypt", "encryption", "pgp", "email", "set", - "backup", "passphrase", "best", "pass", "phrases", "are", "long", "and", "have", - "several", "words", "in", "them", "Best pass phrases are long", "have several words", - "in them", "bestpassphrasesarelong", "haveseveralwords", "inthem", - "Loss of this pass phrase", "cannot be recovered", "Note it down", "on a paper", - "lossofthispassphrase", "cannotberecovered", "noteitdown", "onapaper", "setpassword", - "set password", "set pass word", "setpassphrase", "set pass phrase", "set passphrase" - ) + return "less than a second" + } + + private fun numberWordEnding(n: BigDecimal): String { + return if (n > BigDecimal.ONE) "s" else "" + } + + // (10k pc)*(2 core p/pc)*(4k guess p/core) + // https://www.abuse.ch/?p=3294 + // https://threatpost.com/how-much-does-botnet-cost-022813/77573/ + // https://www.abuse.ch/?p=3294 + private val CRACK_GUESSES_PER_SECOND = BigInteger.valueOf(10000 * 2 * 4000) + private val HALF_CRACK_GUESSES_PER_SECOND = CRACK_GUESSES_PER_SECOND.div(BigInteger.valueOf(2)) + + private val SECONDS_PER_MILLENNIUM = DAYS.toSeconds(365 * 100 * 1000).toBigDecimal() + private val SECONDS_PER_CENTURY = DAYS.toSeconds(365 * 100).toBigDecimal() + private val SECONDS_PER_YEAR = DAYS.toSeconds(365).toBigDecimal() + private val SECONDS_PER_MONTH = DAYS.toSeconds(30).toBigDecimal() + private val SECONDS_PER_WEEK = DAYS.toSeconds(7).toBigDecimal() + private val SECONDS_PER_DAY = DAYS.toSeconds(1).toBigDecimal() + private val SECONDS_PER_HOUR = HOURS.toSeconds(1).toBigDecimal() + private val SECONDS_PER_MINUTE = MINUTES.toSeconds(1).toBigDecimal() + + private val CRACK_TIME_WORDS_PASSWORD = arrayOf( + // the requirements for a one-time password are less strict + Word(match = "millenni", word = "perfect", bar = 100, color = "green", pass = true), + Word(match = "centu", word = "perfect", bar = 95, color = "green", pass = true), + Word(match = "year", word = "great", bar = 80, color = "orange", pass = true), + Word(match = "month", word = "good", bar = 70, color = "darkorange", pass = true), + Word(match = "week", word = "good", bar = 30, color = "darkred", pass = true), + Word(match = "day", word = "reasonable", bar = 40, color = "darkorange", pass = true), + Word(match = "hour", word = "bare minimum", bar = 20, color = "darkred", pass = true), + Word(match = "minute", word = "poor", bar = 15, color = "red", pass = false), + Word(match = "", word = "weak", bar = 10, color = "red", pass = false) + ) + + private val CRACK_TIME_WORDS_PASSPHRASE = arrayOf( + // the requirements for a pass phrase are meant to be strict + Word(match = "millenni", word = "perfect", bar = 100, color = "green", pass = true), + Word(match = "centu", word = "great", bar = 80, color = "green", pass = true), + Word(match = "year", word = "good", bar = 60, color = "orange", pass = true), + Word(match = "month", word = "reasonable", bar = 40, color = "darkorange", pass = true), + Word(match = "week", word = "poor", bar = 30, color = "darkred", pass = false), + Word(match = "day", word = "poor", bar = 20, color = "darkred", pass = false), + Word(match = "", word = "weak", bar = 10, color = "red", pass = false) + ) + + @Suppress("unused") + val weakWords = listOf( + "crypt", "up", "cryptup", "flow", "flowcrypt", "encryption", "pgp", "email", "set", + "backup", "passphrase", "best", "pass", "phrases", "are", "long", "and", "have", + "several", "words", "in", "them", "Best pass phrases are long", "have several words", + "in them", "bestpassphrasesarelong", "haveseveralwords", "inthem", + "Loss of this pass phrase", "cannot be recovered", "Note it down", "on a paper", + "lossofthispassphrase", "cannotberecovered", "noteitdown", "onapaper", "setpassword", + "set password", "set pass word", "setpassphrase", "set pass phrase", "set passphrase" + ) } diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpPwdTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpPwdTest.kt index bce93d22c6..0a0f0daa8c 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpPwdTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/security/pgp/PgpPwdTest.kt @@ -5,27 +5,26 @@ package com.flowcrypt.email.security.pgp -import org.junit.Test import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue -import java.lang.IllegalArgumentException +import org.junit.Test import java.math.BigInteger class PgpPwdTest { @Test fun testEstimateStrength() { val actualResult = PgpPwd.estimateStrength( - BigInteger("88946283684264"), PgpPwd.PwdType.PASSPHRASE) + BigInteger("88946283684264"), PgpPwd.PwdType.PASSPHRASE) val expectedResult = PgpPwd.PwdStrengthResult( - word = PgpPwd.Word( - match = "week", - word = "poor", - bar = 30, - color = "darkred", - pass = false - ), - seconds = BigInteger.valueOf(1111829), - time = "2 weeks" + word = PgpPwd.Word( + match = "week", + word = "poor", + bar = 30, + color = "darkred", + pass = false + ), + seconds = BigInteger.valueOf(1111829), + time = "2 weeks" ) assertEquals(expectedResult, actualResult) }