/* * ==================== * 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 byte array implementation that solves the problems associated with * keeping confidential data as <code>byte[]</code>. That is, anything * represented as a <code>byte[]</code> is kept in memory in clear text and * stays in memory <b>at least</b> until it is garbage collected. * <p> * The GuardedByteArray class alleviates this problem by storing the bytes in * memory in an encrypted form. The encryption key will be a randomly-generated * key. * <p> * In their serialized form, GuardedByteArrays 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. * <p> * Applications may also wish to persist GuardedByteArrays. In the case of * Identity Manager, it should convert GuardedByteArrays 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 GuardedByteArray). */ public final class GuardedByteArray { /** * Callback interface for those times that it is necessary to access the * clear text of the guarded bytes. */ public interface Accessor { /** * This method will be called with the clear text of the bytes. * * After the call the clearBytes array will be automatically zeroed out, * thus keeping the window of potential exposure to a bare-minimum. * * @param clearBytes */ public void access(byte[] clearBytes); } private static Encryptor encryptor; private boolean readOnly; private boolean disposed; private byte[] encryptedBytes; private String base64SHA1Hash; /** * Creates an empty secure byte array. */ public GuardedByteArray() { this(new byte[0]); } /** * Initializes the GuardedByteArray from the given clear text bytes. * * Caller is responsible for zeroing out the array of bytes after the call. * * @param clearBytes * The clear-text bytes */ public GuardedByteArray(byte[] clearBytes) { encryptBytes(clearBytes); } /** * Provides access to the clear-text value of the byte array in a controlled * fashion. * * The clear-text bytes will only be available for the duration of the call * and automatically zeroed out following the call. * <p> * <b>NOTE:</b> Callers are encouraged to use * {@link #verifyBase64SHA1Hash(String)} where possible if the intended use * is merely to verify the contents of the byte array match an expected hash * value. * * @param accessor * Accessor callback. * @throws IllegalStateException * If the byte array has been disposed */ public void access(Accessor accessor) { checkNotDisposed(); byte[] clearBytes = null; try { clearBytes = decryptBytes(); accessor.access(clearBytes); } finally { SecurityUtil.clear(clearBytes); } } /** * Appends a single clear-text byte to the secure byte array. * * The in-memory data will be decrypted, the byte will be appended, and then * it will be re-encrypted. * * @param b * The byte to append. * @throws IllegalStateException * If the byte array is read-only * @throws IllegalStateException * If the byte array has been disposed */ public void appendByte(byte b) { checkNotDisposed(); checkWriteable(); byte[] clearBytes = null; byte[] clearBytes2 = null; try { clearBytes = decryptBytes(); clearBytes2 = new byte[clearBytes.length + 1]; System.arraycopy(clearBytes, 0, clearBytes2, 0, clearBytes.length); clearBytes2[clearBytes2.length - 1] = b; encryptBytes(clearBytes2); } finally { SecurityUtil.clear(clearBytes); SecurityUtil.clear(clearBytes2); } } /** * Clears the in-memory representation of the byte array. */ public void dispose() { SecurityUtil.clear(encryptedBytes); disposed = true; } /** * Returns true if this byte array has been marked read-only. * * @return true if this byte array has been marked read-only * @throws IllegalStateException * If the byte array has been disposed */ public boolean isReadOnly() { checkNotDisposed(); return readOnly; } /** * Mark this byte array as read-only. * * @throws IllegalStateException * If the byte array has been disposed */ public void makeReadOnly() { checkNotDisposed(); readOnly = true; } /** * Create a copy of the byte array. * * If this instance is read-only, the copy will not be read-only. * * @return A copy of the byte array. * @throws IllegalStateException * If the byte array has been disposed */ public GuardedByteArray copy() { checkNotDisposed(); byte[] encryptedBytes2 = new byte[encryptedBytes.length]; System.arraycopy(encryptedBytes, 0, encryptedBytes2, 0, encryptedBytes.length); GuardedByteArray rv = new GuardedByteArray(); rv.encryptedBytes = encryptedBytes2; return rv; } /** * Verifies that this base-64 encoded SHA1 hash of this byte array matches * the given value. * * @param hash * The hash to verify against. * @return True if the hash matches the given parameter. * @throws IllegalStateException * If the byte array has been disposed */ public boolean verifyBase64SHA1Hash(String hash) { checkNotDisposed(); return base64SHA1Hash.equals(hash); } private void checkWriteable() { if (readOnly) { throw new IllegalStateException("Byte array is read-only"); } } private void checkNotDisposed() { if (disposed) { throw new IllegalStateException("Byte array is disposed"); } } private static synchronized Encryptor getEncryptor() { if (encryptor == null) { encryptor = EncryptorFactory.getInstance().newRandomEncryptor(); } return encryptor; } static synchronized void setEncryptor(Encryptor encryptor) { GuardedByteArray.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 GuardedByteArray) { GuardedByteArray other = (GuardedByteArray) o; // not the true contract of equals. however, // due to the high mathematical improbability of // two unequal byte arrays 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(); } }