/*
GASH 2
PasswordDBField.java
The GANYMEDE object storage system.
Created: 21 July 1997
Module By: Jonathan Abbey, jonabbey@arlut.utexas.edu
-----------------------------------------------------------------------
Ganymede Directory Management System
Copyright (C) 1996-2013
The University of Texas at Austin
Ganymede is a registered trademark of The University of Texas at Austin
Contact information
Web site: http://www.arlut.utexas.edu/gash2
Author Email: ganymede_author@arlut.utexas.edu
Email mailing list: ganymede@arlut.utexas.edu
US Mail:
Computer Science Division
Applied Research Laboratories
The University of Texas at Austin
PO Box 8029, Austin TX 78713-8029
Telephone: (512) 835-3200
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package arlut.csd.ganymede.server;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import org.solinger.cracklib.CrackLib;
import org.mindrot.BCrypt;
import arlut.csd.Util.TranslationService;
import arlut.csd.crypto.MD5Crypt;
import arlut.csd.crypto.Sha256Crypt;
import arlut.csd.crypto.Sha512Crypt;
import arlut.csd.crypto.SSHA;
import arlut.csd.crypto.jcrypt;
import arlut.csd.crypto.smbencrypt;
import arlut.csd.ganymede.common.ReturnVal;
import arlut.csd.ganymede.rmi.pass_field;
/*------------------------------------------------------------------------------
class
PasswordDBField
------------------------------------------------------------------------------*/
/**
* <p>PasswordDBField is a subclass of {@link
* arlut.csd.ganymede.server.DBField DBField} for the storage and
* handling of password fields in the {@link
* arlut.csd.ganymede.server.DBStore DBStore} on the Ganymede server.</p>
*
* <p>The Ganymede client talks to PasswordDBFields through the {@link
* arlut.csd.ganymede.rmi.pass_field pass_field} RMI interface.</p>
*
* <p>This class differs a bit from most subclasses of {@link
* arlut.csd.ganymede.server.DBField DBField} in that the normal
* setValue()/getValue() methods are non-functional. Instead, there
* are special methods used to set or access password information in
* hashed and non-hashed forms.</p>
*
* <p>PasswordDBField supports a significant variety of password hash
* formats, to allow Ganymede to (optionally) avoid storing passwords
* in plain text, while still retaining the ability to emit password
* information to a variety of information systems.</p>
*
* <p>Here are the hash algorithms supported by PasswordDBField:</p>
*
* <ul>
* <li>Traditional DES-based Unix Crypt()</li>
* <li>OpenBSD-style md5Crypt ($1$ prefix)</li>
* <li>OpenBSD-style md5Crypt, as modified for use with Apache ($apr1$ prefix)</li>
* <li>OpenBSD-style BCrypt ($2a$ prefix)</li>
* <li>Traditional LAN Manager hash</li>
* <li>Windows NT Unicode Hash algorithm</li>
* <li>SSHA, Salted SHA-1 hash, as used in OpenLDAP</li>
* <li>SHA Crypt, SHA256 and SHA512 based scalable hash
* algorithm, supported in Linux starting with glibc version 2.7. ($5$ and $6$ prefixes)</li>
* </ul>
*
* <p>There are no methods provided to allow remote access to password
* information.. server-side code must locally call methods to get
* access to stored password information. Even in that case, only
* hashed password information will generally be available, though the
* schema can be configured to have password fields maintain plaintext
* (necessary for sync'ing to Kerberos based systems like Active
* Directory).</p>
*
* <p> If this password field is configured to store only hashed
* passwords by way of its {@link
* arlut.csd.ganymede.server.DBObjectBaseField DBObjectBaseField},
* this password field will never emit() the plaintext to disk.</p>
*
* <p>In such cases, only the hash text password information will be
* retained on disk for user authentication. The plaintext of the
* password can be retained in memory for the duration of a run of the
* Ganymede server process, but as the plaintext is not stored in the
* Ganymede server's ganymede.db file, the plaintext is lost when the
* server is stopped.</p>
*
* <p>This transient retention of plaintext password information can
* still be useful in the context of the Ganymede 2.0 {@link
* arlut.csd.ganymede.server.SyncRunner Sync Channel} mechanism,
* however. If a user changes his password, a Sync Channel can be
* configured to write the plaintext password change information out,
* even though the server is prone to forget the plaintext if it is
* stopped and restarted.</p>
*
* <p>At ARL, we use this transient plaintext retention to allow us to
* synchronize passwords to Active Directory without having the risk
* of long term plaintext storage in the ganymede.db file.</p>
*/
public class PasswordDBField extends DBField implements pass_field {
static final boolean debug = false;
/**
* TranslationService object for handling string localization in the
* Ganymede server.
*/
static final TranslationService ts = TranslationService.getTranslationService("arlut.csd.ganymede.server.PasswordDBField");
// ---
/**
* Traditional Unix crypt()'ed pass. Only good for validating the
* first 8 characters of a plaintext.
*/
private String cryptedPass;
/**
* <p>The complex md5crypt()'ed password, as in OpenBSD, FreeBSD,
* Linux PAM, etc. Good for validating indefinite length
* strings.</p>
*
* <p>Algorithm by Paul-Hennig Kamp. Uses a $1$ prefix.</p>
*/
private String md5CryptPass;
/**
* <p>The complex bCrypted password, as in OpenBSD. Very high,
* scalable security.</p>
*
* <p>This hash format can have a very large digest size, a large
* salt, and a very significant computational cost, which makes this
* hash (along with Ulrich Drepper's SHA-CRYPT) one of the
* most resistant to brute force attacks.</p>
*
* <p>Algorithm by Neils Provos and the Blowfish creators. Uses a
* $2a$ prefix.</p>
*/
private String bCryptPass;
/**
* <p>The complex md5crypt()'ed password, with the magic string used
* by Apache for their htpasswd file format. Good for validating
* indefinite length strings.</p>
*
* <p>Variant of Paul-Hennig Kamps's algorithm. Uses a $apr1$
* prefix.</p>
*/
private String apacheMd5CryptPass;
/**
* <p>Plaintext password.. will never be saved to disk if we have
* another hash format available to validate, unless this field has
* been specifically configured to always save plaintext to disk in
* the schema editor. See {@link
* arlut.csd.ganymede.server.DBObjectBaseField#isPlainText()} for more
* detail.</p>
*/
private String uncryptedPass;
/**
* <p>Samba LANMAN hash, for Win95 clients. Only good for
* validating the first 14 characters of a plaintext. This hash is
* actually incredibly, mind-crushingly weak.. weaker than
* traditional Unix crypt, even. If you're basing your password
* security on this hash still, you're in trouble.</p>
*/
private String lanHash;
/**
* <p>Samba md4 Unicode hash, for WinNT/2k clients. Good for
* validating up to 2^64 bits of plaintext.. effectively indefinite
* in extent</p>
*/
private String ntHash;
/**
* <p>Salted SHA-1 hash, for OpenLDAP. Good for validating up to 2^64
* bits of plaintext.. effectively indefinite in extent. A very
* strong hash format in terms of the difficulty of finding
* collisions in the hash range, but it's very quick to evaluate, so
* a dictionary attack against this hash can proceed rapidly.</p>
*
* <p>Note that we keep the sshaHash string here in the same form
* as would be used in an LDAP store.</p>
*
* <p>This is Netscape's salted variant of the FIPS SHA-1 standard.
* SHA-1 is described at <a href="http://en.wikipedia.org/wiki/SHA-1">http://en.wikipedia.org/wiki/SHA-1</a>,
* while SSHA is described at
* <a href="http://www.openldap.org/faq/data/cache/347.html">http://www.openldap.org/faq/data/cache/347.html</a>.</p>
*/
private String sshaHash;
/**
* <p>Password hashed using the SHA Unix Crypt algorithm published
* by Ulrich Drepper at</p>
*
* <p><a href="http://people.redhat.com/drepper/sha-crypt.html">http://people.redhat.com/drepper/sha-crypt.html</a></p>
*
* <p>The hash text in shaUnixCrypt can be generated using either the
* Sha256Crypt or Sha512Crypt variants of the SHA Unix Crypt
* algorithm described at the above URL.</p>
*
* <p>This hash format can have a very large digest size, a large
* salt, and a very significant computational cost, which makes this
* hash (along with Niels Provos's bCrypt) one of the most resistant
* to brute force attacks.</p>
*/
private String shaUnixCrypt;
/**
* <p>History archive of previous password hashes and the dates that
* the previous passwords were committed into the database.</p>
*
* <p>This variable will only be non-null if the DBObjectBaseField
* definition for this field has history_check set.</p>
*/
private passwordHistoryArchive history = null;
/* -- */
/**
* Receive constructor. Used to create a PasswordDBField from a DBStore/DBJournal
* DataInput stream.
*/
PasswordDBField(DBObject owner, DataInput in, DBObjectBaseField definition) throws IOException
{
super(owner, definition.getID());
this.value = null;
receive(in, definition);
}
/**
* <p>No-value constructor. Allows the construction of a
* 'non-initialized' field, for use where the {@link
* arlut.csd.ganymede.server.DBObjectBase DBObjectBase} definition
* indicates that a given field may be present, but for which no
* value has been stored in the {@link arlut.csd.ganymede.server.DBStore
* DBStore}.</p>
*
* <p>Used to provide the client a template for 'creating' this
* field if so desired.</p>
*/
PasswordDBField(DBObject owner, DBObjectBaseField definition)
{
super(owner, definition.getID());
if (definition.isHistoryChecked())
{
this.history = new passwordHistoryArchive(definition.getHistoryDepth());
}
value = null;
}
/**
* <p>Copy constructor, used when checking edit objects in and out.</p>
*
* <p>As a field copy constructor, this method must not throw an
* exception, or else commits can be seriously broken.</p>
*/
public PasswordDBField(DBObject owner, PasswordDBField field)
{
super(owner, field.getID());
cryptedPass = field.cryptedPass;
md5CryptPass = field.md5CryptPass;
apacheMd5CryptPass = field.apacheMd5CryptPass;
uncryptedPass = field.uncryptedPass;
lanHash = field.lanHash;
ntHash = field.ntHash;
sshaHash = field.sshaHash;
shaUnixCrypt = field.shaUnixCrypt;
bCryptPass = field.bCryptPass;
history = field.history;
try
{
// If we're keeping history and we're copying from an editable
// object to a non-editable object and the field we're copying
// from changed during the transaction we're consolidating, we
// need to remember the password that is being set with this
// commit.
if (getFieldDef().isHistoryChecked())
{
if (history == null)
{
history = new passwordHistoryArchive(getFieldDef().getHistoryDepth());
}
else if (history.getPoolSize() != getFieldDef().getHistoryDepth())
{
history.setPoolSize(getFieldDef().getHistoryDepth());
}
if (this.uncryptedPass != null &&
!(this.owner instanceof DBEditObject) &&
field.hasChanged())
{
history.add(uncryptedPass, new Date());
}
}
else
{
history = null;
}
}
catch (Throwable ex)
{
// we're *not* going to allow an exception to be thrown here
// for the sake of the password history tracking
ex.printStackTrace();
}
}
/**
* Returns true if this field has a value associated
* with it, or false if it is an unfilled 'placeholder'.
*
* @see arlut.csd.ganymede.rmi.db_field
*/
@Override public boolean isDefined()
{
return (cryptedPass != null || md5CryptPass != null ||
apacheMd5CryptPass != null || uncryptedPass != null || lanHash != null
|| ntHash != null || bCryptPass != null | sshaHash != null || shaUnixCrypt != null);
}
/**
* <p>This method is used to mark a field as undefined when it is
* checked out for editing. Different subclasses of {@link
* arlut.csd.ganymede.server.DBField DBField} may implement this in
* different ways, if simply setting the field's value member to
* null is not appropriate. Any namespace values claimed by the
* field will be released, and when the transaction is committed,
* this field will be released.</p>
*
* <p>Note that this method is really only intended for those fields
* which have some significant internal structure to them, such as
* permission matrix, field option matrix, and password fields.</p>
*
* <p>NOTE: There is, at present, no defined DBEditObject callback
* method that tracks generic field nullification. This means that
* if your code uses setUndefined on a PermissionMatrixDBField,
* FieldOptionDBField, or PasswordDBField, the plugin code is not
* currently given the opportunity to review and refuse that
* operation. Caveat Coder.</p>
*/
@Override public synchronized ReturnVal setUndefined(boolean local)
{
if (!isEditable(local))
{
// "Permissions Error"
// "You do not have permission to clear the "{0}" password field in object "{1}"."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setUndefined.perm_error_subj"),
ts.l("setUndefined.perm_error_text", this.getName(),
this.owner.getLabel()));
}
clear_stored();
return null;
}
/**
* private helper to clear all stored password information in this field
*/
private synchronized final void clear_stored()
{
cryptedPass = null;
md5CryptPass = null;
apacheMd5CryptPass = null;
bCryptPass = null;
uncryptedPass = null;
ntHash = null;
lanHash = null;
sshaHash = null;
shaUnixCrypt = null;
// NB: Don't clear history.. this password field might be set to a
// defined value later on and we don't want to forget previous
// values
}
/**
* private helper to clear stored unnecessary stored password
* information in this field
*/
private synchronized final void clear_unused_stored()
{
if (!getFieldDef().isCrypted())
{
cryptedPass = null;
}
if (!getFieldDef().isMD5Crypted())
{
md5CryptPass = null;
}
if (!getFieldDef().isApacheMD5Crypted())
{
apacheMd5CryptPass = null;
}
if (!getFieldDef().isWinHashed())
{
lanHash = null;
ntHash = null;
}
if (!getFieldDef().isBCrypted())
{
bCryptPass = null;
}
if (!getFieldDef().isSSHAHashed())
{
sshaHash = null;
}
if (!getFieldDef().isShaUnixCrypted())
{
shaUnixCrypt = null; // force new hash
}
}
/**
* We don't expect these fields to ever be stored in a hash.
*/
@Override public int hashCode()
{
throw new UnsupportedOperationException();
}
/**
* <p>Returns true if obj is a field with the same value(s) as
* this one.</p>
*
* <p>This method is ok to be synchronized because it does not
* call synchronized methods on any other object.</p>
*/
@Override public synchronized boolean equals(Object obj)
{
if (obj == null)
{
return false;
}
if (!(obj.getClass().equals(this.getClass())))
{
return false;
}
PasswordDBField origP = (PasswordDBField) obj;
return (streq(cryptedPass, origP.cryptedPass) &&
streq(md5CryptPass, origP.md5CryptPass) &&
streq(apacheMd5CryptPass, origP.apacheMd5CryptPass) &&
streq(uncryptedPass, origP.uncryptedPass) &&
streq(lanHash, origP.lanHash) &&
streq(ntHash, origP.ntHash) &&
streq(bCryptPass, origP.bCryptPass) &&
streq(sshaHash, origP.sshaHash) &&
streq(shaUnixCrypt, origP.shaUnixCrypt));
}
/**
* Convenience null-friendly string comparison helper.
*/
private final boolean streq(String str1, String str2)
{
if (str1 == null && str2 == null)
{
return true;
}
if (str1 == null || str2 == null)
{
return false;
}
return str1.equals(str2);
}
/**
* <p>This method copies the current value of this DBField
* to target. The target DBField must be contained within a
* checked-out DBEditObject in order to be updated. Any actions
* that would normally occur from a user manually setting a value
* into the field will occur.</p>
*
* <p>NOTE: this method is mainly used in cloning objects, and
* {@link
* arlut.csd.ganymede.server.DBEditObject#cloneFromObject(arlut.csd.ganymede.server.DBSession,
* arlut.csd.ganymede.server.DBObject, boolean) cloneFromObject}
* doesn't allow cloning of password fields by default.</p>
*
* @param target The DBField to copy this field's contents to.
* @param local If true, permissions checking is skipped.
*/
@Override public synchronized ReturnVal copyFieldTo(DBField target, boolean local)
{
PasswordDBField targetField = (PasswordDBField) target;
if (!local)
{
if (!verifyReadPermission())
{
// "Error Copying Password Field"
// "Can''t copy field "{0}" in object "{1}", no read privileges on source."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("copyFieldTo.error_subj"),
ts.l("copyFieldTo.no_read", this.getName(),
this.owner.getLabel()));
}
}
if (!targetField.isEditable(local))
{
// "Error Copying Password Field"
// "Can''t copy field "{0}" in object "{1}", no write privileges on target."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("copyFieldTo.error_subj"),
ts.l("copyFieldTo.no_write", this.getName(),
this.owner.getLabel()));
}
targetField.cryptedPass = cryptedPass;
targetField.md5CryptPass = md5CryptPass;
targetField.apacheMd5CryptPass = apacheMd5CryptPass;
targetField.lanHash = lanHash;
targetField.ntHash = ntHash;
targetField.uncryptedPass = uncryptedPass;
targetField.bCryptPass = bCryptPass;
targetField.sshaHash = sshaHash;
targetField.shaUnixCrypt = shaUnixCrypt;
targetField.history = history;
return null; // simple success value
}
/**
* Object value of DBField. Used to represent value in value hashes.
* Subclasses need to override this method in subclass.
*/
@Override public Object key()
{
throw new IllegalArgumentException("PasswordDBFields may not be tracked in namespaces");
}
@Override public Object clone() throws CloneNotSupportedException
{
throw new CloneNotSupportedException();
}
/**
* <p>This method is responsible for writing out the contents of
* this field to an binary output stream. It is used in writing
* fields to the ganymede.db file and to the journal file.</p>
*
* <p>This method only writes out the value contents of this field.
* The {@link arlut.csd.ganymede.server.DBObject DBObject}
* {@link arlut.csd.ganymede.server.DBObject#emit(java.io.DataOutput) emit()}
* method is responsible for writing out the field identifier information
* ahead of the field's contents.</p>
*/
@Override void emit(DataOutput out) throws IOException
{
boolean need_to_write_all_hashes = writeOutAllStoredValues();
boolean wrote_hash = false;
/* -- */
// at 2.1 we write out all hashes all the time, and the
// plaintext if we are told to, or if we don't have any
// hashed form of it to use
if (getFieldDef().isCrypted() || (cryptedPass != null && need_to_write_all_hashes))
{
cryptedPass = getUNIXCryptText();
wrote_hash = emitHelper(out, cryptedPass, wrote_hash);
}
else
{
out.writeUTF("");
}
if (getFieldDef().isMD5Crypted() || (md5CryptPass != null && need_to_write_all_hashes))
{
md5CryptPass = getMD5CryptText();
wrote_hash = emitHelper(out, md5CryptPass, wrote_hash);
}
else
{
out.writeUTF("");
}
if (getFieldDef().isApacheMD5Crypted() || (apacheMd5CryptPass != null && need_to_write_all_hashes))
{
apacheMd5CryptPass = getApacheMD5CryptText();
wrote_hash = emitHelper(out, apacheMd5CryptPass, wrote_hash);
}
else
{
out.writeUTF("");
}
if (getFieldDef().isWinHashed() || ((lanHash != null || ntHash != null) && need_to_write_all_hashes))
{
lanHash = getLANMANCryptText();
wrote_hash = emitHelper(out, lanHash, wrote_hash);
ntHash = getNTUNICODECryptText();
wrote_hash = emitHelper(out, ntHash, wrote_hash);
}
else
{
out.writeUTF("");
out.writeUTF("");
}
if (getFieldDef().isSSHAHashed() || (sshaHash != null && need_to_write_all_hashes))
{
sshaHash = getSSHAHashText();
wrote_hash = emitHelper(out, sshaHash, wrote_hash);
}
else
{
out.writeUTF("");
}
// starting at file version 2.13
//
// (see DBStore.major_version and DBStore.minor_version)
if (getFieldDef().isShaUnixCrypted() || (shaUnixCrypt != null && need_to_write_all_hashes))
{
shaUnixCrypt = getShaUnixCryptText();
wrote_hash = emitHelper(out, shaUnixCrypt, wrote_hash);
}
else
{
out.writeUTF("");
}
// starting at file version 2.21
//
// (see DBStore.major_version and DBStore.minor_version)
if (getFieldDef().isBCrypted() || (bCryptPass != null && need_to_write_all_hashes))
{
bCryptPass = getBCryptText();
wrote_hash = emitHelper(out, bCryptPass, wrote_hash);
}
else
{
out.writeUTF("");
}
// at file version 2.1, we write out plaintext if the field
// definition requires it, or if we were not able to write
// out any crypttext
if (getFieldDef().isPlainText() || !wrote_hash)
{
emitHelper(out, uncryptedPass, true);
}
else
{
out.writeUTF("");
}
// starting at 2.19, we store a history archive, if defined
if (getFieldDef().isHistoryChecked())
{
if (history != null)
{
// if this field's pool size has been changed since the last
// time the history archive was adjusted, tweak it as a side
// effect while we're writing out.
if (getFieldDef().getHistoryDepth() != history.getPoolSize())
{
history.setPoolSize(getFieldDef().getHistoryDepth());
}
history.emit(out);
}
else
{
out.writeInt(0);
}
}
}
/**
* Helper method for the emit() method, which takes care of encoding
* null strings as empty strings on disk.
*
* @param out The DataOutput that we are emitting to
* @param val The String to write out
* @param wrote_hash If we write out a non-null value, we'll return
* true. Otherwise, we'll just return the value of this boolean.
*/
private boolean emitHelper(DataOutput out, String val, boolean wrote_hash) throws IOException
{
if (val == null)
{
out.writeUTF("");
return wrote_hash;
}
else
{
out.writeUTF(val);
return true;
}
}
/**
* <p>This method helps calculate what we should do if the
* administrator has reconfigured the hash requirements for this
* field.</p>
*
* <p>Effectively, we're looking to see if we have any validly
* constructed hash text that suits any hash algorithms left enabled
* in the schema editor. If we find anything that we have been told
* to use (i.e., getFieldDef().isCrypted(), etc.) and for which we
* have a non-null text available, we won't need to take any special
* action, and we'll return false here.</p>
*
* <p>If we get through the plaintext and all the supported hash forms,
* and we determine that we have no requested hash information for
* the user at all, we'll return true, which will cause the emit()
* routine above to write out any hash text which it still has
* possession of, even if this field is no longer configured to
* cause that hash to be generated for further usage.</p>
*
* <p>Thanks to our password capture logic, if a user validates his
* password against this password field, we'll generate the new hash
* format when the user validates, and from then on, we won't need
* to save the old type of hash text.</p>
*/
private boolean writeOutAllStoredValues()
{
DBObjectBaseField def = getFieldDef();
/* -- */
// If we know the plaintext, we're guaranteed to write out
// something we can use during emit, either through the tracking
// the wrote_hash variable in emit() does for us, or through
// generation of new hash text on demand at emit time.
//
// In either case, we don't need to make a point of writing out
// old hash text which we've kept around.
if (uncryptedPass != null)
{
return false;
}
if (def.isCrypted() && cryptedPass != null)
{
return false;
}
if (def.isMD5Crypted() && md5CryptPass != null)
{
return false;
}
if (def.isApacheMD5Crypted() && apacheMd5CryptPass != null)
{
return false;
}
if (def.isWinHashed() && (lanHash != null || ntHash != null))
{
return false;
}
if (def.isSSHAHashed() && sshaHash != null)
{
return false;
}
if (def.isBCrypted() && bCryptPass != null)
{
return false;
}
if (def.isShaUnixCrypted() && shaUnixCrypt != null)
{
return false;
}
return true;
}
/**
* <p>This method is responsible for reading in the contents of
* this field from an binary input stream. It is used in reading
* fields from the ganymede.db file and from the journal file.</p>
*
* <p>The code that calls receive() on this field is responsible for
* having read enough of the binary input stream's context to
* place the read cursor at the point in the file immediately after
* the field's id and type information has been read.</p>
*/
@Override void receive(DataInput in, DBObjectBaseField definition) throws IOException
{
clear_stored();
// we radically simplified PasswordDBField's on-disk format at
// file version 2.1
if (Ganymede.db.isAtLeast(2,1))
{
cryptedPass = readUTF(in);
md5CryptPass = readUTF(in);
if (Ganymede.db.isAtLeast(2,4))
{
apacheMd5CryptPass = readUTF(in);
}
lanHash = readUTF(in);
ntHash = readUTF(in);
if (Ganymede.db.isAtLeast(2,5))
{
sshaHash = readUTF(in);
}
if (Ganymede.db.isAtLeast(2,13))
{
shaUnixCrypt = readUTF(in);
}
if (Ganymede.db.isAtLeast(2,21))
{
bCryptPass = readUTF(in);
}
uncryptedPass = readUTF(in);
// we added passwordHistoryArchive at 2.19
if (Ganymede.db.isAtLeast(2,19))
{
// At 2.19, I had things quite broken, so I need some
// special logic for reading the history pool during
// journal loading when treating that db version.
//
// At 2.20, we only write out an archive (including the
// count) if the field is configured for history
// archiving/checking, and things are stable and as we
// like it.
if ((Ganymede.db.isAtRev(2,19) && Ganymede.db.journalLoading) || definition.isHistoryChecked())
{
int count = in.readInt();
history = new passwordHistoryArchive(definition.getHistoryDepth(), count, in);
}
}
else
{
if (getFieldDef().isHistoryChecked())
{
history = new passwordHistoryArchive(definition.getHistoryDepth());
}
else
{
history = null;
}
}
return;
}
// From here on down we do things the old, hard way
// at file format 1.10, we were keeping both crypted and unecrypted
// passwords on disk. Since then, we have decided to only write
// out encrypted passwords if we are using them.
if (Ganymede.db.isAtRev(1,10))
{
cryptedPass = readUTF(in);
uncryptedPass = readUTF(in);
return;
}
// if we're not looking at file version 1.10, the crypted password is
// the first thing we'll see, if the field definition specifies the
// use of it
if (definition.isCrypted())
{
cryptedPass = readUTF(in);
if (Ganymede.db.isBetweenRevs(1,13,1,16))
{
in.readUTF(); // skip old-style (buggy) md5 pass
}
}
// now we see if we expect to see an MD5Crypt()'ed password
// note that even though we test for >= 1.16, we won't get to this point
// if we are using the >= 2.1 logic
if (Ganymede.db.isAtLeast(1,16))
{
if (definition.isMD5Crypted())
{
md5CryptPass = readUTF(in);
}
}
if (!definition.isCrypted() && !definition.isMD5Crypted())
{
uncryptedPass = readUTF(in);
}
}
/**
* This helper method reads a UTF string from in, decoding an empty
* String as null.
*/
private String readUTF(DataInput in) throws IOException
{
String val = in.readUTF();
if (val.equals(""))
{
return null;
}
else
{
return val;
}
}
/**
* This method is used when the database is being dumped, to write
* out this field to disk.
*/
@Override void emitXML(XMLDumpContext dump) throws IOException
{
this.emitXML(dump, true);
}
/**
* This method is used when the database is being dumped, to write
* out this field to disk.
*/
synchronized void emitXML(XMLDumpContext dump, boolean writeSurroundContext) throws IOException
{
if (writeSurroundContext)
{
dump.indent();
dump.startElement(this.getXMLName());
}
dump.startElement("password");
if (!dump.doDumpPasswords())
{
dump.endElement("password");
if (writeSurroundContext)
{
dump.endElement(this.getXMLName());
}
return;
}
if (uncryptedPass != null &&
(dump.doDumpPlaintext() ||
(cryptedPass == null &&
md5CryptPass == null &&
apacheMd5CryptPass == null &&
lanHash == null &&
ntHash == null &&
sshaHash == null &&
shaUnixCrypt == null &&
bCryptPass == null)))
{
dump.attribute("plaintext", uncryptedPass);
}
if (cryptedPass != null)
{
dump.attribute("crypt", cryptedPass);
}
if (md5CryptPass != null)
{
dump.attribute("md5crypt", md5CryptPass);
}
if (apacheMd5CryptPass != null)
{
dump.attribute("apachemd5crypt", apacheMd5CryptPass);
}
if (lanHash != null)
{
dump.attribute("lanman", lanHash);
}
if (ntHash != null)
{
dump.attribute("ntmd4", ntHash);
}
if (sshaHash != null)
{
dump.attribute("ssha", sshaHash);
}
if (shaUnixCrypt != null)
{
dump.attribute("shaUnixCrypt", shaUnixCrypt);
}
if (bCryptPass != null)
{
dump.attribute("bCrypt", bCryptPass);
}
dump.endElement("password");
if (writeSurroundContext)
{
dump.endElement(this.getXMLName());
}
}
/**
* <p>Standard {@link arlut.csd.ganymede.rmi.db_field db_field}
* method to retrieve the value of this field. Because we are
* holding sensitive password information, this method always
* returns null. We don't want to make password values available to
* a remote client under any circumstances.</p>
*
* @return null
* @see arlut.csd.ganymede.rmi.db_field
*/
@Override public Object getValue()
{
return null;
}
/**
* <p>This is intended to be used within the Ganymede server, it
* bypasses the permissions checking that getValues() does.</p>
*
* <p>Note that this method will always return null, as you need to
* use the special Password-specific value accessors to get access
* to the password information in crypted or non-crypted form.</p>
*
* @return null
*/
@Override public Object getValueLocal()
{
return null;
}
// ****
//
// type specific value accessors
//
// ****
/**
* <p>Returns a descriptive text value for this PasswordDBField
* without checking permissions. The returned string will include
* information about what hash formats are present in this field,
* but will not include any other information about the contents of
* this password field.</p>
*
* <p>This method avoids checking permissions because it is used on
* the server side only and because it is involved in the {@link
* arlut.csd.ganymede.server.DBObject#getLabel() getLabel()} logic
* for {@link arlut.csd.ganymede.server.DBObject DBObject}.</p>
*
* <p>If this method checked permissions and the getPerm() method
* failed for some reason and tried to report the failure using
* object.getLabel(), as it does at present, the server could get
* into an infinite loop.</p>
*/
@Override public synchronized String getValueString()
{
if (this.isDefined())
{
StringBuilder result = new StringBuilder();
result.append("< ");
if (cryptedPass != null)
{
result.append("crypt ");
}
if (md5CryptPass != null)
{
result.append("md5crypt ");
}
if (apacheMd5CryptPass != null)
{
result.append("apachemd5crypt ");
}
if (lanHash != null)
{
result.append("lanman ");
}
if (ntHash != null)
{
result.append("ntmd4 ");
}
if (sshaHash != null)
{
result.append("ssha ");
}
if (shaUnixCrypt != null)
{
result.append("shaUnixCrypt ");
}
if (uncryptedPass != null)
{
result.append("text ");
}
result.append(">");
return result.toString();
}
else
{
return null;
}
}
/**
* The default getValueString() encoding is acceptable.
*/
@Override public String getEncodingString()
{
return getValueString();
}
/**
* <p>Returns a String representing the change in value between this
* field and orig. This String is intended for logging and email,
* not for any sort of programmatic activity. The format of the
* generated string is not defined, but is intended to be suitable
* for inclusion in a log entry and in an email message.</p>
*
* <p>In the case of the PasswordDBField, the string returned simply
* indicates that the password has changed.</p>
*
* <p>If there is no change in the field, null will be returned.</p>
*/
@Override public String getDiffString(DBField orig)
{
PasswordDBField origP;
/* -- */
if (!(orig instanceof PasswordDBField))
{
throw new IllegalArgumentException("bad field comparison");
}
origP = (PasswordDBField) orig;
if (!this.equals(origP))
{
// "\tPassword changed\n"
return ts.l("getDiffString.changed");
}
else
{
return null;
}
}
// ****
//
// pass_field methods
//
// ****
/**
* Returns the maximum acceptable string length
* for this field.
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public int maxSize()
{
return getFieldDef().getMaxLength();
}
/**
* Returns the minimum acceptable string length
* for this field.
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public int minSize()
{
return getFieldDef().getMinLength();
}
/**
* Returns a string containing the list of acceptable characters.
* If the string is null, it should be interpreted as meaning all
* characters not listed in disallowedChars() are allowable by
* default.
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public String allowedChars()
{
return getFieldDef().getOKChars();
}
/**
* Returns a string containing the list of forbidden
* characters for this field. If the string is null,
* it should be interpreted as meaning that no characters
* are specifically disallowed.
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public String disallowedChars()
{
return getFieldDef().getBadChars();
}
/**
* Convenience method to identify if a particular
* character is acceptable in this field.
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public boolean allowed(char c)
{
if (allowedChars() != null && (allowedChars().indexOf(c) == -1))
{
return false;
}
if (disallowedChars() != null && (disallowedChars().indexOf(c) != -1))
{
return false;
}
return true;
}
/**
* <p>Returns true if the password stored in this field is hash-crypted.</p>
*/
public boolean crypted()
{
return (getFieldDef().isCrypted());
}
/**
* <p>Authenticates a provided plaintext password against the stored
* contents of this password field.</p>
*
* <p>The password field may have stored the password in plaintext,
* or in any of a variety of cryptographic hash formats.
* matchPlainText() will perform whatever operation on the provided
* plaintext as is required to determine whether or not it matches
* with the stored password data.</p>
*
* <p>If this field is configured to create and retain other hash
* formats, this method will create the missing hash formats as
* needed if the input text is successfully matched against the
* password data held in this field.</p>
*
* @return true if the given plaintext matches the stored password
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public synchronized boolean matchPlainText(String plaintext)
{
boolean success = false;
/* -- */
if (plaintext == null || !this.isDefined())
{
return false;
}
// Test against our hashes in decreasing order of hashing
// fidelity. note that we check here to see if we are in
// possession of a piece of hash text, not whether we are
// currently configured to generate that form of hash.
//
// This is to allow us to transition from one requested hash text
// to the other. See the emit() and the writeOutAllStoredValues()
// methods, above for more details.
if (uncryptedPass != null)
{
success = uncryptedPass.equals(plaintext); // most accurate
}
else if (sshaHash != null) // 2^64 bits, but fast
{
success = SSHA.matchSHAHash(sshaHash, plaintext);
}
else if (shaUnixCrypt != null) // large precision, but potentially quite slow
{
if (shaUnixCrypt.startsWith("$5$"))
{
success = Sha256Crypt.verifyPassword(plaintext, shaUnixCrypt);
}
else if (shaUnixCrypt.startsWith("$6$"))
{
success = Sha512Crypt.verifyPassword(plaintext, shaUnixCrypt);
}
}
else if (bCryptPass != null) // ditto
{
success = BCrypt.checkpw(plaintext, bCryptPass);
}
else if (md5CryptPass != null) // indefinite
{
success = md5CryptPass.equals(MD5Crypt.crypt(plaintext, getMD5Salt()));
}
else if (apacheMd5CryptPass != null) // indefinite
{
success = apacheMd5CryptPass.equals(MD5Crypt.apacheCrypt(plaintext, getApacheMD5Salt()));
}
else if (ntHash != null) // 2^64 bits
{
success = ntHash.equals(smbencrypt.NTUNICODEHash(plaintext));
}
else if (lanHash != null) // 14 chars
{
success = lanHash.equals(smbencrypt.LANMANHash(plaintext));
}
else if (cryptedPass != null) // 8 chars
{
success = cryptedPass.equals(jcrypt.crypt(getSalt(), plaintext));
}
// if we matched against a stored hash that has sufficient
// representational capacity to verify the full plaintext (at
// least to the limits of chance collision), go ahead and take the
// opportunity to capture the plaintext and set up any hashes that
// we want to use but don't have initialized at this point.
//
// we call this 'passive password capture'.
if (success && uncryptedPass == null)
{
int precision = getHashPrecision();
if (precision == -1 ||
(precision > 0 && precision >= plaintext.length()))
{
uncryptedPass = plaintext;
clear_unused_stored();
setHashes(plaintext, false);
}
}
return success;
}
/**
* <p>This server-side only method returns the UNIX-encrypted
* password text.</p>
*
* <p>This method is never meant to be available remotely.</p>
*/
public String getUNIXCryptText()
{
if (cryptedPass != null)
{
return cryptedPass;
}
else
{
if (uncryptedPass != null)
{
return jcrypt.crypt(uncryptedPass);
}
else
{
return null;
}
}
}
/**
* <p>This server-side only method returns the md5crypt()-encrypted
* hashed password text.</p>
*
* <p>This method is never meant to be available remotely.</p>
*/
public String getMD5CryptText()
{
if (md5CryptPass != null)
{
return md5CryptPass;
}
else
{
if (uncryptedPass != null)
{
return MD5Crypt.crypt(uncryptedPass);
}
else
{
return null;
}
}
}
/**
* <p>This server-side only method returns the Apache md5crypt()-encrypted
* hashed password text.</p>
*
* <p>This method is never meant to be available remotely.</p>
*/
public String getApacheMD5CryptText()
{
if (apacheMd5CryptPass != null)
{
return apacheMd5CryptPass;
}
else
{
if (uncryptedPass != null)
{
return MD5Crypt.apacheCrypt(uncryptedPass);
}
else
{
return null;
}
}
}
/**
* <p>This server-side only method returns the LANMAN-compatible
* password hash of the password data held in this field.</p>
*
* <p>This method is never meant to be available remotely.</p>
*/
public String getLANMANCryptText()
{
if (lanHash != null)
{
return lanHash;
}
else
{
if (uncryptedPass != null)
{
return smbencrypt.LANMANHash(uncryptedPass);
}
else
{
return null;
}
}
}
/**
* <p>This server-side only method returns the Windows NT 4
* SP3-compatible md4/Unicode password hash of the password data
* held in this field.</p>
*
* <p>This method is never meant to be available remotely.</p>
*/
public String getNTUNICODECryptText()
{
if (ntHash != null)
{
return ntHash;
}
else
{
if (uncryptedPass != null)
{
return smbencrypt.NTUNICODEHash(uncryptedPass);
}
else
{
return null;
}
}
}
/**
* <p>This server-side only method returns the Netscape SSHA (salted
* SHA) LDAP hash of the password data held in this field.</p>
*
* <p>This method is never meant to be available remotely.</p>
*/
public String getSSHAHashText()
{
if (sshaHash != null)
{
return sshaHash;
}
else
{
if (uncryptedPass != null)
{
return SSHA.getLDAPSSHAHash(uncryptedPass, null);
}
else
{
return null;
}
}
}
/**
* <p>This server-side only method returns the OpenBSD BCrypt hash text
* of the password data held in this field, generating the hash text
* from scratch if it is not contained in the local bCryptPass
* variable.</p>
*
* <p>This method is never meant to be available remotely.</p>
*/
public String getBCryptText()
{
if (bCryptPass != null)
{
return bCryptPass;
}
else
{
if (uncryptedPass == null)
{
return null;
}
bCryptPass = BCrypt.hashpw(uncryptedPass, BCrypt.gensalt(getFieldDef().getBCryptRounds()));
return bCryptPass;
}
}
/**
* <p>This server-side only method returns the Sha Unix Crypt hash text
* of the password data held in this field, generating the hash text
* from scratch if it is not contained in the local shaUnixCrypt
* variable.</p>
*
* <p>This method is never meant to be available remotely.</p>
*
* <p>The hashText returned by this method will match one of the
* following four forms:</p>
*
* <pre>
* $5$<saltstring>$<32 bytes of hash text, base 64 encoded>
* $5$rounds=<round-count>$<saltstring>$<32 bytes of hash text, base 64 encoded>
*
* $6$<saltstring>$<64 bytes of hash text, base 64 encoded>
* $6$rounds=<round-count>$<saltstring>$<32 bytes of hash text, base 64 encoded>
* </pre>
*
* <p>If the round count is specified using the '$rounds=n' syntax, the
* higher the round count, the more computational work will be
* required to verify passwords against this hash text.</p>
*
* <p>See <a href="http://people.redhat.com/drepper/sha-crypt.html">http://people.redhat.com/drepper/sha-crypt.html</a>
* for full details of the hash format this method is expecting.</p>
*
* <p>This method is never meant to be available remotely.</p>
*/
public String getShaUnixCryptText()
{
if (shaUnixCrypt != null)
{
return shaUnixCrypt;
}
else
{
if (uncryptedPass == null)
{
return null;
}
if (getFieldDef().isShaUnixCrypted512())
{
shaUnixCrypt = Sha512Crypt.Sha512_crypt(uncryptedPass, null, getFieldDef().getShaUnixCryptRounds());
}
else
{
shaUnixCrypt = Sha256Crypt.Sha256_crypt(uncryptedPass, null, getFieldDef().getShaUnixCryptRounds());
}
return shaUnixCrypt;
}
}
/**
* This server-side only method returns the plaintext password text,
* if available.
*/
public String getPlainText()
{
return uncryptedPass;
}
/**
* <p>Method to obtain the SALT for a stored (traditional Unix)
* crypted password. If the client is going to submit a pre-crypted
* password for comparison via matchCryptText(), it must be salted
* by the salt returned by this method.</p>
*
* <p>If the password is not stored in crypt() form, null will be
* returned.</p>
*
* <p>This method is never meant to be available remotely.</p>
*/
private String getSalt()
{
if (cryptedPass != null)
{
return cryptedPass.substring(0,2);
}
else
{
return null;
}
}
/**
* <p>Method to obtain the SALT for a stored OpenBSD-style
* md5crypt()'ed password.</p>
*
* <p>If the password is not stored in md5crypt() form,
* null will be returned.</p>
*
* <p>This method is never meant to be available remotely.</p>
*/
private String getMD5Salt()
{
if (getFieldDef().isMD5Crypted() && md5CryptPass != null)
{
String salt = md5CryptPass;
String magic = "$1$";
if (salt.startsWith(magic))
{
salt = salt.substring(magic.length());
}
/* It stops at the first '$', max 8 chars */
if (salt.indexOf('$') != -1)
{
salt = salt.substring(0, salt.indexOf('$'));
}
if (salt.length() > 8)
{
salt = salt.substring(0, 8);
}
return salt;
}
else
{
return null;
}
}
/**
* <p>Method to obtain the SALT for a stored Apache-style
* md5crypt()'ed password.</p>
*
* <p>If the password is not stored in apacheMd5crypt() form,
* null will be returned.</p>
*
* <p>This method is never meant to be available remotely.</p>
*/
private String getApacheMD5Salt()
{
if (getFieldDef().isApacheMD5Crypted() && apacheMd5CryptPass != null)
{
String salt = apacheMd5CryptPass;
String magic = "$apr1$";
if (salt.startsWith(magic))
{
salt = salt.substring(magic.length());
}
/* It stops at the first '$', max 8 chars */
if (salt.indexOf('$') != -1)
{
salt = salt.substring(0, salt.indexOf('$'));
}
if (salt.length() > 8)
{
salt = salt.substring(0, 8);
}
return salt;
}
else
{
return null;
}
}
/**
* <p>Not supported for PasswordDBField.</p>
*
* @see arlut.csd.ganymede.rmi.db_field
*/
@Override public ReturnVal setValue(Object value, boolean local, boolean noWizards)
{
// "The setValue() method is not supported on the PasswordDBField."
throw new IllegalArgumentException(ts.l("setValue.invalid_call"));
}
/**
* <p>This method is used to set the password for this field,
* crypting it in various ways if this password field is stored
* crypted.</p>
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public synchronized ReturnVal setPlainTextPass(String plaintext)
{
return setPlainTextPass(plaintext, false, false);
}
/**
* <p>Set the plain text password for this field.</p>
*
* <p>If this field is configured to create and retain other hash
* formats, it will do so as needed during the call to this
* method.</p>
*
* <p>Not exported for access by remote clients.</p>
*
* @param plaintext The crypt text to load into this PasswordDBField
* @param local If true, permission checking is skipped
* @param noWizards If true, the wizardHook() call on the containing DBEditObject will be inhibited.
*/
public synchronized ReturnVal setPlainTextPass(String plaintext, boolean local, boolean noWizards)
{
ReturnVal retVal;
DBEditObject eObj;
/* -- */
retVal = verifyNewValue(plaintext);
if (!ReturnVal.didSucceed(retVal))
{
return retVal;
}
eObj = (DBEditObject) this.owner;
if (!noWizards && !local && eObj.getGSession().enableOversight)
{
// Wizard check
retVal = ReturnVal.merge(retVal, eObj.wizardHook(this,
DBEditObject.SETPASSPLAIN,
plaintext,
null));
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
// call finalizeSetValue to allow for chained reactions, even
// though we're not doing a traditional setValue and we're not
// passing the new password to finalizeSetValue for approval.
// we'll still retain our first retVal so that we can return
// advisory-only messages that the wizardHook generated, even if
// the finalizeSetValue() doesn't generate any.
retVal = ReturnVal.merge(retVal, ((DBEditObject) this.owner).finalizeSetValue(this, null));
if (!ReturnVal.didSucceed(retVal))
{
return retVal;
}
// reset all hashes to start things off
clear_stored();
// if we've got an empty string, clear the plaintext, too
if (plaintext == null || plaintext.equals(""))
{
uncryptedPass = null;
return retVal;
}
// else, go ahead and set everything
uncryptedPass = plaintext;
setHashes(plaintext, true);
return retVal;
}
/**
* <p>This method is used to set a pre-hashed password for this field,
* using the traditional (weak) Unix Crypt algorithm.</p>
*
* <p>This method will return an error code if this password field is
* not configured to store Crypt hashed password text.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public ReturnVal setCryptPass(String text)
{
return setCryptPass(text, false, false);
}
/**
* <p>This server-side method is used to set a pre-crypted password
* for this field.</p>
*
* <p>This method will return an error dialog if this field does not store
* passwords in UNIX crypted format.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* <p>Not exported for access by remote clients.</p>
*
* @param text The crypt text to load into this PasswordDBField
* @param local If true, permission checking is skipped
* @param noWizards If true, the wizardHook() call on the containing DBEditObject will be inhibited.
*/
public ReturnVal setCryptPass(String text, boolean local, boolean noWizards)
{
ReturnVal retVal = null;
DBEditObject eObj;
/* -- */
if (!isEditable(local))
{
// "Password Field Error"
// "Don''t have permission to edit field {0} in object {1}."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("global.perm_error_text", this.getName(),
this.owner.getLabel()));
}
if (!getFieldDef().isCrypted())
{
// "Server: Error in PasswordDBField.setCryptTextPass()"
// "Password field not configured to support traditional Unix crypt hashing."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setCryptPass.error_title"),
ts.l("setCryptPass.error_text"));
}
eObj = (DBEditObject) this.owner;
if (!noWizards && !local && eObj.getGSession().enableOversight)
{
// Wizard check
retVal = eObj.wizardHook(this, DBEditObject.SETPASSCRYPT, text, null);
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
// call finalizeSetValue to allow for chained reactions, even
// though we're not doing a traditional setValue and we're not
// passing the new password to finalizeSetValue for approval.
retVal = ReturnVal.merge(retVal, ((DBEditObject) this.owner).finalizeSetValue(this, null));
if (ReturnVal.didSucceed(retVal))
{
// whenever the crypt password is directly set, we lose
// plaintext and alternate hashes
clear_stored();
if ((text == null) || (text.equals("")))
{
cryptedPass = null;
}
else
{
cryptedPass = text;
}
}
return retVal;
}
/**
* <p>This method is used to set a pre-crypted FreeBSD-style MD5Crypt
* password for this field.</p>
*
* <p>This method will return an error code if this password field is
* not configured to store MD5Crypt hashed password text.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public ReturnVal setMD5CryptedPass(String text)
{
return setMD5CryptedPass(text, false, false);
}
/**
* <p>This method is used to set a pre-crypted FreeBSD-style MD5Crypt
* password for this field.</p>
*
* <p>This method will return an error code if this password field is
* not configured to store MD5Crypt hashed password text.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* <p>Not exported for access by remote clients.</p>
*
* @param text The crypt text to load into this PasswordDBField
* @param local If true, permission checking is skipped
* @param noWizards If true, the wizardHook() call on the containing DBEditObject will be inhibited.
*/
public ReturnVal setMD5CryptedPass(String text, boolean local, boolean noWizards)
{
ReturnVal retVal = null;
DBEditObject eObj;
/* -- */
if (!isEditable(local))
{
// "Password Field Error"
// "Don''t have permission to edit field {0} in object {1}."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("global.perm_error_text", this.getName(),
this.owner.getLabel()));
}
if (!getFieldDef().isMD5Crypted())
{
// "Server: Error in PasswordDBField.setMD5CryptPass()"
// "Password field not configured to support MD5Crypt hashing."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setMD5CryptPass.error_title"),
ts.l("setMD5CryptPass.error_text"));
}
if (text != null && !text.equals("") && (!text.startsWith("$1$") || (text.indexOf('$', 3) == -1)))
{
// "Password Field Error"
// "The hash text passed to setMD5CryptPass(), "{0}", is not a well-formed MD5Crypt hash text."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("setMD5CryptPass.format_error", text));
}
eObj = (DBEditObject) this.owner;
if (!noWizards && !local && eObj.getGSession().enableOversight)
{
// Wizard check
retVal = eObj.wizardHook(this, DBEditObject.SETPASSMD5, text, null);
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
// call finalizeSetValue to allow for chained reactions, even
// though we're not doing a traditional setValue and we're not
// passing the new password to finalizeSetValue for approval.
retVal = ReturnVal.merge(retVal, ((DBEditObject) this.owner).finalizeSetValue(this, null));
if (ReturnVal.didSucceed(retVal))
{
// whenever the md5CryptPass password is directly set, we lose
// plaintext and alternate hashes
clear_stored();
if ((text == null) || (text.equals("")))
{
md5CryptPass = null;
}
else
{
md5CryptPass = text;
}
}
return retVal;
}
/**
* <p>This method is used to set a pre-crypted Apache-style MD5Crypt
* password for this field.</p>
*
* <p>This method will return an error code if this password field is
* not configured to store Apache-style MD5Crypt hashed password
* text.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public ReturnVal setApacheMD5CryptedPass(String text)
{
return setApacheMD5CryptedPass(text, false, false);
}
/**
* <p>This method is used to set a pre-crypted Apache-style MD5Crypt
* password for this field.</p>
*
* <p>This method will return an error code if this password field is
* not configured to store Apache-style MD5Crypt hashed password
* text.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* <p>Not exported for access by remote clients.</p>
*
* @param text The crypt text to load into this PasswordDBField
* @param local If true, permission checking is skipped
* @param noWizards If true, the wizardHook() call on the containing DBEditObject will be inhibited.
*/
public ReturnVal setApacheMD5CryptedPass(String text, boolean local, boolean noWizards)
{
ReturnVal retVal = null;
DBEditObject eObj;
/* -- */
if (!isEditable(local))
{
// "Password Field Error"
// "Don''t have permission to edit field {0} in object {1}."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("global.perm_error_text", this.getName(),
this.owner.getLabel()));
}
if (!getFieldDef().isApacheMD5Crypted())
{
// "Server: Error in PasswordDBField.setApacheMD5CryptTextPass()"
// "Password field not configured to support ApacheMD5Crypt hashing."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setApacheMD5CryptPass.error_title"),
ts.l("setApacheMD5CryptPass.error_text"));
}
if (text != null && !text.equals("") && (!text.startsWith("$apr1$") || (text.indexOf('$', 6) == -1)))
{
// "Password Field Error"
// "The hash text passed to setMD5CryptPass(), "{0}", is not a well-formed ApacheMD5Crypt hash text."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("setApacheMD5CryptPass.format_error", text));
}
eObj = (DBEditObject) this.owner;
if (!noWizards && !local && eObj.getGSession().enableOversight)
{
// Wizard check
retVal = eObj.wizardHook(this, DBEditObject.SETPASSAPACHEMD5, text, null);
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
// call finalizeSetValue to allow for chained reactions, even
// though we're not doing a traditional setValue and we're not
// passing the new password to finalizeSetValue for approval.
retVal = ReturnVal.merge(retVal, ((DBEditObject) this.owner).finalizeSetValue(this, null));
if (ReturnVal.didSucceed(retVal))
{
// whenever the apacheMd5CryptPass password is directly set, we lose
// plaintext and alternate hashes
clear_stored();
if ((text == null) || (text.equals("")))
{
apacheMd5CryptPass = null;
}
else
{
apacheMd5CryptPass = text;
}
}
return retVal;
}
/**
* <p>This method is used to set pre-crypted Windows-style password
* hashes for this field. These strings are formatted as used in
* Samba's encrypted password files.</p>
*
* <p>This method will return an error code if this password field is
* not configured to accept Windows-hashed password strings.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public ReturnVal setWinCryptedPass(String LANMAN, String NTUnicodeMD4)
{
return setWinCryptedPass(LANMAN, NTUnicodeMD4, false, false);
}
/**
* <p>This method is used to set pre-crypted Windows-style password
* hashes for this field. These strings are formatted as used in
* Samba's encrypted password files.</p>
*
* <p>This method will return an error code if this password field is
* not configured to accept Windows-hashed password strings.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* <p>Not exported for access by remote clients.</p>
*
* @param LANMAN The LANMAN hash text to load into this PasswordDBField
* @param NTUnicodeMD4 The NTUnicodeMD4 hash text to load into this PasswordDBField
* @param local If true, permission checking is skipped
* @param noWizards If true, the wizardHook() call on the containing DBEditObject will be inhibited.
*/
public ReturnVal setWinCryptedPass(String LANMAN, String NTUnicodeMD4, boolean local, boolean noWizards)
{
ReturnVal retVal = null;
DBEditObject eObj;
/* -- */
if (!isEditable(local))
{
// "Password Field Error"
// "Don''t have permission to edit field {0} in object {1}."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("global.perm_error_text", this.getName(),
this.owner.getLabel()));
}
if (!getFieldDef().isWinHashed())
{
// "Server: Error in PasswordDBField.setWinCryptedPass()"
// "Password field is not configured to accept Samba hashed password strings."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setWinCryptedPass.error_title"),
ts.l("setWinCryptedPass.error_text"));
}
eObj = (DBEditObject) this.owner;
if (!noWizards && !local && eObj.getGSession().enableOversight)
{
// Wizard check
retVal = eObj.wizardHook(this, DBEditObject.SETPASSWINHASHES, LANMAN, NTUnicodeMD4);
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
// call finalizeSetValue to allow for chained reactions, even
// though we're not doing a traditional setValue and we're not
// passing the new password to finalizeSetValue for approval.
retVal = ReturnVal.merge(retVal, ((DBEditObject) this.owner).finalizeSetValue(this, null));
if (ReturnVal.didSucceed(retVal))
{
// whenever the windows hashes are set directly, we lose
// plaintext and alternate hashes
clear_stored();
if ((LANMAN == null) || (LANMAN.equals("")))
{
lanHash = null;
}
else
{
lanHash = LANMAN;
}
if ((NTUnicodeMD4 == null) || (NTUnicodeMD4.equals("")))
{
ntHash = null;
}
else
{
ntHash = NTUnicodeMD4;
}
}
return retVal;
}
/**
* <p>This method is used to set a pre-crypted OpenLDAP/Netscape
* Directory Server Salted SHA (SSHA) password for this field.</p>
*
* <p>This method will return an error code if this password field is
* not configured to store SSHA hashed password text.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public ReturnVal setSSHAPass(String text)
{
return this.setSSHAPass(text, false, false);
}
/**
* <p>Sets a pre-crypted OpenLDAP/Netscape Directory Server Salted
* SHA (SSHA) password for this field.</p>
*
* <p>This method will return an error code if this password field is
* not configured to store SSHA hashed password text.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* <p>Not exported for access by remote clients.</p>
*
* @param text The crypt text to load into this PasswordDBField
* @param local If true, permission checking is skipped
* @param noWizards If true, the wizardHook() call on the containing DBEditObject will be inhibited.
*/
public ReturnVal setSSHAPass(String text, boolean local, boolean noWizards)
{
ReturnVal retVal = null;
DBEditObject eObj;
/* -- */
if (!isEditable(local))
{
// "Password Field Error"
// "Don''t have permission to edit field {0} in object {1}."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("global.perm_error_text", this.getName(),
this.owner.getLabel()));
}
if (!getFieldDef().isSSHAHashed())
{
// "Server: Error in PasswordDBField.setSSHAPass()"
// "Password field is not configured to accept SSHA-1 hashed password strings."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setSSHAPass.error_title"),
ts.l("setSSHAPass.error_text"));
}
if (text != null && !text.equals("") && !text.startsWith("{SSHA}"))
{
// "Server: Error in PasswordDBField.setSSHAPass()"
// "The hash text passed to setSSHAPass(), "{0}", is not a well-formed, OpenLDAP-encoded SSHA-1 hash text."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setSSHAPass.error_title"),
ts.l("setSSHAPass.format_error", this.getName()));
}
eObj = (DBEditObject) this.owner;
if (!noWizards && !local && eObj.getGSession().enableOversight)
{
// Wizard check
retVal = eObj.wizardHook(this, DBEditObject.SETPASSSSHA, text, null);
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
// call finalizeSetValue to allow for chained reactions, even
// though we're not doing a traditional setValue and we're not
// passing the new password to finalizeSetValue for approval.
retVal = ReturnVal.merge(retVal, ((DBEditObject) this.owner).finalizeSetValue(this, null));
if (ReturnVal.didSucceed(retVal))
{
// whenever the SSHA password is directly set, we lose
// plaintext and alternate hashes
clear_stored();
if ((text == null) || (text.equals("")))
{
sshaHash = null;
}
else
{
sshaHash = text;
}
}
return retVal;
}
/**
* <p>This method is used to set a pre-crypted Sha256Crypt or
* Sha512Crypt password for this field.</p>
*
* <p>This method will return an error code if this password field is
* not configured to store ShaCrypt hashed password text.</p>
*
* <p>The hashText submitted to this method must match one of the
* following four forms:</p>
*
* <pre>
* $5$<saltstring>$<32 bytes of hash text, base 64 encoded>
* $5$rounds=<round-count>$<saltstring>$<32 bytes of hash text, base 64 encoded>
*
* $6$<saltstring>$<64 bytes of hash text, base 64 encoded>
* $6$rounds=<round-count>$<saltstring>$<32 bytes of hash text, base 64 encoded>
* </pre>
*
* <p>If the round count is specified using the '$rounds=n' syntax, the
* higher the round count, the more computational work will be
* required to verify passwords against this hash text.</p>
*
* <p>See <a href="http://people.redhat.com/drepper/sha-crypt.html">http://people.redhat.com/drepper/sha-crypt.html</a>
* for full details of the hash format this method is expecting.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public ReturnVal setShaUnixCryptPass(String hashText)
{
return this.setShaUnixCryptPass(hashText, false, false);
}
/**
* <p>This method is used to set a pre-crypted Sha256Crypt or
* Sha512Crypt password for this field.</p>
*
* <p>This method will return an error code if this password field is
* not configured to store ShaCrypt hashed password text.</p>
*
* <p>The hashText submitted to this method must match one of the
* following four forms:</p>
*
* <pre>
* $5$<saltstring>$<32 bytes of hash text, base 64 encoded>
* $5$rounds=<round-count>$<saltstring>$<32 bytes of hash text, base 64 encoded>
*
* $6$<saltstring>$<64 bytes of hash text, base 64 encoded>
* $6$rounds=<round-count>$<saltstring>$<32 bytes of hash text, base 64 encoded>
* </pre>
*
* <p>If the round count is specified using the '$rounds=n' syntax, the
* higher the round count, the more computational work will be
* required to verify passwords against this hash text.</p>
*
* <p>See <a href="http://people.redhat.com/drepper/sha-crypt.html">http://people.redhat.com/drepper/sha-crypt.html</a>
* for full details of the hash format this method is expecting.</p>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* <p>Not exported for access by remote clients.</p>
*
* @param hashText The crypt text to load into this PasswordDBField
* @param local If true, permission checking is skipped
* @param noWizards If true, the wizardHook() call on the containing DBEditObject will be inhibited.
*/
public ReturnVal setShaUnixCryptPass(String hashText, boolean local, boolean noWizards)
{
ReturnVal retVal = null;
DBEditObject eObj;
/* -- */
if (!isEditable(local))
{
// "Password Field Error"
// "Don''t have permission to edit field {0} in object {1}."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("global.perm_error_text", this.getName(),
this.owner.getLabel()));
}
if (!getFieldDef().isShaUnixCrypted())
{
// "Server: Error in PasswordDBField.setShaUnixCryptPass()"
// "Password field not configured to accept SHA Unix Crypt hashed password strings."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setShaUnixCryptPass.error_title"),
ts.l("setShaUnixCryptPass.error_text"));
}
if (hashText != null)
{
if (!Sha256Crypt.verifyHashTextFormat(hashText) && !Sha512Crypt.verifyHashTextFormat(hashText))
{
// "Server: Error in PasswordDBField.setShaUnixCryptPass()"
// "The hash text passed to setShaUnixCryptPass(), "{0}", is
// not a well-formed, SHA Unix Crypt hash text"
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setShaUnixCryptPass.error_title"),
ts.l("setShaUnixCryptPass.format_error", this.getName()));
}
}
eObj = (DBEditObject) this.owner;
if (!noWizards && !local && eObj.getGSession().enableOversight)
{
// Wizard check
retVal = eObj.wizardHook(this, DBEditObject.SETPASS_SHAUNIXCRYPT, hashText, null);
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
// call finalizeSetValue to allow for chained reactions, even
// though we're not doing a traditional setValue and we're not
// passing the new password to finalizeSetValue for approval.
retVal = ReturnVal.merge(retVal, ((DBEditObject) this.owner).finalizeSetValue(this, null));
if (ReturnVal.didSucceed(retVal))
{
// whenever the ShaUnixCrypt password is directly set, we lose
// plaintext and alternate hashes
clear_stored();
if (hashText == null || hashText.equals(""))
{
shaUnixCrypt = null;
}
else
{
shaUnixCrypt = hashText;
}
}
return retVal;
}
/**
* <p>This method is used to set a pre-crypted BCrypt password for
* this field.</p>
*
* <p>This method will return an error code if this password field
* is not configured to store BCrypt hashed password text.</p>
*
* <p>The hashText submitted to this method must match one of the
* following two formats:</p>
*
* <pre>
* $2$<2 digit cost parameter>$<22 characters of salt followed immediately by 31 characters of hash text, encoded in non-standard base 64>
* $2a$<2 digit cost parameter>$<22 characters of salt followed immediately by 31 characters of hash text, encoded in non-standard base 64>
* </pre>
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* @see arlut.csd.ganymede.rmi.pass_field
*/
public ReturnVal setBCryptPass(String hashText)
{
return setBCryptPass(hashText, false, false);
}
/**
* <p>This method is used to set a pre-crypted BCrypt password for
* this field.</p>
*
* <p>This method will return an error code if this password field
* is not configured to store BCrypt hashed password text.</p>
*
* <p>The hashText submitted to this method must match one of the
* following two formats:</p>
*
* <pre>
* $2$<2 digit cost parameter>$<22 characters of salt followed immediately by 31 characters of hash text, encoded in non-standard base 64>
* $2a$<2 digit cost parameter>$<22 characters of salt followed immediately by 31 characters of hash text, encoded in non-standard base 64>
* </pre>
*
* <p>Note
*
* <p>When this method is called, all other data for this password
* field are cleared. Any plaintext held by the field is erased,
* and any other stored hash formats are deleted. If the field is
* configured to create and retain other hash formats, it will do so
* opportunistically if the user successfully logs into Ganymede
* using the password stored in this field.</p>
*
* <p>Not exported for access by remote clients.</p>
*
* @param hashText The crypt text to load into this PasswordDBField
* @param local If true, permission checking is skipped
* @param noWizards If true, the wizardHook() call on the containing DBEditObject will be inhibited.
*/
public ReturnVal setBCryptPass(String hashText, boolean local, boolean noWizards)
{
ReturnVal retVal = null;
DBEditObject eObj;
/* -- */
if (!isEditable(local))
{
// "Password Field Error"
// "Don''t have permission to edit field {0} in object {1}."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("global.perm_error_text", this.getName(),
this.owner.getLabel()));
}
if (!getFieldDef().isBCrypted())
{
// "Server: Error in PasswordDBField.setBCryptPass()"
// "Password field not configured to accept bCrypt hashed password strings."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setBCryptPass.error_title"),
ts.l("setBCryptPass.error_text"));
}
if (!BCrypt.verifyHashTextFormat(hashText))
{
// "Server: Error in PasswordDBField.setBCryptPass()"
// "The hash text passed to setBCryptPass(), "{0}", is
// not a well-formed, bCrypt hash text"
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setBCryptPass.error_title"),
ts.l("setBCryptPass.format_error", this.getName()));
}
eObj = (DBEditObject) this.owner;
if (!noWizards && !local && eObj.getGSession().enableOversight)
{
// Wizard check
retVal = eObj.wizardHook(this, DBEditObject.SETPASS_BCRYPT, hashText, null);
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
// call finalizeSetValue to allow for chained reactions, even
// though we're not doing a traditional setValue and we're not
// passing the new password to finalizeSetValue for approval.
retVal = ReturnVal.merge(retVal, ((DBEditObject) this.owner).finalizeSetValue(this, null));
if (ReturnVal.didSucceed(retVal))
{
// whenever the BCrypt password is directly set, we lose
// plaintext and alternate hashes
clear_stored();
if (hashText == null || hashText.equals(""))
{
bCryptPass = null;
}
else
{
bCryptPass = hashText;
}
}
return retVal;
}
/**
* <p>This method is used to force all known hashes into this password
* field. Ganymede does no verifications to insure that all of these
* hashes really match the same password, so caveat emptor. If any of
* these hashes are null or empty string, those hashes will be cleared.</p>
*
* <p>Calling this method will clear the password's stored plaintext,
* if any.</p>
*
* <p>This method is intended to be called via the {@link
* arlut.csd.ganymede.server.GanymedeXMLSession GanymedeXMLSession}
* in support of the xmlclient, and is specifically designed to
* allow all types of hash text to be loaded, without regard for
* which hash formats this password field is configured to
* generate.</p>
*
* <p>We're deliberately permissive in accepting hash text from the
* xmlclient so that we can take hash text in for the purpose of
* authenticating users logging to Ganymede itself, even if the
* adopter doesn't necessarily intend to use that hash text format
* going forward. This is mainly for the purpose of bootstrapping a
* new Ganymede server with pre-hashed password data from a
* pre-existing authentication system.</p>
*
* <p>Not exported for access by remote clients.</p>
*
* @param local If true, permission checking is skipped
* @param noWizards If true, the wizardHook() call on the containing DBEditObject will be inhibited.
*/
public ReturnVal setAllHashes(String crypt,
String md5crypt,
String apacheMd5Crypt,
String LANMAN,
String NTUnicodeMD4,
String SSHAText,
String ShaUnixCryptText,
String bCryptText,
boolean local,
boolean noWizards)
{
ReturnVal retVal = null;
DBEditObject eObj;
boolean settingCrypt, settingMD5, settingApacheMD5, settingWin, settingSSHA, settingShaUnixCrypt, settingBCrypt;
/* -- */
if (!isEditable(local))
{
// "Password Field Error"
// "Don''t have permission to edit field {0} in object {1}."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("global.perm_error_text", this.getName(),
this.owner.getLabel()));
}
settingCrypt = (crypt != null && !crypt.equals(""));
settingMD5 = (md5crypt != null && !md5crypt.equals(""));
settingApacheMD5 = (apacheMd5Crypt != null && !apacheMd5Crypt.equals(""));
settingWin = (LANMAN != null && !LANMAN.equals("")) || (NTUnicodeMD4 != null && !NTUnicodeMD4.equals(""));
settingSSHA = (SSHAText != null && !SSHAText.equals(""));
settingShaUnixCrypt = (ShaUnixCryptText != null && !ShaUnixCryptText.equals(""));
settingBCrypt = (bCryptText != null && !bCryptText.equals(""));
if (!settingCrypt && !settingWin && !settingMD5 && !settingApacheMD5 && !settingSSHA && !settingShaUnixCrypt && !settingBCrypt)
{
// clear it!
return setPlainTextPass(null);
}
// nope, we're setting something.. let's find out what
if (settingSSHA)
{
if (!SSHAText.startsWith("{SSHA}"))
{
// "Server: Error in PasswordDBField.setAllHashes()"
// "The SSHA hash text passed to setAllHashes() is not a well-formed, OpenLDAP-encoded SSHA-1 hash text."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setAllHashes.error_title"),
ts.l("setAllHashes.ssha_format_error"));
}
}
if (settingShaUnixCrypt)
{
if (!Sha256Crypt.verifyHashTextFormat(ShaUnixCryptText) && !Sha512Crypt.verifyHashTextFormat(ShaUnixCryptText))
{
// "Server: Error in PasswordDBField.setAllHashes()"
// "The hash text passed to setShaUnixCryptPass(), "{0}", is not a well-formed, SHA Unix Crypt hash text"
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setAllHashes.error_title"),
ts.l("setShaUnixCryptPass.format_error", ShaUnixCryptText));
}
}
if (settingMD5)
{
if (!md5crypt.startsWith("$1$") || (md5crypt.indexOf('$', 3) == -1))
{
// "Server: Error in PasswordDBField.setAllHashes()"
// "The MD5Crypt hash text passed to setAllHashes(), "{0}", is not well-formed."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setAllHashes.error_title"),
ts.l("setAllHashes.md5_format_error", md5crypt));
}
}
if (settingApacheMD5)
{
if (!apacheMd5Crypt.startsWith("$apr1$") || (md5crypt.indexOf('$', 6) == -1))
{
// "Server: Error in PasswordDBField.setAllHashes()"
// "The Apache MD5Crypt hash text passed to setAllHashes(), "{0}", is not well-formed."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("setAllHashes.error_title"),
ts.l("setAllHashes.apache_format_error", apacheMd5Crypt));
}
}
eObj = (DBEditObject) this.owner;
if (!noWizards && !local && eObj.getGSession().enableOversight)
{
// Wizard check
if (settingWin)
{
retVal = eObj.wizardHook(this, DBEditObject.SETPASSWINHASHES, LANMAN, NTUnicodeMD4);
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
if (settingMD5)
{
retVal = ReturnVal.merge(retVal, eObj.wizardHook(this,
DBEditObject.SETPASSMD5,
md5crypt,
null));
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
if (settingApacheMD5)
{
retVal = ReturnVal.merge(retVal, eObj.wizardHook(this,
DBEditObject.SETPASSAPACHEMD5,
apacheMd5Crypt,
null));
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
if (settingCrypt)
{
retVal = ReturnVal.merge(retVal, eObj.wizardHook(this,
DBEditObject.SETPASSCRYPT,
crypt,
null));
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
if (settingSSHA)
{
retVal = ReturnVal.merge(retVal, eObj.wizardHook(this,
DBEditObject.SETPASSSSHA,
sshaHash,
null));
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
if (settingShaUnixCrypt)
{
retVal = ReturnVal.merge(retVal, eObj.wizardHook(this,
DBEditObject.SETPASS_SHAUNIXCRYPT,
ShaUnixCryptText,
null));
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
if (settingBCrypt)
{
retVal = ReturnVal.merge(retVal, eObj.wizardHook(this,
DBEditObject.SETPASS_BCRYPT,
bCryptText,
null));
// if a wizard intercedes, we are going to let it take the ball.
if (ReturnVal.wizardHandled(retVal))
{
return retVal;
}
}
}
// call finalizeSetValue to allow for chained reactions. note
// that we don't actually pass any password data to the
// finalizeSetValue method.. this is just to allow for a generic
// veto on all changes
retVal = ReturnVal.merge(retVal, ((DBEditObject) this.owner).finalizeSetValue(this, null));
if (ReturnVal.didSucceed(retVal))
{
// whenever the hashes are set directly, we lose
// plaintext and alternate hashes
clear_stored();
if ((LANMAN == null) || (LANMAN.equals("")))
{
lanHash = null;
}
else
{
lanHash = LANMAN;
}
if ((NTUnicodeMD4 == null) || (NTUnicodeMD4.equals("")))
{
ntHash = null;
}
else
{
ntHash = NTUnicodeMD4;
}
if (settingCrypt)
{
cryptedPass = crypt;
}
if (settingMD5)
{
md5CryptPass = md5crypt;
}
if (settingApacheMD5)
{
apacheMd5CryptPass = apacheMd5Crypt;
}
if (settingSSHA)
{
sshaHash = SSHAText;
}
if (settingShaUnixCrypt)
{
shaUnixCrypt = ShaUnixCryptText;
}
if (settingBCrypt)
{
bCryptPass = bCryptText;
}
}
return retVal;
}
// ****
//
// Overridable methods for implementing intelligent behavior
//
// ****
@Override public boolean verifyTypeMatch(Object o)
{
return ((o == null) || (o instanceof String));
}
/**
* Generally only for when we get a plaintext submission..
*/
@Override public ReturnVal verifyNewValue(Object o)
{
DBEditObject eObj;
String s;
/* -- */
if (!isEditable(true))
{
// "Password Field Error"
// "Don''t have permission to edit field {0} in object {1}."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("global.perm_error_text", this.getName(),
this.owner.getLabel()));
}
eObj = (DBEditObject) this.owner;
if (!verifyTypeMatch(o))
{
// "Password Field Error"
// "Submitted value "{0}" is not a String object! Major client error while trying to edit password field."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("verifyNewValue.type_error", o));
}
if (o == null)
{
return null; // assume we can null out this field
}
s = (String) o;
if (s.length() > maxSize())
{
// string too long
// "Password Field Error"
// "The submitted password is too long. The maximum plaintext password length accepted is {0,number,#} characters."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("verifyNewValue.too_long", Integer.valueOf(this.maxSize())));
}
if (s.length() < minSize())
{
// "Password Field Error"
// "The submitted password is too short. The minimum plaintext password length accepted is {0,number,#} characters."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("verifyNewValue.too_short", Integer.valueOf(this.minSize())));
}
if (allowedChars() != null)
{
String okChars = allowedChars();
for (int i = 0; i < s.length(); i++)
{
if (okChars.indexOf(s.charAt(i)) == -1)
{
// "Password Field Error"
// "Submitted password contains an unacceptable character ('{0}')."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("verifyNewValue.bad_char",
Character.valueOf(s.charAt(i))));
}
}
}
if (disallowedChars() != null)
{
String badChars = disallowedChars();
for (int i = 0; i < s.length(); i++)
{
if (badChars.indexOf(s.charAt(i)) != -1)
{
// "Password Field Error"
// "Submitted password contains an unacceptable character ('{0}')."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("global.error_subj"),
ts.l("verifyNewValue.bad_char",
Character.valueOf(s.charAt(i))));
}
}
}
ReturnVal returnValInProgress = ReturnVal.success();
if (getFieldDef().isCracklibChecked() && Ganymede.crackLibPacker != null)
{
try
{
String cracklibCheck = CrackLib.fascistLook(Ganymede.crackLibPacker, s, this.owner.getLabel());
if (cracklibCheck != null)
{
if (getFieldDef().hasCracklibCheckException() && getGSession().getPermManager().isSuperGash())
{
// "Password Quality Problem"
// "The password fails quality checking.\nThe checker reported the following problem:\n{0}"
returnValInProgress = Ganymede.createInfoDialog(ts.l("verifyNewValue.cracklib_failure_title"),
ts.l("verifyNewValue.cracklib_failure_error", cracklibCheck));
}
else
{
// "Password Quality Problem"
// "The password fails quality checking.\nThe checker reported the following problem:\n{0}"
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("verifyNewValue.cracklib_failure_title"),
ts.l("verifyNewValue.cracklib_failure_error", cracklibCheck));
}
}
}
catch (IOException ex)
{
ex.printStackTrace();
}
}
if (getFieldDef().isHistoryChecked())
{
if (history != null)
{
Date previousDate = history.contains(s);
if (previousDate != null)
{
if (getFieldDef().hasHistoryCheckException() && getGSession().getPermManager().isSuperGash())
{
// "Password Used Before"
// "This password has been used too recently with this account.\n\nIt was last used with this account at {0, time} on {0, date, full}."
returnValInProgress = ReturnVal.merge(returnValInProgress,
Ganymede.createInfoDialog(ts.l("verifyNewValue.history_reuse_title"),
ts.l("verifyNewValue.history_reuse_error",
previousDate)));
}
else
{
// "Password Used Before"
// "This password has been used too recently with this account.\n\nIt was last used with this account at {0, time} on {0, date, full}."
return Ganymede.createErrorDialog(this.getGSession(),
ts.l("verifyNewValue.history_reuse_title"),
ts.l("verifyNewValue.history_reuse_error",
previousDate));
}
}
}
}
// have our parent make the final ok on the value
return ReturnVal.merge(returnValInProgress,
eObj.verifyNewValue(this, s));
}
/**
* <p>This method returns an int indicating to what precision the
* password in this PasswordDBField is known. Certain cryptographic
* hashes have limits on how many characters of the input text are
* taken into account in the hash.</p>
*
* <p>This method returns -1 if the password is known with no limits
* on its precision (plaintext, or md5crypt, or ssha which is
* precise to 2^64 bits.. close enough), 0 if the password is not
* known, or a positive integer indicating the number of characters
* of precision that we believe we can recognize from our hash
* authenticators.</p>
*/
private int getHashPrecision()
{
if (uncryptedPass != null || md5CryptPass != null ||
apacheMd5CryptPass != null || sshaHash != null || ntHash != null ||
shaUnixCrypt != null || bCryptPass != null)
{
return -1; // full precision
}
if (lanHash != null)
{
return 14; // Old-school Windows hashes are good
// for 14 chars
}
if (cryptedPass != null)
{
return 8; // Old-school UNIX sux0rs.. we should
// only be using this for importing
// users from old /etc/passwd-style
// files
}
return 0; // i got nothing, boss
}
/**
* This method does the work of storing the given plaintext into
* whatever hashes we are configured to retain. If forceChange is
* true, this calculation and storage is non-optional. If
* forceChange is false, we will only store the plaintext into a
* hash if we are configured to use it but for some reason do not
* have a hash value of that kind stored.
*/
private void setHashes(String plaintext, boolean forceChange)
{
if (getFieldDef().isCrypted() && (forceChange || cryptedPass == null))
{
cryptedPass = jcrypt.crypt(plaintext);
}
if (getFieldDef().isMD5Crypted() && (forceChange || md5CryptPass == null))
{
md5CryptPass = MD5Crypt.crypt(plaintext);
}
if (getFieldDef().isApacheMD5Crypted() && (forceChange || apacheMd5CryptPass == null))
{
apacheMd5CryptPass = MD5Crypt.apacheCrypt(plaintext);
}
if (getFieldDef().isWinHashed())
{
if (forceChange || lanHash == null)
{
lanHash = smbencrypt.LANMANHash(plaintext);
}
if (forceChange || ntHash == null)
{
ntHash = smbencrypt.NTUNICODEHash(plaintext);
}
}
if (getFieldDef().isSSHAHashed() && (forceChange || sshaHash == null))
{
sshaHash = SSHA.getLDAPSSHAHash(plaintext, null);
}
if (getFieldDef().isShaUnixCrypted() && (forceChange || shaUnixCrypt == null))
{
shaUnixCrypt = null; // force new hash
shaUnixCrypt = getShaUnixCryptText();
}
if (getFieldDef().isBCrypted() && (forceChange || bCryptPass == null))
{
bCryptPass = null; // force new hash
bCryptPass = getBCryptText();
}
}
/*----------------------------------------------------------------------------
nested class
passwordHistoryArchive
----------------------------------------------------------------------------*/
/**
* <p>Holds previous passwords that have been associated with this
* password field.</p>
*
* <p>This archive is stored on disk in the ganymede.db and journal
* files as part of the on-disk storage of the PasswordDBField.</p>
*
* <p>All passwords in the archive are stored in an unsalted SSHA
* hash format, LDAP style.</p>
*/
static class passwordHistoryArchive {
/**
* Queue of password history entries. New elements are added to
* the beginning of the list, and old elements are removed from
* the end after the pool is filled.
*/
private List<passwordHistoryEntry> pool;
private int poolSize;
/* -- */
public passwordHistoryArchive(int poolSize)
{
this.poolSize = poolSize;
this.pool = null;
}
/**
* Receive constructor
*/
public passwordHistoryArchive(int poolSize, int count, DataInput in) throws IOException
{
setPoolSize(poolSize);
this.receive(count, in);
}
public synchronized void receive(int count, DataInput in) throws IOException
{
pool = new ArrayList<passwordHistoryEntry>(count);
for (int i = 0; i < count; i++)
{
pool.add(new passwordHistoryEntry(in));
}
}
public synchronized void emit(DataOutput out) throws IOException
{
if (pool != null)
{
out.writeInt(pool.size());
for (passwordHistoryEntry entry: pool)
{
entry.emit(out);
}
}
else
{
out.writeInt(0);
}
}
/**
* Returns the size of the pool.
*/
public synchronized int getPoolSize()
{
return poolSize;
}
/**
* <p>This method changes the size of this archive. If poolSize is
* less than the current size of this archive, older passwords are
* trimmed from this archive.</p>
*
* <p>If poolSize is greater than the current size of this archive,
* space is added, but no values are put in the newly available
* slots until add() is called.</p>
*/
public synchronized void setPoolSize(int poolSize)
{
this.poolSize = poolSize;
if (pool != null)
{
while (pool.size() > poolSize)
{
pool.remove(pool.size() - 1);
}
}
}
/**
* Add a new password to the password archive.
*
* @param password The plaintext of the password
* @param date The date the password was committed into the
* database.
*/
public synchronized void add(String password, Date date)
{
if (pool == null)
{
pool = new ArrayList<passwordHistoryEntry>();
}
// remove a password if it's already in the pool, so we can add
// it back to the start of the queue with our new date.
for (passwordHistoryEntry entry: pool)
{
if (entry.matches(password))
{
pool.remove(entry);
break;
}
}
pool.add(0, new passwordHistoryEntry(password, date));
if (pool.size() > poolSize)
{
pool.remove(pool.size() - 1);
}
}
/**
* <p>This method checks to see if the plaintext password was
* previously associated with this passwordHistoryArchive.</p>
*
* <p>If a previous instance of this password is found in this
* archive, the Date of the previous use is returned.</p>
*
* <p>If not, this method returns null.</p>
*/
public synchronized Date contains(String password)
{
if (pool == null)
{
return null;
}
for (passwordHistoryEntry entry: pool)
{
if (entry.matches(password))
{
return entry.getDate();
}
}
return null;
}
}
/*----------------------------------------------------------------------------
nested class
passwordHistoryEntry
----------------------------------------------------------------------------*/
/**
* This nested class holds a previous password hash along with the
* date that it was committed into this field.
*/
static class passwordHistoryEntry {
/**
* The date this password history entry was committed to the
* Ganymede database.
*/
private Date date;
/**
* An unsalted, fast-evalated SSHA hash, LDAP style.
*/
private String hash;
/* -- */
public passwordHistoryEntry(String plaintext, Date date)
{
this.hash = SSHA.getLDAPSSHAHash(plaintext, null);
this.date = date;
}
public passwordHistoryEntry(DataInput in) throws IOException
{
this.hash = in.readUTF();
this.date = new Date(in.readLong());
}
public void emit(DataOutput out) throws IOException
{
out.writeUTF(hash);
out.writeLong(date.getTime());
}
public Date getDate()
{
return this.date;
}
public String getHash()
{
return this.hash;
}
public boolean matches(String plaintext)
{
return SSHA.matchSHAHash(hash, plaintext);
}
}
}