org.identityconnectors.common.security.GuardedString Maven / Gradle / Ivy
/*
* ====================
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License("CDDL") (the "License"). You may not use this file
* except in compliance with the License.
*
* You can obtain a copy of the License at
* http://opensource.org/licenses/cddl1.php
* See the License for the specific language governing permissions and limitations
* under the License.
*
* When distributing the Covered Code, include this CDDL Header Notice in each file
* and include the License file at http://opensource.org/licenses/cddl1.php.
* If applicable, add the following below this CDDL Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
*/
package org.identityconnectors.common.security;
/**
* Secure string implementation that solves the problems associated with keeping
* passwords as java.lang.String
. That is, anything represented as
* a String
is kept in memory as a clear text password and stays in
* memory at least until it is garbage collected.
*
* The GuardedString class alleviates this problem by storing the characters in
* memory in an encrypted form. The encryption key will be a randomly-generated
* key.
*
* In their serialized form, GuardedString will be encrypted using a known
* default key. This is to provide a minimum level of protection regardless of
* the transport. For communications with the Remote Connector Framework it is
* recommended that deployments enable SSL for true encryption.
*
* Applications may also wish to persist GuardedStrings. In the case of Identity
* Manager, it should convert GuardedStrings to EncryptedData so that they can
* be stored and managed using the Manage Encryption features of Identity
* Manager. Other applications may wish to serialize APIConfiguration as a
* whole. These applications are responsible for encrypting the APIConfiguration
* blob for an additional layer of security (beyond the basic default key
* encryption provided by GuardedString).
*/
public final class GuardedString {
/**
* Callback interface for those times that it is necessary to access the
* clear text of the secure string.
*/
public interface Accessor {
/**
* This method will be called with the clear text of the string.
*
* After the call the clearChars array will be automatically zeroed out,
* thus keeping the window of potential exposure to a bare-minimum.
*
* @param clearChars
*/
public void access(char[] clearChars);
}
static Encryptor encryptor;
private boolean readOnly;
private boolean disposed;
private byte[] encryptedBytes;
private String base64SHA1Hash;
/**
* Creates an empty secure string
*/
public GuardedString() {
this(new char[0]);
}
/**
* Initialized the GuardedString from the given clear characters.
*
* Caller is responsible for zeroing out the array of characters after the
* call.
*
* @param clearChars
* The clear-text characters
*/
public GuardedString(char[] clearChars) {
encryptChars(clearChars);
}
/**
* Provides access to the clear-text value of the string in a controlled
* fashion.
*
* The clear-text characters will only be available for the duration of the
* call and automatically zeroed out following the call.
*
* NOTE: Callers are encouraged to use
* {@link #verifyBase64SHA1Hash(String)} where possible if the intended use
* is merely to verify the contents of the string match an expected hash
* value.
*
* @param accessor
* Accessor callback.
* @throws IllegalStateException
* If the string has been disposed
*/
public void access(Accessor accessor) {
checkNotDisposed();
char[] clearChars = null;
try {
clearChars = decryptChars();
accessor.access(clearChars);
} finally {
SecurityUtil.clear(clearChars);
}
}
/**
* Appends a single clear-text character to the secure string.
*
* The in-memory data will be decrypted, the character will be appended, and
* then it will be re-encrypted.
*
* @param c
* The character to append.
* @throws IllegalStateException
* If the string is read-only
* @throws IllegalStateException
* If the string has been disposed
*/
public void appendChar(char c) {
checkNotDisposed();
checkWriteable();
char[] clearChars = null;
char[] clearChars2 = null;
try {
clearChars = decryptChars();
clearChars2 = new char[clearChars.length + 1];
System.arraycopy(clearChars, 0, clearChars2, 0, clearChars.length);
clearChars2[clearChars2.length - 1] = c;
encryptChars(clearChars2);
} finally {
SecurityUtil.clear(clearChars);
SecurityUtil.clear(clearChars2);
}
}
/**
* Clears the in-memory representation of the string.
*/
public void dispose() {
SecurityUtil.clear(encryptedBytes);
disposed = true;
}
/**
* Returns true if this string has been marked read-only.
*
* @return true if this string has been marked read-only.
* @throws IllegalStateException
* If the string has been disposed
*/
public boolean isReadOnly() {
checkNotDisposed();
return readOnly;
}
/**
* Mark this string as read-only.
*
* @throws IllegalStateException
* If the string has been disposed
*/
public void makeReadOnly() {
checkNotDisposed();
readOnly = true;
}
/**
* Create a copy of the string.
*
* If this instance is read-only, the copy will not be read-only.
*
* @return A copy of the string.
* @throws IllegalStateException
* If the string has been disposed
*/
public GuardedString copy() {
checkNotDisposed();
byte[] encryptedBytes2 = new byte[encryptedBytes.length];
System.arraycopy(encryptedBytes, 0, encryptedBytes2, 0, encryptedBytes.length);
GuardedString rv = new GuardedString();
rv.encryptedBytes = encryptedBytes2;
return rv;
}
/**
* Verifies that this base-64 encoded SHA1 hash of this string matches the
* given value.
*
* @param hash
* The hash to verify against.
* @return True if the hash matches the given parameter.
* @throws IllegalStateException
* If the string has been disposed
*/
public boolean verifyBase64SHA1Hash(String hash) {
checkNotDisposed();
return base64SHA1Hash.equals(hash);
}
private void checkWriteable() {
if (readOnly) {
throw new IllegalStateException("String is read-only");
}
}
private void checkNotDisposed() {
if (disposed) {
throw new IllegalStateException("String is disposed");
}
}
private char[] decryptChars() {
byte[] clearBytes = null;
try {
clearBytes = decryptBytes();
return SecurityUtil.bytesToChars(clearBytes);
} finally {
SecurityUtil.clear(clearBytes);
}
}
private void encryptChars(char[] chars) {
byte[] clearBytes = null;
try {
clearBytes = SecurityUtil.charsToBytes(chars);
encryptBytes(clearBytes);
} finally {
SecurityUtil.clear(clearBytes);
}
}
private static synchronized Encryptor getEncryptor() {
if (encryptor == null) {
encryptor = EncryptorFactory.getInstance().newRandomEncryptor();
}
return encryptor;
}
static synchronized void setEncryptor(Encryptor encryptor) {
GuardedString.encryptor = encryptor;
}
private byte[] decryptBytes() {
Encryptor encryptor = getEncryptor();
return encryptor.decrypt(encryptedBytes);
}
private void encryptBytes(byte[] bytes) {
Encryptor encryptor = getEncryptor();
byte[] newBytes = encryptor.encrypt(bytes);
SecurityUtil.clear(encryptedBytes);
encryptedBytes = newBytes;
base64SHA1Hash = SecurityUtil.computeBase64SHA1Hash(bytes);
}
@Override
public boolean equals(Object o) {
if (o instanceof GuardedString) {
GuardedString other = (GuardedString) o;
// not the true contract of equals. however,
// due to the high mathematical improbability of
// two unequal strings having the same secure hash,
// this approach feels good. the alternative,
// decrypting for comparison, is simply too
// performance intensive to be used for equals
return base64SHA1Hash.equals(other.base64SHA1Hash);
}
return false;
}
@Override
public int hashCode() {
return base64SHA1Hash.hashCode();
}
}