|
| 1 | +/* |
| 2 | + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com |
| 3 | + * Contributors: ivan |
| 4 | + */ |
| 5 | + |
| 6 | +package com.flowcrypt.email.security.pgp |
| 7 | + |
| 8 | +import java.math.BigDecimal |
| 9 | +import java.math.BigInteger |
| 10 | +import java.math.RoundingMode |
| 11 | +import java.security.SecureRandom |
| 12 | +import java.util.concurrent.TimeUnit.DAYS |
| 13 | +import java.util.concurrent.TimeUnit.HOURS |
| 14 | +import java.util.concurrent.TimeUnit.MINUTES |
| 15 | + |
| 16 | +object PgpPwd { |
| 17 | + data class Word( |
| 18 | + val match: String, |
| 19 | + val word: String, |
| 20 | + val bar: Long, |
| 21 | + var color: String, |
| 22 | + val pass: Boolean |
| 23 | + ) |
| 24 | + |
| 25 | + data class PwdStrengthResult( |
| 26 | + val word: Word, |
| 27 | + val seconds: BigInteger, |
| 28 | + val time: String |
| 29 | + ) |
| 30 | + |
| 31 | + enum class PwdType { |
| 32 | + PASSPHRASE, |
| 33 | + PASSWORD |
| 34 | + } |
| 35 | + |
| 36 | + fun estimateStrength(guesses: BigInteger, type: PwdType = PwdType.PASSPHRASE): PwdStrengthResult { |
| 37 | + val timeToCrack = guesses.divideAndRemainder(CRACK_GUESSES_PER_SECOND) |
| 38 | + if (timeToCrack[1] >= HALF_CRACK_GUESSES_PER_SECOND) { |
| 39 | + timeToCrack[0] = timeToCrack[0].inc() |
| 40 | + } |
| 41 | + val readableTime = readableCrackTime(timeToCrack[0]) |
| 42 | + val words = when (type) { |
| 43 | + PwdType.PASSPHRASE -> CRACK_TIME_WORDS_PASSPHRASE |
| 44 | + PwdType.PASSWORD -> CRACK_TIME_WORDS_PASSWORD |
| 45 | + } |
| 46 | + for (word in words) { |
| 47 | + if (readableTime.contains(word.match)) { |
| 48 | + return PwdStrengthResult(word, timeToCrack[0], readableTime) |
| 49 | + } |
| 50 | + } |
| 51 | + throw IllegalArgumentException("Can't estimate strength for the number of guesses $guesses") |
| 52 | + } |
| 53 | + |
| 54 | + /** |
| 55 | + * Generates random password using digits and uppercase English letters, for example: |
| 56 | + * TDW6-DU5M-TANI-LJXY |
| 57 | + */ |
| 58 | + fun random(): String { |
| 59 | + val bytes = ByteArray(16) |
| 60 | + val rnd = SecureRandom() |
| 61 | + rnd.nextBytes(bytes) |
| 62 | + return bytesToPassword(bytes) |
| 63 | + } |
| 64 | + |
| 65 | + fun bytesToPassword(bytes: ByteArray): String { |
| 66 | + val minLength = 16 |
| 67 | + if (bytes.size < minLength) { |
| 68 | + throw IllegalArgumentException( |
| 69 | + "Source byte array is too short: required minimum length is $minLength, " + |
| 70 | + "but the actual length is ${bytes.size}" |
| 71 | + ) |
| 72 | + } |
| 73 | + val s = StringBuilder() |
| 74 | + bytes.forEachIndexed { i, b0 -> |
| 75 | + if (i > 0 && i % 4 == 0) s.append('-') |
| 76 | + var b = b0 % 36 |
| 77 | + if (b < 0) b += 36 |
| 78 | + s.append(if (b < 10) '0' + b else 'A' + (b - 10)) |
| 79 | + } |
| 80 | + return s.toString() |
| 81 | + } |
| 82 | + |
| 83 | + // https://stackoverflow.com/questions/8211744/convert-time-interval-given-in-seconds-into-more-human-readable-form |
| 84 | + private fun readableCrackTime(totalSeconds: BigInteger): String { |
| 85 | + val n = BigDecimal(totalSeconds) |
| 86 | + val millennia = n.div(SECONDS_PER_MILLENNIUM).setScale(0, RoundingMode.HALF_UP) |
| 87 | + if (millennia > BigDecimal.ZERO) { |
| 88 | + return if (millennia == BigDecimal.ONE) "a millennium" else "millennia" |
| 89 | + } |
| 90 | + |
| 91 | + val centuries = n.div(SECONDS_PER_CENTURY).setScale(0, RoundingMode.HALF_UP) |
| 92 | + if (centuries > BigDecimal.ZERO) { |
| 93 | + return if (centuries == BigInteger.ONE) "a century" else "centuries" |
| 94 | + } |
| 95 | + |
| 96 | + val years = n.div(SECONDS_PER_YEAR).setScale(0, RoundingMode.HALF_UP) |
| 97 | + if (years > BigDecimal.ZERO) { |
| 98 | + return "$years year${numberWordEnding(years)}" |
| 99 | + } |
| 100 | + |
| 101 | + val months = n.div(SECONDS_PER_MONTH).setScale(0, RoundingMode.HALF_UP) |
| 102 | + if (months > BigDecimal.ZERO) { |
| 103 | + return "$months month${numberWordEnding(months)}" |
| 104 | + } |
| 105 | + |
| 106 | + val weeks = n.div(SECONDS_PER_WEEK).setScale(0, RoundingMode.HALF_UP) |
| 107 | + if (weeks > BigDecimal.ZERO) { |
| 108 | + return "$weeks week${numberWordEnding(weeks)}" |
| 109 | + } |
| 110 | + |
| 111 | + val days = n.div(SECONDS_PER_DAY).setScale(0, RoundingMode.HALF_UP) |
| 112 | + if (days > BigDecimal.ZERO) { |
| 113 | + return "$days day${numberWordEnding(days)}" |
| 114 | + } |
| 115 | + |
| 116 | + val hours = n.div(SECONDS_PER_HOUR).setScale(0, RoundingMode.HALF_UP) |
| 117 | + if (hours > BigDecimal.ZERO) { |
| 118 | + return "$hours hour${numberWordEnding(hours)}" |
| 119 | + } |
| 120 | + |
| 121 | + val minutes = n.div(SECONDS_PER_MINUTE).setScale(0, RoundingMode.HALF_UP) |
| 122 | + if (minutes > BigDecimal.ZERO) { |
| 123 | + return "$minutes minute${numberWordEnding(minutes)}" |
| 124 | + } |
| 125 | + |
| 126 | + if (n > BigDecimal.ZERO) { |
| 127 | + return "$n second${numberWordEnding(n)}" |
| 128 | + } |
| 129 | + |
| 130 | + return "less than a second" |
| 131 | + } |
| 132 | + |
| 133 | + private fun numberWordEnding(n: BigDecimal): String { |
| 134 | + return if (n > BigDecimal.ONE) "s" else "" |
| 135 | + } |
| 136 | + |
| 137 | + // (10k pc)*(2 core p/pc)*(4k guess p/core) |
| 138 | + // https://www.abuse.ch/?p=3294 |
| 139 | + // https://threatpost.com/how-much-does-botnet-cost-022813/77573/ |
| 140 | + // https://www.abuse.ch/?p=3294 |
| 141 | + private val CRACK_GUESSES_PER_SECOND = BigInteger.valueOf(10000 * 2 * 4000) |
| 142 | + private val HALF_CRACK_GUESSES_PER_SECOND = CRACK_GUESSES_PER_SECOND.div(BigInteger.valueOf(2)) |
| 143 | + |
| 144 | + private val SECONDS_PER_MILLENNIUM = DAYS.toSeconds(365 * 100 * 1000).toBigDecimal() |
| 145 | + private val SECONDS_PER_CENTURY = DAYS.toSeconds(365 * 100).toBigDecimal() |
| 146 | + private val SECONDS_PER_YEAR = DAYS.toSeconds(365).toBigDecimal() |
| 147 | + private val SECONDS_PER_MONTH = DAYS.toSeconds(30).toBigDecimal() |
| 148 | + private val SECONDS_PER_WEEK = DAYS.toSeconds(7).toBigDecimal() |
| 149 | + private val SECONDS_PER_DAY = DAYS.toSeconds(1).toBigDecimal() |
| 150 | + private val SECONDS_PER_HOUR = HOURS.toSeconds(1).toBigDecimal() |
| 151 | + private val SECONDS_PER_MINUTE = MINUTES.toSeconds(1).toBigDecimal() |
| 152 | + |
| 153 | + private val CRACK_TIME_WORDS_PASSWORD = arrayOf( |
| 154 | + // the requirements for a one-time password are less strict |
| 155 | + Word(match = "millenni", word = "perfect", bar = 100, color = "green", pass = true), |
| 156 | + Word(match = "centu", word = "perfect", bar = 95, color = "green", pass = true), |
| 157 | + Word(match = "year", word = "great", bar = 80, color = "orange", pass = true), |
| 158 | + Word(match = "month", word = "good", bar = 70, color = "darkorange", pass = true), |
| 159 | + Word(match = "week", word = "good", bar = 30, color = "darkred", pass = true), |
| 160 | + Word(match = "day", word = "reasonable", bar = 40, color = "darkorange", pass = true), |
| 161 | + Word(match = "hour", word = "bare minimum", bar = 20, color = "darkred", pass = true), |
| 162 | + Word(match = "minute", word = "poor", bar = 15, color = "red", pass = false), |
| 163 | + Word(match = "", word = "weak", bar = 10, color = "red", pass = false) |
| 164 | + ) |
| 165 | + |
| 166 | + private val CRACK_TIME_WORDS_PASSPHRASE = arrayOf( |
| 167 | + // the requirements for a pass phrase are meant to be strict |
| 168 | + Word(match = "millenni", word = "perfect", bar = 100, color = "green", pass = true), |
| 169 | + Word(match = "centu", word = "great", bar = 80, color = "green", pass = true), |
| 170 | + Word(match = "year", word = "good", bar = 60, color = "orange", pass = true), |
| 171 | + Word(match = "month", word = "reasonable", bar = 40, color = "darkorange", pass = true), |
| 172 | + Word(match = "week", word = "poor", bar = 30, color = "darkred", pass = false), |
| 173 | + Word(match = "day", word = "poor", bar = 20, color = "darkred", pass = false), |
| 174 | + Word(match = "", word = "weak", bar = 10, color = "red", pass = false) |
| 175 | + ) |
| 176 | + |
| 177 | + @Suppress("unused") |
| 178 | + val weakWords = listOf( |
| 179 | + "crypt", "up", "cryptup", "flow", "flowcrypt", "encryption", "pgp", "email", "set", |
| 180 | + "backup", "passphrase", "best", "pass", "phrases", "are", "long", "and", "have", |
| 181 | + "several", "words", "in", "them", "Best pass phrases are long", "have several words", |
| 182 | + "in them", "bestpassphrasesarelong", "haveseveralwords", "inthem", |
| 183 | + "Loss of this pass phrase", "cannot be recovered", "Note it down", "on a paper", |
| 184 | + "lossofthispassphrase", "cannotberecovered", "noteitdown", "onapaper", "setpassword", |
| 185 | + "set password", "set pass word", "setpassphrase", "set pass phrase", "set passphrase" |
| 186 | + ) |
| 187 | +} |
0 commit comments