1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.shiro.crypto.hash.format;
20
21 import org.apache.shiro.crypto.hash.AbstractCryptHash;
22 import org.apache.shiro.crypto.hash.Hash;
23 import org.apache.shiro.crypto.hash.HashProvider;
24 import org.apache.shiro.crypto.hash.HashSpi;
25
26 import static java.util.Objects.requireNonNull;
27
28 /**
29 * The {@code Shiro2CryptFormat} is a fully reversible
30 * <a href="http://packages.python.org/passlib/modular_crypt_format.html">Modular Crypt Format</a> (MCF). It is based
31 * on the posix format for storing KDF-hashed passwords in {@code /etc/shadow} files on linux and unix-alike systems.
32 * <h2>Format</h2>
33 * <p>Hash instances formatted with this implementation will result in a String with the following dollar-sign ($)
34 * delimited format:</p>
35 * <pre>
36 * <b>$</b>mcfFormatId<b>$</b>algorithmName<b>$</b>algorithm-specific-data.
37 * </pre>
38 * <p>Each token is defined as follows:</p>
39 * <table>
40 * <tr>
41 * <th>Position</th>
42 * <th>Token</th>
43 * <th>Description</th>
44 * <th>Required?</th>
45 * </tr>
46 * <tr>
47 * <td>1</td>
48 * <td>{@code mcfFormatId}</td>
49 * <td>The Modular Crypt Format identifier for this implementation, equal to <b>{@code shiro2}</b>.
50 * ( This implies that all {@code shiro2} MCF-formatted strings will always begin with the prefix
51 * {@code $shiro2$} ).</td>
52 * <td>true</td>
53 * </tr>
54 * <tr>
55 * <td>2</td>
56 * <td>{@code algorithmName}</td>
57 * <td>The name of the hash algorithm used to perform the hash. Either a hash class exists, or
58 * otherwise a {@link UnsupportedOperationException} will be thrown.
59 * <td>true</td>
60 * </tr>
61 * <tr>
62 * <td>3</td>
63 * <td>{@code algorithm-specific-data}</td>
64 * <td>In contrast to the previous {@code shiro1} format, the shiro2 format does not make any assumptions
65 * about how an algorithm stores its data. Therefore, everything beyond the first token is handled over
66 * to the Hash implementation.</td>
67 * </tr>
68 * </table>
69 *
70 * @see ModularCryptFormat
71 * @see ParsableHashFormat
72 * @since 2.0
73 */
74 public class Shiro2CryptFormat implements ModularCryptFormat, ParsableHashFormat {
75
76 /**
77 * Identifier for the shiro2 crypt format.
78 */
79 public static final String ID = "shiro2";
80 /**
81 * Enclosed identifier of the shiro2 crypt format.
82 */
83 public static final String MCF_PREFIX = TOKEN_DELIMITER + ID + TOKEN_DELIMITER;
84
85 public Shiro2CryptFormat() {
86 }
87
88 @Override
89 public String getId() {
90 return ID;
91 }
92
93 /**
94 * Converts a Hash-extending class to a string understood by the hash class. Usually this string will follow
95 * posix standards for passwords stored in {@code /etc/passwd}.
96 *
97 * <p>This method should only delegate to the corresponding formatter and prepend {@code $shiro2$}.</p>
98 *
99 * @param hash the hash instance to format into a String.
100 * @return a string representing the hash.
101 */
102 @Override
103 public String format(final Hash hash) {
104 requireNonNull(hash, "hash in Shiro2CryptFormat.format(Hash hash)");
105
106 if (!(hash instanceof AbstractCryptHash)) {
107 throw new UnsupportedOperationException("Shiro2CryptFormat can only format classes extending AbstractCryptHash.");
108 }
109
110 AbstractCryptHash cryptHash = (AbstractCryptHash) hash;
111 return TOKEN_DELIMITER + ID + cryptHash.formatToCryptString();
112 }
113
114 @Override
115 public Hash parse(final String formatted) {
116 requireNonNull(formatted, "formatted in Shiro2CryptFormat.parse(String formatted)");
117
118 // backwards compatibility
119 if (formatted.startsWith(Shiro1CryptFormat.MCF_PREFIX)) {
120 return new Shiro1CryptFormat().parse(formatted);
121 }
122
123 if (!formatted.startsWith(MCF_PREFIX)) {
124 final String msg = "The argument is not a valid '" + ID + "' formatted hash.";
125 throw new IllegalArgumentException(msg);
126 }
127
128 final String suffix = formatted.substring(MCF_PREFIX.length());
129 final String[] parts = suffix.split("\\$");
130 final String algorithmName = parts[0];
131
132 HashSpi kdfHash = HashProvider.getByAlgorithmName(algorithmName)
133 .orElseThrow(() -> new UnsupportedOperationException("Algorithm " + algorithmName + " is not implemented."));
134 return kdfHash.fromString("$" + suffix);
135 }
136
137 }