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 -> string, string -> 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 }