1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.apache.commons.codec.digest;
19
20 import static org.junit.jupiter.api.Assertions.assertEquals;
21 import static org.junit.jupiter.api.Assertions.assertNotNull;
22 import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
23
24 import java.nio.charset.StandardCharsets;
25 import java.util.stream.Stream;
26
27 import org.junit.jupiter.api.Named;
28 import org.junit.jupiter.api.Test;
29 import org.junit.jupiter.params.ParameterizedTest;
30 import org.junit.jupiter.params.provider.Arguments;
31 import org.junit.jupiter.params.provider.MethodSource;
32 import org.junit.jupiter.params.provider.ValueSource;
33
34 class Sha2CryptTest {
35
36 private static void assertSaltRejected(final int bits, final String salt) {
37 final byte[] key = "secret".getBytes(StandardCharsets.UTF_8);
38 assertThrowsExactly(IllegalArgumentException.class, () -> {
39 if (bits == 256) {
40 Sha2Crypt.sha256Crypt(key, salt);
41 } else {
42 Sha2Crypt.sha512Crypt(key, salt);
43 }
44 });
45 }
46
47 static Stream<Arguments> invalidSaltCharacters() {
48 return saltsForBothVariants(new String[][] {
49
50 { "Unicode after valid prefix", "rounds=1000$abcäöüäöü" },
51 { "space after valid prefix", "abc def" },
52 { "tab after valid prefix", "abc\tdef" },
53 { "punctuation after valid prefix", "abc!" },
54 { "invalid character after 16 valid characters", "abcdefghijklmnop!" }
55
56 });
57 }
58
59 static Stream<Arguments> malformedRounds() {
60 return saltsForBothVariants(new String[][] {
61
62 { "incorrect rounds keyword", "notrounds=1000$asdfasdf" },
63 { "empty rounds", "rounds=$abc" },
64 { "non-numeric rounds", "rounds=abc$abc" },
65 { "negative rounds", "rounds=-1$abc" }
66
67 });
68 }
69
70 private static Stream<Arguments> saltsForBothVariants(final String[][] cases) {
71 return Stream.of(256, 512).flatMap(bits -> Stream.of(cases)
72 .map(testCase -> Arguments.of(bits, Named.of(testCase[0], (bits == 256 ? "$5$" : "$6$") + testCase[1]))));
73 }
74
75 static Stream<Arguments> trailingLineTerminators() {
76 return saltsForBothVariants(new String[][] {
77
78 { "LF", "rounds=1000$abc\n" },
79 { "CR", "rounds=1000$abc\r" },
80 { "CRLF", "rounds=1000$abc\r\n" },
81 { "NEL (U+0085)", "rounds=1000$abc\u0085" },
82 { "LINE SEPARATOR (U+2028)", "rounds=1000$abc\u2028" },
83 { "PARAGRAPH SEPARATOR (U+2029)", "rounds=1000$abc\u2029" }
84
85 });
86 }
87
88 @SuppressWarnings("deprecation")
89 @Test
90 void testCtor() {
91 assertNotNull(new Sha2Crypt());
92 }
93
94 @ParameterizedTest(name = "SHA-{0}: {1}")
95 @MethodSource("invalidSaltCharacters")
96 void testInvalidSaltCharactersRejected(final int bits, final String salt) {
97 assertSaltRejected(bits, salt);
98 }
99
100 @ParameterizedTest(name = "SHA-{0}: invalid salt prefix")
101 @ValueSource(ints = { 256, 512 })
102 void testInvalidSaltPrefixRejected(final int bits) {
103 assertSaltRejected(bits, "xx");
104 }
105
106 @ParameterizedTest
107 @ValueSource(ints = { 100_000, 1_000_000 })
108 void testLargeRounds(final int rounds) {
109 final String salt = "$6$rounds=" + rounds + "$abcdefghijklmnop";
110 Crypt.crypt("anything".getBytes(StandardCharsets.UTF_8), salt);
111 }
112
113 @ParameterizedTest(name = "SHA-{0}: {1}")
114 @MethodSource("malformedRounds")
115 void testMalformedRoundsRejected(final int bits, final String salt) {
116 assertSaltRejected(bits, salt);
117 }
118
119 @Test
120 void testRoundsAboveCeilingRejected() {
121 assertThrowsExactly(IllegalArgumentException.class,
122 () -> Sha2Crypt.sha512Crypt("secret".getBytes(StandardCharsets.UTF_8), "$6$rounds=1000001$abcdefghijklmnop"));
123 assertThrowsExactly(IllegalArgumentException.class,
124 () -> Sha2Crypt.sha512Crypt("secret".getBytes(StandardCharsets.UTF_8), "$6$rounds=999999999$abcdefghijklmnop"));
125 assertThrowsExactly(IllegalArgumentException.class,
126 () -> Sha2Crypt.sha512Crypt("secret".getBytes(StandardCharsets.UTF_8), "$6$rounds=99999999999$abcdefghijklmnop"));
127 }
128
129 @Test
130 void testRoundsCeilingOverride() {
131 final String previous = System.getProperty(Sha2Crypt.ROUNDS_MAX_PROPERTY);
132 System.setProperty(Sha2Crypt.ROUNDS_MAX_PROPERTY, "2000000");
133 try {
134 assertNotNull(Sha2Crypt.sha512Crypt("secret".getBytes(StandardCharsets.UTF_8), "$6$rounds=2000000$abcdefghijklmnop"));
135 } finally {
136 if (previous == null) {
137 System.clearProperty(Sha2Crypt.ROUNDS_MAX_PROPERTY);
138 } else {
139 System.setProperty(Sha2Crypt.ROUNDS_MAX_PROPERTY, previous);
140 }
141 }
142 }
143
144 @Test
145 void testRoundsLeadingZeroes() {
146 final String expected = Sha2Crypt.sha512Crypt("secret".getBytes(StandardCharsets.UTF_8), "$6$rounds=1000$abcdefghijklmnop");
147 final String actual = Sha2Crypt.sha512Crypt("secret".getBytes(StandardCharsets.UTF_8), "$6$rounds=0000001000$abcdefghijklmnop");
148 assertEquals(expected, actual);
149 }
150
151 @ParameterizedTest(name = "SHA-{0}: {1}")
152 @MethodSource("trailingLineTerminators")
153 void testTrailingLineTerminatorsRejected(final int bits, final String salt) {
154 assertSaltRejected(bits, salt);
155 }
156 }