Package hirondelle.fish.access.user

Source Code of hirondelle.fish.access.user.User

package hirondelle.fish.access.user;

import java.util.regex.*;
import java.util.logging.*;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.model.Check;
import hirondelle.web4j.model.Validator;
import hirondelle.web4j.security.SafeText;
import static hirondelle.web4j.util.Consts.FAILS;

/**
User for the Fish and Chips Club.
 
<P>When first added, the User has a fixed, inconvenient password, which 
they are encouraged to change. This is not enforced by the application, however.

<P>The password is "hashed".
A one-way hash function ensures the password is NOT stored in cleartext.
When the container enforces a security-constraint defined in <tt>web.xml</tt>, the container
must be instructed to call the exact same hash function (SHA-1), in order to match the database.

<P>Performing the hash here, instead of in the database, provides independance from
database implementations of hash functions (or lack thereof).

<P>Note that a one-way <em>hash</em> function is used here, NOT an <em>encryption</em>
function: encrypted items are ultimately intended for decryption. Here, no decryption is ever attempted.
In fact, the whole point of using a hash function is that it is nearly impossible to
deduce the password from the hashed value itself.
*/
public final class User {

  /** Verify presence of hash function in current JRE.  */
  static {
    MessageDigest sha = null;
    try {
      sha = MessageDigest.getInstance("SHA-1");
    }
    catch (NoSuchAlgorithmException ex){
      throw new RuntimeException("Unable to hash passwords. MessageDigest class cannot find the SHA-1 hash function.");
    }
  }
 
  /**
   Constructor taking a password that is already hashed.
 
   <P>This constructor is used when retrieving from the database, where passwords are
   already stored in a hashed form.
  
   @param aName user name (required), 6..50 characters, no spaces.
   @param aHashedPassword <em>hashed</em> user password (required), 6..50 characters, no spaces, and never the
   same as the user name.
  */
  public User(SafeText aName, SafeText aHashedPassword) throws ModelCtorException {
    fName = aName;
    fHashedPassword = aHashedPassword;
    validateState();
  }
 
  /**
   Factory method for a new {@link User}, with a fixed, initial password.
  
   <P><span class="highlight">In this implementation, the initial password is very
   long, and thus inconvenient to use. New users are encouraged to change it immediately,
   upon first use. This is not enforced, however.</span>
  */
  public static User forNewUserOrPasswordReset(SafeText aName) throws ModelCtorException {
    return new User(aName, SafeText.from(hash(MAGIC_INITIAL_PASSWORD)));
  }
 
  /**
   Factory method for a new {@link User}, reflecting a new password.
  
   <P>The arguments must pass the same constraints as {@link #User(SafeText, SafeText)}.
  */
  public static User forPasswordChange(SafeText aName, SafeText aClearTextPassword) throws ModelCtorException {
    User temp = new User(aName, aClearTextPassword); //will throw ex if fails
    return new User(aName, SafeText.from(hash(aClearTextPassword.getRawString())));
  }
 
  /** Return the user name passed to the constructor.  */
  public SafeText getName() { 
    return fName;
  }
 
  /** Return the <em>hashed</em> password (never cleartext).  */
  public SafeText getPassword() { 
    return fHashedPassword; 
  }
 
  /**
   Return <tt>true</tt> only if the password matches the initial, reset value.
  
   <P>Passwords which match the initial, reset value should be changed by the end user.
  */
  public boolean isResetValue(){
    return hash(MAGIC_INITIAL_PASSWORD).equalsIgnoreCase(fHashedPassword.getRawString());
  }
 
  /** Intended for debugging only. The return value will mask the password. */
  @Override public String toString() {
    return hirondelle.fish.access.user.User.class + " User Name : " + fName + " Password : ****";
  }

  @Override public boolean equals( Object aThat ) {
    Boolean result = ModelUtil.quickEquals(this, aThat);
    if ( result == null ){
      User that = (User) aThat;
      result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
    }
    return result;   
  }

  @Override public int hashCode() {
    if ( fHashCode == 0 ) {
      fHashCode = ModelUtil.hashCodeFor(getSignificantFields());
    }
    return fHashCode;
  }
  
  //PRIVATE//
  private final SafeText fName;
  private final SafeText fHashedPassword;
  private int fHashCode;
 
  /**
   Not public, since could be used in an attack, if visible in javadoc.
  
   <P>HIGHLY RECOMMENDED that this value be changed for your application, but that the basic idea be
   preserved : this initial/reset password should be inconvenient to type, so that the user will
   be strongly encouraged to change it as soon as possible.
  */
  private static final String MAGIC_INITIAL_PASSWORD = "changemetosomethingalotmoreconvenienttotype";
  private static final Pattern ACCEPTED_PATTERN = Pattern.compile("(?:\\S){6,50}");
  private static final Logger fLogger = Util.getLogger(User.class);

  private void validateState() throws ModelCtorException {
    ModelCtorException ex = new ModelCtorException();
    Validator validPattern = Check.pattern(ACCEPTED_PATTERN);
    if ( FAILS == Check.required(fName, validPattern) ) {
      ex.add("Name is required, 6..50 chars, no spaces.");
    }
    if ( FAILS == Check.required(fHashedPassword, validPattern)) {
      ex.add("Password is required, 6..50 chars, no spaces.");
    }
    if( fName != null && fHashedPassword != null ) {
      if( fName.getRawString().equalsIgnoreCase(fHashedPassword.getRawString()) ){
        ex.add("Password cannot be the same as the user name.");
      }
    }
    if ( ! ex.isEmpty() ) throw ex;
  }
  
  private Object[] getSignificantFields(){
    return new Object[] {fName, fHashedPassword};
  }

  /**
   The static initializer of this class will barf if the hash function is not present.
  */
  private static String hash(String aCleartext) {
    String result = null;
   
    MessageDigest sha = null;
    try {
      sha = MessageDigest.getInstance("SHA-1");
    }
    catch (NoSuchAlgorithmException ex){
      fLogger.severe("Cannot find SHA-1 hash function.");
    }

    if (sha != null){
      byte[] digest =  sha.digest( aCleartext.getBytes() );
      result = hexEncode(digest);
    }
    else {
      result = aCleartext;
    }
    return result;
  }
 
  /**
   The byte[] returned by MessageDigest does not have a nice
   textual representation, so some form of encoding is usually performed.
 
   This implementation follows the example of David Flanagan's book
   "Java In A Nutshell", and converts a byte array into a String
   of hex characters.
  */
  private static String hexEncode( byte[] aInput){
    StringBuffer result = new StringBuffer();
    char[] digits = {'0', '1', '2', '3', '4','5','6','7','8','9','a','b','c','d','e','f'};
    for ( int idx = 0; idx < aInput.length; ++idx) {
      byte b = aInput[idx];
      result.append( digits[ (b&0xf0) >> 4 ] );
      result.append( digits[ b&0x0f] );
    }
    return result.toString();
 
 
  /** Informal test harness. Change to public to exercise. */
  private static void main(String[] args){
    System.out.println("Hash: " + hash("testtest"));
  }
}
TOP

Related Classes of hirondelle.fish.access.user.User

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.