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 module hunt.shiro.crypto.hash.format.Shiro1CryptFormat;
20 
21 import hunt.shiro.crypto.hash.format.ModularCryptFormat;
22 import hunt.shiro.crypto.hash.format.ParsableHashFormat;
23 
24 import hunt.shiro.codec.Base64;
25 import hunt.shiro.crypto.hash.Hash;
26 import hunt.shiro.crypto.hash.SimpleHash;
27 import hunt.shiro.util.ByteSource;
28 import hunt.shiro.util.SimpleByteSource;
29 // import hunt.shiro.util.StringUtils;
30 
31 import hunt.Exceptions;
32 import hunt.util.StringBuilder;
33 
34 import std.conv;
35 import std.string;
36 
37 /**
38  * The {@code Shiro1CryptFormat} is a fully reversible
39  * <a href="http://packages.python.org/passlib/modular_crypt_format.html">Modular Crypt Format</a> (MCF).  Because it is
40  * fully reversible (i.e. Hash -&gt; string, string -&gt; Hash), it does NOT use the traditional MCF encoding alphabet
41  * (the traditional MCF encoding, aka H64, is bit-destructive and cannot be reversed).  Instead, it uses fully
42  * reversible Base64 encoding for the Hash digest and any salt value.
43  * <h2>Format</h2>
44  * <p>Hash instances formatted with this implementation will result in a string with the following dollar-sign ($)
45  * delimited format:</p>
46  * <pre>
47  * <b>$</b>mcfFormatId!(b)$</b>algorithmName!(b)$</b>iterationCount!(b)$</b>base64EncodedSalt!(b)$</b>base64EncodedDigest
48  * </pre>
49  * <p>Each token is defined as follows:</p>
50  * <table>
51  *     <tr>
52  *         <th>Position</th>
53  *         <th>Token</th>
54  *         <th>Description</th>
55  *         <th>Required?</th>
56  *     </tr>
57  *     <tr>
58  *         <td>1</td>
59  *         <td>{@code mcfFormatId}</td>
60  *         <td>The Modular Crypt Format identifier for this implementation, equal to <b>{@code shiro1}</b>.
61  *             ( This implies that all {@code shiro1} MCF-formatted strings will always begin with the prefix
62  *             {@code $shiro1$} ).</td>
63  *         <td>true</td>
64  *     </tr>
65  *     <tr>
66  *         <td>2</td>
67  *         <td>{@code algorithmName}</td>
68  *         <td>The name of the hash algorithm used to perform the hash.  This is an algorithm name understood by
69  *         {@code MessageDigest}.{@link java.security.MessageDigest#getInstance(string) getInstance}, for example
70  *         {@code MD5}, {@code SHA-256}, {@code SHA-256}, etc.</td>
71  *         <td>true</td>
72  *     </tr>
73  *     <tr>
74  *         <td>3</td>
75  *         <td>{@code iterationCount}</td>
76  *         <td>The number of hash iterations performed.</td>
77  *         <td>true (1 <= N <= Integer.MAX_VALUE)</td>
78  *     </tr>
79  *     <tr>
80  *         <td>4</td>
81  *         <td>{@code base64EncodedSalt}</td>
82  *         <td>The Base64-encoded salt byte array.  This token only exists if a salt was used to perform the hash.</td>
83  *         <td>false</td>
84  *     </tr>
85  *     <tr>
86  *         <td>5</td>
87  *         <td>{@code base64EncodedDigest}</td>
88  *         <td>The Base64-encoded digest byte array.  This is the actual hash result.</td>
89  *         <td>true</td>
90  *     </tr>
91  * </table>
92  *
93  * @see ModularCryptFormat
94  * @see ParsableHashFormat
95  *
96  */
97 class Shiro1CryptFormat : ModularCryptFormat, ParsableHashFormat {
98 
99     enum string ID = "shiro1";
100     enum string MCF_PREFIX = TOKEN_DELIMITER ~ ID ~ TOKEN_DELIMITER;
101 
102     this() {
103     }
104 
105     string getId() {
106         return ID;
107     }
108 
109     string format(Hash hash) {
110         if (hash  is null) {
111             return null;
112         }
113 
114         string algorithmName = hash.getAlgorithmName();
115         ByteSource salt = hash.getSalt();
116         int iterations = hash.getIterations();
117         StringBuilder sb = new StringBuilder(MCF_PREFIX).append(algorithmName).append(TOKEN_DELIMITER).append(iterations).append(TOKEN_DELIMITER);
118 
119         if (salt !is null) {
120             sb.append(salt.toBase64());
121         }
122 
123         sb.append(TOKEN_DELIMITER);
124         sb.append(hash.toBase64());
125 
126         return sb.toString();
127     }
128 
129     Hash parse(string formatted) {
130         if (formatted  is null) {
131             return null;
132         }
133         if (!formatted.startsWith(MCF_PREFIX)) {
134             //TODO create a HashFormatException class
135             string msg = "The argument is not a valid '" ~ ID ~ "' formatted hash.";
136             throw new IllegalArgumentException(msg);
137         }
138 
139         string suffix = formatted[MCF_PREFIX.length .. $];
140         string[] parts = suffix.split("\\$");
141 
142         //last part is always the digest/checksum, Base64-encoded:
143         int i = cast(int)parts.length-1;
144         string digestBase64 = parts[i--];
145         //second-to-last part is always the salt, Base64-encoded:
146         string saltBase64 = parts[i--];
147         string iterationsString = parts[i--];
148         string algorithmName = parts[i];
149 
150         byte[] digest = Base64.decode(digestBase64);
151         ByteSource salt = null;
152 
153         if (!saltBase64.empty()) {
154             byte[] saltBytes = Base64.decode(saltBase64);
155             salt = ByteSourceUtil.bytes(saltBytes);
156         }
157 
158         int iterations;
159         try {
160             iterations = to!int(iterationsString);
161         } catch (NumberFormatException e) {
162             string msg = "Unable to parse formatted hash string: " ~ formatted;
163             throw new IllegalArgumentException(msg, e);
164         }
165 
166         SimpleHash hash = new SimpleHash(algorithmName);
167         hash.setBytes(digest);
168         if (salt !is null) {
169             hash.setSalt(salt);
170         }
171         hash.setIterations(iterations);
172 
173         return hash;
174     }
175 }