Skip to content

Commit ef468ad

Browse files
SONARKT-639 Rule S5344: Passwords should not be stored in plaintext or with a fast hashing algorithm
1 parent a47d315 commit ef468ad

File tree

8 files changed

+536
-2
lines changed

8 files changed

+536
-2
lines changed
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package checks
2+
3+
import javax.crypto.SecretKeyFactory
4+
import javax.crypto.spec.PBEKeySpec
5+
6+
typealias SecretKeyFactoryAlias = SecretKeyFactory
7+
typealias PBEKeySpecAlias = PBEKeySpec
8+
9+
class PasswordPlaintextFastHashingCheckSample {
10+
// region Noncompliant cases
11+
12+
class NoncompliantConstantIterations {
13+
companion object {
14+
private const val PBKDF2_ITERATIONS = 120000
15+
// ^^^^^^>
16+
}
17+
18+
fun noncompliantConstantIterations(password: String, salt: ByteArray) {
19+
val keySpec = PBEKeySpec(password.toCharArray(), salt, PBKDF2_ITERATIONS, 256) // Noncompliant {{Use at least 210000 PBKDF2 iterations.}}
20+
// ^^^^^^^^^^^^^^^^^
21+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512")
22+
// ^^^^^^^^^^^^^^^^^^^^^^<
23+
secretKeyFactory.generateSecret(keySpec)
24+
}
25+
}
26+
27+
class NoncompliantConstantAlgorithm {
28+
companion object {
29+
private const val SHA512_ALGORITHM = "PBKDF2withHmacSHA512"
30+
// ^^^^^^^^^^^^^^^^^^^^^^>
31+
}
32+
33+
fun noncompliantConstantAlgorithm(password: String, salt: ByteArray) {
34+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 120000, 256) // Noncompliant {{Use at least 210000 PBKDF2 iterations.}}
35+
// ^^^^^^
36+
val secretKeyFactory = SecretKeyFactory.getInstance(SHA512_ALGORITHM)
37+
secretKeyFactory.generateSecret(keySpec)
38+
}
39+
}
40+
41+
fun noncompliantNoKeyLength(password: String, salt: ByteArray) {
42+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 120000) // Noncompliant
43+
// ^^^^^^
44+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512")
45+
// ^^^^^^^^^^^^^^^^^^^^^^<
46+
secretKeyFactory.generateSecret(keySpec)
47+
}
48+
49+
fun noncompliantIntLiteralIterationWithSha512(password: String, salt: ByteArray) {
50+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 120000, 256) // Noncompliant {{Use at least 210000 PBKDF2 iterations.}}
51+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512")
52+
secretKeyFactory.generateSecret(keySpec)
53+
}
54+
55+
fun noncompliantIntLiteralIterationWithSha256(password: String, salt: ByteArray) {
56+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 300000, 256) // Noncompliant {{Use at least 600000 PBKDF2 iterations.}}
57+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA256")
58+
secretKeyFactory.generateSecret(keySpec)
59+
}
60+
61+
fun noncompliantIntLiteralIterationWithSha1(password: String, salt: ByteArray) {
62+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 1_200_000, 256) // Noncompliant {{Use at least 1300000 PBKDF2 iterations.}}
63+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA1")
64+
secretKeyFactory.generateSecret(keySpec)
65+
}
66+
67+
fun noncompliantLocalVariableIteration(password: String, salt: ByteArray) {
68+
val iterations = 120000
69+
// ^^^^^^>
70+
val keySpec = PBEKeySpec(password.toCharArray(), salt, iterations, 256) // Noncompliant
71+
// ^^^^^^^^^^
72+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512")
73+
// ^^^^^^^^^^^^^^^^^^^^^^<
74+
secretKeyFactory.generateSecret(keySpec)
75+
}
76+
77+
fun noncompliantLocalVariableIterationAndAlgorithm(password: String, salt: ByteArray) {
78+
val algorithm = "PBKDF2withHmacSHA512"
79+
// ^^^^^^^^^^^^^^^^^^^^^^>
80+
val iterations = 120000
81+
// ^^^^^^>
82+
val keySpec = PBEKeySpec(password.toCharArray(), salt, iterations, 256) // Noncompliant
83+
// ^^^^^^^^^^
84+
val secretKeyFactory = SecretKeyFactory.getInstance(algorithm)
85+
secretKeyFactory.generateSecret(keySpec)
86+
}
87+
88+
fun noncompliantComplexFlow(password: String, salt: ByteArray) {
89+
val iterations = 500_000
90+
// ^^^^^^^>
91+
if (salt.size > 10) {
92+
print("Some flow between relevant code")
93+
}
94+
val keySpec = PBEKeySpec(password.toCharArray(), salt, iterations, 256) // Noncompliant
95+
// ^^^^^^^^^^
96+
while (salt.size < 10) {
97+
if (salt.hashCode() == 42) {
98+
print("Some other flow between relevant code")
99+
100+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA256")
101+
// ^^^^^^^^^^^^^^^^^^^^^^<
102+
val aLambda = {
103+
secretKeyFactory.generateSecret(keySpec)
104+
}
105+
}
106+
}
107+
}
108+
109+
fun noncompliantStringConcatenation(password: String, salt: ByteArray) {
110+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 120000, 256) // Noncompliant
111+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmac" + "SHA512")
112+
secretKeyFactory.generateSecret(keySpec)
113+
}
114+
115+
fun noncompliantToString(password: String, salt: ByteArray) {
116+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 120000, 256) // FN
117+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512".toString())
118+
secretKeyFactory.generateSecret(keySpec)
119+
}
120+
121+
fun noncompliantInlineSecretKeyFactoryGetInstance(password: String, salt: ByteArray) {
122+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 120000, 256) // Noncompliant
123+
SecretKeyFactory.getInstance("PBKDF2withHmacSHA512").generateSecret(keySpec)
124+
}
125+
126+
fun noncompliantEverythingInline(password: String, salt: ByteArray) {
127+
SecretKeyFactory.getInstance("PBKDF2withHmacSHA512").generateSecret(
128+
// ^^^^^^^^^^^^^^^^^^^^^^>
129+
PBEKeySpec(password.toCharArray(), salt, 110000, 256) // Noncompliant
130+
// ^^^^^^
131+
)
132+
}
133+
134+
fun noncompliantEverythingInlineWithLet(password: String, salt: ByteArray) {
135+
PBEKeySpec(password.toCharArray(), salt, 110000, 256).let { // Noncompliant
136+
// ^^^^^^
137+
SecretKeyFactory.getInstance("PBKDF2withHmacSHA512").generateSecret(it)
138+
// ^^^^^^^^^^^^^^^^^^^^^^<
139+
}
140+
}
141+
142+
fun noncompliantWithAliases(password: String, salt: ByteArray) {
143+
val keySpec = PBEKeySpecAlias(password.toCharArray(), salt, 120000, 256) // Noncompliant
144+
SecretKeyFactoryAlias.getInstance("PBKDF2withHmacSHA512").generateSecret(keySpec)
145+
}
146+
147+
fun noncompliantDefaultArgumentIteration(password: String, salt: ByteArray, iteration: Int = 120000) {
148+
// ^^^^^^>
149+
val keySpec = PBEKeySpec(password.toCharArray(), salt, iteration, 256) // Noncompliant
150+
// ^^^^^^^^^
151+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512")
152+
// ^^^^^^^^^^^^^^^^^^^^^^<
153+
secretKeyFactory.generateSecret(keySpec)
154+
}
155+
156+
fun noncompliantDefaultArgumentIterationAndAlgorithm(
157+
password: String,
158+
salt: ByteArray,
159+
iteration: Int = 110000,
160+
// ^^^^^^>
161+
algorithm: String = "PBKDF2withHmacSHA512",
162+
// ^^^^^^^^^^^^^^^^^^^^^^>
163+
) {
164+
val keySpec = PBEKeySpec(password.toCharArray(), salt, iteration, 256) // Noncompliant
165+
// ^^^^^^^^^
166+
val secretKeyFactory = SecretKeyFactory.getInstance(algorithm)
167+
secretKeyFactory.generateSecret(keySpec)
168+
}
169+
170+
class NoncompliantDefaultPrimaryConstructorArgumentIterationAndAlgorithm(
171+
password: String,
172+
salt: ByteArray,
173+
val iteration: Int = 110000,
174+
// ^^^^^^>
175+
val algorithm: String = "PBKDF2withHmacSHA512",
176+
// ^^^^^^^^^^^^^^^^^^^^^^>
177+
) {
178+
fun test(password: String, salt: ByteArray) {
179+
val keySpec = PBEKeySpec(password.toCharArray(), salt, iteration, 256) // Noncompliant
180+
// ^^^^^^^^^
181+
val secretKeyFactory = SecretKeyFactory.getInstance(algorithm)
182+
secretKeyFactory.generateSecret(keySpec)
183+
}
184+
}
185+
186+
// endregion
187+
188+
// region Compliant cases
189+
190+
fun compliantIntLiteralAboveThresholdForSHA512(password: String, salt: ByteArray) {
191+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 210000, 256) // Compliant: 210_000 >= 210_000
192+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512")
193+
secretKeyFactory.generateSecret(keySpec)
194+
}
195+
196+
fun compliantIntLiteralAboveThresholdForSHA256(password: String, salt: ByteArray) {
197+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 700000, 256) // Compliant: 700_000 >= 600_000
198+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA256")
199+
secretKeyFactory.generateSecret(keySpec)
200+
}
201+
202+
fun compliantIntLiteralAboveThresholdForSHA1(password: String, salt: ByteArray) {
203+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 1_400_000, 256) // Compliant: 1_400_000 >= 1_300_000
204+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA1")
205+
secretKeyFactory.generateSecret(keySpec)
206+
}
207+
208+
fun compliantUnknownAlgorithm(password: String, salt: ByteArray) {
209+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 120000, 256) // Compliant: unknown algorithm
210+
val secretKeyFactory = SecretKeyFactory.getInstance("unknown")
211+
secretKeyFactory.generateSecret(keySpec)
212+
}
213+
214+
fun compliantComplexFlow(password: String, salt: ByteArray) {
215+
val iterations = if (salt.size > 10) 210000 else 60000
216+
val keySpec = PBEKeySpec(password.toCharArray(), salt, iterations, 256) // Compliant: salt.size can be anything
217+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512")
218+
secretKeyFactory.generateSecret(keySpec)
219+
}
220+
221+
fun compliantWithoutSecretKeyFactoryGenerateSecret(password: String, salt: ByteArray) {
222+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 120000, 256) // Compliant: no generateSecret
223+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512")
224+
}
225+
226+
fun compliantWithoutSecretKeyFactoryGetInstance(password: String, salt: ByteArray, secretKeyFactory: SecretKeyFactory) {
227+
val keySpec = PBEKeySpec(password.toCharArray(), salt, 120000, 256) // Compliant: no getInstance
228+
secretKeyFactory.generateSecret(keySpec)
229+
}
230+
231+
fun compliantMultipleKeySpec(password: String, salt: ByteArray) {
232+
val keySpec1 = PBEKeySpec(password.toCharArray(), salt, 200000, 256)
233+
val keySpec2 = PBEKeySpec(password.toCharArray(), salt, 220000, 256)
234+
val secretKeyFactory = SecretKeyFactory.getInstance("PBKDF2withHmacSHA512")
235+
// This is commented out: secretKeyFactory.generateSecret(keySpec1)
236+
secretKeyFactory.generateSecret(keySpec2) // Compliant: keySpec2 has enough iterations
237+
}
238+
239+
fun compliantDefaultArgumentIterationAndAlgorithm(
240+
password: String,
241+
salt: ByteArray,
242+
iteration: Int = 220000,
243+
algorithm: String = "PBKDF2withHmacSHA512",
244+
) {
245+
val keySpec = PBEKeySpec(password.toCharArray(), salt, iteration, 256) // Compliant: 220_000 >= 210_000
246+
val secretKeyFactory = SecretKeyFactory.getInstance(algorithm)
247+
secretKeyFactory.generateSecret(keySpec)
248+
}
249+
250+
// endregion
251+
}

