View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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              // @formatter:off
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              // @formatter:on
56          });
57      }
58  
59      static Stream<Arguments> malformedRounds() {
60          return saltsForBothVariants(new String[][] {
61              // @formatter:off
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              // @formatter:on
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              // @formatter:off
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              // @formatter:on
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 }