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}