sonar-kotlin-api/src/main/java/org/sonarsource/kotlin/api/checks/CommonConstants.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package org.sonarsource.kotlin.api.checks
1919
const val INT_TYPE = "kotlin.Int"
2020
const val STRING_TYPE = "kotlin.String"
2121
const val BOOLEAN_TYPE = "kotlin.Boolean"
22+
const val CHAR_ARRAY_TYPE = "kotlin.CharArray"
23+
const val BYTE_ARRAY_TYPE = "kotlin.ByteArray"
2224
const val ANY_TYPE = "kotlin.Any"
2325
const val GET_INSTANCE = "getInstance"
2426
const val WITH_CONTEXT = "withContext"
@@ -35,8 +37,8 @@ const val JAVA_UTIL_PATTERN = "java.util.regex.Pattern"
3537
const val HASHCODE_METHOD_NAME = "hashCode"
3638
const val EQUALS_METHOD_NAME = "equals"
3739

38-
val BYTE_ARRAY_CONSTRUCTOR = ConstructorMatcher("kotlin.ByteArray")
39-
val BYTE_ARRAY_CONSTRUCTOR_SIZE_ARG_ONLY = ConstructorMatcher("kotlin.ByteArray") { withArguments("kotlin.Int") }
40+
val BYTE_ARRAY_CONSTRUCTOR = ConstructorMatcher(BYTE_ARRAY_TYPE)
41+
val BYTE_ARRAY_CONSTRUCTOR_SIZE_ARG_ONLY = ConstructorMatcher(BYTE_ARRAY_TYPE) { withArguments(INT_TYPE) }
4042

