blob: f781c2b7429d6b7947ee0d21ece1b36a3469979e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.shiro.crypto.hash.format;
import org.apache.shiro.crypto.hash.AbstractCryptHash;
import org.apache.shiro.crypto.hash.Hash;
import org.apache.shiro.crypto.hash.HashProvider;
import org.apache.shiro.crypto.hash.HashSpi;
import static java.util.Objects.requireNonNull;
/**
* The {@code Shiro2CryptFormat} is a fully reversible
* <a href="http://packages.python.org/passlib/modular_crypt_format.html">Modular Crypt Format</a> (MCF). It is based
* on the posix format for storing KDF-hashed passwords in {@code /etc/shadow} files on linux and unix-alike systems.
* <h2>Format</h2>
* <p>Hash instances formatted with this implementation will result in a String with the following dollar-sign ($)
* delimited format:</p>
* <pre>
* <b>$</b>mcfFormatId<b>$</b>algorithmName<b>$</b>algorithm-specific-data.
* </pre>
* <p>Each token is defined as follows:</p>
* <table>
* <tr>
* <th>Position</th>
* <th>Token</th>
* <th>Description</th>
* <th>Required?</th>
* </tr>
* <tr>
* <td>1</td>
* <td>{@code mcfFormatId}</td>
* <td>The Modular Crypt Format identifier for this implementation, equal to <b>{@code shiro2}</b>.
* ( This implies that all {@code shiro2} MCF-formatted strings will always begin with the prefix
* {@code $shiro2$} ).</td>
* <td>true</td>
* </tr>
* <tr>
* <td>2</td>
* <td>{@code algorithmName}</td>
* <td>The name of the hash algorithm used to perform the hash. Either a hash class exists, or
* otherwise a {@link UnsupportedOperationException} will be thrown.
* <td>true</td>
* </tr>
* <tr>
* <td>3</td>
* <td>{@code algorithm-specific-data}</td>
* <td>In contrast to the previous {@code shiro1} format, the shiro2 format does not make any assumptions
* about how an algorithm stores its data. Therefore, everything beyond the first token is handled over
* to the Hash implementation.</td>
* </tr>
* </table>
*
* @see ModularCryptFormat
* @see ParsableHashFormat
* @since 2.0
*/
public class Shiro2CryptFormat implements ModularCryptFormat, ParsableHashFormat {
/**
* Identifier for the shiro2 crypt format.
*/
public static final String ID = "shiro2";
/**
* Enclosed identifier of the shiro2 crypt format.
*/
public static final String MCF_PREFIX = TOKEN_DELIMITER + ID + TOKEN_DELIMITER;
public Shiro2CryptFormat() {
}
@Override
public String getId() {
return ID;
}
/**
* Converts a Hash-extending class to a string understood by the hash class. Usually this string will follow
* posix standards for passwords stored in {@code /etc/passwd}.
*
* <p>This method should only delegate to the corresponding formatter and prepend {@code $shiro2$}.</p>
*
* @param hash the hash instance to format into a String.
* @return a string representing the hash.
*/
@Override
public String format(final Hash hash) {
requireNonNull(hash, "hash in Shiro2CryptFormat.format(Hash hash)");
if (!(hash instanceof AbstractCryptHash)) {
throw new UnsupportedOperationException("Shiro2CryptFormat can only format classes extending AbstractCryptHash.");
}
AbstractCryptHash cryptHash = (AbstractCryptHash) hash;
return TOKEN_DELIMITER + ID + cryptHash.formatToCryptString();
}
@Override
public Hash parse(final String formatted) {
requireNonNull(formatted, "formatted in Shiro2CryptFormat.parse(String formatted)");
// backwards compatibility
if (formatted.startsWith(Shiro1CryptFormat.MCF_PREFIX)) {
return new Shiro1CryptFormat().parse(formatted);
}
if (!formatted.startsWith(MCF_PREFIX)) {
final String msg = "The argument is not a valid '" + ID + "' formatted hash.";
throw new IllegalArgumentException(msg);
}
final String suffix = formatted.substring(MCF_PREFIX.length());
final String[] parts = suffix.split("\\$");
final String algorithmName = parts[0];
HashSpi kdfHash = HashProvider.getByAlgorithmName(algorithmName)
.orElseThrow(() -> new UnsupportedOperationException("Algorithm " + algorithmName + " is not implemented."));
return kdfHash.fromString("$" + suffix);
}
}