001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.crypto.hash.format;
020
021import org.apache.shiro.crypto.hash.AbstractCryptHash;
022import org.apache.shiro.crypto.hash.Hash;
023import org.apache.shiro.crypto.hash.HashProvider;
024import org.apache.shiro.crypto.hash.HashSpi;
025
026import static java.util.Objects.requireNonNull;
027
028/**
029 * The {@code Shiro2CryptFormat} is a fully reversible
030 * <a href="http://packages.python.org/passlib/modular_crypt_format.html">Modular Crypt Format</a> (MCF). It is based
031 * on the posix format for storing KDF-hashed passwords in {@code /etc/shadow} files on linux and unix-alike systems.
032 * <h2>Format</h2>
033 * <p>Hash instances formatted with this implementation will result in a String with the following dollar-sign ($)
034 * delimited format:</p>
035 * <pre>
036 * <b>$</b>mcfFormatId<b>$</b>algorithmName<b>$</b>algorithm-specific-data.
037 * </pre>
038 * <p>Each token is defined as follows:</p>
039 * <table>
040 *     <tr>
041 *         <th>Position</th>
042 *         <th>Token</th>
043 *         <th>Description</th>
044 *         <th>Required?</th>
045 *     </tr>
046 *     <tr>
047 *         <td>1</td>
048 *         <td>{@code mcfFormatId}</td>
049 *         <td>The Modular Crypt Format identifier for this implementation, equal to <b>{@code shiro2}</b>.
050 *             ( This implies that all {@code shiro2} MCF-formatted strings will always begin with the prefix
051 *             {@code $shiro2$} ).</td>
052 *         <td>true</td>
053 *     </tr>
054 *     <tr>
055 *         <td>2</td>
056 *         <td>{@code algorithmName}</td>
057 *         <td>The name of the hash algorithm used to perform the hash. Either a hash class exists, or
058 *         otherwise a {@link UnsupportedOperationException} will be thrown.
059 *         <td>true</td>
060 *     </tr>
061 *     <tr>
062 *         <td>3</td>
063 *         <td>{@code algorithm-specific-data}</td>
064 *         <td>In contrast to the previous {@code shiro1} format, the shiro2 format does not make any assumptions
065 *         about how an algorithm stores its data. Therefore, everything beyond the first token is handled over
066 *         to the Hash implementation.</td>
067 *     </tr>
068 * </table>
069 *
070 * @see ModularCryptFormat
071 * @see ParsableHashFormat
072 * @since 2.0
073 */
074public class Shiro2CryptFormat implements ModularCryptFormat, ParsableHashFormat {
075
076    /**
077     * Identifier for the shiro2 crypt format.
078     */
079    public static final String ID = "shiro2";
080    /**
081     * Enclosed identifier of the shiro2 crypt format.
082     */
083    public static final String MCF_PREFIX = TOKEN_DELIMITER + ID + TOKEN_DELIMITER;
084
085    public Shiro2CryptFormat() {
086    }
087
088    @Override
089    public String getId() {
090        return ID;
091    }
092
093    /**
094     * Converts a Hash-extending class to a string understood by the hash class. Usually this string will follow
095     * posix standards for passwords stored in {@code /etc/passwd}.
096     *
097     * <p>This method should only delegate to the corresponding formatter and prepend {@code $shiro2$}.</p>
098     *
099     * @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}