4143
val SECURE_RANDOM_FUNS = FunMatcher(qualifier = "java.security.SecureRandom")
4244

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
/*
2+
* SonarSource Kotlin
3+
* Copyright (C) 2018-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonarsource.kotlin.checks
18+
19+
import org.jetbrains.kotlin.analysis.api.resolution.KaFunctionCall
20+
import org.jetbrains.kotlin.analysis.api.resolution.successfulConstructorCallOrNull
21+
import org.jetbrains.kotlin.analysis.api.resolution.successfulFunctionCallOrNull
22+
import org.jetbrains.kotlin.analysis.api.resolution.successfulVariableAccessCall
23+
import org.jetbrains.kotlin.analysis.api.resolution.symbol
24+
import org.jetbrains.kotlin.psi.KtCallExpression
25+
import org.jetbrains.kotlin.psi.KtExpression
26+
import org.jetbrains.kotlin.psi.KtParameter
27+
import org.sonar.check.Rule
28+
import org.sonarsource.kotlin.api.checks.BYTE_ARRAY_TYPE
29+
import org.sonarsource.kotlin.api.checks.CHAR_ARRAY_TYPE
30+
import org.sonarsource.kotlin.api.checks.CallAbstractCheck
31+
import org.sonarsource.kotlin.api.checks.FunMatcher
32+
import org.sonarsource.kotlin.api.checks.INT_TYPE
33+
import org.sonarsource.kotlin.api.checks.STRING_TYPE
34+
import org.sonarsource.kotlin.api.checks.predictReceiverExpression
35+
import org.sonarsource.kotlin.api.checks.predictRuntimeIntValue
36+
import org.sonarsource.kotlin.api.checks.predictRuntimeValueExpression
37+
import org.sonarsource.kotlin.api.checks.stringValue
38+
import org.sonarsource.kotlin.api.frontend.KotlinFileContext
39+
import org.sonarsource.kotlin.api.reporting.KotlinTextRanges.textRange
40+
import org.sonarsource.kotlin.api.reporting.SecondaryLocation
41+
import org.sonarsource.kotlin.api.visiting.withKaSession
42+
43+
private const val MESSAGE = """Use at least %d PBKDF2 iterations."""
44+
45+
private val minIterations = mapOf(
46+
"PBKDF2withHmacSHA1" to 1_300_000,
47+
"PBKDF2withHmacSHA256" to 600_000,
48+
"PBKDF2withHmacSHA512" to 210_000,
49+
)
50+
51+
private val generateSecretFunMatcher = FunMatcher {
52+
qualifier = "javax.crypto.SecretKeyFactory"
53+
name = "generateSecret"
54+
withArguments("java.security.spec.KeySpec")
55+
}
56+
private val getInstanceFunMatcher = FunMatcher {
57+
qualifier = "javax.crypto.SecretKeyFactory"
58+
name = "getInstance"
59+
withArguments(STRING_TYPE)
60+
}
61+
private val pbeKeySpecConstructorMatchers = listOf(
62+
FunMatcher {
63+
qualifier = "javax.crypto.spec.PBEKeySpec"
64+
matchConstructor = true
65+
withArguments(CHAR_ARRAY_TYPE, BYTE_ARRAY_TYPE, INT_TYPE)
66+
},
67+
FunMatcher {
68+
qualifier = "javax.crypto.spec.PBEKeySpec"
69+
matchConstructor = true
70+
withArguments(CHAR_ARRAY_TYPE, BYTE_ARRAY_TYPE, INT_TYPE, INT_TYPE)
71+
},
72+
)
73+
74+
@Rule(key = "S5344")
75+
class PasswordPlaintextFastHashingCheck : CallAbstractCheck() {
76+
77+
override val functionsToVisit = listOf(generateSecretFunMatcher)
78+
79+
override fun visitFunctionCall(
80+
callExpression: KtCallExpression,
81+
resolvedCall: KaFunctionCall<*>,
82+
kotlinFileContext: KotlinFileContext,
83+
) = withKaSession {
84+
val secretKeyFactoryCall = callExpression.predictReceiverExpression()
85+
?.resolveToCall()
86+
?.successfulFunctionCallOrNull()
87+
?.takeIf { getInstanceFunMatcher.matches(it) }
88+
?: return@withKaSession
89+
val keySpecCall = resolvedCall.argumentMapping.keys.single().predictRuntimeValueExpression()
90+
.resolveToCall()
91+
?.successfulConstructorCallOrNull()
92+
?.takeIf { call -> pbeKeySpecConstructorMatchers.any { matcher -> matcher.matches(call) } }
93+
?: return@withKaSession
94+
95+
val iterationCountExpression = keySpecCall.argumentMapping.keys.elementAtOrNull(2) ?: return@withKaSession
96+
val iterationCountValueExpression = iterationCountExpression
97+
.predictRuntimeValueFromParameterDefault()
98+
.predictRuntimeValueExpression()
99+
val iterationCount = iterationCountValueExpression.predictRuntimeIntValue() ?: return@withKaSession
100+
101+
val algorithmValueExpression = secretKeyFactoryCall.argumentMapping.keys.single()
102+
.predictRuntimeValueFromParameterDefault()
103+
.predictRuntimeValueExpression()
104+
val algorithm = algorithmValueExpression.stringValue() ?: return@withKaSession
105+
val minIteration = minIterations[algorithm] ?: return@withKaSession
106+
107+
if (iterationCount < minIteration) {
108+
val secondaryLocations = mutableListOf(SecondaryLocation(kotlinFileContext.textRange(algorithmValueExpression)))
109+
if (iterationCountValueExpression.textRange != iterationCountExpression.textRange) {
110+
secondaryLocations.add(SecondaryLocation(kotlinFileContext.textRange(iterationCountValueExpression)))
111+
}
112+
113+
kotlinFileContext.reportIssue(iterationCountExpression, MESSAGE.format(minIteration), secondaryLocations)
114+
}
115+
}
116+
117+
private fun KtExpression.predictRuntimeValueFromParameterDefault(): KtExpression = withKaSession {
118+
resolveToCall()
119+
?.successfulVariableAccessCall()
120+
?.symbol
121+
?.psi
122+
?.let { it as? KtParameter }
123+
?.defaultValue
124+
?: this@predictRuntimeValueFromParameterDefault
125+
}
126+
}

0 commit comments

Comments
 (0)