From 1a2cdfb5cf5f89348e8fee7ceeaa699d4aa54cea Mon Sep 17 00:00:00 2001
From: Fabio Pistolesi <fabio.pistolesi@forgerock.com>
Date: Thu, 21 Apr 2016 15:17:15 +0000
Subject: [PATCH] OPENDJ-2616 Support protection of pluggable backend data at rest
---
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ID2Entry.java | 210 ++++++++++++++++++++++++++++++++++++++--------------
1 files changed, 153 insertions(+), 57 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ID2Entry.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ID2Entry.java
index 117c5f5..678bd20 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ID2Entry.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/ID2Entry.java
@@ -12,10 +12,11 @@
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2006-2010 Sun Microsystems, Inc.
- * Portions Copyright 2012-2015 ForgeRock AS.
+ * Portions Copyright 2012-2016 ForgeRock AS.
*/
package org.opends.server.backends.pluggable;
+import static org.forgerock.opendj.ldap.ResultCode.UNWILLING_TO_PERFORM;
import static org.forgerock.util.Reject.*;
import static org.forgerock.util.Utils.*;
import static org.opends.messages.BackendMessages.*;
@@ -23,16 +24,18 @@
import static org.opends.server.core.DirectoryServer.*;
import java.io.IOException;
+import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.DataFormatException;
import java.util.zip.DeflaterOutputStream;
+import java.util.zip.InflaterInputStream;
import java.util.zip.InflaterOutputStream;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.io.ASN1;
import org.forgerock.opendj.io.ASN1Reader;
-import org.forgerock.opendj.io.ASN1Writer;
import org.forgerock.opendj.ldap.ByteSequence;
+import org.forgerock.opendj.ldap.ByteSequenceReader;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ByteStringBuilder;
import org.forgerock.opendj.ldap.DecodeException;
@@ -45,6 +48,7 @@
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.core.DirectoryServer;
+import org.opends.server.types.CryptoManagerException;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.LDAPException;
@@ -103,40 +107,87 @@
/** A cached set of ByteStringBuilder buffers and ASN1Writer used to encode entries. */
private static final class EntryCodec
{
+ /**
+ * The format version used encode and decode entries in previous versions.
+ * Not used anymore, kept for compatibility during upgrade.
+ */
+ static final byte FORMAT_VERSION = 0x01;
+
/** The ASN1 tag for the ByteString type. */
private static final byte TAG_TREE_ENTRY = 0x60;
private static final int BUFFER_INIT_SIZE = 512;
+ private static final byte PLAIN_ENTRY = 0x00;
+ private static final byte COMPRESS_ENTRY = 0x01;
+ private static final byte ENCRYPT_ENTRY = 0x02;
+
+ /** The format version for entry encoding. */
+ static final byte FORMAT_VERSION_V2 = 0x02;
private final ByteStringBuilder encodedBuffer = new ByteStringBuilder();
private final ByteStringBuilder entryBuffer = new ByteStringBuilder();
private final ByteStringBuilder compressedEntryBuffer = new ByteStringBuilder();
- private final ASN1Writer writer;
private final int maxBufferSize;
private EntryCodec()
{
this.maxBufferSize = getMaxInternalBufferSize();
- this.writer = ASN1.getWriter(encodedBuffer, maxBufferSize);
}
private void release()
{
- closeSilently(writer);
encodedBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE);
entryBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE);
- compressedEntryBuffer.clearAndTruncate(maxBufferSize, BUFFER_INIT_SIZE);
}
private Entry decode(ByteString bytes, CompressedSchema compressedSchema)
throws DirectoryException, DecodeException, IOException
{
- // Get the format version.
- byte formatVersion = bytes.byteAt(0);
- if(formatVersion != DnKeyFormat.FORMAT_VERSION)
+ final byte formatVersion = bytes.byteAt(0);
+ switch(formatVersion)
{
+ case FORMAT_VERSION:
+ return decodeV1(bytes, compressedSchema);
+ case FORMAT_VERSION_V2:
+ return decodeV2(bytes, compressedSchema);
+ default:
throw DecodeException.error(ERR_INCOMPATIBLE_ENTRY_VERSION.get(formatVersion));
}
+ }
+ /**
+ * Decodes an entry from the old format.
+ * <p>
+ * An entry on disk is ASN1 encoded in this format:
+ *
+ * <pre>
+ * ByteString ::= [APPLICATION 0] IMPLICIT SEQUENCE {
+ * uncompressedSize INTEGER, -- A zero value means not compressed.
+ * dataBytes OCTET STRING -- Optionally compressed encoding of
+ * the data bytes.
+ * }
+ *
+ * ID2EntryValue ::= ByteString
+ * -- Where dataBytes contains an encoding of DirectoryServerEntry.
+ *
+ * DirectoryServerEntry ::= [APPLICATION 1] IMPLICIT SEQUENCE {
+ * dn LDAPDN,
+ * objectClasses SET OF LDAPString,
+ * userAttributes AttributeList,
+ * operationalAttributes AttributeList
+ * }
+ * </pre>
+ *
+ * @param bytes A byte array containing the encoded tree value.
+ * @param compressedSchema The compressed schema manager to use when decoding.
+ * @return The decoded entry.
+ * @throws DecodeException If the data is not in the expected ASN.1 encoding
+ * format.
+ * @throws DirectoryException If a Directory Server error occurs.
+ * @throws IOException if an error occurs while reading the ASN1 sequence.
+ */
+ private Entry decodeV1(ByteString bytes, CompressedSchema compressedSchema)
+ throws DirectoryException, DecodeException, IOException
+ {
// Read the ASN1 sequence.
ASN1Reader reader = ASN1.getReader(bytes.subSequence(1, bytes.length()));
reader.readStartSequence();
@@ -173,6 +224,71 @@
}
}
+ /**
+ * Decodes an entry in the new extensible format.
+ * Enties are encoded according to the sequence
+ * {VERSION_BYTE, FLAG_BYTE, COMPACT_INTEGER_LENGTH, ID2ENTRY_VALUE}
+ * where
+ *
+ * ID2ENTRY_VALUE = encoding of Entry as in decodeV1()
+ * VERSION_BYTE = 0x2
+ * FLAG_BYTE = bit field of OR'ed values indicating post-encoding processing.
+ * possible meaningful flags are COMPRESS_ENTRY and ENCRYPT_ENTRY.
+ * COMPACT_INTEGER_LENGTH = length of ID2ENTRY_VALUE
+ *
+ * @param bytes A byte array containing the encoded tree value.
+ * @param compressedSchema The compressed schema manager to use when decoding.
+ * @return The decoded entry.
+ * @throws DecodeException If the data is not in the expected ASN.1 encoding
+ * format or a decryption error occurs.
+ * @throws DirectoryException If a Directory Server error occurs.
+ * @throws IOException if an error occurs while reading the ASN1 sequence.
+ */
+ private Entry decodeV2(ByteString bytes, CompressedSchema compressedSchema)
+ throws DirectoryException, DecodeException, IOException
+ {
+ ByteSequenceReader reader = bytes.asReader();
+ // skip version byte
+ reader.position(1);
+ int format = reader.readByte();
+ int encodedEntryLen = reader.readCompactUnsignedInt();
+ try
+ {
+ if (format == PLAIN_ENTRY)
+ {
+ return Entry.decode(reader, compressedSchema);
+ }
+ InputStream is = reader.asInputStream();
+ if ((format & ENCRYPT_ENTRY) == ENCRYPT_ENTRY)
+ {
+ is = getCryptoManager().getCipherInputStream(is);
+ }
+ if ((format & COMPRESS_ENTRY) == COMPRESS_ENTRY)
+ {
+ is = new InflaterInputStream(is);
+ }
+ byte[] data = new byte[encodedEntryLen];
+ int readBytes;
+ int position = 0;
+ int leftToRead = encodedEntryLen;
+ // CipherInputStream does not read more than block size...
+ do
+ {
+ if ((readBytes = is.read(data, position, leftToRead)) == -1 )
+ {
+ throw DecodeException.error(ERR_CANNOT_DECODE_ENTRY.get());
+ }
+ position += readBytes;
+ leftToRead -= readBytes;
+ } while (leftToRead > 0 && readBytes > 0);
+ return Entry.decode(ByteString.wrap(data).asReader(), compressedSchema);
+ }
+ catch (CryptoManagerException cme)
+ {
+ throw DecodeException.error(cme.getMessageObject());
+ }
+ }
+
private ByteString encode(Entry entry, DataConfig dataConfig) throws DirectoryException
{
encodeVolatile(entry, dataConfig);
@@ -181,44 +297,44 @@
private void encodeVolatile(Entry entry, DataConfig dataConfig) throws DirectoryException
{
- // Encode the entry for later use.
entry.encode(entryBuffer, dataConfig.getEntryEncodeConfig());
- // First write the DB format version byte.
- encodedBuffer.appendByte(DnKeyFormat.FORMAT_VERSION);
-
+ OutputStream os = encodedBuffer.asOutputStream();
try
{
- // Then start the ASN1 sequence.
- writer.writeStartSequence(TAG_TREE_ENTRY);
-
+ byte[] formatFlags = { FORMAT_VERSION_V2, 0};
+ os.write(formatFlags);
+ encodedBuffer.appendCompactUnsigned(entryBuffer.length());
if (dataConfig.isCompressed())
{
- OutputStream compressor = null;
- try {
- compressor = new DeflaterOutputStream(compressedEntryBuffer.asOutputStream());
- entryBuffer.copyTo(compressor);
- }
- finally {
- closeSilently(compressor);
- }
-
- // Compression needed and successful.
- writer.writeInteger(entryBuffer.length());
- writer.writeOctetString(compressedEntryBuffer);
+ os = new DeflaterOutputStream(os);
+ formatFlags[1] = COMPRESS_ENTRY;
}
- else
+ if (dataConfig.isEncrypted())
{
- writer.writeInteger(0);
- writer.writeOctetString(entryBuffer);
+ os = dataConfig.getCryptoSuite().getCipherOutputStream(os);
+ formatFlags[1] |= ENCRYPT_ENTRY;
}
+ encodedBuffer.setByte(1, formatFlags[1]);
- writer.writeEndSequence();
+ entryBuffer.copyTo(os);
+ os.flush();
}
- catch(IOException ioe)
+ catch(CryptoManagerException | IOException e)
{
- // TODO: This should never happen with byte buffer.
- logger.traceException(ioe);
+ logger.traceException(e);
+ throw new DirectoryException(UNWILLING_TO_PERFORM, ERR_CANNOT_ENCODE_ENTRY.get(e.getLocalizedMessage()));
+ }
+ finally
+ {
+ try
+ {
+ os.close();
+ }
+ catch (IOException ioe)
+ {
+ throw new DirectoryException(UNWILLING_TO_PERFORM, ERR_CANNOT_ENCODE_ENTRY.get(ioe.getLocalizedMessage()));
+ }
}
}
}
@@ -250,26 +366,6 @@
/**
* Decodes an entry from its tree representation.
- * <p>
- * An entry on disk is ASN1 encoded in this format:
- *
- * <pre>
- * ByteString ::= [APPLICATION 0] IMPLICIT SEQUENCE {
- * uncompressedSize INTEGER, -- A zero value means not compressed.
- * dataBytes OCTET STRING -- Optionally compressed encoding of
- * the data bytes.
- * }
- *
- * ID2EntryValue ::= ByteString
- * -- Where dataBytes contains an encoding of DirectoryServerEntry.
- *
- * DirectoryServerEntry ::= [APPLICATION 1] IMPLICIT SEQUENCE {
- * dn LDAPDN,
- * objectClasses SET OF LDAPString,
- * userAttributes AttributeList,
- * operationalAttributes AttributeList
- * }
- * </pre>
*
* @param bytes A byte array containing the encoded tree value.
* @param compressedSchema The compressed schema manager to use when decoding.
@@ -283,7 +379,7 @@
* @throws DirectoryException If a Directory Server error occurs.
* @throws IOException if an error occurs while reading the ASN1 sequence.
*/
- static Entry entryFromDatabase(ByteString bytes,
+ Entry entryFromDatabase(ByteString bytes,
CompressedSchema compressedSchema) throws DirectoryException,
DecodeException, LDAPException, DataFormatException, IOException
{
@@ -308,7 +404,7 @@
* @throws DirectoryException If a problem occurs while attempting to encode
* the entry.
*/
- static ByteString entryToDatabase(Entry entry, DataConfig dataConfig) throws DirectoryException
+ ByteString entryToDatabase(Entry entry, DataConfig dataConfig) throws DirectoryException
{
EntryCodec codec = acquireEntryCodec();
try
--
Gitblit v1.10.0