diff --git a/java/common/src/main/resources/org/apache/tsfile/i18n/messages.properties b/java/common/src/main/resources/org/apache/tsfile/i18n/messages.properties index 2836b8c1a8..3700a46f61 100644 --- a/java/common/src/main/resources/org/apache/tsfile/i18n/messages.properties +++ b/java/common/src/main/resources/org/apache/tsfile/i18n/messages.properties @@ -636,6 +636,7 @@ log.read.block_reader_fill_measurements_error = Cannot fill measurements # TableResultSet — failed to close tsBlockReader log.read.table_result_set_close_error = Failed to close tsBlockReader +log.read.tree_result_set_close_error = Failed to close tree query dataset # DeviceMetaIterator — failed to load device meta data log.read.device_meta_iterator_error = Failed to load device meta data @@ -1117,6 +1118,44 @@ error.encrypt.encrypt_no_constructor = Get constructor for encryptor failed: %1$ # EncryptUtils.getEncrypt — instantiation failed (encryptType arg) error.encrypt.encrypt_instantiation_failed = New encryptor instance failed: %1$s +# EncryptionProviderRegistry — provider registration and lookup +error.encrypt.invalid_provider_id = Encryption provider id must not be null or empty +error.encrypt.duplicate_provider = Encryption provider id is already registered: %1$s +error.encrypt.provider_not_found = Encryption provider is not registered: %1$s +error.encrypt.provider_returned_null = Encryption provider returned null: %1$s + +# EncryptParameter — use after sensitive state was destroyed +error.encrypt.parameter_destroyed = Encryption parameters have been destroyed +error.encrypt.invalid_page_body_overhead = Invalid page body overhead %1$s for encryption profile %2$s +error.encrypt.page_body_too_short = Encrypted page body size %1$s is smaller than AEAD overhead %2$s +error.encrypt.page_context_invalid_parameter = Page crypto context requires page AEAD parameters +error.encrypt.page_aead_encrypt_unsupported = Encryptor %1$s does not implement authenticated page encryption +error.encrypt.page_aead_decrypt_unsupported = Decryptor %1$s does not implement authenticated page decryption +error.encrypt.page_context_invalid_file_id = Page crypto context requires a 16-byte file crypto id +error.encrypt.page_context_invalid_chunk_ordinal = Page crypto context requires a nonnegative chunk ordinal +error.encrypt.page_read_requires_chunk_ordinal = Page-AEAD files require readPage(header, type, pageIndex, chunkOrdinal) +error.encrypt.chunk_ordinal_exhausted = File chunk ordinal space is exhausted +error.write.invalid_chunk_ordinal = Encrypted chunks require a file-unique chunk ordinal +error.write.unencrypted_chunk_has_ordinal = Unencrypted chunks must not have a chunk ordinal +error.write.aead_append_requires_file_header = Cannot append encrypted data to a file without an encryption header +error.write.encrypted_chunk_copy_unsupported = Encrypted chunks must be decrypted and re-encrypted before copying +error.encrypt.page_context_invalid_sizes = Invalid page crypto context values, uncompressed size: %1$s, compressed plaintext size: %2$s, page index: %3$s +error.encrypt.page_output_size_mismatch = AEAD page output size mismatch, expected %1$s bytes but got %2$s +error.encrypt.page_plaintext_size_mismatch = AEAD page plaintext size mismatch, expected %1$s bytes but got %2$s +error.chunk.merge_page_aead_unsupported = Page-AEAD encrypted chunks must be decrypted and rewritten before merging + +# FileEncryptionHeader — malformed or unsupported file encryption metadata +error.file.encryption_header_invalid_size = Invalid file encryption header size: %1$s +error.file.encryption_header_unsupported_version = Unsupported file encryption header version: %1$s +error.file.encryption_header_malformed = Malformed file encryption header +error.file.encryption_header_trailing_bytes = File encryption header contains trailing bytes +error.file.encryption_header_invalid_component_size = Invalid file encryption header component size: %1$s +error.file.encryption_header_truncated = File encryption header is truncated, expected %1$s bytes but read %2$s +error.file.encryption_header_invalid_field = Invalid file encryption header field: %1$s +error.write.encryption_header_must_precede_data = File encryption header must be written before TsFile data +error.write.encrypted_chunk_context_mismatch = Encrypted chunks must use the destination file encryption context +error.write.encryption_context_immutable = The file encryption context cannot change after its header is written + # === compress === # ICompressor.getCompressor / IUnCompressor.getUnCompressor — null type @@ -1573,3 +1612,15 @@ error.utils.table_point_count_tool_file_not_found = TsFile does not exist: %1$s error.utils.table_point_count_tool_incomplete_file = TsFile is incomplete: %1$s error.utils.table_point_count_tool_copy_failed = Failed to copy the TsFile data and metadata prefix info.utils.table_point_count_tool_result = Processed %1$s: %2$s +error.write.aead_raw_page_copy_unsupported = Raw page copying is not supported for page AEAD; decrypt and re-encrypt with the target file context +error.encrypt.file_context_already_used = Encryption context already belongs to a new file; generate a new DEK and file identity +error.encrypt.provider_discovery_failed = Encryption provider discovery failed +error.encrypt.page_aead_operations_missing = Page AEAD provider must supply an encryptor and a decryptor +error.encrypt.page_aead_requires_page_context = Page AEAD requires the page-aware encryption or decryption API +error.encrypt.page_aead_decryption_failed = Page AEAD decryption failed +error.encrypt.chunk_context_invalid = Chunk position authentication requires a file identity, a nonnegative marker offset and a chunk ordinal +error.encrypt.chunk_authentication_unsupported = Provider %1$s does not support Chunk position authentication +error.encrypt.invalid_chunk_authentication = The encrypted Chunk position authentication proof is missing or has an invalid size +error.encrypt.invalid_chunk_authentication_size = Invalid chunk authentication size: %1$d +error.encrypt.chunk_authentication_failed = Chunk position authentication failed +error.encrypt.invalid_page_body_lengths = Invalid authenticated page body length encoding diff --git a/java/common/src/main/resources/org/apache/tsfile/i18n/messages_zh.properties b/java/common/src/main/resources/org/apache/tsfile/i18n/messages_zh.properties index 502df532e1..e86263ab0f 100644 --- a/java/common/src/main/resources/org/apache/tsfile/i18n/messages_zh.properties +++ b/java/common/src/main/resources/org/apache/tsfile/i18n/messages_zh.properties @@ -636,6 +636,7 @@ log.read.block_reader_fill_measurements_error = 无法填充 measurements # TableResultSet — failed to close tsBlockReader log.read.table_result_set_close_error = 关闭 tsBlockReader 失败 +log.read.tree_result_set_close_error = 关闭树模型查询结果集失败 # DeviceMetaIterator — failed to load device meta data log.read.device_meta_iterator_error = 加载设备 meta data 失败 @@ -1117,6 +1118,44 @@ error.encrypt.encrypt_no_constructor = 获取 encryptor 构造函数失败: %1$s # EncryptUtils.getEncrypt — instantiation failed (encryptType arg) error.encrypt.encrypt_instantiation_failed = 实例化 encryptor 失败: %1$s +# EncryptionProviderRegistry — provider registration and lookup +error.encrypt.invalid_provider_id = encryption provider id 不能为空 +error.encrypt.duplicate_provider = encryption provider id 已注册: %1$s +error.encrypt.provider_not_found = encryption provider 未注册: %1$s +error.encrypt.provider_returned_null = encryption provider 返回 null: %1$s + +# EncryptParameter — use after sensitive state was destroyed +error.encrypt.parameter_destroyed = encryption parameters 已销毁 +error.encrypt.invalid_page_body_overhead = encryption profile %2$s 的 page body 额外开销 %1$s 无效 +error.encrypt.page_body_too_short = encrypted page body 大小 %1$s 小于 AEAD 额外开销 %2$s +error.encrypt.page_context_invalid_parameter = page crypto context 需要 page AEAD 参数 +error.encrypt.page_aead_encrypt_unsupported = 加密器 %1$s 未实现经过认证的 Page 加密 +error.encrypt.page_aead_decrypt_unsupported = 解密器 %1$s 未实现经过认证的 Page 解密 +error.encrypt.page_context_invalid_file_id = page crypto context 需要 16 字节的 file crypto id +error.encrypt.page_context_invalid_chunk_ordinal = page crypto context 需要非负的 chunk ordinal +error.encrypt.page_read_requires_chunk_ordinal = Page-AEAD 文件需要调用 readPage(header, type, pageIndex, chunkOrdinal) +error.encrypt.chunk_ordinal_exhausted = 文件 Chunk 序号已耗尽 +error.write.invalid_chunk_ordinal = 加密 Chunk 必须具有文件内唯一的 Chunk 序号 +error.write.unencrypted_chunk_has_ordinal = 未加密 Chunk 不得携带 Chunk 序号 +error.write.aead_append_requires_file_header = 无加密头的文件不能追加加密数据 +error.write.encrypted_chunk_copy_unsupported = 复制加密 Chunk 前必须先解密并重新加密 +error.encrypt.page_context_invalid_sizes = page crypto context 值无效,uncompressed size: %1$s,compressed plaintext size: %2$s,page index: %3$s +error.encrypt.page_output_size_mismatch = AEAD page 输出大小不匹配,预期 %1$s 字节,实际 %2$s 字节 +error.encrypt.page_plaintext_size_mismatch = AEAD page plaintext 大小不匹配,预期 %1$s 字节,实际 %2$s 字节 +error.chunk.merge_page_aead_unsupported = Page-AEAD encrypted chunk 合并前必须先解密并重写 + +# FileEncryptionHeader — malformed or unsupported file encryption metadata +error.file.encryption_header_invalid_size = file encryption header 大小无效: %1$s +error.file.encryption_header_unsupported_version = 不支持的 file encryption header 版本: %1$s +error.file.encryption_header_malformed = file encryption header 格式错误 +error.file.encryption_header_trailing_bytes = file encryption header 包含多余字节 +error.file.encryption_header_invalid_component_size = file encryption header 组件大小无效: %1$s +error.file.encryption_header_truncated = file encryption header 已截断,预期 %1$s 字节,实际读取 %2$s 字节 +error.file.encryption_header_invalid_field = file encryption header 字段无效: %1$s +error.write.encryption_header_must_precede_data = file encryption header 必须写在 TsFile data 之前 +error.write.encrypted_chunk_context_mismatch = encrypted Chunk 必须使用目标文件的加密上下文 +error.write.encryption_context_immutable = 文件加密头写入后不能更改文件加密上下文 + # === compress === # ICompressor.getCompressor / IUnCompressor.getUnCompressor — null type @@ -1573,3 +1612,15 @@ error.utils.table_point_count_tool_file_not_found = TsFile 不存在:%1$s error.utils.table_point_count_tool_incomplete_file = TsFile 不完整:%1$s error.utils.table_point_count_tool_copy_failed = 复制 TsFile 数据和元数据前缀失败 info.utils.table_point_count_tool_result = 已处理 %1$s:%2$s +error.write.aead_raw_page_copy_unsupported = Page AEAD 不支持原样复制页面;必须解密后使用目标文件上下文重新加密 +error.encrypt.file_context_already_used = 加密上下文已用于创建新文件;请生成新的 DEK 和文件身份 +error.encrypt.provider_discovery_failed = 加密 Provider 发现失败 +error.encrypt.page_aead_operations_missing = Page AEAD Provider 必须提供加密器和解密器 +error.encrypt.page_aead_requires_page_context = Page AEAD 必须使用带页面上下文的加解密接口 +error.encrypt.page_aead_decryption_failed = Page AEAD 解密失败 +error.encrypt.chunk_context_invalid = Chunk 位置认证需要文件身份、非负的 marker 偏移和 Chunk 序号 +error.encrypt.chunk_authentication_unsupported = Provider %1$s 不支持 Chunk 位置认证 +error.encrypt.invalid_chunk_authentication = 加密 Chunk 的位置认证凭据缺失或长度无效 +error.encrypt.invalid_chunk_authentication_size = 无效的 Chunk 认证长度:%1$d +error.encrypt.chunk_authentication_failed = Chunk 位置认证失败 +error.encrypt.invalid_page_body_lengths = 认证 Page 正文长度编码无效 diff --git a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/ChunkCryptoContext.java b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/ChunkCryptoContext.java new file mode 100644 index 0000000000..cced05c056 --- /dev/null +++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/ChunkCryptoContext.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.tsfile.encrypt; + +import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.i18n.Messages; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** Binds a chunk's page identity to the absolute position of its on-disk marker. */ +public final class ChunkCryptoContext { + /** Format bound for opaque proofs; the immutable provider profile defines their exact size. */ + public static final int MAX_AUTHENTICATION_SIZE = 1024; + + private static final int ASSOCIATED_DATA_VERSION = 1; + private static final byte[] DOMAIN = "TsFile/ChunkPosition".getBytes(TSFileConfig.STRING_CHARSET); + private final byte[] associatedData; + + private ChunkCryptoContext(byte[] fileCryptoId, long chunkOffset, long chunkOrdinal) { + associatedData = + ByteBuffer.allocate( + Integer.BYTES * 2 + DOMAIN.length + fileCryptoId.length + Long.BYTES * 2) + .putInt(DOMAIN.length) + .put(DOMAIN) + .putInt(ASSOCIATED_DATA_VERSION) + .put(fileCryptoId) + .putLong(chunkOffset) + .putLong(chunkOrdinal) + .array(); + } + + public static ChunkCryptoContext forChunk( + EncryptParameter parameter, long chunkOffset, long chunkOrdinal) { + if (parameter == null || !parameter.isTdePageAead() || chunkOffset < 0 || chunkOrdinal < 0) { + throw new EncryptException(Messages.get("error.encrypt.chunk_context_invalid")); + } + byte[] fileId = parameter.getFileCryptoId(); + if (fileId == null || fileId.length != EncryptParameter.FILE_CRYPTO_ID_LENGTH) { + throw new EncryptException(Messages.get("error.encrypt.chunk_context_invalid")); + } + return new ChunkCryptoContext(fileId, chunkOffset, chunkOrdinal); + } + + public byte[] getAssociatedData() { + return Arrays.copyOf(associatedData, associatedData.length); + } +} diff --git a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptParameter.java b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptParameter.java index f7adb52217..815bec5534 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptParameter.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptParameter.java @@ -18,14 +18,165 @@ */ package org.apache.tsfile.encrypt; -public class EncryptParameter { +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.i18n.Messages; + +import javax.security.auth.Destroyable; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Parameters required to encrypt or decrypt one TsFile. + * + *

The two-argument constructor preserves the legacy encryption API. New file-level encryption + * schemes should use {@link #pageAeadBuilder()} and identify an {@link IEncryptProvider} and one of + * its immutable encryption profiles by stable ids. Runtime implementation class names and algorithm + * implementation details are deliberately not part of the persisted parameters. + */ +public class EncryptParameter implements AutoCloseable, Destroyable { + + public static final int FILE_CRYPTO_ID_LENGTH = 16; private final String type; private final byte[] key; + private final boolean pageAead; + private final String providerId; + private final String profileId; + private final String keyId; + private final String keyVersion; + private final byte[] wrappedDataKey; + private final byte[] fileCryptoId; + private final AtomicLong nextChunkOrdinal; + private final AtomicBoolean newFileClaimed; + + private final FileEncryptContext fileEncryptContext; + private transient volatile boolean destroyed; public EncryptParameter(String type, byte[] key) { this.type = type; this.key = key; + this.pageAead = false; + this.providerId = null; + this.profileId = null; + this.keyId = null; + this.keyVersion = null; + this.wrappedDataKey = null; + this.fileCryptoId = null; + this.nextChunkOrdinal = new AtomicLong(); + this.newFileClaimed = new AtomicBoolean(); + this.fileEncryptContext = null; + } + + private EncryptParameter(Builder builder) { + this.type = null; + this.key = copy(builder.key); + this.pageAead = true; + this.providerId = builder.providerId; + this.profileId = builder.profileId; + this.keyId = builder.keyId; + this.keyVersion = builder.keyVersion; + this.wrappedDataKey = copy(builder.wrappedDataKey); + this.fileCryptoId = copy(builder.fileCryptoId); + this.nextChunkOrdinal = + builder.nextChunkOrdinal == null ? new AtomicLong() : builder.nextChunkOrdinal; + this.newFileClaimed = + builder.newFileClaimed == null ? new AtomicBoolean() : builder.newFileClaimed; + this.fileEncryptContext = new FileEncryptContext(); + } + + private EncryptParameter(EncryptParameter source) { + this.type = source.type; + this.key = copy(source.key); + this.pageAead = source.pageAead; + this.providerId = source.providerId; + this.profileId = source.profileId; + this.keyId = source.keyId; + this.keyVersion = source.keyVersion; + this.wrappedDataKey = copy(source.wrappedDataKey); + this.fileCryptoId = copy(source.fileCryptoId); + this.nextChunkOrdinal = source.nextChunkOrdinal; + this.newFileClaimed = source.newFileClaimed; + this.fileEncryptContext = source.fileEncryptContext; + this.fileEncryptContext.retain(); + } + + public static Builder pageAeadBuilder() { + return new Builder(); + } + + /** + * Copies this file's descriptor without initialized provider state. This does not generate a new + * data key or file identity and must not be used to create another new file. + */ + public synchronized EncryptParameter copy() { + if (destroyed) { + throw new EncryptException(Messages.get("error.encrypt.parameter_destroyed")); + } + if (!isTdePageAead()) { + return new EncryptParameter(type, key); + } + return pageAeadBuilder() + .key(key) + .providerId(providerId) + .profileId(profileId) + .keyId(keyId) + .keyVersion(keyVersion) + .wrappedDataKey(wrappedDataKey) + .fileCryptoId(fileCryptoId) + .nextChunkOrdinal(nextChunkOrdinal) + .newFileClaimed(newFileClaimed) + .build(); + } + + /** Reserves this context for one new file. Append/recovery use the existing on-disk header. */ + public synchronized void claimForNewFile() { + if (destroyed) { + throw new EncryptException(Messages.get("error.encrypt.parameter_destroyed")); + } + if (!pageAead || !newFileClaimed.compareAndSet(false, true)) { + throw new EncryptException(Messages.get("error.encrypt.file_context_already_used")); + } + } + + /** + * Returns an owned reference sharing this file's runtime provider. Each reference must be closed; + * the provider is destroyed only after the last reference closes. Legacy parameters are copied. + */ + public synchronized EncryptParameter retain() { + if (destroyed) { + throw new EncryptException(Messages.get("error.encrypt.parameter_destroyed")); + } + return pageAead ? new EncryptParameter(this) : copy(); + } + + /** Allocates a file-unique identity before the first page of a chunk is encrypted. */ + public long nextChunkOrdinal() { + if (!pageAead || destroyed) { + throw new EncryptException(Messages.get("error.encrypt.page_context_invalid_parameter")); + } + long ordinal = + nextChunkOrdinal.getAndUpdate(value -> value == Long.MAX_VALUE ? value : value + 1); + if (ordinal == Long.MAX_VALUE) { + throw new EncryptException(Messages.get("error.encrypt.chunk_ordinal_exhausted")); + } + return ordinal; + } + + /** Continues allocation after the highest retained chunk during append or crash recovery. */ + public void resumeAfterChunkOrdinal(long ordinal) { + if (!pageAead || destroyed) { + throw new EncryptException(Messages.get("error.encrypt.page_context_invalid_parameter")); + } + if (ordinal == Long.MAX_VALUE) { + throw new EncryptException(Messages.get("error.encrypt.chunk_ordinal_exhausted")); + } + if (ordinal >= 0) { + nextChunkOrdinal.accumulateAndGet(ordinal + 1, Math::max); + } } public byte[] getKey() { @@ -35,4 +186,245 @@ public byte[] getKey() { public String getType() { return type; } + + public String getProviderId() { + return providerId; + } + + public String getProfileId() { + return profileId; + } + + public String getKeyId() { + return keyId; + } + + public String getKeyVersion() { + return keyVersion; + } + + public byte[] getWrappedDataKey() { + return copy(wrappedDataKey); + } + + public byte[] getFileCryptoId() { + return copy(fileCryptoId); + } + + boolean hasValidFileCryptoId() { + return fileCryptoId != null && fileCryptoId.length == FILE_CRYPTO_ID_LENGTH; + } + + synchronized ByteBuffer newPageAssociatedDataBuffer(int pageFieldsSize) { + if (destroyed) { + throw new EncryptException(Messages.get("error.encrypt.parameter_destroyed")); + } + return fileEncryptContext.newPageAssociatedDataBuffer(this, pageFieldsSize); + } + + public int getPageBodyOverhead() { + int overhead = getOrCreateFileEncrypt().getPageBodyOverhead(); + if (overhead <= 0) { + throw new EncryptException( + Messages.format("error.encrypt.invalid_page_body_overhead", overhead, profileId)); + } + return overhead; + } + + public boolean isTdePageAead() { + return pageAead; + } + + public int getEncryptedPageBodySize(int plaintextSize) { + int size = getOrCreateFileEncrypt().getEncryptedPageBodySize(plaintextSize); + if (plaintextSize < 0 || size <= plaintextSize) { + throw new EncryptException(Messages.get("error.encrypt.invalid_page_body_lengths")); + } + return size; + } + + public int getPagePlaintextSize(ByteBuffer encryptedBody) { + int size = getOrCreateFileEncrypt().getPagePlaintextSize(encryptedBody.asReadOnlyBuffer()); + if (size < 0 + || size >= encryptedBody.remaining() + || getEncryptedPageBodySize(size) != encryptedBody.remaining()) { + throw new EncryptException(Messages.get("error.encrypt.invalid_page_body_lengths")); + } + return size; + } + + public int getChunkAuthenticationSize() { + return getOrCreateFileEncrypt().getChunkAuthenticationSize(); + } + + public byte[] authenticateChunk(long chunkOffset, long chunkOrdinal) { + return getOrCreateFileEncrypt() + .authenticateChunk(ChunkCryptoContext.forChunk(this, chunkOffset, chunkOrdinal)); + } + + public void verifyChunkAuthentication( + byte[] authentication, long chunkOffset, long chunkOrdinal) { + getOrCreateFileEncrypt() + .verifyChunkAuthentication( + authentication, ChunkCryptoContext.forChunk(this, chunkOffset, chunkOrdinal)); + } + + /** Copies of one file share the ordinal allocator; independently created files never do. */ + public boolean sharesPageAeadFileContext(EncryptParameter other) { + return other != null + && pageAead + && other.pageAead + && !destroyed + && !other.destroyed + && nextChunkOrdinal == other.nextChunkOrdinal + && Objects.equals(providerId, other.providerId) + && Objects.equals(profileId, other.profileId) + && Objects.equals(keyId, other.keyId) + && Objects.equals(keyVersion, other.keyVersion) + && Arrays.equals(fileCryptoId, other.fileCryptoId) + && Arrays.equals(wrappedDataKey, other.wrappedDataKey) + && Arrays.equals(key, other.key); + } + + synchronized IEncrypt getOrCreateFileEncrypt() { + if (destroyed) { + throw new EncryptException(Messages.get("error.encrypt.parameter_destroyed")); + } + return fileEncryptContext.getOrCreate(this); + } + + @Override + public boolean isDestroyed() { + return destroyed; + } + + @Override + public void destroy() { + synchronized (this) { + if (destroyed) { + return; + } + destroyed = true; + if (key != null && isTdePageAead()) { + Arrays.fill(key, (byte) 0); + } + } + if (fileEncryptContext != null) { + fileEncryptContext.release(); + } + } + + @Override + public void close() { + destroy(); + } + + private static byte[] copy(byte[] value) { + return value == null ? null : Arrays.copyOf(value, value.length); + } + + private static final class FileEncryptContext { + private int references = 1; + private IEncrypt fileEncrypt; + private byte[] pageAssociatedDataPrefix; + + private synchronized ByteBuffer newPageAssociatedDataBuffer( + EncryptParameter parameter, int pageFieldsSize) { + if (pageAssociatedDataPrefix == null) { + pageAssociatedDataPrefix = PageCryptoContext.buildFileAssociatedDataPrefix(parameter); + } + // Never expose the shared prefix to providers or individual page contexts. + return ByteBuffer.allocate(Math.addExact(pageAssociatedDataPrefix.length, pageFieldsSize)) + .put(pageAssociatedDataPrefix); + } + + private synchronized void retain() { + references++; + } + + private synchronized IEncrypt getOrCreate(EncryptParameter parameter) { + if (fileEncrypt == null) { + fileEncrypt = EncryptionProviderRegistry.create(parameter); + } + return fileEncrypt; + } + + private void release() { + IEncrypt toClose = null; + synchronized (this) { + if (--references == 0) { + toClose = fileEncrypt; + fileEncrypt = null; + pageAssociatedDataPrefix = null; + } + } + if (toClose != null) { + toClose.close(); + } + } + } + + public static final class Builder { + + private byte[] key; + private String providerId; + private String profileId; + private String keyId; + private String keyVersion; + private byte[] wrappedDataKey; + private byte[] fileCryptoId; + private AtomicLong nextChunkOrdinal; + private AtomicBoolean newFileClaimed; + + private Builder() {} + + public Builder key(byte[] key) { + this.key = copy(key); + return this; + } + + public Builder providerId(String providerId) { + this.providerId = providerId; + return this; + } + + public Builder profileId(String profileId) { + this.profileId = profileId; + return this; + } + + public Builder keyId(String keyId) { + this.keyId = keyId; + return this; + } + + public Builder keyVersion(String keyVersion) { + this.keyVersion = keyVersion; + return this; + } + + public Builder wrappedDataKey(byte[] wrappedDataKey) { + this.wrappedDataKey = copy(wrappedDataKey); + return this; + } + + public Builder fileCryptoId(byte[] fileCryptoId) { + this.fileCryptoId = copy(fileCryptoId); + return this; + } + + private Builder nextChunkOrdinal(AtomicLong nextChunkOrdinal) { + this.nextChunkOrdinal = nextChunkOrdinal; + return this; + } + + private Builder newFileClaimed(AtomicBoolean newFileClaimed) { + this.newFileClaimed = newFileClaimed; + return this; + } + + public EncryptParameter build() { + return new EncryptParameter(this); + } + } } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptUtils.java b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptUtils.java index 7c8dbcd253..bd303d728a 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptUtils.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptUtils.java @@ -258,8 +258,17 @@ public static EncryptParameter getEncryptParameter() { return encryptParam; } - /** Get the second EncryptParameter object according to the given type and first key. */ + /** + * Returns a page-AEAD file descriptor unchanged, or derives the legacy second key from the given + * type and first key. A null parameter selects unencrypted operation. + */ public static EncryptParameter getEncryptParameter(EncryptParameter param) { + if (param == null) { + return new EncryptParameter("org.apache.tsfile.encrypt.UNENCRYPTED", null); + } + if (param.isTdePageAead()) { + return param; + } return encryptParamCache.computeIfAbsent(param, EncryptUtils::generateEncryptParameter); } @@ -297,6 +306,16 @@ public static IEncrypt getEncrypt() { return getEncrypt(TSFileDescriptor.getInstance().getConfig()); } + public static IEncrypt getEncrypt(EncryptParameter encryptParameter) { + if (encryptParameter == null) { + return getEncrypt("org.apache.tsfile.encrypt.UNENCRYPTED", null); + } + if (encryptParameter.isTdePageAead()) { + return encryptParameter.getOrCreateFileEncrypt(); + } + return getEncrypt(encryptParameter.getType(), encryptParameter.getKey()); + } + public static IEncrypt getEncrypt(String encryptType, byte[] dataEncryptKey) { try { String className = getEncryptClass(encryptType); diff --git a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptionProviderRegistry.java b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptionProviderRegistry.java new file mode 100644 index 0000000000..3634c0827b --- /dev/null +++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptionProviderRegistry.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.encrypt; + +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.i18n.Messages; + +import java.util.Iterator; +import java.util.ServiceConfigurationError; +import java.util.ServiceLoader; +import java.util.concurrent.ConcurrentHashMap; + +/** Registry for file-level encryption providers identified by stable provider ids. */ +public final class EncryptionProviderRegistry { + + private static final ConcurrentHashMap PROVIDERS = + new ConcurrentHashMap<>(); + private static volatile boolean servicesLoaded; + + private EncryptionProviderRegistry() {} + + public static void registerProvider(IEncryptProvider provider) { + String providerId = provider == null ? null : provider.getProviderId(); + if (providerId == null || providerId.trim().isEmpty()) { + throw new EncryptException(Messages.get("error.encrypt.invalid_provider_id")); + } + providerId = providerId.trim(); + IEncryptProvider existing = PROVIDERS.putIfAbsent(providerId, provider); + if (existing != null && !existing.getClass().equals(provider.getClass())) { + throw new EncryptException(Messages.format("error.encrypt.duplicate_provider", providerId)); + } + } + + public static void unregisterProvider(String providerId) { + if (providerId != null) { + PROVIDERS.remove(providerId.trim()); + } + } + + static IEncrypt create(EncryptParameter encryptParameter) { + loadServices(); + String providerId = encryptParameter.getProviderId(); + IEncryptProvider provider = providerId == null ? null : PROVIDERS.get(providerId.trim()); + if (provider == null) { + throw new EncryptException(Messages.format("error.encrypt.provider_not_found", providerId)); + } + IEncrypt encrypt = provider.create(encryptParameter); + if (encrypt == null) { + throw new EncryptException( + Messages.format("error.encrypt.provider_returned_null", providerId)); + } + try { + return new PageAeadEncryption(encrypt); + } catch (RuntimeException | Error e) { + try { + encrypt.close(); + } catch (RuntimeException | Error closeFailure) { + e.addSuppressed(closeFailure); + } + throw e; + } + } + + private static synchronized void loadServices() { + if (servicesLoaded) { + return; + } + Iterator providers = ServiceLoader.load(IEncryptProvider.class).iterator(); + EncryptException failure = null; + while (true) { + try { + if (!providers.hasNext()) { + break; + } + } catch (ServiceConfigurationError e) { + if (failure == null) { + failure = + new EncryptException(Messages.get("error.encrypt.provider_discovery_failed"), e); + } else { + failure.addSuppressed(e); + } + break; + } + try { + registerProvider(providers.next()); + } catch (ServiceConfigurationError | RuntimeException e) { + if (failure == null) { + failure = + new EncryptException(Messages.get("error.encrypt.provider_discovery_failed"), e); + } else { + failure.addSuppressed(e); + } + } + } + if (failure != null) { + throw failure; + } + servicesLoaded = true; + } +} diff --git a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IDecryptor.java b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IDecryptor.java index de7735daee..1d31d3fc8e 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IDecryptor.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IDecryptor.java @@ -57,6 +57,12 @@ static IDecryptor getDecryptor(String type, byte[] key) { } static IDecryptor getDecryptor(EncryptParameter encryptParam) { + if (encryptParam == null) { + return getDecryptor("org.apache.tsfile.encrypt.UNENCRYPTED", null); + } + if (encryptParam.isTdePageAead()) { + return EncryptUtils.getEncrypt(encryptParam).getDecryptor(); + } String type = encryptParam.getType(); byte[] key = encryptParam.getKey(); return getDecryptor(type, key); @@ -66,5 +72,11 @@ static IDecryptor getDecryptor(EncryptParameter encryptParam) { byte[] decrypt(byte[] data, int offset, int size); + default byte[] decryptPage( + byte[] data, int offset, int size, PageCryptoContext pageCryptoContext) { + throw new EncryptException( + Messages.format("error.encrypt.page_aead_decrypt_unsupported", getClass().getName())); + } + EncryptionType getEncryptionType(); } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncrypt.java b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncrypt.java index 0ea6bbef7e..66d33d014d 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncrypt.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncrypt.java @@ -18,9 +18,13 @@ */ package org.apache.tsfile.encrypt; +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.i18n.Messages; + +import java.nio.ByteBuffer; import java.util.concurrent.ConcurrentHashMap; -public interface IEncrypt { +public interface IEncrypt extends AutoCloseable { static ConcurrentHashMap> encryptMap = new ConcurrentHashMap<>(); @@ -30,4 +34,64 @@ public interface IEncrypt { IDecryptor getDecryptor(); IEncryptor getEncryptor(); + + /** + * Returns the fixed number of bytes added to every encrypted page body by this profile. + * + *

Page AEAD providers must override this method. The value is part of the profile contract and + * must not change while files using that profile exist. + */ + default int getPageBodyOverhead() { + return -1; + } + + /** Returns the exact encoded size, including profile-specific framing and padding. */ + default int getEncryptedPageBodySize(int plaintextSize) { + int overhead = getPageBodyOverhead(); + if (plaintextSize < 0 || overhead <= 0) { + throw new EncryptException(Messages.get("error.encrypt.invalid_page_body_lengths")); + } + return Math.addExact(plaintextSize, overhead); + } + + /** + * Returns the candidate compressed plaintext size from a bounded, read-only encoded body. This is + * untrusted framing until decryptPage authenticates it together with the page context. + * Implementations must not decrypt, allocate based on this value, or modify the input. + */ + default int getPagePlaintextSize(ByteBuffer encryptedBody) { + int overhead = getPageBodyOverhead(); + if (overhead <= 0 || encryptedBody.remaining() < overhead) { + throw new EncryptException(Messages.get("error.encrypt.invalid_page_body_lengths")); + } + return encryptedBody.remaining() - overhead; + } + + /** + * Returns this immutable profile's chunk-position proof size. Page-AEAD providers must override + * this method; unsupported providers fail closed. The size must be positive and no larger than + * {@link ChunkCryptoContext#MAX_AUTHENTICATION_SIZE}. + */ + default int getChunkAuthenticationSize() { + return -1; + } + + /** + * Returns a fixed-size opaque proof authenticating a chunk's physical position. The proof is + * exactly {@link #getChunkAuthenticationSize()} bytes. Providers using AEAD must use a fresh + * nonce under the file DEK, independently of page encryption calls. + */ + default byte[] authenticateChunk(ChunkCryptoContext context) { + throw new EncryptException( + Messages.format("error.encrypt.chunk_authentication_unsupported", getClass().getName())); + } + + /** Verifies the proof before a chunk identity is used; unsupported providers fail closed. */ + default void verifyChunkAuthentication(byte[] authentication, ChunkCryptoContext context) { + throw new EncryptException( + Messages.format("error.encrypt.chunk_authentication_unsupported", getClass().getName())); + } + + @Override + default void close() {} } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncryptProvider.java b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncryptProvider.java new file mode 100644 index 0000000000..a4a3311f13 --- /dev/null +++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncryptProvider.java @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.encrypt; + +/** + * Service provider interface for file-level encryption implementations. + * + *

Provider ids are persisted in a TsFile and therefore must remain stable across releases. A + * provider implementation may be registered explicitly through {@link EncryptionProviderRegistry} + * or discovered through {@link java.util.ServiceLoader}. Providers own the encryption profiles + * referenced by {@link EncryptParameter#getProfileId()}; a profile's algorithms, page layout, key + * envelope format, and page body overhead must remain immutable while files using it exist. + * + *

The caller must generate an independent DEK and fileCryptoId for every new file. An + * EncryptParameter, including its copies, describes one file, not a reusable encryption template. + * Providers must ensure that nonces are not reused under the same DEK, including after append, + * recovery, or recreation of a runtime context. PageCryptoContext is authenticated context, not a + * guaranteed-unique nonce source. Random-nonce profiles must define per-key usage limits and reject + * use beyond those limits, counting both page encryption and chunk position authentication. + * Version-1 encrypted chunk headers also require the fixed-size opaque position proof defined by + * {@link ChunkCryptoContext}; unsupported providers must reject these operations. + */ +public interface IEncryptProvider { + + String getProviderId(); + + /** + * Creates a file context that may be shared by concurrent chunk readers. Implementations must own + * their runtime key resources independently of the supplied parameter's lifetime. TsFile closes + * the context after its last retained reference is released. + */ + IEncrypt create(EncryptParameter encryptParameter); +} diff --git a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncryptor.java b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncryptor.java index 7d60ecfc45..5da7262116 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncryptor.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncryptor.java @@ -57,6 +57,12 @@ static IEncryptor getEncryptor(String type, byte[] key) { } static IEncryptor getEncryptor(EncryptParameter encryptParam) { + if (encryptParam == null) { + return getEncryptor("org.apache.tsfile.encrypt.UNENCRYPTED", null); + } + if (encryptParam.isTdePageAead()) { + return EncryptUtils.getEncrypt(encryptParam).getEncryptor(); + } String type = encryptParam.getType(); byte[] key = encryptParam.getKey(); return getEncryptor(type, key); @@ -66,5 +72,11 @@ static IEncryptor getEncryptor(EncryptParameter encryptParam) { byte[] encrypt(byte[] data, int offset, int size); + default byte[] encryptPage( + byte[] data, int offset, int size, PageCryptoContext pageCryptoContext) { + throw new EncryptException( + Messages.format("error.encrypt.page_aead_encrypt_unsupported", getClass().getName())); + } + EncryptionType getEncryptionType(); } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/PageAeadEncryption.java b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/PageAeadEncryption.java new file mode 100644 index 0000000000..c0d1934320 --- /dev/null +++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/PageAeadEncryption.java @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.encrypt; + +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.file.metadata.enums.EncryptionType; +import org.apache.tsfile.i18n.Messages; + +import java.nio.ByteBuffer; +import java.util.concurrent.atomic.AtomicBoolean; + +/** Enforces page-only, fail-closed semantics independently of a provider's legacy methods. */ +final class PageAeadEncryption implements IEncrypt { + private final IEncrypt delegate; + private final IEncryptor encryptor; + private final IDecryptor decryptor; + private final AtomicBoolean closed = new AtomicBoolean(); + + PageAeadEncryption(IEncrypt delegate) { + this.delegate = delegate; + IEncryptor providerEncryptor = delegate.getEncryptor(); + IDecryptor providerDecryptor = delegate.getDecryptor(); + if (providerEncryptor == null || providerDecryptor == null) { + throw new EncryptException(Messages.get("error.encrypt.page_aead_operations_missing")); + } + encryptor = + new IEncryptor() { + @Override + public byte[] encrypt(byte[] data) { + throw legacyOperation(); + } + + @Override + public byte[] encrypt(byte[] data, int offset, int size) { + throw legacyOperation(); + } + + @Override + public byte[] encryptPage(byte[] data, int offset, int size, PageCryptoContext context) { + ensureOpen(); + return providerEncryptor.encryptPage(data, offset, size, context); + } + + @Override + public EncryptionType getEncryptionType() { + return EncryptionType.NewWay; + } + }; + decryptor = + new IDecryptor() { + @Override + public byte[] decrypt(byte[] data) { + throw legacyOperation(); + } + + @Override + public byte[] decrypt(byte[] data, int offset, int size) { + throw legacyOperation(); + } + + @Override + public byte[] decryptPage(byte[] data, int offset, int size, PageCryptoContext context) { + ensureOpen(); + try { + return providerDecryptor.decryptPage(data, offset, size, context); + } catch (EncryptException e) { + throw e; + } catch (RuntimeException e) { + throw new EncryptException( + Messages.get("error.encrypt.page_aead_decryption_failed"), e); + } + } + + @Override + public EncryptionType getEncryptionType() { + return EncryptionType.NewWay; + } + }; + } + + private static EncryptException legacyOperation() { + return new EncryptException(Messages.get("error.encrypt.page_aead_requires_page_context")); + } + + private void ensureOpen() { + if (closed.get()) { + throw new EncryptException(Messages.get("error.encrypt.parameter_destroyed")); + } + } + + @Override + public IDecryptor getDecryptor() { + ensureOpen(); + return decryptor; + } + + @Override + public IEncryptor getEncryptor() { + ensureOpen(); + return encryptor; + } + + @Override + public int getPageBodyOverhead() { + ensureOpen(); + return delegate.getPageBodyOverhead(); + } + + @Override + public int getEncryptedPageBodySize(int plaintextSize) { + ensureOpen(); + return delegate.getEncryptedPageBodySize(plaintextSize); + } + + @Override + public int getPagePlaintextSize(ByteBuffer encryptedBody) { + ensureOpen(); + return delegate.getPagePlaintextSize(encryptedBody.asReadOnlyBuffer()); + } + + @Override + public int getChunkAuthenticationSize() { + ensureOpen(); + int size = delegate.getChunkAuthenticationSize(); + if (size <= 0 || size > ChunkCryptoContext.MAX_AUTHENTICATION_SIZE) { + throw new EncryptException( + Messages.format("error.encrypt.invalid_chunk_authentication_size", size)); + } + return size; + } + + @Override + public byte[] authenticateChunk(ChunkCryptoContext context) { + ensureOpen(); + int expectedSize = getChunkAuthenticationSize(); + byte[] authentication = delegate.authenticateChunk(context); + validateChunkAuthentication(authentication, expectedSize); + return authentication; + } + + @Override + public void verifyChunkAuthentication(byte[] authentication, ChunkCryptoContext context) { + ensureOpen(); + validateChunkAuthentication(authentication, getChunkAuthenticationSize()); + try { + delegate.verifyChunkAuthentication(authentication, context); + } catch (EncryptException e) { + throw e; + } catch (RuntimeException e) { + throw new EncryptException(Messages.get("error.encrypt.chunk_authentication_failed"), e); + } + } + + private static void validateChunkAuthentication(byte[] authentication, int expectedSize) { + if (authentication == null || authentication.length != expectedSize) { + throw new EncryptException(Messages.get("error.encrypt.invalid_chunk_authentication")); + } + } + + @Override + public void close() { + if (closed.compareAndSet(false, true)) { + delegate.close(); + } + } +} diff --git a/java/tsfile/src/main/java/org/apache/tsfile/encrypt/PageCryptoContext.java b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/PageCryptoContext.java new file mode 100644 index 0000000000..3b3628cf30 --- /dev/null +++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/PageCryptoContext.java @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.encrypt; + +import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.i18n.Messages; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +/** + * Canonical context for page-level authenticated encryption. + * + *

Encryption and decryption construct the same associated data from persisted file encryption + * metadata and stable page fields. Providers should pass {@link #getAssociatedData()} to their AEAD + * implementation unchanged. + */ +public final class PageCryptoContext { + + private static final int ASSOCIATED_DATA_VERSION = 1; + private static final byte[] EMPTY_BYTES = new byte[0]; + + private final int uncompressedSize; + private final int compressedPlaintextSize; + private final int encryptedPageBodySize; + private final int pageIndex; + private final byte[] associatedData; + + private PageCryptoContext( + EncryptParameter parameter, + int uncompressedSize, + int compressedPlaintextSize, + int encryptedPageBodySize, + int pageIndex, + long chunkOrdinal) { + validate(parameter, uncompressedSize, compressedPlaintextSize, pageIndex, chunkOrdinal); + this.uncompressedSize = uncompressedSize; + this.compressedPlaintextSize = compressedPlaintextSize; + this.encryptedPageBodySize = encryptedPageBodySize; + this.pageIndex = pageIndex; + this.associatedData = + buildAssociatedData( + parameter, uncompressedSize, compressedPlaintextSize, pageIndex, chunkOrdinal); + } + + public static PageCryptoContext forEncryption( + EncryptParameter parameter, + int uncompressedSize, + int compressedPlaintextSize, + int pageIndex, + long chunkOrdinal) { + validate(parameter, uncompressedSize, compressedPlaintextSize, pageIndex, chunkOrdinal); + int encryptedPageBodySize = parameter.getEncryptedPageBodySize(compressedPlaintextSize); + return new PageCryptoContext( + parameter, + uncompressedSize, + compressedPlaintextSize, + encryptedPageBodySize, + pageIndex, + chunkOrdinal); + } + + public static PageCryptoContext forDecryption( + EncryptParameter parameter, + int uncompressedSize, + int encryptedPageBodySize, + int pageIndex, + long chunkOrdinal) { + validate(parameter, uncompressedSize, 0, pageIndex, chunkOrdinal); + int overhead = parameter.getPageBodyOverhead(); + int compressedPlaintextSize = encryptedPageBodySize - overhead; + if (compressedPlaintextSize < 0) { + throw new EncryptException( + Messages.format("error.encrypt.page_body_too_short", encryptedPageBodySize, overhead)); + } + return new PageCryptoContext( + parameter, + uncompressedSize, + compressedPlaintextSize, + encryptedPageBodySize, + pageIndex, + chunkOrdinal); + } + + public int getUncompressedSize() { + return uncompressedSize; + } + + /** Builds AAD using profile framing; the candidate length is authenticated by decryptPage. */ + public static PageCryptoContext forDecryption( + EncryptParameter parameter, + int uncompressedSize, + ByteBuffer encryptedBody, + int pageIndex, + long chunkOrdinal) { + validate(parameter, uncompressedSize, 0, pageIndex, chunkOrdinal); + int plaintextSize = parameter.getPagePlaintextSize(encryptedBody); + return new PageCryptoContext( + parameter, + uncompressedSize, + plaintextSize, + encryptedBody.remaining(), + pageIndex, + chunkOrdinal); + } + + /** Returns the compressed plaintext size on both encryption and decryption paths. */ + public int getInputSize() { + return compressedPlaintextSize; + } + + public int getCompressedPlaintextSize() { + return compressedPlaintextSize; + } + + public int getEncryptedPageBodySize() { + return encryptedPageBodySize; + } + + public int getPageIndex() { + return pageIndex; + } + + public boolean isFirstPage() { + return pageIndex == 0; + } + + /** Returns a defensive copy so providers cannot mutate the authenticated page identity. */ + public byte[] getAssociatedData() { + return Arrays.copyOf(associatedData, associatedData.length); + } + + private static void validate( + EncryptParameter parameter, + int uncompressedSize, + int compressedPlaintextSize, + int pageIndex, + long chunkOrdinal) { + if (parameter == null || !parameter.isTdePageAead()) { + throw new EncryptException(Messages.get("error.encrypt.page_context_invalid_parameter")); + } + if (!parameter.hasValidFileCryptoId()) { + throw new EncryptException(Messages.get("error.encrypt.page_context_invalid_file_id")); + } + if (chunkOrdinal < 0) { + throw new EncryptException(Messages.get("error.encrypt.page_context_invalid_chunk_ordinal")); + } + if (uncompressedSize < 0 || compressedPlaintextSize < 0 || pageIndex < 0) { + throw new EncryptException( + Messages.format( + "error.encrypt.page_context_invalid_sizes", + uncompressedSize, + compressedPlaintextSize, + pageIndex)); + } + } + + private static byte[] buildAssociatedData( + EncryptParameter parameter, + int uncompressedSize, + int compressedPlaintextSize, + int pageIndex, + long chunkOrdinal) { + ByteBuffer buffer = parameter.newPageAssociatedDataBuffer(Long.BYTES + Integer.BYTES * 3); + buffer.putLong(chunkOrdinal); + buffer.putInt(pageIndex); + buffer.putInt(uncompressedSize); + buffer.putInt(compressedPlaintextSize); + return buffer.array(); + } + + static byte[] buildFileAssociatedDataPrefix(EncryptParameter parameter) { + byte[][] components = + new byte[][] { + bytes(parameter.getProviderId()), + bytes(parameter.getProfileId()), + bytes(parameter.getKeyId()), + bytes(parameter.getKeyVersion()), + parameter.getFileCryptoId() + }; + int size = Integer.BYTES * (1 + components.length); + for (byte[] component : components) { + size = Math.addExact(size, component.length); + } + + ByteBuffer buffer = ByteBuffer.allocate(size); + buffer.putInt(ASSOCIATED_DATA_VERSION); + for (byte[] component : components) { + buffer.putInt(component.length); + buffer.put(component); + } + return buffer.array(); + } + + private static byte[] bytes(String value) { + return value == null ? EMPTY_BYTES : value.getBytes(TSFileConfig.STRING_CHARSET); + } +} diff --git a/java/tsfile/src/main/java/org/apache/tsfile/exception/encrypt/EncryptException.java b/java/tsfile/src/main/java/org/apache/tsfile/exception/encrypt/EncryptException.java index 1c540ede01..e099a1c37b 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/exception/encrypt/EncryptException.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/exception/encrypt/EncryptException.java @@ -20,6 +20,16 @@ package org.apache.tsfile.exception.encrypt; public class EncryptException extends RuntimeException { + /** Crypto failures are fatal even when a read API historically tolerates ordinary I/O errors. */ + public static boolean isEncryptionFailure(Throwable failure) { + for (Throwable cause = failure; cause != null; cause = cause.getCause()) { + if (cause instanceof EncryptException) { + return true; + } + } + return false; + } + public EncryptException(String message) { super("Encrypt problem: " + message); } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/file/MetaMarker.java b/java/tsfile/src/main/java/org/apache/tsfile/file/MetaMarker.java index 5cca705945..ce8bf8b379 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/file/MetaMarker.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/file/MetaMarker.java @@ -51,6 +51,9 @@ public class MetaMarker { /** Chunk header marker and this chunk has only one page. */ public static final byte ONLY_ONE_PAGE_CHUNK_HEADER = 5; + /** Optional file encryption header immediately following the TsFile version byte. */ + public static final byte ENCRYPTION_HEADER = 6; + /** Time Chunk header marker and this chunk has more than one page. */ public static final byte TIME_CHUNK_HEADER = (byte) (CHUNK_HEADER | TsFileConstant.TIME_COLUMN_MASK); diff --git a/java/tsfile/src/main/java/org/apache/tsfile/file/header/ChunkHeader.java b/java/tsfile/src/main/java/org/apache/tsfile/file/header/ChunkHeader.java index a03209fdca..a862a670ed 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/file/header/ChunkHeader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/file/header/ChunkHeader.java @@ -22,11 +22,14 @@ import org.apache.tsfile.common.conf.TSFileConfig; import org.apache.tsfile.encoding.decoder.Decoder; import org.apache.tsfile.encoding.decoder.DecoderWrapper; +import org.apache.tsfile.encrypt.ChunkCryptoContext; import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.exception.encrypt.EncryptException; import org.apache.tsfile.file.MetaMarker; import org.apache.tsfile.file.metadata.TimeseriesMetadata; import org.apache.tsfile.file.metadata.enums.CompressionType; import org.apache.tsfile.file.metadata.enums.TSEncoding; +import org.apache.tsfile.i18n.Messages; import org.apache.tsfile.read.TsFileSequenceReader; import org.apache.tsfile.read.reader.TsFileInput; import org.apache.tsfile.utils.Pair; @@ -34,10 +37,13 @@ import org.apache.tsfile.utils.ReadWriteForEncodingUtils; import org.apache.tsfile.utils.ReadWriteIOUtils; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; +import java.util.Arrays; import java.util.function.Function; import java.util.function.LongConsumer; @@ -63,6 +69,8 @@ public class ChunkHeader { private final TSDataType dataType; private final CompressionType compressionType; private final TSEncoding encodingType; + private final long chunkOrdinal; + private byte[] chunkAuthentication; // the following fields do not need to be serialized. private int numOfPages; @@ -87,16 +95,29 @@ public ChunkHeader( TSEncoding encoding, int numOfPages, int mask) { + this(measurementID, dataSize, dataType, compressionType, encoding, numOfPages, mask, -1); + } + + public ChunkHeader( + String measurementID, + int dataSize, + TSDataType dataType, + CompressionType compressionType, + TSEncoding encoding, + int numOfPages, + int mask, + long chunkOrdinal) { this( (byte) ((numOfPages <= 1 ? MetaMarker.ONLY_ONE_PAGE_CHUNK_HEADER : MetaMarker.CHUNK_HEADER) | (byte) mask), measurementID, dataSize, - getSerializedSize(measurementID, dataSize), + getSerializedPrefixSize(measurementID, dataSize, chunkOrdinal >= 0), dataType, compressionType, - encoding); + encoding, + chunkOrdinal); this.numOfPages = numOfPages; } @@ -117,6 +138,25 @@ public ChunkHeader( encoding); } + public ChunkHeader( + byte chunkType, + String measurementID, + int dataSize, + TSDataType dataType, + CompressionType compressionType, + TSEncoding encoding, + long chunkOrdinal) { + this( + chunkType, + measurementID, + dataSize, + getSerializedPrefixSize(measurementID, dataSize, chunkOrdinal >= 0), + dataType, + compressionType, + encoding, + chunkOrdinal); + } + public ChunkHeader( byte chunkType, String measurementID, @@ -125,12 +165,25 @@ public ChunkHeader( TSDataType dataType, CompressionType compressionType, TSEncoding encoding) { + this(chunkType, measurementID, dataSize, headerSize, dataType, compressionType, encoding, -1); + } + + private ChunkHeader( + byte chunkType, + String measurementID, + int dataSize, + int headerSize, + TSDataType dataType, + CompressionType compressionType, + TSEncoding encoding, + long chunkOrdinal) { this.chunkType = chunkType; this.measurementID = measurementID; this.dataSize = dataSize; this.dataType = dataType; this.compressionType = compressionType; this.encodingType = encoding; + this.chunkOrdinal = chunkOrdinal; this.serializedSize = headerSize; } @@ -147,6 +200,19 @@ public static int getSerializedSize(String measurementID, int dataSize) { + TSEncoding.getSerializedSize(); // encodingType } + /** Exact size with a profile-defined proof; zero selects the plaintext header format. */ + public static int getSerializedSize(String measurementID, int dataSize, int authenticationSize) { + if (authenticationSize != 0) { + validateAuthenticationSize(authenticationSize, 0); + } + return getSerializedPrefixSize(measurementID, dataSize, authenticationSize != 0) + + authenticationSize; + } + + private static int getSerializedPrefixSize(String measurementID, int dataSize, boolean pageAead) { + return getSerializedSize(measurementID, dataSize) + (pageAead ? Long.BYTES + Integer.BYTES : 0); + } + /** * The estimated serialized size of chunk header. Only used when we don't know the actual dataSize * attribute @@ -165,7 +231,10 @@ public static int getSerializedSize(String measurementID) { } public int getSerializedSize() { - return serializedSize; + if (chunkOrdinal >= 0 && chunkAuthentication == null) { + throw new EncryptException(Messages.get("error.encrypt.invalid_chunk_authentication")); + } + return serializedSize + getChunkAuthenticationSize(); } /** @@ -176,13 +245,59 @@ public int getSerializedSize() { */ public static ChunkHeader deserializeFrom(InputStream inputStream, byte chunkType) throws IOException { + return deserializeFrom(inputStream, chunkType, false); + } + + public static ChunkHeader deserializeFrom( + InputStream inputStream, byte chunkType, boolean pageAead) throws IOException { + return deserializeFrom(inputStream, chunkType, pageAead, 0); + } + + /** Reads and bounds-checks the proof length against the file's provider profile. */ + public static ChunkHeader deserializeFrom( + InputStream inputStream, byte chunkType, int authenticationSize) throws IOException { + if (authenticationSize != 0) { + validateAuthenticationSize(authenticationSize, 0); + } + return deserializeFrom(inputStream, chunkType, authenticationSize != 0, authenticationSize); + } + + private static ChunkHeader deserializeFrom( + InputStream inputStream, byte chunkType, boolean pageAead, int expectedAuthenticationSize) + throws IOException { // read measurementID String measurementID = ReadWriteIOUtils.readVarIntString(inputStream); int dataSize = ReadWriteForEncodingUtils.readUnsignedVarInt(inputStream); TSDataType dataType = ReadWriteIOUtils.readDataType(inputStream); CompressionType type = ReadWriteIOUtils.readCompressionType(inputStream); TSEncoding encoding = ReadWriteIOUtils.readEncoding(inputStream); - return new ChunkHeader(chunkType, measurementID, dataSize, dataType, type, encoding); + long chunkOrdinal = -1; + byte[] authentication = null; + if (pageAead) { + chunkOrdinal = ReadWriteIOUtils.readLong(inputStream); + byte[] sizeBytes = inputStream.readNBytes(Integer.BYTES); + if (sizeBytes.length != Integer.BYTES) { + throw new EOFException(); + } + int size = ByteBuffer.wrap(sizeBytes).getInt(); + validateAuthenticationSize(size, expectedAuthenticationSize); + authentication = inputStream.readNBytes(size); + if (authentication.length != size) { + throw new EOFException(); + } + } + ChunkHeader header = + new ChunkHeader( + chunkType, + measurementID, + dataSize, + getSerializedPrefixSize(measurementID, dataSize, pageAead), + dataType, + type, + encoding, + chunkOrdinal); + header.chunkAuthentication = authentication; + return header; } /** @@ -194,7 +309,7 @@ public static ChunkHeader deserializeFrom(InputStream inputStream, byte chunkTyp * @throws IOException IOException */ public static ChunkHeader deserializeFrom(TsFileInput input, long offset) throws IOException { - return deserializeFrom(input, offset, null); + return deserializeFrom(input, offset, null, false); } /** @@ -208,43 +323,128 @@ public static ChunkHeader deserializeFrom(TsFileInput input, long offset) throws */ public static ChunkHeader deserializeFrom( TsFileInput input, long offset, LongConsumer ioSizeRecorder) throws IOException { + return deserializeFrom(input, offset, ioSizeRecorder, false); + } + + public static ChunkHeader deserializeFrom( + TsFileInput input, long offset, LongConsumer ioSizeRecorder, boolean pageAead) + throws IOException { + return deserializeFrom(input, offset, ioSizeRecorder, pageAead, 0); + } - // only 6 bytes, no need to call ioSizeRecorder.accept alone, combine into the remaining read - // operation - ByteBuffer buffer = ByteBuffer.allocate(Byte.BYTES + Integer.BYTES + 1); - input.read(buffer, offset); - buffer.flip(); - - // read chunk header from input to buffer - byte chunkType = buffer.get(); - int strLength = ReadWriteForEncodingUtils.readVarInt(buffer); - int alreadyReadLength = buffer.position(); - - int remainingBytes = - strLength - + Integer.BYTES - + 1 // uVarInt dataSize - + TSDataType.getSerializedSize() // dataType - + CompressionType.getSerializedSize() // compressionType - + TSEncoding.getSerializedSize(); - buffer = ByteBuffer.allocate(remainingBytes); - - if (ioSizeRecorder != null) { - ioSizeRecorder.accept((long) alreadyReadLength + remainingBytes); + /** Positional counterpart with profile validation before reading the proof payload. */ + public static ChunkHeader deserializeFrom( + TsFileInput input, long offset, LongConsumer ioSizeRecorder, int authenticationSize) + throws IOException { + if (authenticationSize != 0) { + validateAuthenticationSize(authenticationSize, 0); } + return deserializeFrom( + input, offset, ioSizeRecorder, authenticationSize != 0, authenticationSize); + } - input.read(buffer, offset + alreadyReadLength); - buffer.flip(); + private static ChunkHeader deserializeFrom( + TsFileInput input, + long offset, + LongConsumer ioSizeRecorder, + boolean pageAead, + int expectedAuthenticationSize) + throws IOException { + try { + + // only 6 bytes, no need to call ioSizeRecorder.accept alone, combine into the remaining read + // operation + ByteBuffer buffer = ByteBuffer.allocate(Byte.BYTES + Integer.BYTES + 1); + readAtMost(input, offset, buffer); + buffer.flip(); + + // read chunk header from input to buffer + byte chunkType = buffer.get(); + int strLength = ReadWriteForEncodingUtils.readVarInt(buffer); + int alreadyReadLength = buffer.position(); + + int remainingBytes = + strLength + + Integer.BYTES + + 1 // uVarInt dataSize + + TSDataType.getSerializedSize() // dataType + + CompressionType.getSerializedSize() // compressionType + + TSEncoding.getSerializedSize() + + (pageAead ? Long.BYTES + Integer.BYTES : 0); + buffer = ByteBuffer.allocate(remainingBytes); + + if (ioSizeRecorder != null) { + ioSizeRecorder.accept((long) alreadyReadLength + remainingBytes); + } + + readAtMost(input, offset + alreadyReadLength, buffer); + buffer.flip(); + + // read measurementID + String measurementID = ReadWriteIOUtils.readStringWithLength(buffer, strLength); + int dataSize = ReadWriteForEncodingUtils.readUnsignedVarInt(buffer); + TSDataType dataType = ReadWriteIOUtils.readDataType(buffer); + CompressionType type = ReadWriteIOUtils.readCompressionType(buffer); + TSEncoding encoding = ReadWriteIOUtils.readEncoding(buffer); + long chunkOrdinal = -1; + byte[] authentication = null; + if (pageAead) { + if (buffer.remaining() < Long.BYTES + Integer.BYTES) { + throw new EOFException(); + } + chunkOrdinal = buffer.getLong(); + int size = buffer.getInt(); + validateAuthenticationSize(size, expectedAuthenticationSize); + authentication = new byte[size]; + } + int chunkHeaderSize = alreadyReadLength + buffer.position(); + if (authentication != null) { + ByteBuffer proof = ByteBuffer.wrap(authentication); + int bufferedBytes = Math.min(buffer.remaining(), proof.remaining()); + proof.put(buffer.array(), buffer.position(), bufferedBytes); + if (ioSizeRecorder != null) { + ioSizeRecorder.accept(proof.remaining()); + } + readAtMost(input, offset + chunkHeaderSize + bufferedBytes, proof); + if (proof.hasRemaining()) { + throw new EOFException(); + } + } + ChunkHeader header = + new ChunkHeader( + chunkType, + measurementID, + dataSize, + chunkHeaderSize, + dataType, + type, + encoding, + chunkOrdinal); + header.chunkAuthentication = authentication; + return header; + } catch (BufferUnderflowException e) { + throw new EOFException(); + } + } - // read measurementID - String measurementID = ReadWriteIOUtils.readStringWithLength(buffer, strLength); - int dataSize = ReadWriteForEncodingUtils.readUnsignedVarInt(buffer); - TSDataType dataType = ReadWriteIOUtils.readDataType(buffer); - CompressionType type = ReadWriteIOUtils.readCompressionType(buffer); - TSEncoding encoding = ReadWriteIOUtils.readEncoding(buffer); - int chunkHeaderSize = alreadyReadLength + buffer.position(); - return new ChunkHeader( - chunkType, measurementID, dataSize, chunkHeaderSize, dataType, type, encoding); + private static void readAtMost(TsFileInput input, long offset, ByteBuffer target) + throws IOException { + int initialPosition = target.position(); + while (target.hasRemaining()) { + int count = input.read(target, offset + target.position() - initialPosition); + if (count <= 0) { + break; + } + } + } + + private static void validateAuthenticationSize(int size, int expectedSize) { + if (size <= 0 + || size > ChunkCryptoContext.MAX_AUTHENTICATION_SIZE + || (expectedSize > 0 && size != expectedSize)) { + throw new EncryptException( + Messages.format("error.encrypt.invalid_chunk_authentication_size", size)); + } } /** @@ -269,6 +469,20 @@ public String getMeasurementID() { return measurementID; } + public long getChunkOrdinal() { + return chunkOrdinal; + } + + public byte[] getChunkAuthentication() { + return chunkAuthentication == null + ? null + : Arrays.copyOf(chunkAuthentication, chunkAuthentication.length); + } + + public int getChunkAuthenticationSize() { + return chunkAuthentication == null ? 0 : chunkAuthentication.length; + } + public void setMeasurementID(String measurementID) { this.measurementID = measurementID; } @@ -305,6 +519,12 @@ public void setReplaceDecoder(Function replaceDecoder) * @throws IOException IOException */ public int serializeTo(OutputStream outputStream) throws IOException { + return serializeTo(outputStream, chunkAuthentication); + } + + /** Writes a proof generated for the destination position without mutating the source header. */ + public int serializeTo(OutputStream outputStream, byte[] authentication) throws IOException { + validateChunkAuthentication(authentication); int length = 0; length += ReadWriteIOUtils.write(chunkType, outputStream); length += ReadWriteIOUtils.writeVar(measurementID, outputStream); @@ -312,6 +532,12 @@ public int serializeTo(OutputStream outputStream) throws IOException { length += ReadWriteIOUtils.write(dataType, outputStream); length += ReadWriteIOUtils.write(compressionType, outputStream); length += ReadWriteIOUtils.write(encodingType, outputStream); + if (chunkOrdinal >= 0) { + length += ReadWriteIOUtils.write(chunkOrdinal, outputStream); + length += ReadWriteIOUtils.write(authentication.length, outputStream); + outputStream.write(authentication); + length += authentication.length; + } return length; } @@ -322,6 +548,7 @@ public int serializeTo(OutputStream outputStream) throws IOException { * @return length */ public int serializeTo(ByteBuffer buffer) { + validateChunkAuthentication(chunkAuthentication); int length = 0; length += ReadWriteIOUtils.write(chunkType, buffer); length += ReadWriteIOUtils.writeVar(measurementID, buffer); @@ -329,9 +556,25 @@ public int serializeTo(ByteBuffer buffer) { length += ReadWriteIOUtils.write(dataType, buffer); length += ReadWriteIOUtils.write(compressionType, buffer); length += ReadWriteIOUtils.write(encodingType, buffer); + if (chunkOrdinal >= 0) { + length += ReadWriteIOUtils.write(chunkOrdinal, buffer); + length += ReadWriteIOUtils.write(chunkAuthentication.length, buffer); + buffer.put(chunkAuthentication); + length += chunkAuthentication.length; + } return length; } + private void validateChunkAuthentication(byte[] authentication) { + if ((chunkOrdinal >= 0 + && (authentication == null + || authentication.length <= 0 + || authentication.length > ChunkCryptoContext.MAX_AUTHENTICATION_SIZE)) + || (chunkOrdinal < 0 && authentication != null)) { + throw new EncryptException(Messages.get("error.encrypt.invalid_chunk_authentication")); + } + } + public int getNumOfPages() { return numOfPages; } @@ -362,6 +605,8 @@ public String toString() { + numOfPages + ", serializedSize=" + serializedSize + + ", chunkOrdinal=" + + chunkOrdinal + '}'; } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/file/header/FileEncryptionHeader.java b/java/tsfile/src/main/java/org/apache/tsfile/file/header/FileEncryptionHeader.java new file mode 100644 index 0000000000..bd95056a83 --- /dev/null +++ b/java/tsfile/src/main/java/org/apache/tsfile/file/header/FileEncryptionHeader.java @@ -0,0 +1,194 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.file.header; + +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.file.MetaMarker; +import org.apache.tsfile.i18n.Messages; +import org.apache.tsfile.utils.PublicBAOS; +import org.apache.tsfile.utils.ReadWriteForEncodingUtils; +import org.apache.tsfile.utils.ReadWriteIOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * Versioned file encryption metadata stored before the first TsFile data marker. + * + *

The header contains only public identifiers and a wrapped data key. Plaintext key material and + * runtime implementation class names are never serialized. + */ +public final class FileEncryptionHeader { + + public static final byte VERSION = 1; + public static final int MAX_HEADER_SIZE = 1024 * 1024; + + private FileEncryptionHeader() {} + + public static int serialize(EncryptParameter parameter, OutputStream outputStream) + throws IOException { + validate(parameter); + + byte[] fileCryptoId = parameter.getFileCryptoId(); + byte[] wrappedDataKey = parameter.getWrappedDataKey(); + long expectedSize = + (long) Byte.BYTES + + ReadWriteForEncodingUtils.varIntStringSize(parameter.getProviderId()) + + ReadWriteForEncodingUtils.varIntStringSize(parameter.getProfileId()) + + ReadWriteForEncodingUtils.varIntStringSize(parameter.getKeyId()) + + ReadWriteForEncodingUtils.varIntStringSize(parameter.getKeyVersion()) + + ReadWriteForEncodingUtils.uVarIntSize(fileCryptoId.length) + + fileCryptoId.length + + ReadWriteForEncodingUtils.uVarIntSize(wrappedDataKey.length) + + wrappedDataKey.length; + if (expectedSize > MAX_HEADER_SIZE) { + throw new EncryptException( + Messages.format("error.file.encryption_header_invalid_size", expectedSize)); + } + PublicBAOS payload = new PublicBAOS((int) expectedSize); + int payloadSize = 0; + payloadSize += ReadWriteIOUtils.write(VERSION, payload); + payloadSize += ReadWriteIOUtils.writeVar(parameter.getProviderId(), payload); + payloadSize += ReadWriteIOUtils.writeVar(parameter.getProfileId(), payload); + payloadSize += ReadWriteIOUtils.writeVar(parameter.getKeyId(), payload); + payloadSize += ReadWriteIOUtils.writeVar(parameter.getKeyVersion(), payload); + payloadSize += writeBytes(fileCryptoId, payload); + payloadSize += writeBytes(wrappedDataKey, payload); + + if (payloadSize != payload.size() || payloadSize > MAX_HEADER_SIZE) { + throw new EncryptException( + Messages.format("error.file.encryption_header_invalid_size", payloadSize)); + } + + int size = ReadWriteIOUtils.write(MetaMarker.ENCRYPTION_HEADER, outputStream); + size += ReadWriteForEncodingUtils.writeUnsignedVarInt(payloadSize, outputStream); + payload.writeTo(outputStream); + return size + payloadSize; + } + + /** Deserializes a header after its {@link MetaMarker#ENCRYPTION_HEADER} marker was consumed. */ + public static EncryptParameter deserialize(InputStream inputStream) throws IOException { + int payloadSize = ReadWriteForEncodingUtils.readUnsignedVarInt(inputStream); + if (payloadSize <= 0 || payloadSize > MAX_HEADER_SIZE) { + throw new EncryptException( + Messages.format("error.file.encryption_header_invalid_size", payloadSize)); + } + + byte[] payload = readExactly(inputStream, payloadSize); + ByteBuffer buffer = ByteBuffer.wrap(payload); + byte version = buffer.get(); + if (version != VERSION) { + throw new EncryptException( + Messages.format("error.file.encryption_header_unsupported_version", version)); + } + + EncryptParameter parameter; + try { + parameter = + EncryptParameter.pageAeadBuilder() + .providerId(ReadWriteIOUtils.readVarIntString(buffer)) + .profileId(ReadWriteIOUtils.readVarIntString(buffer)) + .keyId(ReadWriteIOUtils.readVarIntString(buffer)) + .keyVersion(ReadWriteIOUtils.readVarIntString(buffer)) + .fileCryptoId(readBytes(buffer)) + .wrappedDataKey(readBytes(buffer)) + .build(); + } catch (RuntimeException e) { + throw new EncryptException(Messages.get("error.file.encryption_header_malformed"), e); + } + if (buffer.hasRemaining()) { + throw new EncryptException(Messages.get("error.file.encryption_header_trailing_bytes")); + } + validate(parameter); + return parameter; + } + + private static int writeBytes(byte[] bytes, OutputStream outputStream) throws IOException { + int size = ReadWriteForEncodingUtils.writeUnsignedVarInt(bytes.length, outputStream); + outputStream.write(bytes); + return size + bytes.length; + } + + private static byte[] readBytes(ByteBuffer buffer) { + int length = ReadWriteForEncodingUtils.readUnsignedVarInt(buffer); + if (length < 0 || length > MAX_HEADER_SIZE || length > buffer.remaining()) { + throw new EncryptException( + Messages.format("error.file.encryption_header_invalid_component_size", length)); + } + byte[] bytes = new byte[length]; + buffer.get(bytes); + return bytes; + } + + private static byte[] readExactly(InputStream inputStream, int size) throws IOException { + byte[] bytes = new byte[size]; + int offset = 0; + while (offset < size) { + int read = inputStream.read(bytes, offset, size - offset); + if (read < 0) { + throw new EncryptException( + Messages.format("error.file.encryption_header_truncated", size, offset)); + } + if (read == 0) { + int value = inputStream.read(); + if (value < 0) { + throw new EncryptException( + Messages.format("error.file.encryption_header_truncated", size, offset)); + } + bytes[offset++] = (byte) value; + } else { + offset += read; + } + } + return bytes; + } + + private static void validate(EncryptParameter parameter) { + if (parameter == null || !parameter.isTdePageAead()) { + invalidField("pageAead"); + } + requireText(parameter.getProviderId(), "providerId"); + requireText(parameter.getProfileId(), "profileId"); + requireText(parameter.getKeyId(), "keyId"); + requireText(parameter.getKeyVersion(), "keyVersion"); + + byte[] fileCryptoId = parameter.getFileCryptoId(); + if (fileCryptoId == null || fileCryptoId.length != EncryptParameter.FILE_CRYPTO_ID_LENGTH) { + invalidField("fileCryptoId"); + } + byte[] wrappedDataKey = parameter.getWrappedDataKey(); + if (wrappedDataKey == null || wrappedDataKey.length == 0) { + invalidField("wrappedDataKey"); + } + } + + private static void requireText(String value, String field) { + if (value == null || value.trim().isEmpty()) { + invalidField(field); + } + } + + private static void invalidField(String field) { + throw new EncryptException( + Messages.format("error.file.encryption_header_invalid_field", field)); + } +} diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileAlignedSeriesReaderIterator.java b/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileAlignedSeriesReaderIterator.java index ed0254a691..c04fe77cbe 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileAlignedSeriesReaderIterator.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileAlignedSeriesReaderIterator.java @@ -65,25 +65,34 @@ public NextAlignedChunkInfo nextReader() throws IOException { long totalSize = 0; long totalPointNum = 0; int notNullChunkNum = 0; - for (IChunkMetadata valueChunkMetadata : valueChunkMetadataList) { - if (valueChunkMetadata == null) { - continue; + try { + for (IChunkMetadata valueChunkMetadata : valueChunkMetadataList) { + if (valueChunkMetadata == null) { + continue; + } + while (!valueChunkMetadata + .getMeasurementUid() + .equals(schemaList.get(schemaIdx).getMeasurementName())) { + schemaIdx++; + } + Chunk chunk = reader.readMemChunk((ChunkMetadata) valueChunkMetadata); + valueChunks[schemaIdx++] = chunk; + notNullChunkNum++; + totalPointNum += ((ChunkMetadata) valueChunkMetadata).getNumOfPoints(); + totalSize += chunk.getHeader().getSerializedSize() + chunk.getHeader().getDataSize(); } - while (!valueChunkMetadata - .getMeasurementUid() - .equals(schemaList.get(schemaIdx).getMeasurementName())) { - schemaIdx++; + AlignedChunkReader chunkReader = + new AlignedChunkReader(timeChunk, Arrays.asList(valueChunks)); + return new NextAlignedChunkInfo(chunkReader, totalSize, notNullChunkNum, totalPointNum); + } catch (IOException | RuntimeException | Error e) { + timeChunk.close(); + for (Chunk valueChunk : valueChunks) { + if (valueChunk != null) { + valueChunk.close(); + } } - Chunk chunk = reader.readMemChunk((ChunkMetadata) valueChunkMetadata); - valueChunks[schemaIdx++] = chunk; - notNullChunkNum++; - totalPointNum += ((ChunkMetadata) valueChunkMetadata).getNumOfPoints(); - totalSize += chunk.getHeader().getSerializedSize() + chunk.getHeader().getDataSize(); + throw e; } - - AlignedChunkReader chunkReader = new AlignedChunkReader(timeChunk, Arrays.asList(valueChunks)); - - return new NextAlignedChunkInfo(chunkReader, totalSize, notNullChunkNum, totalPointNum); } public class NextAlignedChunkInfo { diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileReader.java index f333afed6b..5e216b7bc4 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileReader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileReader.java @@ -34,8 +34,11 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; public class TsFileReader implements AutoCloseable { @@ -44,6 +47,7 @@ public class TsFileReader implements AutoCloseable { private IMetadataQuerier metadataQuerier; private IChunkLoader chunkLoader; private TsFileExecutor tsfileExecutor; + private final Set queryDataSets = Collections.synchronizedSet(new HashSet<>()); @TsFileApi public TsFileReader(File file) throws IOException { @@ -59,13 +63,24 @@ public TsFileReader(TsFileSequenceReader fileReader) throws IOException { } public QueryDataSet query(QueryExpression queryExpression) throws IOException { - return tsfileExecutor.execute(queryExpression); + return retainQuery(tsfileExecutor.execute(queryExpression)); } public QueryDataSet query( QueryExpression queryExpression, long partitionStartOffset, long partitionEndOffset) throws IOException { - return tsfileExecutor.execute(queryExpression, partitionStartOffset, partitionEndOffset); + return retainQuery( + tsfileExecutor.execute(queryExpression, partitionStartOffset, partitionEndOffset)); + } + + private QueryDataSet retainQuery(QueryDataSet dataSet) throws IOException { + if (fileReader.hasFileEncryptionHeader()) { + synchronized (queryDataSets) { + queryDataSets.removeIf(QueryDataSet::isClosed); + queryDataSets.add(dataSet); + } + } + return dataSet; } public List getAllDeviceIds() throws IOException { @@ -87,6 +102,35 @@ public List getMeasurement(IDeviceID deviceID) throws IOExcep @Override @TsFileApi public void close() throws IOException { - fileReader.close(); + IOException failure = null; + try { + synchronized (queryDataSets) { + for (QueryDataSet dataSet : queryDataSets) { + try { + dataSet.close(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + } + } + } finally { + queryDataSets.clear(); + try { + fileReader.close(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + } + if (failure != null) { + throw failure; + } } } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileSequenceReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileSequenceReader.java index 2eea4089fd..a32f783211 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileSequenceReader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileSequenceReader.java @@ -30,16 +30,19 @@ import org.apache.tsfile.encrypt.EncryptParameter; import org.apache.tsfile.encrypt.EncryptUtils; import org.apache.tsfile.encrypt.IDecryptor; +import org.apache.tsfile.encrypt.PageCryptoContext; import org.apache.tsfile.enums.TSDataType; import org.apache.tsfile.exception.NotCompatibleTsFileException; import org.apache.tsfile.exception.StopReadTsFileByInterruptException; import org.apache.tsfile.exception.TsFileRuntimeException; import org.apache.tsfile.exception.TsFileStatisticsMistakesException; +import org.apache.tsfile.exception.encrypt.EncryptException; import org.apache.tsfile.exception.read.FileVersionTooOldException; import org.apache.tsfile.file.IMetadataIndexEntry; import org.apache.tsfile.file.MetaMarker; import org.apache.tsfile.file.header.ChunkGroupHeader; import org.apache.tsfile.file.header.ChunkHeader; +import org.apache.tsfile.file.header.FileEncryptionHeader; import org.apache.tsfile.file.header.PageHeader; import org.apache.tsfile.file.metadata.AbstractAlignedChunkMetadata; import org.apache.tsfile.file.metadata.AbstractAlignedTimeSeriesMetadata; @@ -85,6 +88,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.EOFException; import java.io.File; import java.io.IOException; import java.io.Serializable; @@ -114,6 +118,9 @@ public class TsFileSequenceReader implements AutoCloseable { + private static final int BASE_FILE_HEADER_SIZE = + TSFileConfig.MAGIC_STRING.getBytes(TSFileConfig.STRING_CHARSET).length + Byte.BYTES; + private static final Logger logger = LoggerFactory.getLogger(TsFileSequenceReader.class); private static final Logger resourceLogger = LoggerFactory.getLogger("FileMonitor"); protected static final TSFileConfig config = TSFileDescriptor.getInstance().getConfig(); @@ -147,6 +154,13 @@ public class TsFileSequenceReader implements AutoCloseable { private EncryptParameter dataEncryptParam = null; + private EncryptParameter fileEncryptionParam; + private long maxChunkOrdinal = -1; + + private long dataStartOffset = BASE_FILE_HEADER_SIZE; + private boolean fileEncryptionHeaderLoaded; + private int firstDataMarker = -1; + /** * Create a file reader of the given file. The reader will read the tail of the file to get the * file metadata size.Then the reader will skip the first @@ -168,7 +182,7 @@ public Map countChunksPerChunkGroup() throws IOException { return result; } - int headerLength = TSFileConfig.MAGIC_STRING.getBytes().length + Byte.BYTES; + long headerLength = getDataStartOffset(); if (checkFile.length() < headerLength) { return result; } @@ -315,6 +329,7 @@ public TsFileSequenceReader(String file, boolean loadMetadataSize, LongConsumer try { loadFileVersion(ioSizeRecorder); + ensureFileEncryptionHeaderLoaded(ioSizeRecorder); if (loadMetadataSize) { loadMetadataSize(ioSizeRecorder); } @@ -379,6 +394,7 @@ public TsFileSequenceReader(TsFileInput input, boolean loadMetadataSize) throws this.tsFileInput = input; this.file = input.getFilePath(); try { + ensureFileEncryptionHeaderLoaded(null); if (loadMetadataSize) { // NOTE no autoRepair here loadMetadataSize(); } @@ -403,6 +419,7 @@ public TsFileSequenceReader( try { loadFileVersion(ioSizeRecorder); + ensureFileEncryptionHeaderLoaded(ioSizeRecorder); if (loadMetadataSize) { loadMetadataSize(ioSizeRecorder); } @@ -430,6 +447,7 @@ public TsFileSequenceReader( */ public TsFileSequenceReader(TsFileInput input, long fileMetadataPos, int fileMetadataSize) { this.tsFileInput = input; + this.file = input.getFilePath(); this.fileMetadataPos = fileMetadataPos; this.fileMetadataSize = fileMetadataSize; } @@ -447,13 +465,14 @@ public TsFileSequenceReader( private void loadFileVersion(LongConsumer ioSizeRecorder) throws IOException { try { tsFileInput.position(TSFileConfig.MAGIC_STRING.getBytes(TSFileConfig.STRING_CHARSET).length); - final ByteBuffer buffer = ByteBuffer.allocate(Byte.BYTES); + final ByteBuffer buffer = ByteBuffer.allocate(2 * Byte.BYTES); + int bytesRead = ReadWriteIOUtils.readAsPossible(tsFileInput, buffer); if (ioSizeRecorder != null) { - ioSizeRecorder.accept(Byte.BYTES); + ioSizeRecorder.accept(bytesRead); } - tsFileInput.read(buffer); buffer.flip(); fileVersion = buffer.get(); + firstDataMarker = buffer.hasRemaining() ? Byte.toUnsignedInt(buffer.get()) : -1; checkFileVersion(); configDeserializer(); @@ -465,6 +484,71 @@ private void loadFileVersion(LongConsumer ioSizeRecorder) throws IOException { } } + private void loadFileEncryptionHeader(LongConsumer ioSizeRecorder) throws IOException { + long savedPosition = tsFileInput.position(); + dataStartOffset = BASE_FILE_HEADER_SIZE; + fileEncryptionParam = null; + try { + if (tsFileInput.size() <= BASE_FILE_HEADER_SIZE) { + return; + } + boolean markerPrefetched = firstDataMarker >= 0; + tsFileInput.position(BASE_FILE_HEADER_SIZE + (markerPrefetched ? Byte.BYTES : 0)); + int marker = markerPrefetched ? firstDataMarker : tsFileInput.wrapAsInputStream().read(); + if (marker < 0) { + throw new EOFException(); + } + if (marker != MetaMarker.ENCRYPTION_HEADER) { + switch ((byte) marker) { + case MetaMarker.CHUNK_GROUP_HEADER, + MetaMarker.CHUNK_HEADER, + MetaMarker.SEPARATOR, + MetaMarker.VERSION, + MetaMarker.OPERATION_INDEX_RANGE, + MetaMarker.ONLY_ONE_PAGE_CHUNK_HEADER, + MetaMarker.TIME_CHUNK_HEADER, + MetaMarker.VALUE_CHUNK_HEADER, + MetaMarker.ONLY_ONE_PAGE_TIME_CHUNK_HEADER, + MetaMarker.ONLY_ONE_PAGE_VALUE_CHUNK_HEADER -> {} + default -> MetaMarker.handleUnexpectedMarker((byte) marker); + } + if (ioSizeRecorder != null && !markerPrefetched) { + ioSizeRecorder.accept(Byte.BYTES); + } + return; + } + + fileEncryptionParam = FileEncryptionHeader.deserialize(tsFileInput.wrapAsInputStream()); + dataStartOffset = tsFileInput.position(); + if (ioSizeRecorder != null) { + ioSizeRecorder.accept( + dataStartOffset - BASE_FILE_HEADER_SIZE - (markerPrefetched ? Byte.BYTES : 0)); + } + } catch (EncryptException e) { + throw new IOException(e.getMessage(), e); + } finally { + tsFileInput.position(savedPosition); + } + } + + public long getDataStartOffset() throws IOException { + ensureFileEncryptionHeaderLoaded(null); + return dataStartOffset; + } + + public boolean hasFileEncryptionHeader() throws IOException { + ensureFileEncryptionHeaderLoaded(null); + return fileEncryptionParam != null; + } + + private synchronized void ensureFileEncryptionHeaderLoaded(LongConsumer ioSizeRecorder) + throws IOException { + if (!fileEncryptionHeaderLoaded) { + loadFileEncryptionHeader(ioSizeRecorder); + fileEncryptionHeaderLoaded = true; + } + } + private void configDeserializer() { if (fileVersion == TSFileConfig.VERSION_NUMBER_V3) { deserializeConfig = CompatibilityUtils.v3DeserializeConfig; @@ -670,8 +754,12 @@ public EncryptParameter getEncryptParam() throws IOException { * @param ioSizeRecorder can be null */ public EncryptParameter getEncryptParam(LongConsumer ioSizeRecorder) throws IOException { + ensureFileEncryptionHeaderLoaded(ioSizeRecorder); if (dataEncryptParam != null) { return dataEncryptParam; + } else if (fileEncryptionParam != null) { + dataEncryptParam = fileEncryptionParam; + return dataEncryptParam; } else { if (fileMetadataSize != 0) { readFileMetadata(ioSizeRecorder); @@ -692,7 +780,10 @@ public EncryptParameter getEncryptParam(LongConsumer ioSizeRecorder) throws IOEx return dataEncryptParam; } } - return EncryptUtils.getEncryptParameter(firstEncryptParam); + return EncryptUtils.getEncryptParameter( + firstEncryptParam != null && firstEncryptParam.isTdePageAead() + ? new EncryptParameter("org.apache.tsfile.encrypt.UNENCRYPTED", null) + : firstEncryptParam); } } @@ -2035,7 +2126,15 @@ public void readPlanIndex() throws IOException { */ public ChunkHeader readChunkHeader(byte chunkType) throws IOException { try { - return ChunkHeader.deserializeFrom(tsFileInput.wrapAsInputStream(), chunkType); + ensureFileEncryptionHeaderLoaded(null); + long chunkOffset = tsFileInput.position() - Byte.BYTES; + ChunkHeader header = + ChunkHeader.deserializeFrom( + tsFileInput.wrapAsInputStream(), + chunkType, + fileEncryptionParam == null ? 0 : fileEncryptionParam.getChunkAuthenticationSize()); + authenticateChunkHeader(header, chunkOffset); + return header; } catch (StopReadTsFileByInterruptException e) { throw e; } catch (Throwable t) { @@ -2054,7 +2153,15 @@ public ChunkHeader readChunkHeader(byte chunkType) throws IOException { private ChunkHeader readChunkHeader(long position, LongConsumer ioSizeRecorder) throws IOException { try { - return ChunkHeader.deserializeFrom(tsFileInput, position, ioSizeRecorder); + ensureFileEncryptionHeaderLoaded(ioSizeRecorder); + ChunkHeader header = + ChunkHeader.deserializeFrom( + tsFileInput, + position, + ioSizeRecorder, + fileEncryptionParam == null ? 0 : fileEncryptionParam.getChunkAuthenticationSize()); + authenticateChunkHeader(header, position); + return header; } catch (StopReadTsFileByInterruptException e) { throw e; } catch (Throwable t) { @@ -2064,6 +2171,27 @@ private ChunkHeader readChunkHeader(long position, LongConsumer ioSizeRecorder) } } + /** Reads and authenticates a chunk header at its absolute marker offset. */ + public ChunkHeader readChunkHeader(long position) throws IOException { + return readChunkHeader(position, null); + } + + private void authenticateChunkHeader(ChunkHeader header, long chunkOffset) throws IOException { + if (fileEncryptionParam != null && header.getChunkOrdinal() < 0) { + throw new org.apache.tsfile.exception.encrypt.EncryptException( + Messages.get("error.write.invalid_chunk_ordinal")); + } + if (fileEncryptionParam != null) { + fileEncryptionParam.verifyChunkAuthentication( + header.getChunkAuthentication(), chunkOffset, header.getChunkOrdinal()); + } + maxChunkOrdinal = Math.max(maxChunkOrdinal, header.getChunkOrdinal()); + } + + public long getMaxChunkOrdinal() { + return maxChunkOrdinal; + } + /** * notice, this function will modify channel's position. * @@ -2235,17 +2363,63 @@ public ByteBuffer readCompressedPage(PageHeader header) throws IOException { } public ByteBuffer readPage(PageHeader header, CompressionType type) throws IOException { + return readPage(header, type, 0); + } + + public ByteBuffer readPage(PageHeader header, CompressionType type, int pageIndex) + throws IOException { + if (fileEncryptionParam != null && fileEncryptionParam.isTdePageAead()) { + throw new IOException(Messages.get("error.encrypt.page_read_requires_chunk_ordinal")); + } + return readPage(header, type, pageIndex, -1); + } + + public ByteBuffer readPage( + PageHeader header, CompressionType type, int pageIndex, long chunkOrdinal) + throws IOException { ByteBuffer buffer = readData(-1, header.getCompressedSize()); - IDecryptor decryptor = IDecryptor.getDecryptor(getEncryptParam()); + EncryptParameter encryptParameter = getEncryptParam(); + IDecryptor decryptor = IDecryptor.getDecryptor(encryptParameter); if (header.getUncompressedSize() == 0) { return buffer; } - ByteBuffer finalBuffer = decrypt(decryptor, buffer); + ByteBuffer finalBuffer = + decrypt(decryptor, encryptParameter, buffer, header, pageIndex, chunkOrdinal); finalBuffer = uncompress(type, finalBuffer, header.getUncompressedSize()); return finalBuffer; } - private static ByteBuffer decrypt(IDecryptor decryptor, ByteBuffer buffer) { + private static ByteBuffer decrypt( + IDecryptor decryptor, + EncryptParameter encryptParameter, + ByteBuffer buffer, + PageHeader header, + int pageIndex, + long chunkOrdinal) + throws IOException { + if (encryptParameter != null && encryptParameter.isTdePageAead()) { + PageCryptoContext pageCryptoContext = + PageCryptoContext.forDecryption( + encryptParameter, + header.getUncompressedSize(), + buffer.asReadOnlyBuffer(), + pageIndex, + chunkOrdinal); + byte[] plaintext = + decryptor.decryptPage( + buffer.array(), + buffer.arrayOffset() + buffer.position(), + buffer.remaining(), + pageCryptoContext); + if (plaintext.length != pageCryptoContext.getCompressedPlaintextSize()) { + throw new IOException( + Messages.format( + "error.encrypt.page_plaintext_size_mismatch", + pageCryptoContext.getCompressedPlaintextSize(), + plaintext.length)); + } + return ByteBuffer.wrap(plaintext); + } if (decryptor == null || decryptor.getEncryptionType() == EncryptionType.UNENCRYPTED) { return buffer; } @@ -2296,7 +2470,16 @@ public void close() throws IOException { if (resourceLogger.isDebugEnabled()) { resourceLogger.debug("{} reader is closed.", file); } - this.tsFileInput.close(); + try { + this.tsFileInput.close(); + } finally { + if (dataEncryptParam != null && dataEncryptParam.isTdePageAead()) { + dataEncryptParam.close(); + } + if (fileEncryptionParam != null && fileEncryptionParam != dataEncryptParam) { + fileEncryptionParam.close(); + } + } } public String getFileName() { @@ -2437,7 +2620,7 @@ public long selfCheck( // ChunkMetadata of current ChunkGroup List chunkMetadataList = new ArrayList<>(); - int headerLength = TSFileConfig.MAGIC_STRING.getBytes().length + Byte.BYTES; + long headerLength = getDataStartOffset(); if (fileSize < headerLength) { return TsFileCheckStatus.INCOMPATIBLE_FILE; } @@ -2524,7 +2707,12 @@ public long selfCheck( Decoder valueDecoder = Decoder.getDecoderByType( chunkHeader.getEncodingType(), chunkHeader.getDataType()); - ByteBuffer pageData = readPage(pageHeader, chunkHeader.getCompressionType()); + ByteBuffer pageData = + readPage( + pageHeader, + chunkHeader.getCompressionType(), + 0, + chunkHeader.getChunkOrdinal()); TSEncoding configuredTimeEncoding = TSEncoding.valueOf(TSFileDescriptor.getInstance().getConfig().getTimeEncoder()); boolean isTimeColumn = @@ -2663,6 +2851,9 @@ public long selfCheck( truncatedSize = this.position() - 1; } } catch (Exception e) { + if (org.apache.tsfile.exception.encrypt.EncryptException.isEncryptionFailure(e)) { + throw new IOException(e.getMessage(), e); + } logger.warn( Messages.get("log.read.sequence_reader_self_check_warn"), file, @@ -2699,7 +2890,7 @@ public long selfCheckWithInfo( long fileSize = checkFile.length(); logger.info(Messages.get("log.read.sequence_reader_file_length"), fileSize); - int headerLength = TSFileConfig.MAGIC_STRING.getBytes().length + Byte.BYTES; + long headerLength = getDataStartOffset(); if (fileSize < headerLength) { return TsFileCheckStatus.INCOMPATIBLE_FILE; } @@ -2776,7 +2967,8 @@ public long checkChunkAndPagesStatistics(IChunkMetadata chunkMetadata) throws IO PageHeader pageHeader = this.readPageHeader(chunkHeader.getDataType(), false); Decoder valueDecoder = Decoder.getDecoderByType(chunkHeader.getEncodingType(), chunkHeader.getDataType()); - ByteBuffer pageData = readPage(pageHeader, chunkHeader.getCompressionType()); + ByteBuffer pageData = + readPage(pageHeader, chunkHeader.getCompressionType(), 0, chunkHeader.getChunkOrdinal()); Decoder timeDecoder = Decoder.getDecoderByType( TSEncoding.valueOf(TSFileDescriptor.getInstance().getConfig().getTimeEncoder()), diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/UnClosedTsFileReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/UnClosedTsFileReader.java index 230a2e1643..fcf11c9d6e 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/UnClosedTsFileReader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/UnClosedTsFileReader.java @@ -21,7 +21,6 @@ import org.apache.tsfile.common.conf.TSFileDescriptor; import org.apache.tsfile.encrypt.EncryptParameter; -import org.apache.tsfile.encrypt.EncryptUtils; import org.apache.tsfile.exception.NotImplementedException; import org.apache.tsfile.file.metadata.TsFileMetadata; @@ -46,7 +45,7 @@ public UnClosedTsFileReader(String file, LongConsumer ioSizeRecorder) throws IOE public UnClosedTsFileReader( String file, EncryptParameter encryptParam, LongConsumer ioSizeRecorder) throws IOException { super(file, false, ioSizeRecorder, encryptParam); - this.dataEncryptParam = EncryptUtils.getEncryptParameter(encryptParam); + this.dataEncryptParam = super.getEncryptParam(); } /** unclosed file has no tail magic data. */ diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/common/Chunk.java b/java/tsfile/src/main/java/org/apache/tsfile/read/common/Chunk.java index d41ad14a32..91e89ec913 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/common/Chunk.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/common/Chunk.java @@ -49,8 +49,11 @@ import static org.apache.tsfile.utils.RamUsageEstimator.sizeOfByteArray; -/** used in query. */ -public class Chunk { +/** + * A materialized chunk owning a page-AEAD reference that survives file reader closure. Close it + * after use, or transfer it to a chunk reader that releases the reference when closed. + */ +public class Chunk implements AutoCloseable { private static final long INSTANCE_SIZE = RamUsageEstimator.shallowSizeOfInstance(Chunk.class) @@ -88,7 +91,7 @@ public Chunk( this.chunkData = buffer; this.deleteIntervalList = deleteIntervalList; this.chunkStatistic = chunkStatistic; - this.encryptParam = encryptParam; + this.encryptParam = retainPageAeadParameter(encryptParam); } public Chunk(ChunkHeader header, ByteBuffer buffer) { @@ -100,7 +103,18 @@ public Chunk(ChunkHeader header, ByteBuffer buffer) { public Chunk(ChunkHeader header, ByteBuffer buffer, EncryptParameter encryptParam) { this.chunkHeader = header; this.chunkData = buffer; - this.encryptParam = encryptParam; + this.encryptParam = retainPageAeadParameter(encryptParam); + } + + private static EncryptParameter retainPageAeadParameter(EncryptParameter parameter) { + return parameter != null && parameter.isTdePageAead() ? parameter.retain() : parameter; + } + + @Override + public void close() { + if (encryptParam != null && encryptParam.isTdePageAead()) { + encryptParam.close(); + } } public EncryptParameter getEncryptParam() { @@ -124,6 +138,10 @@ public void setDeleteIntervalList(List list) { } public void mergeChunkByAppendPage(Chunk chunk) throws IOException { + if ((encryptParam != null && encryptParam.isTdePageAead()) + || (chunk.encryptParam != null && chunk.encryptParam.isTdePageAead())) { + throw new IOException(Messages.get("error.chunk.merge_page_aead_unsupported")); + } int dataSize = 0; // from where the page data of the merged chunk starts, if -1, it means the merged chunk has // more than one page @@ -213,13 +231,20 @@ public Statistics getChunkStatistic() { * and deleteIntervalList are all null in cache */ public long getRetainedSizeInBytes() { - return INSTANCE_SIZE + sizeOfByteArray(chunkData.capacity()); + int authenticationSize = chunkHeader.getChunkAuthenticationSize(); + return INSTANCE_SIZE + + sizeOfByteArray(chunkData.capacity()) + + (authenticationSize == 0 ? 0 : sizeOfByteArray(authenticationSize)); } public Chunk rewrite(TSDataType newType, Chunk timeChunk) throws IOException { if (newType == null || newType == chunkHeader.getDataType()) { return this; } + if (encryptParam != null && encryptParam.isTdePageAead()) { + encryptParam.resumeAfterChunkOrdinal(chunkHeader.getChunkOrdinal()); + encryptParam.resumeAfterChunkOrdinal(timeChunk.getHeader().getChunkOrdinal()); + } TSEncoding encoding = TSFileDescriptor.getInstance().getConfig().getValueEncoder(newType); IMeasurementSchema schema = new MeasurementSchema( @@ -233,34 +258,35 @@ public Chunk rewrite(TSDataType newType, Chunk timeChunk) throws IOException { encoding, schema.getValueEncoder(), encryptParam); - List valueChunks = new ArrayList<>(); - valueChunks.add(this); - TableChunkReader chunkReader = - new TableChunkReader( + try (Chunk readTimeChunk = new Chunk( timeChunk.getHeader(), timeChunk.getData(), null, timeChunk.getChunkStatistic(), - timeChunk.getEncryptParam()), - valueChunks, - null); - List pages = chunkReader.loadPageReaderList(); - for (IPageReader page : pages) { - IPointReader pointReader = page.getAllSatisfiedPageData().getBatchDataIterator(); - while (pointReader.hasNextTimeValuePair()) { - TimeValuePair point = pointReader.nextTimeValuePair(); - Object convertedValue = null; - if (point.getValue().getVector()[0] != null) { - convertedValue = - newType.castFromSingleValue( - chunkHeader.getDataType(), point.getValue().getVector()[0].getValue()); + timeChunk.getEncryptParam()); + Chunk readValueChunk = + new Chunk(chunkHeader, chunkData, deleteIntervalList, chunkStatistic, encryptParam)) { + List valueChunks = new ArrayList<>(); + valueChunks.add(readValueChunk); + TableChunkReader chunkReader = new TableChunkReader(readTimeChunk, valueChunks, null); + List pages = chunkReader.loadPageReaderList(); + for (IPageReader page : pages) { + IPointReader pointReader = page.getAllSatisfiedPageData().getBatchDataIterator(); + while (pointReader.hasNextTimeValuePair()) { + TimeValuePair point = pointReader.nextTimeValuePair(); + Object convertedValue = null; + if (point.getValue().getVector()[0] != null) { + convertedValue = + newType.castFromSingleValue( + chunkHeader.getDataType(), point.getValue().getVector()[0].getValue()); + } + long timestamp = point.getTimestamp(); + Type.fromTsDataType(newType) + .write(chunkWriter, timestamp, convertedValue, convertedValue == null); } - long timestamp = point.getTimestamp(); - Type.fromTsDataType(newType) - .write(chunkWriter, timestamp, convertedValue, convertedValue == null); + chunkWriter.sealCurrentPage(); } - chunkWriter.sealCurrentPage(); } ByteBuffer newChunkData = chunkWriter.getByteBuffer(); ChunkHeader newChunkHeader = @@ -270,7 +296,8 @@ public Chunk rewrite(TSDataType newType, Chunk timeChunk) throws IOException { newChunkData.capacity(), newType, chunkHeader.getCompressionType(), - encoding); + encoding, + chunkWriter.getChunkOrdinal()); chunkData.flip(); timeChunk.chunkData.flip(); return new Chunk( @@ -285,27 +312,33 @@ public Chunk rewrite(TSDataType newType) throws IOException { if (newType == null || newType == chunkHeader.getDataType()) { return this; } + if (encryptParam != null && encryptParam.isTdePageAead()) { + encryptParam.resumeAfterChunkOrdinal(chunkHeader.getChunkOrdinal()); + } TSEncoding encoding = TSFileDescriptor.getInstance().getConfig().getValueEncoder(newType); IMeasurementSchema schema = new MeasurementSchema( chunkHeader.getMeasurementID(), newType, encoding, chunkHeader.getCompressionType()); ChunkWriterImpl chunkWriter = new ChunkWriterImpl(schema, encryptParam); - ChunkReader chunkReader = new ChunkReader(this); - List pages = chunkReader.loadPageReaderList(); - for (IPageReader page : pages) { - BatchData batchData = page.getAllSatisfiedPageData(); - IPointReader pointReader = batchData.getBatchDataIterator(); - while (pointReader.hasNextTimeValuePair()) { - TimeValuePair point = pointReader.nextTimeValuePair(); - Object convertedValue = - newType.castFromSingleValue(chunkHeader.getDataType(), point.getValue().getValue()); - long timestamp = point.getTimestamp(); - if (convertedValue == null) { - throw new IOException(Messages.format("error.read.chunk_non_aligned_null", timestamp)); + try (Chunk readChunk = + new Chunk(chunkHeader, chunkData, deleteIntervalList, chunkStatistic, encryptParam)) { + ChunkReader chunkReader = new ChunkReader(readChunk); + List pages = chunkReader.loadPageReaderList(); + for (IPageReader page : pages) { + BatchData batchData = page.getAllSatisfiedPageData(); + IPointReader pointReader = batchData.getBatchDataIterator(); + while (pointReader.hasNextTimeValuePair()) { + TimeValuePair point = pointReader.nextTimeValuePair(); + Object convertedValue = + newType.castFromSingleValue(chunkHeader.getDataType(), point.getValue().getValue()); + long timestamp = point.getTimestamp(); + if (convertedValue == null) { + throw new IOException(Messages.format("error.read.chunk_non_aligned_null", timestamp)); + } + Type.fromTsDataType(newType).write(chunkWriter, timestamp, convertedValue); } - Type.fromTsDataType(newType).write(chunkWriter, timestamp, convertedValue); + chunkWriter.sealCurrentPage(); } - chunkWriter.sealCurrentPage(); } ByteBuffer newChunkData = chunkWriter.getByteBuffer(); ChunkHeader newChunkHeader = @@ -317,7 +350,8 @@ public Chunk rewrite(TSDataType newType) throws IOException { newChunkData.capacity(), newType, chunkHeader.getCompressionType(), - encoding); + encoding, + chunkWriter.getChunkOrdinal()); chunkData.flip(); return new Chunk( newChunkHeader, diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/controller/CachedChunkLoaderImpl.java b/java/tsfile/src/main/java/org/apache/tsfile/read/controller/CachedChunkLoaderImpl.java index 92925ea82f..43ef003c8c 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/controller/CachedChunkLoaderImpl.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/controller/CachedChunkLoaderImpl.java @@ -20,6 +20,7 @@ package org.apache.tsfile.read.controller; import org.apache.tsfile.common.cache.LRUCache; +import org.apache.tsfile.encrypt.EncryptParameter; import org.apache.tsfile.file.metadata.ChunkMetadata; import org.apache.tsfile.file.metadata.IChunkMetadata; import org.apache.tsfile.file.metadata.statistics.Statistics; @@ -61,7 +62,9 @@ public CachedChunkLoaderImpl(TsFileSequenceReader fileSequenceReader, int cacheS @Override protected Chunk loadObjectByKey(ChunkCacheKey chunkCacheKey) throws IOException { - return reader.readMemChunk(chunkCacheKey); + try (Chunk chunk = reader.readMemChunk(chunkCacheKey)) { + return new Chunk(chunk.getHeader(), chunk.getData(), (EncryptParameter) null); + } } }; } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/DataSetWithTimeGenerator.java b/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/DataSetWithTimeGenerator.java index 63999a91ad..a5ca445e41 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/DataSetWithTimeGenerator.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/DataSetWithTimeGenerator.java @@ -65,6 +65,41 @@ public boolean hasNextWithoutConstraint() throws IOException { return timeGenerator.hasNext(); } + @Override + public void close() throws IOException { + if (isClosed()) { + return; + } + super.close(); + IOException failure = null; + for (FileSeriesReaderByTimestamp reader : readers) { + if (reader == null) { + continue; + } + try { + reader.close(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + } + try { + timeGenerator.close(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + if (failure != null) { + throw failure; + } + } + @Override public RowRecord nextWithoutConstraint() throws IOException { long timestamp = timeGenerator.next(); diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/DataSetWithoutTimeGenerator.java b/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/DataSetWithoutTimeGenerator.java index 00faed8fdb..d588421fb4 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/DataSetWithoutTimeGenerator.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/DataSetWithoutTimeGenerator.java @@ -60,7 +60,16 @@ public DataSetWithoutTimeGenerator( throws IOException { super(paths, dataTypes); this.readers = readers; - initHeap(); + try { + initHeap(); + } catch (IOException | RuntimeException | Error e) { + try { + close(); + } catch (IOException closeError) { + e.addSuppressed(closeError); + } + throw e; + } } private void initHeap() throws IOException { @@ -93,6 +102,29 @@ public boolean hasNextWithoutConstraint() { return !timeHeap.isEmpty(); } + @Override + public void close() throws IOException { + if (isClosed()) { + return; + } + super.close(); + IOException failure = null; + for (AbstractFileSeriesReader reader : readers) { + try { + reader.closeChunkReader(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + } + if (failure != null) { + throw failure; + } + } + @SuppressWarnings("squid:S3776") // Suppress high Cognitive Complexity warning @Override public RowRecord nextWithoutConstraint() throws IOException { diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/QueryDataSet.java b/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/QueryDataSet.java index 0f50d59ec9..70c0a9c25d 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/QueryDataSet.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/QueryDataSet.java @@ -28,7 +28,8 @@ import java.util.List; import java.util.Set; -public abstract class QueryDataSet { +public abstract class QueryDataSet implements AutoCloseable { + private boolean closed; protected List paths; protected List dataTypes; @@ -122,6 +123,37 @@ public void setWithoutNullColumnsIndex(Set withoutNullColumnsIndex) { } public boolean hasNext() throws IOException { + if (closed) { + return false; + } + boolean hasNext; + try { + hasNext = hasNextRow(); + } catch (IOException | RuntimeException | Error e) { + try { + close(); + } catch (IOException closeError) { + e.addSuppressed(closeError); + } + throw e; + } + if (!hasNext) { + close(); + } + return hasNext; + } + + /** Releases query-owned resources; the underlying file remains owned by its file reader. */ + @Override + public void close() throws IOException { + closed = true; + } + + public final boolean isClosed() { + return closed; + } + + private boolean hasNextRow() throws IOException { // proceed to the OFFSET row by skipping rows while (rowOffset > 0) { if (hasNextWithoutConstraint()) { diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/TreeResultSet.java b/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/TreeResultSet.java index 6321e06226..618424bc02 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/TreeResultSet.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/query/dataset/TreeResultSet.java @@ -21,11 +21,15 @@ import org.apache.tsfile.annotations.TsFileApi; import org.apache.tsfile.file.metadata.StringArrayDeviceID; +import org.apache.tsfile.i18n.Messages; import org.apache.tsfile.read.common.Field; import org.apache.tsfile.read.common.Path; import org.apache.tsfile.read.common.type.Type; import org.apache.tsfile.write.record.TSRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.util.HashMap; import java.util.Iterator; @@ -37,6 +41,7 @@ import java.util.stream.IntStream; public class TreeResultSet extends AbstractResultSet { + private static final Logger LOGGER = LoggerFactory.getLogger(TreeResultSet.class); private QueryDataSet queryDataSet; private List deviceList; private List measurementList; @@ -79,7 +84,11 @@ public boolean next() throws IOException { @TsFileApi public void close() { - // nothing to be done + try { + queryDataSet.close(); + } catch (IOException e) { + LOGGER.error(Messages.get("log.read.tree_result_set_close_error"), e); + } } @Override diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/query/timegenerator/TimeGenerator.java b/java/tsfile/src/main/java/org/apache/tsfile/read/query/timegenerator/TimeGenerator.java index c073a45b71..75269efc8e 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/query/timegenerator/TimeGenerator.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/query/timegenerator/TimeGenerator.java @@ -44,7 +44,10 @@ * that satisfies the filter condition. Then we use this timestamp to get values in other series * that are not included in IExpression */ -public abstract class TimeGenerator { +public abstract class TimeGenerator implements AutoCloseable { + + @Override + public void close() throws IOException {} private HashMap> leafNodeCache = new HashMap<>(); private HashMap> leafValuesCache; diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/query/timegenerator/TsFileTimeGenerator.java b/java/tsfile/src/main/java/org/apache/tsfile/read/query/timegenerator/TsFileTimeGenerator.java index c4e021e96e..e623b60cb3 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/query/timegenerator/TsFileTimeGenerator.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/query/timegenerator/TsFileTimeGenerator.java @@ -29,12 +29,14 @@ import org.apache.tsfile.read.reader.series.FileSeriesReader; import java.io.IOException; +import java.util.ArrayList; import java.util.List; public class TsFileTimeGenerator extends TimeGenerator { private IChunkLoader chunkLoader; private IMetadataQuerier metadataQuerier; + private final List readers = new ArrayList<>(); public TsFileTimeGenerator( IExpression iexpression, IChunkLoader chunkLoader, IMetadataQuerier metadataQuerier) @@ -42,7 +44,16 @@ public TsFileTimeGenerator( this.chunkLoader = chunkLoader; this.metadataQuerier = metadataQuerier; - super.constructNode(iexpression); + try { + super.constructNode(iexpression); + } catch (IOException | RuntimeException | Error e) { + try { + close(); + } catch (IOException closeError) { + e.addSuppressed(closeError); + } + throw e; + } } @Override @@ -50,7 +61,29 @@ protected IBatchReader generateNewBatchReader(SingleSeriesExpression expression) throws IOException { List chunkMetadataList = metadataQuerier.getChunkMetaDataList(expression.getSeriesPath()); - return new FileSeriesReader(chunkLoader, chunkMetadataList, expression.getFilter()); + FileSeriesReader reader = + new FileSeriesReader(chunkLoader, chunkMetadataList, expression.getFilter()); + readers.add(reader); + return reader; + } + + @Override + public void close() throws IOException { + IOException failure = null; + for (FileSeriesReader reader : readers) { + try { + reader.closeChunkReader(); + } catch (IOException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + } + if (failure != null) { + throw failure; + } } @Override diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/IChunkReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/IChunkReader.java index e13d32d974..cec2c361b9 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/IChunkReader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/IChunkReader.java @@ -24,7 +24,7 @@ import java.io.IOException; import java.util.List; -public interface IChunkReader { +public interface IChunkReader extends AutoCloseable { boolean hasNextSatisfiedPage() throws IOException; diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/TsFileLastReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/TsFileLastReader.java index 8700a850e4..0b30ea6341 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/TsFileLastReader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/TsFileLastReader.java @@ -19,14 +19,13 @@ package org.apache.tsfile.read.reader; -import org.apache.tsfile.compress.IUnCompressor; import org.apache.tsfile.encoding.decoder.Decoder; +import org.apache.tsfile.encrypt.IDecryptor; import org.apache.tsfile.enums.TSDataType; import org.apache.tsfile.file.header.PageHeader; import org.apache.tsfile.file.metadata.ChunkMetadata; import org.apache.tsfile.file.metadata.IDeviceID; import org.apache.tsfile.file.metadata.TimeseriesMetadata; -import org.apache.tsfile.file.metadata.enums.CompressionType; import org.apache.tsfile.i18n.Messages; import org.apache.tsfile.read.TimeValuePair; import org.apache.tsfile.read.TsFileSequenceReader; @@ -181,6 +180,8 @@ private TimeValuePair readAlignedLastPoint(Chunk chunk, ChunkMetadata chunkMetad ByteBuffer chunkData = chunk.getData(); PageHeader lastPageHeader = null; ByteBuffer lastPageData = null; + int pageIndex = 0; + int lastPageIndex = -1; while (chunkData.hasRemaining()) { PageHeader pageHeader; if (chunk.isSinglePageChunk()) { @@ -196,17 +197,20 @@ private TimeValuePair readAlignedLastPoint(Chunk chunk, ChunkMetadata chunkMetad || (pageHeader.getStatistics() != null && pageHeader.getStatistics().getCount() > 0)) { lastPageHeader = pageHeader; lastPageData = pageData; + lastPageIndex = pageIndex; } + pageIndex++; } if (lastPageHeader != null) { - CompressionType compressionType = chunk.getHeader().getCompressionType(); - if (compressionType != CompressionType.UNCOMPRESSED) { - ByteBuffer uncompressedPage = ByteBuffer.allocate(lastPageHeader.getUncompressedSize()); - IUnCompressor.getUnCompressor(compressionType).uncompress(lastPageData, uncompressedPage); - lastPageData = uncompressedPage; - lastPageData.flip(); - } + lastPageData = + ChunkReader.deserializePageData( + lastPageHeader, + lastPageData, + chunk.getHeader(), + IDecryptor.getDecryptor(chunk.getEncryptParam()), + chunk.getEncryptParam(), + lastPageIndex); ValuePageReader valuePageReader = new ValuePageReader( @@ -265,15 +269,15 @@ private Pair readLastPoint( return new Pair<>(seriesMeta.getMeasurementId(), null); } - Chunk chunk = sequenceReader.readMemChunk(lastNonEmptyChunkMetadata); - - if (!isAligned) { - return new Pair<>(seriesMeta.getMeasurementId(), readNonAlignedLastPoint(chunk)); - } else { - return new Pair<>( - seriesMeta.getMeasurementId(), - readAlignedLastPoint( - chunk, lastNonEmptyChunkMetadata, seriesMeta.getStatistics().getEndTime())); + try (Chunk chunk = sequenceReader.readMemChunk(lastNonEmptyChunkMetadata)) { + if (!isAligned) { + return new Pair<>(seriesMeta.getMeasurementId(), readNonAlignedLastPoint(chunk)); + } else { + return new Pair<>( + seriesMeta.getMeasurementId(), + readAlignedLastPoint( + chunk, lastNonEmptyChunkMetadata, seriesMeta.getStatistics().getEndTime())); + } } } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/block/DeviceOrderedTsBlockReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/block/DeviceOrderedTsBlockReader.java index 3bf2943a81..4495df4d61 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/block/DeviceOrderedTsBlockReader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/block/DeviceOrderedTsBlockReader.java @@ -19,6 +19,7 @@ package org.apache.tsfile.read.reader.block; +import org.apache.tsfile.exception.encrypt.EncryptException; import org.apache.tsfile.i18n.Messages; import org.apache.tsfile.read.common.block.TsBlock; import org.apache.tsfile.read.controller.IChunkLoader; @@ -31,6 +32,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.NoSuchElementException; public class DeviceOrderedTsBlockReader implements TsBlockReader { @@ -43,6 +45,7 @@ public class DeviceOrderedTsBlockReader implements TsBlockReader { private SingleDeviceTsBlockReader currentReader; private ExpressionTree timeFilter; private ExpressionTree measurementFilter; + private boolean closed; public DeviceOrderedTsBlockReader( DeviceTaskIterator taskIterator, @@ -61,9 +64,13 @@ public DeviceOrderedTsBlockReader( @Override public boolean hasNext() { + if (closed) { + return false; + } if (currentReader != null && currentReader.hasNext()) { return true; } + closeCurrentReaderForIteration(); while (taskIterator.hasNext()) { final DeviceQueryTask nextTask = taskIterator.next(); try { @@ -71,11 +78,16 @@ public boolean hasNext() { new SingleDeviceTsBlockReader( nextTask, metadataQuerier, chunkLoader, blockSize, timeFilter, measurementFilter); } catch (IOException e) { + if (EncryptException.isEncryptionFailure(e)) { + closed = true; + throw new UncheckedIOException(e); + } LOGGER.error(Messages.get("log.read.block_reader_construct_error"), nextTask, e); } if (currentReader != null && currentReader.hasNext()) { return true; } + closeCurrentReaderForIteration(); } return false; } @@ -90,8 +102,25 @@ public TsBlock next() throws IOException { @Override public void close() throws Exception { + closed = true; + closeCurrentReader(); + } + + private void closeCurrentReaderForIteration() { + try { + closeCurrentReader(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + private void closeCurrentReader() throws Exception { if (currentReader != null) { - currentReader.close(); + try { + currentReader.close(); + } finally { + currentReader = null; + } } } } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/block/SingleDeviceTsBlockReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/block/SingleDeviceTsBlockReader.java index da4f05e116..4fd474a90f 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/block/SingleDeviceTsBlockReader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/block/SingleDeviceTsBlockReader.java @@ -39,6 +39,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -59,6 +60,7 @@ public class SingleDeviceTsBlockReader implements TsBlockReader { private final Map idColumnContextMap; private Long nextTime; + private boolean closed; public SingleDeviceTsBlockReader( DeviceQueryTask task, @@ -77,22 +79,27 @@ public SingleDeviceTsBlockReader( this.measureColumnContextMap = new HashMap<>(); this.idColumnContextMap = new HashMap<>(); - final List> chunkMetadataLists = - metadataQuerier.getChunkMetadataLists( - task.getDeviceID(), - task.getColumnMapping().getMeasurementColumns(), - task.getIndexRoot()); + try { + final List> chunkMetadataLists = + metadataQuerier.getChunkMetadataLists( + task.getDeviceID(), + task.getColumnMapping().getMeasurementColumns(), + task.getIndexRoot()); - Filter timeFilter = timeExpression == null ? null : timeExpression.toFilter(); - for (List chunkMetadataList : chunkMetadataLists) { - constructColumnContext(chunkMetadataList, chunkLoader, timeFilter); - } + Filter timeFilter = timeExpression == null ? null : timeExpression.toFilter(); + for (List chunkMetadataList : chunkMetadataLists) { + constructColumnContext(chunkMetadataList, chunkLoader, timeFilter); + } - for (String idColumn : task.getColumnMapping().getIdColumns()) { - final List columnPosInResult = task.getColumnMapping().getColumnPos(idColumn); - // the first segment in DeviceId is the table name - final int columnPosInId = task.getTableSchema().findIdColumnOrder(idColumn) + 1; - idColumnContextMap.put(idColumn, new IdColumnContext(columnPosInResult, columnPosInId)); + for (String idColumn : task.getColumnMapping().getIdColumns()) { + final List columnPosInResult = task.getColumnMapping().getColumnPos(idColumn); + // the first segment in DeviceId is the table name + final int columnPosInId = task.getTableSchema().findIdColumnOrder(idColumn) + 1; + idColumnContextMap.put(idColumn, new IdColumnContext(columnPosInResult, columnPosInId)); + } + } catch (IOException | RuntimeException | Error e) { + closeAfterFailure(e); + throw e; } } @@ -105,33 +112,63 @@ private void constructColumnContext( final IChunkMetadata chunkMetadata = chunkMetadataList.get(0); AbstractFileSeriesReader seriesReader = new FileSeriesReader(chunkLoader, chunkMetadataList, timeFilter, false); - if (seriesReader.hasNextBatch()) { - if (chunkMetadata instanceof AbstractAlignedChunkMetadata) { - final List currentChunkMeasurementNames = - seriesReader.getCurrentChunkMeasurementNames(); - List> posInResult = new ArrayList<>(); - for (String currentChunkMeasurementName : currentChunkMeasurementNames) { - posInResult.add(task.getColumnMapping().getColumnPos(currentChunkMeasurementName)); + try { + if (seriesReader.hasNextBatch()) { + if (chunkMetadata instanceof AbstractAlignedChunkMetadata) { + final List currentChunkMeasurementNames = + seriesReader.getCurrentChunkMeasurementNames(); + List> posInResult = new ArrayList<>(); + for (String currentChunkMeasurementName : currentChunkMeasurementNames) { + posInResult.add(task.getColumnMapping().getColumnPos(currentChunkMeasurementName)); + } + measureColumnContextMap.put( + "", + new VectorMeasurementColumnContext( + posInResult, seriesReader.nextBatch(), seriesReader)); + } else { + final String measurementUid = chunkMetadata.getMeasurementUid(); + measureColumnContextMap.put( + measurementUid, + new SingleMeasurementColumnContext( + measurementUid, + task.getColumnMapping().getColumnPos(measurementUid), + seriesReader.nextBatch(), + seriesReader)); } - measureColumnContextMap.put( - "", - new VectorMeasurementColumnContext( - posInResult, seriesReader.nextBatch(), seriesReader)); } else { - final String measurementUid = chunkMetadata.getMeasurementUid(); - measureColumnContextMap.put( - measurementUid, - new SingleMeasurementColumnContext( - measurementUid, - task.getColumnMapping().getColumnPos(measurementUid), - seriesReader.nextBatch(), - seriesReader)); + seriesReader.closeChunkReader(); } + } catch (IOException | RuntimeException | Error e) { + try { + seriesReader.closeChunkReader(); + } catch (IOException | RuntimeException | Error closeError) { + e.addSuppressed(closeError); + } + throw e; } } @Override public boolean hasNext() { + if (closed) { + return false; + } + try { + return prepareNextBlock(); + } catch (IOException e) { + closeAfterFailure(e); + if (org.apache.tsfile.exception.encrypt.EncryptException.isEncryptionFailure(e)) { + throw new UncheckedIOException(e); + } + LOGGER.error(Messages.get("log.read.block_reader_fill_measurements_error"), e); + return false; + } catch (RuntimeException | Error e) { + closeAfterFailure(e); + throw e; + } + } + + private boolean prepareNextBlock() throws IOException { if (!lastBlockReturned) { return true; } @@ -158,13 +195,8 @@ public boolean hasNext() { } } - try { - fillMeasurements(minTimeColumns); - nextTime = null; - } catch (IOException e) { - LOGGER.error(Messages.get("log.read.block_reader_fill_measurements_error"), e); - return false; - } + fillMeasurements(minTimeColumns); + nextTime = null; // all columns have exhausted if (measureColumnContextMap.isEmpty()) { @@ -251,7 +283,46 @@ public TsBlock next() throws IOException { @Override public void close() throws Exception { - // nothing to be done + closeColumnReaders(); + } + + private void closeAfterFailure(Throwable failure) { + try { + closeColumnReaders(); + } catch (IOException | RuntimeException | Error closeError) { + failure.addSuppressed(closeError); + } + } + + private void closeColumnReaders() throws IOException { + if (closed) { + return; + } + closed = true; + Throwable failure = null; + for (MeasurementColumnContext context : measureColumnContextMap.values()) { + try { + context.seriesReader.closeChunkReader(); + } catch (IOException | RuntimeException | Error e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + } + measureColumnContextMap.clear(); + idColumnContextMap.clear(); + nextTime = null; + if (failure instanceof IOException) { + throw (IOException) failure; + } + if (failure instanceof RuntimeException) { + throw (RuntimeException) failure; + } + if (failure instanceof Error) { + throw (Error) failure; + } } public abstract static class MeasurementColumnContext { diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/chunk/AbstractAlignedChunkReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/chunk/AbstractAlignedChunkReader.java index acc9789e40..f42fae00c9 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/chunk/AbstractAlignedChunkReader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/chunk/AbstractAlignedChunkReader.java @@ -41,7 +41,10 @@ import java.util.List; import java.util.function.LongConsumer; +/** Owns the supplied time and value chunks' encryption references until {@link #close()}. */ public abstract class AbstractAlignedChunkReader extends AbstractChunkReader { + private final Chunk timeChunk; + private final List valueChunks; // chunk header of the time column private final ChunkHeader timeChunkHeader; // chunk data of the time column @@ -67,6 +70,8 @@ public abstract class AbstractAlignedChunkReader extends AbstractChunkReader { LongConsumer filterRowsRecorder) throws IOException { super(readStopTime, queryFilter, filterRowsRecorder); + this.timeChunk = timeChunk; + this.valueChunks = new ArrayList<>(valueChunkList); this.timeChunkHeader = timeChunk.getHeader(); this.timeChunkDataBuffer = timeChunk.getData(); this.timeDeleteIntervalList = timeChunk.getDeleteIntervalList(); @@ -81,7 +86,22 @@ public abstract class AbstractAlignedChunkReader extends AbstractChunkReader { valueChunkStatisticsList.add(chunk == null ? null : chunk.getChunkStatistic()); }); this.encryptParam = timeChunk.getEncryptParam(); - initAllPageReaders(timeChunk.getChunkStatistic(), valueChunkStatisticsList); + try { + initAllPageReaders(timeChunk.getChunkStatistic(), valueChunkStatisticsList); + } catch (IOException | RuntimeException | Error e) { + close(); + throw e; + } + } + + @Override + public void close() { + timeChunk.close(); + for (Chunk valueChunk : valueChunks) { + if (valueChunk != null) { + valueChunk.close(); + } + } } /** construct all the page readers in this chunk */ @@ -90,12 +110,15 @@ private void initAllPageReaders( List> valueChunkStatisticsList) throws IOException { // construct next satisfied page header + int pageIndex = 0; while (timeChunkDataBuffer.remaining() > 0) { // deserialize PageHeader from chunkDataBuffer AbstractAlignedPageReader alignedPageReader = isSinglePageChunk() - ? deserializeFromSinglePageChunk(timeChunkStatistics, valueChunkStatisticsList) - : deserializeFromMultiPageChunk(); + ? deserializeFromSinglePageChunk( + timeChunkStatistics, valueChunkStatisticsList, pageIndex) + : deserializeFromMultiPageChunk(pageIndex); + pageIndex++; if (alignedPageReader != null) { pageReaderList.add(alignedPageReader); } @@ -108,7 +131,8 @@ private boolean isSinglePageChunk() { private AbstractAlignedPageReader deserializeFromSinglePageChunk( Statistics timeChunkStatistics, - List> valueChunkStatisticsList) + List> valueChunkStatisticsList, + int pageIndex) throws IOException { PageHeader timePageHeader = PageHeader.deserializeFrom(timeChunkDataBuffer, timeChunkStatistics); @@ -132,12 +156,13 @@ private AbstractAlignedPageReader deserializeFromSinglePageChunk( skipCurrentPage(timePageHeader, valuePageHeaderList); return null; } - return constructAlignedPageReader(timePageHeader, valuePageHeaderList); + return constructAlignedPageReader(timePageHeader, valuePageHeaderList, pageIndex); } abstract boolean needSkipForSinglePageChunk(boolean isAllNull, PageHeader timePageHeader); - private AbstractAlignedPageReader deserializeFromMultiPageChunk() throws IOException { + private AbstractAlignedPageReader deserializeFromMultiPageChunk(int pageIndex) + throws IOException { PageHeader timePageHeader = PageHeader.deserializeFrom(timeChunkDataBuffer, timeChunkHeader.getDataType()); List valuePageHeaderList = new ArrayList<>(); @@ -158,7 +183,7 @@ private AbstractAlignedPageReader deserializeFromMultiPageChunk() throws IOExcep skipCurrentPage(timePageHeader, valuePageHeaderList); return null; } - return constructAlignedPageReader(timePageHeader, valuePageHeaderList); + return constructAlignedPageReader(timePageHeader, valuePageHeaderList, pageIndex); } abstract boolean needSkipForMultiPageChunk(boolean isAllNull, PageHeader timePageHeader); @@ -193,11 +218,17 @@ private void skipCurrentPage(PageHeader timePageHeader, List valuePa } private AbstractAlignedPageReader constructAlignedPageReader( - PageHeader timePageHeader, List rawValuePageHeaderList) throws IOException { + PageHeader timePageHeader, List rawValuePageHeaderList, int pageIndex) + throws IOException { IDecryptor decrytor = IDecryptor.getDecryptor(encryptParam); ByteBuffer timePageData = ChunkReader.deserializePageData( - timePageHeader, timeChunkDataBuffer, timeChunkHeader, decrytor); + timePageHeader, + timeChunkDataBuffer, + timeChunkHeader, + decrytor, + encryptParam, + pageIndex); List valuePageHeaderList = new ArrayList<>(); LazyLoadPageData[] lazyLoadPageDataArray = new LazyLoadPageData[rawValuePageHeaderList.size()]; @@ -238,7 +269,9 @@ private AbstractAlignedPageReader constructAlignedPageReader( valueChunkDataBufferList.get(i).array(), currentPagePosition, IUnCompressor.getUnCompressor(valueChunkHeader.getCompressionType()), - encryptParam); + encryptParam, + pageIndex, + valueChunkHeader.getChunkOrdinal()); valueDataTypeList.add(valueChunkHeader.getDataType()); valueDecoderList.add(valueChunkHeader.calculateDecoderForNonTimeChunk()); isAllNull = false; diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/chunk/ChunkReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/chunk/ChunkReader.java index b555a25e1b..ff4c68d7ce 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/chunk/ChunkReader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/chunk/ChunkReader.java @@ -22,6 +22,7 @@ import org.apache.tsfile.compress.IUnCompressor; import org.apache.tsfile.encrypt.EncryptParameter; import org.apache.tsfile.encrypt.IDecryptor; +import org.apache.tsfile.encrypt.PageCryptoContext; import org.apache.tsfile.file.MetaMarker; import org.apache.tsfile.file.header.ChunkHeader; import org.apache.tsfile.file.header.PageHeader; @@ -40,8 +41,10 @@ import java.util.List; import java.util.function.LongConsumer; +/** Owns the supplied chunk's encryption reference until {@link #close()}. */ public class ChunkReader extends AbstractChunkReader { + private final Chunk chunk; private final ChunkHeader chunkHeader; private final ByteBuffer chunkDataBuffer; private final List deleteIntervalList; @@ -52,11 +55,22 @@ public class ChunkReader extends AbstractChunkReader { public ChunkReader( Chunk chunk, long readStopTime, Filter queryFilter, LongConsumer filterRowsRecorder) { super(readStopTime, queryFilter, filterRowsRecorder); + this.chunk = chunk; this.chunkHeader = chunk.getHeader(); this.chunkDataBuffer = chunk.getData(); this.deleteIntervalList = chunk.getDeleteIntervalList(); this.encryptParam = chunk.getEncryptParam(); - initAllPageReaders(chunk.getChunkStatistic()); + try { + initAllPageReaders(chunk.getChunkStatistic()); + } catch (RuntimeException | Error e) { + chunk.close(); + throw e; + } + } + + @Override + public void close() { + chunk.close(); } public ChunkReader(Chunk chunk) throws IOException { @@ -81,7 +95,9 @@ public ChunkReader(Chunk chunk, long readStopTime) { private void initAllPageReaders(Statistics chunkStatistic) { // construct next satisfied page header + int pageIndex = 0; while (chunkDataBuffer.remaining() > 0) { + int currentPageIndex = pageIndex++; // deserialize a PageHeader from chunkDataBuffer PageHeader pageHeader; if (((byte) (chunkHeader.getChunkType() & 0x3F)) == MetaMarker.ONLY_ONE_PAGE_CHUNK_HEADER) { @@ -100,7 +116,7 @@ private void initAllPageReaders(Statistics chunkStatisti if (pageDeleted(pageHeader)) { skipCurrentPage(pageHeader); } else { - pageReaderList.add(constructPageReader(pageHeader)); + pageReaderList.add(constructPageReader(pageHeader, currentPageIndex)); } } } @@ -141,7 +157,7 @@ private void skipCurrentPage(PageHeader pageHeader) { chunkDataBuffer.position(chunkDataBuffer.position() + pageHeader.getCompressedSize()); } - private PageReader constructPageReader(PageHeader pageHeader) { + private PageReader constructPageReader(PageHeader pageHeader, int pageIndex) { IUnCompressor unCompressor = IUnCompressor.getUnCompressor(chunkHeader.getCompressionType()); // record the current position of chunkDataBuffer, use this to get the page data in PageReader // through directly accessing the buffer array @@ -151,7 +167,12 @@ private PageReader constructPageReader(PageHeader pageHeader) { new PageReader( pageHeader, new LazyLoadPageData( - chunkDataBuffer.array(), currentPagePosition, unCompressor, encryptParam), + chunkDataBuffer.array(), + currentPagePosition, + unCompressor, + encryptParam, + pageIndex, + chunkHeader.getChunkOrdinal()), chunkHeader.getDataType(), chunkHeader.calculateDecoderForNonTimeChunk(), getConfiguredTimeDecoder(), @@ -221,7 +242,55 @@ public static ByteBuffer decryptAndUncompressPageData( compressedPageData.arrayOffset() + compressedPageData.position(), compressedPageBodyLength); unCompressor.uncompress( - decryptedPageData, 0, compressedPageBodyLength, uncompressedPageData, 0); + decryptedPageData, 0, decryptedPageData.length, uncompressedPageData, 0); + } catch (Exception e) { + throw new IOException( + Messages.format( + "error.read.uncompress_error_with_header", + pageHeader.getUncompressedSize(), + pageHeader.getCompressedSize(), + pageHeader, + e.getMessage()), + e); + } + compressedPageData.position(compressedPageData.position() + compressedPageBodyLength); + return ByteBuffer.wrap(uncompressedPageData); + } + + public static ByteBuffer decryptAndUncompressPageData( + PageHeader pageHeader, + IUnCompressor unCompressor, + ByteBuffer compressedPageData, + IDecryptor decryptor, + EncryptParameter encryptParameter, + int pageIndex, + long chunkOrdinal) + throws IOException { + int compressedPageBodyLength = pageHeader.getCompressedSize(); + byte[] uncompressedPageData = new byte[pageHeader.getUncompressedSize()]; + try { + PageCryptoContext pageCryptoContext = + PageCryptoContext.forDecryption( + encryptParameter, + pageHeader.getUncompressedSize(), + compressedPageData.slice().limit(compressedPageBodyLength), + pageIndex, + chunkOrdinal); + byte[] decryptedPageData = + decryptor.decryptPage( + compressedPageData.array(), + compressedPageData.arrayOffset() + compressedPageData.position(), + compressedPageBodyLength, + pageCryptoContext); + if (decryptedPageData.length != pageCryptoContext.getCompressedPlaintextSize()) { + throw new IOException( + Messages.format( + "error.encrypt.page_plaintext_size_mismatch", + pageCryptoContext.getCompressedPlaintextSize(), + decryptedPageData.length)); + } + unCompressor.uncompress( + decryptedPageData, 0, decryptedPageData.length, uncompressedPageData, 0); } catch (Exception e) { throw new IOException( Messages.format( @@ -243,6 +312,31 @@ public static ByteBuffer deserializePageData( ByteBuffer compressedPageBody = readCompressedPageData(pageHeader, chunkBuffer); if (decryptor == null || decryptor.getEncryptionType() == EncryptionType.UNENCRYPTED) { return uncompressPageData(pageHeader, unCompressor, compressedPageBody); + } + return decryptAndUncompressPageData(pageHeader, unCompressor, compressedPageBody, decryptor); + } + + public static ByteBuffer deserializePageData( + PageHeader pageHeader, + ByteBuffer chunkBuffer, + ChunkHeader chunkHeader, + IDecryptor decryptor, + EncryptParameter encryptParameter, + int pageIndex) + throws IOException { + IUnCompressor unCompressor = IUnCompressor.getUnCompressor(chunkHeader.getCompressionType()); + ByteBuffer compressedPageBody = readCompressedPageData(pageHeader, chunkBuffer); + if (encryptParameter != null && encryptParameter.isTdePageAead()) { + return decryptAndUncompressPageData( + pageHeader, + unCompressor, + compressedPageBody, + decryptor, + encryptParameter, + pageIndex, + chunkHeader.getChunkOrdinal()); + } else if (decryptor == null || decryptor.getEncryptionType() == EncryptionType.UNENCRYPTED) { + return uncompressPageData(pageHeader, unCompressor, compressedPageBody); } else { return decryptAndUncompressPageData(pageHeader, unCompressor, compressedPageBody, decryptor); } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/page/LazyLoadPageData.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/page/LazyLoadPageData.java index 7b01a67f54..3eea62b90b 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/page/LazyLoadPageData.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/page/LazyLoadPageData.java @@ -23,11 +23,14 @@ import org.apache.tsfile.encrypt.EncryptParameter; import org.apache.tsfile.encrypt.EncryptUtils; import org.apache.tsfile.encrypt.IDecryptor; +import org.apache.tsfile.encrypt.PageCryptoContext; import org.apache.tsfile.file.header.PageHeader; +import org.apache.tsfile.file.metadata.enums.EncryptionType; import org.apache.tsfile.i18n.Messages; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Arrays; public class LazyLoadPageData { /** Reference to the data of original chunkDataBuffer. * */ @@ -39,11 +42,16 @@ public class LazyLoadPageData { private final EncryptParameter encryptParam; + private final int pageIndex; + private final long chunkOrdinal; + public LazyLoadPageData(byte[] data, int offset, IUnCompressor unCompressor) { this.chunkData = data; this.pageDataOffset = offset; this.unCompressor = unCompressor; this.encryptParam = EncryptUtils.getEncryptParameter(); + this.pageIndex = -1; + this.chunkOrdinal = -1; } public LazyLoadPageData( @@ -52,17 +60,57 @@ public LazyLoadPageData( this.pageDataOffset = offset; this.unCompressor = unCompressor; this.encryptParam = encryptParam; + this.pageIndex = -1; + this.chunkOrdinal = -1; + } + + public LazyLoadPageData( + byte[] data, + int offset, + IUnCompressor unCompressor, + EncryptParameter encryptParam, + int pageIndex, + long chunkOrdinal) { + this.chunkData = data; + this.pageDataOffset = offset; + this.unCompressor = unCompressor; + this.encryptParam = encryptParam; + this.pageIndex = pageIndex; + this.chunkOrdinal = chunkOrdinal; } public ByteBuffer uncompressPageData(PageHeader pageHeader) throws IOException { int compressedPageBodyLength = pageHeader.getCompressedSize(); byte[] uncompressedPageData = new byte[pageHeader.getUncompressedSize()]; IDecryptor decryptor = IDecryptor.getDecryptor(encryptParam); - byte[] decryptedPageData = - decryptor.decrypt(chunkData, pageDataOffset, compressedPageBodyLength); + byte[] decryptedPageData; + if (encryptParam != null && encryptParam.isTdePageAead()) { + PageCryptoContext pageCryptoContext = + PageCryptoContext.forDecryption( + encryptParam, + pageHeader.getUncompressedSize(), + ByteBuffer.wrap(chunkData, pageDataOffset, compressedPageBodyLength).slice(), + pageIndex, + chunkOrdinal); + decryptedPageData = + decryptor.decryptPage( + chunkData, pageDataOffset, compressedPageBodyLength, pageCryptoContext); + if (decryptedPageData.length != pageCryptoContext.getCompressedPlaintextSize()) { + throw new IOException( + Messages.format( + "error.encrypt.page_plaintext_size_mismatch", + pageCryptoContext.getCompressedPlaintextSize(), + decryptedPageData.length)); + } + } else if (decryptor.getEncryptionType() == EncryptionType.UNENCRYPTED) { + decryptedPageData = + Arrays.copyOfRange(chunkData, pageDataOffset, pageDataOffset + compressedPageBodyLength); + } else { + decryptedPageData = decryptor.decrypt(chunkData, pageDataOffset, compressedPageBodyLength); + } try { unCompressor.uncompress( - decryptedPageData, 0, compressedPageBodyLength, uncompressedPageData, 0); + decryptedPageData, 0, decryptedPageData.length, uncompressedPageData, 0); } catch (Exception e) { throw new IOException( Messages.format( diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/AbstractFileSeriesReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/AbstractFileSeriesReader.java index 3616150150..545071b8be 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/AbstractFileSeriesReader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/AbstractFileSeriesReader.java @@ -67,6 +67,7 @@ public boolean hasNextBatch() throws IOException { if (chunkReader != null && chunkReader.hasNextSatisfiedPage()) { return true; } + closeChunkReader(); // current chunk does not have additional batch, init new chunk reader while (chunkToRead < chunkMetadataList.size()) { @@ -82,6 +83,7 @@ public boolean hasNextBatch() throws IOException { if (chunkReader.hasNextSatisfiedPage()) { return true; } + closeChunkReader(); } return false; } @@ -97,7 +99,22 @@ public BatchData nextBatch() throws IOException { @Override public void close() throws IOException { - chunkLoader.close(); + try { + closeChunkReader(); + } finally { + chunkLoader.close(); + } + } + + /** Releases the current chunk without closing the shared file loader. */ + public void closeChunkReader() throws IOException { + if (chunkReader != null) { + try { + chunkReader.close(); + } finally { + chunkReader = null; + } + } } private IChunkMetadata nextChunkMeta() { diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/FileSeriesReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/FileSeriesReader.java index 9fa26c8298..13b2297a37 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/FileSeriesReader.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/FileSeriesReader.java @@ -65,19 +65,29 @@ protected void initChunkReader(IChunkMetadata chunkMetaData) throws IOException Chunk timeChunk = chunkLoader.loadChunk((ChunkMetadata) (alignedChunkMetadata.getTimeChunkMetadata())); List valueChunkList = new ArrayList<>(); - for (IChunkMetadata metadata : alignedChunkMetadata.getValueChunkMetadataList()) { - if (metadata != null) { - valueChunkList.add(chunkLoader.loadChunk((ChunkMetadata) metadata)); - currentChunkMeasurementNames.add(metadata.getMeasurementUid()); - continue; + try { + for (IChunkMetadata metadata : alignedChunkMetadata.getValueChunkMetadataList()) { + if (metadata != null) { + valueChunkList.add(chunkLoader.loadChunk((ChunkMetadata) metadata)); + currentChunkMeasurementNames.add(metadata.getMeasurementUid()); + continue; + } + valueChunkList.add(null); + currentChunkMeasurementNames.add(null); } - valueChunkList.add(null); - currentChunkMeasurementNames.add(null); - } - if (ignoreAllNullRows) { - this.chunkReader = new AlignedChunkReader(timeChunk, valueChunkList, filter); - } else { - this.chunkReader = new TableChunkReader(timeChunk, valueChunkList, filter); + if (ignoreAllNullRows) { + this.chunkReader = new AlignedChunkReader(timeChunk, valueChunkList, filter); + } else { + this.chunkReader = new TableChunkReader(timeChunk, valueChunkList, filter); + } + } catch (IOException | RuntimeException | Error e) { + timeChunk.close(); + for (Chunk valueChunk : valueChunkList) { + if (valueChunk != null) { + valueChunk.close(); + } + } + throw e; } } } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/FileSeriesReaderByTimestamp.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/FileSeriesReaderByTimestamp.java index ae6c394ca7..06c38a10a0 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/FileSeriesReaderByTimestamp.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/series/FileSeriesReaderByTimestamp.java @@ -38,7 +38,7 @@ * Series reader is used to query one series of one tsfile, using this reader to query the value of * a series with given timestamps. */ -public class FileSeriesReaderByTimestamp { +public class FileSeriesReaderByTimestamp implements AutoCloseable { protected IChunkLoader chunkLoader; protected List chunkMetadataList; @@ -99,6 +99,8 @@ public Object getValueInTimestamp(long timestamp) throws IOException { data = chunkReader.nextPageData(); } else if (!constructNextSatisfiedChunkReader()) { return null; + } else { + data = chunkReader.nextPageData(); } } } @@ -137,11 +139,15 @@ public boolean hasNext() throws IOException { } private boolean constructNextSatisfiedChunkReader() throws IOException { + closeChunkReader(); while (currentChunkIndex < chunkMetadataList.size()) { IChunkMetadata chunkMetaData = chunkMetadataList.get(currentChunkIndex++); if (chunkSatisfied(chunkMetaData)) { initChunkReader(chunkMetaData); - return true; + if (chunkReader.hasNextSatisfiedPage()) { + return true; + } + closeChunkReader(); } } return false; @@ -156,14 +162,51 @@ private void initChunkReader(IChunkMetadata chunkMetaData) throws IOException { Chunk timeChunk = chunkLoader.loadChunk((ChunkMetadata) (alignedChunkMetadata.getTimeChunkMetadata())); List valueChunkList = new ArrayList<>(); - for (IChunkMetadata metadata : alignedChunkMetadata.getValueChunkMetadataList()) { - valueChunkList.add(chunkLoader.loadChunk((ChunkMetadata) metadata)); + try { + for (IChunkMetadata metadata : alignedChunkMetadata.getValueChunkMetadataList()) { + valueChunkList.add(chunkLoader.loadChunk((ChunkMetadata) metadata)); + } + this.chunkReader = new AlignedChunkReader(timeChunk, valueChunkList, currentTimestamp); + } catch (IOException | RuntimeException | Error e) { + closeChunkOnFailure(timeChunk, e); + for (Chunk valueChunk : valueChunkList) { + closeChunkOnFailure(valueChunk, e); + } + throw e; } - this.chunkReader = new AlignedChunkReader(timeChunk, valueChunkList, currentTimestamp); } } private boolean chunkSatisfied(IChunkMetadata chunkMetaData) { return chunkMetaData.getEndTime() >= currentTimestamp; } + + private static void closeChunkOnFailure(Chunk chunk, Throwable failure) { + if (chunk == null) { + return; + } + try { + chunk.close(); + } catch (RuntimeException | Error closeFailure) { + if (closeFailure != failure) { + failure.addSuppressed(closeFailure); + } + } + } + + private void closeChunkReader() throws IOException { + if (chunkReader != null) { + try { + chunkReader.close(); + } finally { + chunkReader = null; + data = null; + } + } + } + + @Override + public void close() throws IOException { + closeChunkReader(); + } } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/utils/ReadWriteIOUtils.java b/java/tsfile/src/main/java/org/apache/tsfile/utils/ReadWriteIOUtils.java index c98be63456..f27d355537 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/utils/ReadWriteIOUtils.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/utils/ReadWriteIOUtils.java @@ -27,6 +27,7 @@ import org.apache.tsfile.read.reader.TsFileInput; import java.io.DataOutputStream; +import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -602,9 +603,21 @@ public static int read(ByteBuffer buffer) { /** Read a long var from inputStream. */ public static long readLong(InputStream inputStream) throws IOException { byte[] bytes = new byte[LONG_LEN]; - int readLen = inputStream.read(bytes); - if (readLen != LONG_LEN) { - throw new IOException(Messages.format(RETURN_ERROR_KEY, LONG_LEN, readLen)); + int readLen = 0; + while (readLen < LONG_LEN) { + int count = inputStream.read(bytes, readLen, LONG_LEN - readLen); + if (count < 0) { + throw new EOFException(Messages.format(RETURN_ERROR_KEY, LONG_LEN, readLen)); + } + if (count == 0) { + int value = inputStream.read(); + if (value < 0) { + throw new EOFException(Messages.format(RETURN_ERROR_KEY, LONG_LEN, readLen)); + } + bytes[readLen++] = (byte) value; + } else { + readLen += count; + } } return BytesUtils.bytesToLong(bytes); } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/utils/TsFileSketchTool.java b/java/tsfile/src/main/java/org/apache/tsfile/utils/TsFileSketchTool.java index a20c0e8883..a04c13aa5e 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/utils/TsFileSketchTool.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/utils/TsFileSketchTool.java @@ -311,8 +311,7 @@ private void printFileInfo() { private void printChunk(List allChunkGroupMetadata) { try { - long nextChunkGroupHeaderPos = - (long) TSFileConfig.MAGIC_STRING.getBytes().length + Byte.BYTES; + long nextChunkGroupHeaderPos = reader.getDataStartOffset(); // ChunkGroup begins for (ChunkGroupMetadata chunkGroupMetadata : allChunkGroupMetadata) { printlnBoth( diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/TsFileWriter.java b/java/tsfile/src/main/java/org/apache/tsfile/write/TsFileWriter.java index 5521a4f723..b2c7563de4 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/write/TsFileWriter.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/TsFileWriter.java @@ -118,7 +118,7 @@ public TsFileWriter(File file) throws IOException { public TsFileWriter(File file, EncryptParameter firstEncryptParam) throws IOException { this( - new TsFileIOWriter(file), + new TsFileIOWriter(file, firstEncryptParam), new Schema(), TSFileDescriptor.getInstance().getConfig(), firstEncryptParam); @@ -151,7 +151,7 @@ public TsFileWriter(File file, Schema schema) throws IOException { public TsFileWriter(File file, Schema schema, EncryptParameter firstEncryptParam) throws IOException { this( - new TsFileIOWriter(file), + new TsFileIOWriter(file, firstEncryptParam), schema, TSFileDescriptor.getInstance().getConfig(), firstEncryptParam); @@ -170,7 +170,7 @@ public TsFileWriter(TsFileOutput output, Schema schema) throws IOException { public TsFileWriter(TsFileOutput output, Schema schema, EncryptParameter firstEncryptParam) throws IOException { this( - new TsFileIOWriter(output), + new TsFileIOWriter(output, firstEncryptParam), schema, TSFileDescriptor.getInstance().getConfig(), firstEncryptParam); @@ -190,7 +190,7 @@ public TsFileWriter(File file, Schema schema, TSFileConfig conf) throws IOExcept public TsFileWriter( File file, Schema schema, TSFileConfig conf, EncryptParameter firstEncryptParam) throws IOException { - this(new TsFileIOWriter(file), schema, conf, firstEncryptParam); + this(new TsFileIOWriter(file, conf, firstEncryptParam), schema, conf, firstEncryptParam); } /** @@ -231,20 +231,41 @@ protected TsFileWriter( if (this.pageSize >= chunkGroupSizeThreshold) { LOG.warn(Messages.get("log.write.page_size_warn"), pageSize, chunkGroupSizeThreshold); } - this.secondEncryptParam = EncryptUtils.getEncryptParameter(firstEncryptParam); - String encryptLevel; - if (firstEncryptParam != null - && !Objects.equals(firstEncryptParam.getType(), "UNENCRYPTED") - && !Objects.equals(firstEncryptParam.getType(), "org.apache.tsfile.encrypt.UNENCRYPTED")) { - encryptLevel = "2"; - String str = - EncryptUtils.getKeyStr( - IEncryptor.getEncryptor(firstEncryptParam.getType(), firstEncryptParam.getKey()) - .encrypt(secondEncryptParam.getKey())); - fileWriter.setEncryptParam(encryptLevel, secondEncryptParam.getType(), str); + EncryptParameter fileEncryptParameter = fileWriter.getEncryptParameter(); + if (fileWriter instanceof RestorableTsFileIOWriter + && firstEncryptParam != null + && firstEncryptParam.isTdePageAead() + && (fileEncryptParameter == null || !fileEncryptParameter.isTdePageAead())) { + firstEncryptParam = fileEncryptParameter; + } + EncryptParameter effectiveEncryptParameter = + fileEncryptParameter != null && fileEncryptParameter.isTdePageAead() + ? fileEncryptParameter + : firstEncryptParam; + if (effectiveEncryptParameter != null && effectiveEncryptParameter.isTdePageAead()) { + this.secondEncryptParam = + effectiveEncryptParameter == fileEncryptParameter + ? effectiveEncryptParameter + : effectiveEncryptParameter.copy(); + fileWriter.setEncryptParam(this.secondEncryptParam); + fileWriter.writeEncryptionHeaderIfNecessary(); } else { - encryptLevel = "0"; - fileWriter.setEncryptParam(encryptLevel, "org.apache.tsfile.encrypt.UNENCRYPTED", ""); + this.secondEncryptParam = EncryptUtils.getEncryptParameter(firstEncryptParam); + String encryptLevel; + if (firstEncryptParam != null + && !Objects.equals(firstEncryptParam.getType(), "UNENCRYPTED") + && !Objects.equals( + firstEncryptParam.getType(), "org.apache.tsfile.encrypt.UNENCRYPTED")) { + encryptLevel = "2"; + String str = + EncryptUtils.getKeyStr( + IEncryptor.getEncryptor(firstEncryptParam.getType(), firstEncryptParam.getKey()) + .encrypt(secondEncryptParam.getKey())); + fileWriter.setEncryptParam(encryptLevel, secondEncryptParam.getType(), str); + } else { + encryptLevel = "0"; + fileWriter.setEncryptParam(encryptLevel, "org.apache.tsfile.encrypt.UNENCRYPTED", ""); + } } } @@ -729,8 +750,14 @@ private void reset() { @TsFileApi public void close() throws IOException { LOG.info(Messages.get("log.write.close_file")); - flush(); - fileWriter.endFile(); + try { + flush(); + fileWriter.endFile(); + } finally { + if (secondEncryptParam != null && secondEncryptParam.isTdePageAead()) { + secondEncryptParam.close(); + } + } } /** diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/ChunkWriterImpl.java b/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/ChunkWriterImpl.java index 7d7912b145..b4683382b0 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/ChunkWriterImpl.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/ChunkWriterImpl.java @@ -55,6 +55,7 @@ public class ChunkWriterImpl implements IChunkWriter { private final ICompressor compressor; private final EncryptParameter encryptParam; + private long chunkOrdinal = -1; /** all pages of this chunk. */ private final PublicBAOS pageBuffer; @@ -106,6 +107,7 @@ public ChunkWriterImpl(IMeasurementSchema schema) { this.measurementSchema = schema; this.compressor = ICompressor.getCompressor(schema.getCompressor()); this.encryptParam = EncryptUtils.getEncryptParameter(); + this.chunkOrdinal = nextChunkOrdinal(); this.pageBuffer = new PublicBAOS(); this.pageSizeThreshold = TSFileDescriptor.getInstance().getConfig().getPageSizeInByte(); @@ -118,6 +120,7 @@ public ChunkWriterImpl(IMeasurementSchema schema) { this.statistics = Statistics.getStatsByType(measurementSchema.getType()); this.pageWriter = new PageWriter(measurementSchema, encryptParam); + this.pageWriter.setChunkOrdinal(chunkOrdinal); this.pageWriter.setTimeEncoder(measurementSchema.getTimeEncoder()); this.pageWriter.setValueEncoder(measurementSchema.getValueEncoder()); @@ -130,6 +133,7 @@ public ChunkWriterImpl(IMeasurementSchema schema, EncryptParameter encryptParam) this.measurementSchema = schema; this.compressor = ICompressor.getCompressor(schema.getCompressor()); this.encryptParam = encryptParam; + this.chunkOrdinal = nextChunkOrdinal(); this.pageBuffer = new PublicBAOS(); this.pageSizeThreshold = TSFileDescriptor.getInstance().getConfig().getPageSizeInByte(); @@ -142,6 +146,7 @@ public ChunkWriterImpl(IMeasurementSchema schema, EncryptParameter encryptParam) this.statistics = Statistics.getStatsByType(measurementSchema.getType()); this.pageWriter = new PageWriter(measurementSchema, this.encryptParam); + this.pageWriter.setChunkOrdinal(chunkOrdinal); this.pageWriter.setTimeEncoder(measurementSchema.getTimeEncoder()); this.pageWriter.setValueEncoder(measurementSchema.getValueEncoder()); @@ -161,6 +166,12 @@ public ChunkWriterImpl( this.isMerging = isMerging; } + private long nextChunkOrdinal() { + return encryptParam != null && encryptParam.isTdePageAead() + ? encryptParam.nextChunkOrdinal() + : -1; + } + private void checkSdtEncoding() { if (measurementSchema.getProps() != null && !isMerging) { if (measurementSchema.getProps().getOrDefault(LOSS, "").equals(SDT)) { @@ -322,17 +333,18 @@ private void writePageToPageBuffer() { try { if (numOfPages == 0) { // record the firstPageStatistics this.firstPageStatistics = pageWriter.getStatistics(); - this.sizeWithoutStatistic = pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, true); + this.sizeWithoutStatistic = + pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, true, numOfPages); } else if (numOfPages == 1) { // put the firstPageStatistics into pageBuffer byte[] b = pageBuffer.toByteArray(); pageBuffer.reset(); pageBuffer.write(b, 0, this.sizeWithoutStatistic); firstPageStatistics.serialize(pageBuffer); pageBuffer.write(b, this.sizeWithoutStatistic, b.length - this.sizeWithoutStatistic); - pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false); + pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false, numOfPages); firstPageStatistics = null; } else { - pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false); + pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false, numOfPages); } // update statistics of this chunk @@ -364,6 +376,10 @@ public void writeToFileWriter( sizeWithoutStatistic = 0; firstPageStatistics = null; this.statistics = Statistics.getStatsByType(measurementSchema.getType()); + this.chunkOrdinal = nextChunkOrdinal(); + if (pageWriter != null) { + pageWriter.setChunkOrdinal(chunkOrdinal); + } } @Override @@ -380,7 +396,10 @@ public long getSerializedChunkSize() { return 0; } // return the serialized size of the chunk header + all pages - return ChunkHeader.getSerializedSize(measurementSchema.getMeasurementName(), pageBuffer.size()) + return ChunkHeader.getSerializedSize( + measurementSchema.getMeasurementName(), + pageBuffer.size(), + chunkOrdinal >= 0 ? encryptParam.getChunkAuthenticationSize() : 0) + (long) pageBuffer.size(); } @@ -432,6 +451,10 @@ public TSDataType getDataType() { */ public void writePageHeaderAndDataIntoBuff(ByteBuffer data, PageHeader header) throws PageException { + if (chunkOrdinal >= 0) { + throw new org.apache.tsfile.exception.encrypt.EncryptException( + Messages.get("error.write.aead_raw_page_copy_unsupported")); + } // write the page header to pageBuffer try { logger.debug(Messages.get("log.write.page_header_flush_start"), pageBuffer.size()); @@ -505,7 +528,9 @@ private void writeAllPagesOfChunkToTsFile( statistics, pageBuffer.size(), numOfPages, - 0); + 0, + chunkOrdinal, + encryptParam); long dataOffset = writer.getPos(); @@ -542,6 +567,10 @@ public int getNumOfPages() { return numOfPages; } + public long getChunkOrdinal() { + return chunkOrdinal; + } + public ByteBuffer getByteBuffer() { return ByteBuffer.wrap(pageBuffer.toByteArray()); } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/TimeChunkWriter.java b/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/TimeChunkWriter.java index 392a0cf458..8f4a01d00a 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/TimeChunkWriter.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/TimeChunkWriter.java @@ -57,6 +57,7 @@ public class TimeChunkWriter { private CompressionType compressionType; private EncryptParameter encryptParam; + private long chunkOrdinal = -1; /** all pages of this chunk. */ private PublicBAOS pageBuffer; @@ -96,6 +97,7 @@ public TimeChunkWriter( this.encodingType = encodingType; this.compressionType = compressionType; this.encryptParam = EncryptUtils.getEncryptParameter(); + this.chunkOrdinal = nextChunkOrdinal(); this.pageBuffer = new PublicBAOS(); this.pageSizeThreshold = TSFileDescriptor.getInstance().getConfig().getPageSizeInByte(); @@ -110,6 +112,7 @@ public TimeChunkWriter( this.pageWriter = new TimePageWriter( timeEncoder, ICompressor.getCompressor(compressionType), this.encryptParam); + this.pageWriter.setChunkOrdinal(chunkOrdinal); } public TimeChunkWriter( @@ -122,6 +125,7 @@ public TimeChunkWriter( this.encodingType = encodingType; this.compressionType = compressionType; this.encryptParam = encryptParam; + this.chunkOrdinal = nextChunkOrdinal(); this.pageBuffer = new PublicBAOS(); this.pageSizeThreshold = TSFileDescriptor.getInstance().getConfig().getPageSizeInByte(); @@ -135,12 +139,19 @@ public TimeChunkWriter( this.pageWriter = new TimePageWriter(timeEncoder, ICompressor.getCompressor(compressionType), encryptParam); + this.pageWriter.setChunkOrdinal(chunkOrdinal); } public void write(long time) { pageWriter.write(time); } + private long nextChunkOrdinal() { + return encryptParam != null && encryptParam.isTdePageAead() + ? encryptParam.nextChunkOrdinal() + : -1; + } + public void write(long[] timestamps, int batchSize, int arrayOffset) { pageWriter.write(timestamps, batchSize, arrayOffset); } @@ -184,17 +195,18 @@ public void writePageToPageBuffer() { try { if (numOfPages == 0) { // record the firstPageStatistics this.firstPageStatistics = pageWriter.getStatistics(); - this.sizeWithoutStatistic = pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, true); + this.sizeWithoutStatistic = + pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, true, numOfPages); } else if (numOfPages == 1) { // put the firstPageStatistics into pageBuffer byte[] b = pageBuffer.toByteArray(); pageBuffer.reset(); pageBuffer.write(b, 0, this.sizeWithoutStatistic); firstPageStatistics.serialize(pageBuffer); pageBuffer.write(b, this.sizeWithoutStatistic, b.length - this.sizeWithoutStatistic); - pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false); + pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false, numOfPages); firstPageStatistics = null; } else { - pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false); + pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false, numOfPages); } // update statistics of this chunk @@ -210,6 +222,10 @@ public void writePageToPageBuffer() { public void writePageHeaderAndDataIntoBuff(ByteBuffer data, PageHeader header) throws PageException { + if (chunkOrdinal >= 0) { + throw new org.apache.tsfile.exception.encrypt.EncryptException( + Messages.get("error.write.aead_raw_page_copy_unsupported")); + } // write the page header to pageBuffer try { logger.debug(Messages.get("log.write.page_header_flush_start"), pageBuffer.size()); @@ -262,6 +278,10 @@ public void writeToFileWriter(TsFileIOWriter tsfileWriter) throws IOException { sizeWithoutStatistic = 0; firstPageStatistics = null; this.statistics = new TimeStatistics(); + this.chunkOrdinal = nextChunkOrdinal(); + if (pageWriter != null) { + pageWriter.setChunkOrdinal(chunkOrdinal); + } } public long estimateMaxSeriesMemSize() { @@ -276,7 +296,10 @@ public long getCurrentChunkSize() { return 0; } // return the serialized size of the chunk header + all pages - return ChunkHeader.getSerializedSize(measurementId, pageBuffer.size()) + return ChunkHeader.getSerializedSize( + measurementId, + pageBuffer.size(), + chunkOrdinal >= 0 ? encryptParam.getChunkAuthenticationSize() : 0) + (long) pageBuffer.size(); } @@ -322,7 +345,9 @@ public void writeAllPagesOfChunkToTsFile(TsFileIOWriter writer) throws IOExcepti statistics, pageBuffer.size(), numOfPages, - TsFileConstant.TIME_COLUMN_MASK); + TsFileConstant.TIME_COLUMN_MASK, + chunkOrdinal, + encryptParam); long dataOffset = writer.getPos(); diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/ValueChunkWriter.java b/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/ValueChunkWriter.java index 382df6e808..12eebc59f6 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/ValueChunkWriter.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/chunk/ValueChunkWriter.java @@ -61,6 +61,7 @@ public class ValueChunkWriter { private final CompressionType compressionType; private final EncryptParameter encryptParam; + private long chunkOrdinal = -1; /** all pages of this chunk. */ private final PublicBAOS pageBuffer; @@ -100,6 +101,7 @@ public ValueChunkWriter( this.dataType = dataType; this.compressionType = compressionType; this.encryptParam = EncryptUtils.getEncryptParameter(); + this.chunkOrdinal = nextChunkOrdinal(); this.pageBuffer = new PublicBAOS(); this.pageSizeThreshold = TSFileDescriptor.getInstance().getConfig().getPageSizeInByte(); this.maxNumberOfPointsInPage = @@ -112,6 +114,7 @@ public ValueChunkWriter( this.pageWriter = new ValuePageWriter( valueEncoder, ICompressor.getCompressor(compressionType), dataType, this.encryptParam); + this.pageWriter.setChunkOrdinal(chunkOrdinal); } public ValueChunkWriter( @@ -126,6 +129,7 @@ public ValueChunkWriter( this.dataType = dataType; this.compressionType = compressionType; this.encryptParam = encryptParam; + this.chunkOrdinal = nextChunkOrdinal(); this.pageBuffer = new PublicBAOS(); this.pageSizeThreshold = TSFileDescriptor.getInstance().getConfig().getPageSizeInByte(); this.maxNumberOfPointsInPage = @@ -138,6 +142,13 @@ public ValueChunkWriter( this.pageWriter = new ValuePageWriter( valueEncoder, ICompressor.getCompressor(compressionType), dataType, this.encryptParam); + this.pageWriter.setChunkOrdinal(chunkOrdinal); + } + + private long nextChunkOrdinal() { + return encryptParam != null && encryptParam.isTdePageAead() + ? encryptParam.nextChunkOrdinal() + : -1; } public void write(long time, long value, boolean isNull) { @@ -214,7 +225,8 @@ public void writePageToPageBuffer() { // record the firstPageStatistics if it is not empty page this.firstPageStatistics = pageWriter.getStatistics(); } - this.sizeWithoutStatistic = pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, true); + this.sizeWithoutStatistic = + pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, true, numOfPages); } else if (numOfPages == 1) { // put the firstPageStatistics into pageBuffer if (firstPageStatistics != null) { // Consider previous page is an empty page byte[] b = pageBuffer.toByteArray(); @@ -223,10 +235,10 @@ public void writePageToPageBuffer() { firstPageStatistics.serialize(pageBuffer); pageBuffer.write(b, this.sizeWithoutStatistic, b.length - this.sizeWithoutStatistic); } - pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false); + pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false, numOfPages); firstPageStatistics = null; } else { - pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false); + pageWriter.writePageHeaderAndDataIntoBuff(pageBuffer, false, numOfPages); } // update statistics of this chunk @@ -242,6 +254,10 @@ public void writePageToPageBuffer() { public void writePageHeaderAndDataIntoBuff(ByteBuffer data, PageHeader header) throws PageException { + if (chunkOrdinal >= 0) { + throw new org.apache.tsfile.exception.encrypt.EncryptException( + Messages.get("error.write.aead_raw_page_copy_unsupported")); + } // write the page header to pageBuffer try { logger.debug(Messages.get("log.write.page_header_flush_start"), pageBuffer.size()); @@ -314,6 +330,10 @@ public void writeToFileWriter( sizeWithoutStatistic = 0; firstPageStatistics = null; this.statistics = Statistics.getStatsByType(dataType); + this.chunkOrdinal = nextChunkOrdinal(); + if (pageWriter != null) { + pageWriter.setChunkOrdinal(chunkOrdinal); + } } public long estimateMaxSeriesMemSize() { @@ -335,11 +355,15 @@ public long getCurrentChunkSize() { // Empty chunk, it may happen if pageBuffer stores empty bits and only chunk header will be // flushed. if (statistics.getCount() == 0) { - return ChunkHeader.getSerializedSize(measurementId, 0); + return ChunkHeader.getSerializedSize( + measurementId, 0, chunkOrdinal >= 0 ? encryptParam.getChunkAuthenticationSize() : 0); } // return the serialized size of the chunk header + all pages - return ChunkHeader.getSerializedSize(measurementId, pageBuffer.size()) + return ChunkHeader.getSerializedSize( + measurementId, + pageBuffer.size(), + chunkOrdinal >= 0 ? encryptParam.getChunkAuthenticationSize() : 0) + (long) pageBuffer.size(); } @@ -385,6 +409,10 @@ public int getNumOfPages() { return numOfPages; } + public long getChunkOrdinal() { + return chunkOrdinal; + } + public TSDataType getDataType() { return dataType; } @@ -421,7 +449,9 @@ public void writeAllPagesOfChunkToTsFile( statistics, 0, 0, - TsFileConstant.VALUE_COLUMN_MASK); + TsFileConstant.VALUE_COLUMN_MASK, + chunkOrdinal, + encryptParam); writer.endCurrentChunk(); return; } @@ -435,7 +465,9 @@ public void writeAllPagesOfChunkToTsFile( statistics, pageBuffer.size(), numOfPages, - TsFileConstant.VALUE_COLUMN_MASK); + TsFileConstant.VALUE_COLUMN_MASK, + chunkOrdinal, + encryptParam); long dataOffset = writer.getPos(); diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/page/PageBodyEncoder.java b/java/tsfile/src/main/java/org/apache/tsfile/write/page/PageBodyEncoder.java new file mode 100644 index 0000000000..e86946b5e1 --- /dev/null +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/page/PageBodyEncoder.java @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.write.page; + +import org.apache.tsfile.compress.ICompressor; +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.IEncryptor; +import org.apache.tsfile.encrypt.PageCryptoContext; +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.file.metadata.enums.CompressionType; +import org.apache.tsfile.file.metadata.enums.EncryptionType; +import org.apache.tsfile.i18n.Messages; + +import java.io.IOException; +import java.nio.ByteBuffer; + +class PageBodyEncoder { + + private PageBodyEncoder() {} + + /** Returns an array-backed buffer whose remaining bytes are the encoded page body. */ + static ByteBuffer encode( + ByteBuffer pageData, + int uncompressedSize, + ICompressor compressor, + EncryptParameter encryptParameter, + int pageIndex, + long chunkOrdinal) + throws IOException { + byte[] plaintext; + int plaintextOffset = 0; + int compressedSize; + int inputOffset = pageData.arrayOffset() + pageData.position(); + + if (compressor.getType().equals(CompressionType.UNCOMPRESSED)) { + plaintext = pageData.array(); + plaintextOffset = inputOffset; + compressedSize = uncompressedSize; + } else if (compressor.getType().equals(CompressionType.GZIP)) { + plaintext = compressor.compress(pageData.array(), inputOffset, uncompressedSize); + compressedSize = plaintext.length; + } else { + plaintext = new byte[compressor.getMaxBytesForCompression(uncompressedSize)]; + compressedSize = + compressor.compress(pageData.array(), inputOffset, uncompressedSize, plaintext); + } + + IEncryptor encryptor = IEncryptor.getEncryptor(encryptParameter); + byte[] encryptedPageBody; + if (encryptParameter != null && encryptParameter.isTdePageAead()) { + PageCryptoContext pageCryptoContext = + PageCryptoContext.forEncryption( + encryptParameter, uncompressedSize, compressedSize, pageIndex, chunkOrdinal); + encryptedPageBody = + encryptor.encryptPage(plaintext, plaintextOffset, compressedSize, pageCryptoContext); + if (encryptedPageBody.length != pageCryptoContext.getEncryptedPageBodySize()) { + throw new EncryptException( + Messages.format( + "error.encrypt.page_output_size_mismatch", + pageCryptoContext.getEncryptedPageBodySize(), + encryptedPageBody.length)); + } + } else if (encryptor.getEncryptionType() == EncryptionType.UNENCRYPTED) { + return ByteBuffer.wrap(plaintext, plaintextOffset, compressedSize); + } else { + encryptedPageBody = encryptor.encrypt(plaintext, plaintextOffset, compressedSize); + } + return ByteBuffer.wrap(encryptedPageBody); + } +} diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/page/PageWriter.java b/java/tsfile/src/main/java/org/apache/tsfile/write/page/PageWriter.java index d479ad8d43..ce840dfff0 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/write/page/PageWriter.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/page/PageWriter.java @@ -22,10 +22,7 @@ import org.apache.tsfile.encoding.encoder.Encoder; import org.apache.tsfile.encrypt.EncryptParameter; import org.apache.tsfile.encrypt.EncryptUtils; -import org.apache.tsfile.encrypt.IEncryptor; import org.apache.tsfile.enums.TSDataType; -import org.apache.tsfile.file.metadata.enums.CompressionType; -import org.apache.tsfile.file.metadata.enums.EncryptionType; import org.apache.tsfile.file.metadata.statistics.Statistics; import org.apache.tsfile.utils.Binary; import org.apache.tsfile.utils.PublicBAOS; @@ -38,8 +35,6 @@ import java.io.IOException; import java.io.Serializable; import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.WritableByteChannel; /** * This writer is used to write time-value into a page. It consists of a time encoder, a value @@ -52,6 +47,7 @@ public class PageWriter { private ICompressor compressor; private EncryptParameter encryptParam; + private long chunkOrdinal = -1; // time private Encoder timeEncoder; @@ -231,28 +227,22 @@ public ByteBuffer getUncompressedBytes() throws IOException { /** write the page header and data into the PageWriter's output stream. */ public int writePageHeaderAndDataIntoBuff(PublicBAOS pageBuffer, boolean first) throws IOException { + return writePageHeaderAndDataIntoBuff(pageBuffer, first, first ? 0 : -1); + } + + /** write the page header and data into the PageWriter's output stream. */ + public int writePageHeaderAndDataIntoBuff(PublicBAOS pageBuffer, boolean first, int pageIndex) + throws IOException { if (statistics.getCount() == 0) { return 0; } ByteBuffer pageData = getUncompressedBytes(); int uncompressedSize = pageData.remaining(); - int compressedSize; - byte[] compressedBytes = null; - - if (compressor.getType().equals(CompressionType.UNCOMPRESSED)) { - compressedSize = uncompressedSize; - } else if (compressor.getType().equals(CompressionType.GZIP)) { - compressedBytes = - compressor.compress(pageData.array(), pageData.position(), uncompressedSize); - compressedSize = compressedBytes.length; - } else { - compressedBytes = new byte[compressor.getMaxBytesForCompression(uncompressedSize)]; - // data is never a directByteBuffer now, so we can use data.array() - compressedSize = - compressor.compress( - pageData.array(), pageData.position(), uncompressedSize, compressedBytes); - } + ByteBuffer pageBody = + PageBodyEncoder.encode( + pageData, uncompressedSize, compressor, encryptParam, pageIndex, chunkOrdinal); + int pageBodySize = pageBody.remaining(); // write the page header to IOWriter int sizeWithoutStatistic = 0; @@ -260,41 +250,16 @@ public int writePageHeaderAndDataIntoBuff(PublicBAOS pageBuffer, boolean first) sizeWithoutStatistic += ReadWriteForEncodingUtils.writeUnsignedVarInt(uncompressedSize, pageBuffer); sizeWithoutStatistic += - ReadWriteForEncodingUtils.writeUnsignedVarInt(compressedSize, pageBuffer); + ReadWriteForEncodingUtils.writeUnsignedVarInt(pageBodySize, pageBuffer); } else { ReadWriteForEncodingUtils.writeUnsignedVarInt(uncompressedSize, pageBuffer); - ReadWriteForEncodingUtils.writeUnsignedVarInt(compressedSize, pageBuffer); + ReadWriteForEncodingUtils.writeUnsignedVarInt(pageBodySize, pageBuffer); statistics.serialize(pageBuffer); } - IEncryptor encryptor = IEncryptor.getEncryptor(encryptParam); - // write page content to temp PBAOS logger.trace("start to flush a page data into buffer, buffer position {} ", pageBuffer.size()); - if (compressor.getType().equals(CompressionType.UNCOMPRESSED)) { - if (encryptor.getEncryptionType().equals(EncryptionType.UNENCRYPTED)) { - try (WritableByteChannel channel = Channels.newChannel(pageBuffer)) { - channel.write(pageData); - } - } else { - byte[] encryptedBytes = null; - encryptedBytes = encryptor.encrypt(pageData.array(), pageData.position(), uncompressedSize); - // data is never a directByteBuffer now, so we can use data.array() - int encryptedSize = encryptedBytes.length; - pageBuffer.write(encryptedBytes, 0, encryptedSize); - } - - } else { - if (encryptor.getEncryptionType().equals(EncryptionType.UNENCRYPTED)) { - pageBuffer.write(compressedBytes, 0, compressedSize); - } else { - byte[] encryptedBytes = null; - encryptedBytes = encryptor.encrypt(compressedBytes, 0, compressedSize); - // data is never a directByteBuffer now, so we can use data.array() - int encryptedSize = encryptedBytes.length; - pageBuffer.write(encryptedBytes, 0, encryptedSize); - } - } + pageBuffer.write(pageBody.array(), pageBody.arrayOffset() + pageBody.position(), pageBodySize); logger.trace("start to flush a page data into buffer, buffer position {} ", pageBuffer.size()); return sizeWithoutStatistic; } @@ -319,6 +284,10 @@ public void reset(IMeasurementSchema measurementSchema) { statistics = Statistics.getStatsByType(measurementSchema.getType()); } + public void setChunkOrdinal(long chunkOrdinal) { + this.chunkOrdinal = chunkOrdinal; + } + public void setTimeEncoder(Encoder encoder) { this.timeEncoder = encoder; } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/page/TimePageWriter.java b/java/tsfile/src/main/java/org/apache/tsfile/write/page/TimePageWriter.java index e09e66cdf4..6463ef51be 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/write/page/TimePageWriter.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/page/TimePageWriter.java @@ -22,9 +22,6 @@ import org.apache.tsfile.encoding.encoder.Encoder; import org.apache.tsfile.encrypt.EncryptParameter; import org.apache.tsfile.encrypt.EncryptUtils; -import org.apache.tsfile.encrypt.IEncryptor; -import org.apache.tsfile.file.metadata.enums.CompressionType; -import org.apache.tsfile.file.metadata.enums.EncryptionType; import org.apache.tsfile.file.metadata.statistics.TimeStatistics; import org.apache.tsfile.utils.PublicBAOS; import org.apache.tsfile.utils.ReadWriteForEncodingUtils; @@ -34,8 +31,6 @@ import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.WritableByteChannel; /** * This writer is used to write time into a page. It consists of a time encoder and respective @@ -48,6 +43,7 @@ public class TimePageWriter { private final ICompressor compressor; private final EncryptParameter encryptParam; + private long chunkOrdinal = -1; // time private Encoder timeEncoder; @@ -114,28 +110,22 @@ public ByteBuffer getUncompressedBytes() throws IOException { /** write the page header and data into the PageWriter's output stream. */ public int writePageHeaderAndDataIntoBuff(PublicBAOS pageBuffer, boolean first) throws IOException { + return writePageHeaderAndDataIntoBuff(pageBuffer, first, first ? 0 : -1); + } + + /** write the page header and data into the PageWriter's output stream. */ + public int writePageHeaderAndDataIntoBuff(PublicBAOS pageBuffer, boolean first, int pageIndex) + throws IOException { if (statistics.getCount() == 0) { return 0; } ByteBuffer pageData = getUncompressedBytes(); int uncompressedSize = pageData.remaining(); - int compressedSize; - byte[] compressedBytes = null; - - if (compressor.getType().equals(CompressionType.UNCOMPRESSED)) { - compressedSize = uncompressedSize; - } else if (compressor.getType().equals(CompressionType.GZIP)) { - compressedBytes = - compressor.compress(pageData.array(), pageData.position(), uncompressedSize); - compressedSize = compressedBytes.length; - } else { - compressedBytes = new byte[compressor.getMaxBytesForCompression(uncompressedSize)]; - // data is never a directByteBuffer now, so we can use data.array() - compressedSize = - compressor.compress( - pageData.array(), pageData.position(), uncompressedSize, compressedBytes); - } + ByteBuffer pageBody = + PageBodyEncoder.encode( + pageData, uncompressedSize, compressor, encryptParam, pageIndex, chunkOrdinal); + int pageBodySize = pageBody.remaining(); // write the page header to IOWriter int sizeWithoutStatistic = 0; @@ -143,41 +133,17 @@ public int writePageHeaderAndDataIntoBuff(PublicBAOS pageBuffer, boolean first) sizeWithoutStatistic += ReadWriteForEncodingUtils.writeUnsignedVarInt(uncompressedSize, pageBuffer); sizeWithoutStatistic += - ReadWriteForEncodingUtils.writeUnsignedVarInt(compressedSize, pageBuffer); + ReadWriteForEncodingUtils.writeUnsignedVarInt(pageBodySize, pageBuffer); } else { ReadWriteForEncodingUtils.writeUnsignedVarInt(uncompressedSize, pageBuffer); - ReadWriteForEncodingUtils.writeUnsignedVarInt(compressedSize, pageBuffer); + ReadWriteForEncodingUtils.writeUnsignedVarInt(pageBodySize, pageBuffer); statistics.serialize(pageBuffer); } // write page content to temp PBAOS logger.trace( "start to flush a time page data into buffer, buffer position {} ", pageBuffer.size()); - IEncryptor encryptor = IEncryptor.getEncryptor(encryptParam); - if (compressor.getType().equals(CompressionType.UNCOMPRESSED)) { - if (encryptor.getEncryptionType().equals(EncryptionType.UNENCRYPTED)) { - try (WritableByteChannel channel = Channels.newChannel(pageBuffer)) { - channel.write(pageData); - } - } else { - byte[] encryptedBytes = null; - encryptedBytes = encryptor.encrypt(pageData.array(), pageData.position(), uncompressedSize); - // data is never a directByteBuffer now, so we can use data.array() - int encryptedSize = encryptedBytes.length; - pageBuffer.write(encryptedBytes, 0, encryptedSize); - } - - } else { - if (encryptor.getEncryptionType().equals(EncryptionType.UNENCRYPTED)) { - pageBuffer.write(compressedBytes, 0, compressedSize); - } else { - byte[] encryptedBytes = null; - encryptedBytes = encryptor.encrypt(compressedBytes, 0, compressedSize); - // data is never a directByteBuffer now, so we can use data.array() - int encryptedSize = encryptedBytes.length; - pageBuffer.write(encryptedBytes, 0, encryptedSize); - } - } + pageBuffer.write(pageBody.array(), pageBody.arrayOffset() + pageBody.position(), pageBodySize); logger.trace( "finish flushing a time page data into buffer, buffer position {} ", pageBuffer.size()); return sizeWithoutStatistic; @@ -199,6 +165,10 @@ public void reset() { statistics = new TimeStatistics(); } + public void setChunkOrdinal(long chunkOrdinal) { + this.chunkOrdinal = chunkOrdinal; + } + public void setTimeEncoder(Encoder encoder) { this.timeEncoder = encoder; } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/page/ValuePageWriter.java b/java/tsfile/src/main/java/org/apache/tsfile/write/page/ValuePageWriter.java index f7f375759a..16c101ee67 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/write/page/ValuePageWriter.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/page/ValuePageWriter.java @@ -22,10 +22,7 @@ import org.apache.tsfile.encoding.encoder.Encoder; import org.apache.tsfile.encrypt.EncryptParameter; import org.apache.tsfile.encrypt.EncryptUtils; -import org.apache.tsfile.encrypt.IEncryptor; import org.apache.tsfile.enums.TSDataType; -import org.apache.tsfile.file.metadata.enums.CompressionType; -import org.apache.tsfile.file.metadata.enums.EncryptionType; import org.apache.tsfile.file.metadata.statistics.Statistics; import org.apache.tsfile.utils.Binary; import org.apache.tsfile.utils.BitMap; @@ -38,8 +35,6 @@ import java.io.IOException; import java.io.Serializable; import java.nio.ByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.WritableByteChannel; /** * This writer is used to write value into a page. It consists of a value encoder and respective @@ -51,6 +46,7 @@ public class ValuePageWriter { private final ICompressor compressor; private final EncryptParameter encryptParam; + private long chunkOrdinal = -1; // value private Encoder valueEncoder; @@ -405,6 +401,12 @@ public int writeEmptyPageIntoBuff(PublicBAOS pageBuffer) { /** write the page header and data into the PageWriter's output stream. */ public int writePageHeaderAndDataIntoBuff(PublicBAOS pageBuffer, boolean first) throws IOException { + return writePageHeaderAndDataIntoBuff(pageBuffer, first, first ? 0 : -1); + } + + /** write the page header and data into the PageWriter's output stream. */ + public int writePageHeaderAndDataIntoBuff(PublicBAOS pageBuffer, boolean first, int pageIndex) + throws IOException { if (size == 0) { return 0; } else if (statistics.getCount() == 0) { @@ -414,22 +416,10 @@ public int writePageHeaderAndDataIntoBuff(PublicBAOS pageBuffer, boolean first) ByteBuffer pageData = getUncompressedBytes(); int uncompressedSize = pageData.remaining(); - int compressedSize; - byte[] compressedBytes = null; - - if (compressor.getType().equals(CompressionType.UNCOMPRESSED)) { - compressedSize = uncompressedSize; - } else if (compressor.getType().equals(CompressionType.GZIP)) { - compressedBytes = - compressor.compress(pageData.array(), pageData.position(), uncompressedSize); - compressedSize = compressedBytes.length; - } else { - compressedBytes = new byte[compressor.getMaxBytesForCompression(uncompressedSize)]; - // data is never a directByteBuffer now, so we can use data.array() - compressedSize = - compressor.compress( - pageData.array(), pageData.position(), uncompressedSize, compressedBytes); - } + ByteBuffer pageBody = + PageBodyEncoder.encode( + pageData, uncompressedSize, compressor, encryptParam, pageIndex, chunkOrdinal); + int pageBodySize = pageBody.remaining(); // write the page header to IOWriter int sizeWithoutStatistic = 0; @@ -437,39 +427,15 @@ public int writePageHeaderAndDataIntoBuff(PublicBAOS pageBuffer, boolean first) sizeWithoutStatistic += ReadWriteForEncodingUtils.writeUnsignedVarInt(uncompressedSize, pageBuffer); sizeWithoutStatistic += - ReadWriteForEncodingUtils.writeUnsignedVarInt(compressedSize, pageBuffer); + ReadWriteForEncodingUtils.writeUnsignedVarInt(pageBodySize, pageBuffer); } else { ReadWriteForEncodingUtils.writeUnsignedVarInt(uncompressedSize, pageBuffer); - ReadWriteForEncodingUtils.writeUnsignedVarInt(compressedSize, pageBuffer); + ReadWriteForEncodingUtils.writeUnsignedVarInt(pageBodySize, pageBuffer); statistics.serialize(pageBuffer); } - IEncryptor encryptor = IEncryptor.getEncryptor(encryptParam); // write page content to temp PBAOS logger.trace("start to flush a page data into buffer, buffer position {} ", pageBuffer.size()); - if (compressor.getType().equals(CompressionType.UNCOMPRESSED)) { - if (encryptor.getEncryptionType().equals(EncryptionType.UNENCRYPTED)) { - try (WritableByteChannel channel = Channels.newChannel(pageBuffer)) { - channel.write(pageData); - } - } else { - byte[] encryptedBytes = null; - encryptedBytes = encryptor.encrypt(pageData.array(), pageData.position(), uncompressedSize); - // data is never a directByteBuffer now, so we can use data.array() - int encryptedSize = encryptedBytes.length; - pageBuffer.write(encryptedBytes, 0, encryptedSize); - } - - } else { - if (encryptor.getEncryptionType().equals(EncryptionType.UNENCRYPTED)) { - pageBuffer.write(compressedBytes, 0, compressedSize); - } else { - byte[] encryptedBytes = null; - encryptedBytes = encryptor.encrypt(compressedBytes, 0, compressedSize); - // data is never a directByteBuffer now, so we can use data.array() - int encryptedSize = encryptedBytes.length; - pageBuffer.write(encryptedBytes, 0, encryptedSize); - } - } + pageBuffer.write(pageBody.array(), pageBody.arrayOffset() + pageBody.position(), pageBodySize); logger.trace("start to flush a page data into buffer, buffer position {} ", pageBuffer.size()); return sizeWithoutStatistic; } @@ -493,6 +459,10 @@ public void reset(TSDataType dataType) { statistics = Statistics.getStatsByType(dataType); } + public void setChunkOrdinal(long chunkOrdinal) { + this.chunkOrdinal = chunkOrdinal; + } + public void setValueEncoder(Encoder encoder) { this.valueEncoder = encoder; } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/v4/AbstractTableModelTsFileWriter.java b/java/tsfile/src/main/java/org/apache/tsfile/write/v4/AbstractTableModelTsFileWriter.java index cf7e3b6513..886d6e07e5 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/write/v4/AbstractTableModelTsFileWriter.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/v4/AbstractTableModelTsFileWriter.java @@ -94,7 +94,7 @@ protected AbstractTableModelTsFileWriter(File file, long chunkGroupSizeThreshold protected AbstractTableModelTsFileWriter( File file, long chunkGroupSizeThreshold, EncryptParameter firstEncryptParam) throws IOException { - this(new TsFileIOWriter(file), chunkGroupSizeThreshold, firstEncryptParam); + this(new TsFileIOWriter(file, firstEncryptParam), chunkGroupSizeThreshold, firstEncryptParam); } @TsFileApi @@ -123,20 +123,35 @@ protected AbstractTableModelTsFileWriter( LOG.warn(Messages.get("log.write.page_size_warn"), pageSize, chunkGroupSizeThreshold); } - this.secondEncryptParam = EncryptUtils.getEncryptParameter(firstEncryptParam); - String encryptLevel; - if (firstEncryptParam != null - && !Objects.equals(firstEncryptParam.getType(), "UNENCRYPTED") - && !Objects.equals(firstEncryptParam.getType(), "org.apache.tsfile.encrypt.UNENCRYPTED")) { - encryptLevel = "2"; - String str = - EncryptUtils.getKeyStr( - IEncryptor.getEncryptor(firstEncryptParam.getType(), firstEncryptParam.getKey()) - .encrypt(secondEncryptParam.getKey())); - fileWriter.setEncryptParam(encryptLevel, secondEncryptParam.getType(), str); + EncryptParameter fileEncryptParameter = fileWriter.getEncryptParameter(); + EncryptParameter effectiveEncryptParameter = + fileEncryptParameter != null && fileEncryptParameter.isTdePageAead() + ? fileEncryptParameter + : firstEncryptParam; + if (effectiveEncryptParameter != null && effectiveEncryptParameter.isTdePageAead()) { + this.secondEncryptParam = + effectiveEncryptParameter == fileEncryptParameter + ? effectiveEncryptParameter + : effectiveEncryptParameter.copy(); + fileWriter.setEncryptParam(this.secondEncryptParam); + fileWriter.writeEncryptionHeaderIfNecessary(); } else { - encryptLevel = "0"; - fileWriter.setEncryptParam(encryptLevel, "org.apache.tsfile.encrypt.UNENCRYPTED", ""); + this.secondEncryptParam = EncryptUtils.getEncryptParameter(firstEncryptParam); + String encryptLevel; + if (firstEncryptParam != null + && !Objects.equals(firstEncryptParam.getType(), "UNENCRYPTED") + && !Objects.equals( + firstEncryptParam.getType(), "org.apache.tsfile.encrypt.UNENCRYPTED")) { + encryptLevel = "2"; + String str = + EncryptUtils.getKeyStr( + IEncryptor.getEncryptor(firstEncryptParam.getType(), firstEncryptParam.getKey()) + .encrypt(secondEncryptParam.getKey())); + fileWriter.setEncryptParam(encryptLevel, secondEncryptParam.getType(), str); + } else { + encryptLevel = "0"; + fileWriter.setEncryptParam(encryptLevel, "org.apache.tsfile.encrypt.UNENCRYPTED", ""); + } } } @@ -273,7 +288,13 @@ public void addTsFileProperty(String key, byte[] value) { @TsFileApi public void close() throws IOException { LOG.info(Messages.get("log.write.close_file")); - flush(); - fileWriter.endFile(); + try { + flush(); + fileWriter.endFile(); + } finally { + if (secondEncryptParam != null && secondEncryptParam.isTdePageAead()) { + secondEncryptParam.close(); + } + } } } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/writer/ForceAppendTsFileWriter.java b/java/tsfile/src/main/java/org/apache/tsfile/write/writer/ForceAppendTsFileWriter.java index 1f627d693c..cc94ea8574 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/write/writer/ForceAppendTsFileWriter.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/writer/ForceAppendTsFileWriter.java @@ -45,6 +45,7 @@ public class ForceAppendTsFileWriter extends TsFileIOWriter { private long truncatePosition; private static Logger logger = LoggerFactory.getLogger(ForceAppendTsFileWriter.class); + private EncryptParameter ownedEncryptParameter; public ForceAppendTsFileWriter(File file) throws IOException { this( @@ -58,18 +59,27 @@ public ForceAppendTsFileWriter(File file, EncryptParameter param) throws IOExcep if (logger.isDebugEnabled()) { logger.debug(Messages.get("log.write.writer_opened"), file.getName()); } - this.out = FSFactoryProducer.getFileOutputFactory().getTsFileOutput(file.getPath(), true); this.file = file; - setEncryptParam(param); // file doesn't exist if (file.length() == 0 || !file.exists()) { throw new TsFileNotCompleteException( Messages.format("error.write.force_append_not_complete", file.getPath())); } + this.out = FSFactoryProducer.getFileOutputFactory().getTsFileOutput(file.getPath(), true); + setEncryptParam(param); try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getAbsolutePath(), param, true)) { + if (!reader.hasFileEncryptionHeader() && param != null && param.isTdePageAead()) { + throw new IOException(Messages.get("error.write.aead_append_requires_file_header")); + } + EncryptParameter recoveredParameter = reader.getEncryptParam(); + if (recoveredParameter != null && recoveredParameter.isTdePageAead()) { + ownedEncryptParameter = recoveredParameter.copy(); + setEncryptParam(ownedEncryptParameter); + } + markExistingFileStarted(reader.hasFileEncryptionHeader()); // this tsfile is not complete if (!reader.isComplete()) { @@ -85,9 +95,21 @@ public ForceAppendTsFileWriter(File file, EncryptParameter param) throws IOExcep for (IDeviceID device : devices) { List chunkMetadataList = new ArrayList<>(); reader.readChunkMetadataInDevice(device).values().forEach(chunkMetadataList::addAll); + if (ownedEncryptParameter != null) { + for (ChunkMetadata chunkMetadata : chunkMetadataList) { + reader.position(chunkMetadata.getOffsetOfChunkHeader()); + reader.readChunkHeader(reader.readMarker()); + } + } ChunkGroupMetadata chunkGroupMetadata = new ChunkGroupMetadata(device, chunkMetadataList); chunkGroupMetadataList.add(chunkGroupMetadata); } + if (ownedEncryptParameter != null) { + ownedEncryptParameter.resumeAfterChunkOrdinal(reader.getMaxChunkOrdinal()); + } + } catch (IOException | RuntimeException e) { + closeAfterFailedInitialization(e); + throw e; } } @@ -98,4 +120,38 @@ public void doTruncate() throws IOException { public long getTruncatePosition() { return truncatePosition; } + + @Override + public void endFile() throws IOException { + try { + super.endFile(); + } finally { + closeOwnedEncryptParameter(); + } + } + + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + closeOwnedEncryptParameter(); + } + } + + private void closeOwnedEncryptParameter() { + if (ownedEncryptParameter != null) { + ownedEncryptParameter.close(); + } + } + + private void closeAfterFailedInitialization(Exception exception) { + try { + out.close(); + } catch (IOException e) { + exception.addSuppressed(e); + } finally { + closeOwnedEncryptParameter(); + } + } } diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/writer/RestorableTsFileIOWriter.java b/java/tsfile/src/main/java/org/apache/tsfile/write/writer/RestorableTsFileIOWriter.java index 09866b3eb3..13dd66d471 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/write/writer/RestorableTsFileIOWriter.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/writer/RestorableTsFileIOWriter.java @@ -126,23 +126,41 @@ public RestorableTsFileIOWriter(File file, boolean truncate, EncryptParameter pa } this.file = file; this.out = FSFactoryProducer.getFileOutputFactory().getTsFileOutput(file.getPath(), true); - this.param = param; - setEncryptParam(param); + this.param = param != null && param.isTdePageAead() ? param.copy() : param; + setEncryptParam(this.param); // file doesn't exist if (file.length() == 0) { - startFile(); - crashed = true; - canWrite = true; - return; + try { + startFile(); + crashed = true; + canWrite = true; + return; + } catch (IOException | RuntimeException e) { + closeAfterFailedInitialization(e); + throw e; + } } try { if (file.exists()) { try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getAbsolutePath(), param, false)) { + EncryptParameter recoveredParameter = reader.getEncryptParam(); + if ((recoveredParameter != null && recoveredParameter.isTdePageAead()) + || (this.param != null && this.param.isTdePageAead())) { + if (this.param != null && this.param.isTdePageAead()) { + this.param.close(); + } + this.param = recoveredParameter == null ? null : recoveredParameter.copy(); + setEncryptParam(this.param); + } + markExistingFileStarted(reader.hasFileEncryptionHeader()); schema.setEnabledUpdateSchema(false); truncatedSize = reader.selfCheck(schema, chunkGroupMetadataList, true); + if (this.param != null && this.param.isTdePageAead()) { + this.param.resumeAfterChunkOrdinal(reader.getMaxChunkOrdinal()); + } minPlanIndex = reader.getMinPlanIndex(); maxPlanIndex = reader.getMaxPlanIndex(); if (truncatedSize == TsFileCheckStatus.COMPLETE_FILE) { @@ -165,7 +183,7 @@ public RestorableTsFileIOWriter(File file, boolean truncate, EncryptParameter pa } } } catch (Exception e) { - out.close(); + closeAfterFailedInitialization(e); throw e; } } @@ -209,6 +227,45 @@ public Schema getKnownSchema() { return schema; } + @Override + public EncryptParameter getEncryptParameter() { + return param; + } + + @Override + public void endFile() throws IOException { + try { + super.endFile(); + } finally { + closeOwnedEncryptParameter(); + } + } + + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + closeOwnedEncryptParameter(); + } + } + + private void closeOwnedEncryptParameter() { + if (param != null && param.isTdePageAead()) { + param.close(); + } + } + + private void closeAfterFailedInitialization(Exception exception) { + try { + out.close(); + } catch (IOException e) { + exception.addSuppressed(e); + } finally { + closeOwnedEncryptParameter(); + } + } + /** * For query. * diff --git a/java/tsfile/src/main/java/org/apache/tsfile/write/writer/TsFileIOWriter.java b/java/tsfile/src/main/java/org/apache/tsfile/write/writer/TsFileIOWriter.java index 5e1d33c21b..abf45b0766 100644 --- a/java/tsfile/src/main/java/org/apache/tsfile/write/writer/TsFileIOWriter.java +++ b/java/tsfile/src/main/java/org/apache/tsfile/write/writer/TsFileIOWriter.java @@ -25,10 +25,12 @@ import org.apache.tsfile.encrypt.EncryptUtils; import org.apache.tsfile.encrypt.IEncryptor; import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.exception.encrypt.EncryptException; import org.apache.tsfile.external.commons.io.FileUtils; import org.apache.tsfile.file.MetaMarker; import org.apache.tsfile.file.header.ChunkGroupHeader; import org.apache.tsfile.file.header.ChunkHeader; +import org.apache.tsfile.file.header.FileEncryptionHeader; import org.apache.tsfile.file.metadata.ChunkGroupMetadata; import org.apache.tsfile.file.metadata.ChunkMetadata; import org.apache.tsfile.file.metadata.IChunkMetadata; @@ -136,6 +138,14 @@ public class TsFileIOWriter implements AutoCloseable { protected String encryptKey; + protected EncryptParameter encryptParameter; + private EncryptParameter ownedEncryptParameter; + private boolean newFileContextClaimed; + + private boolean fileStarted; + + private boolean encryptionHeaderWritten; + private final List flushListeners = new ArrayList<>(); protected String currentTable; @@ -187,13 +197,18 @@ public TsFileIOWriter(File file, TSFileConfig conf) throws IOException { } public TsFileIOWriter(File file, TSFileConfig conf, EncryptParameter param) throws IOException { - this.out = FSFactoryProducer.getFileOutputFactory().getTsFileOutput(file.getPath(), false); + prepareNewFileEncryption(param); this.file = file; - if (resourceLogger.isDebugEnabled()) { - resourceLogger.debug("{} writer is opened.", file.getName()); + try { + this.out = FSFactoryProducer.getFileOutputFactory().getTsFileOutput(file.getPath(), false); + if (resourceLogger.isDebugEnabled()) { + resourceLogger.debug("{} writer is opened.", file.getName()); + } + startFile(); + } catch (IOException | RuntimeException | Error e) { + closeAfterNewFileFailure(e); + throw e; } - setEncryptParam(param); - startFile(); } /** @@ -210,8 +225,47 @@ public TsFileIOWriter(TsFileOutput output) throws IOException { public TsFileIOWriter(TsFileOutput output, EncryptParameter param) throws IOException { this.out = output; - setEncryptParam(param); - startFile(); + try { + prepareNewFileEncryption(param); + startFile(); + } catch (IOException | RuntimeException | Error e) { + closeAfterNewFileFailure(e); + throw e; + } + } + + private void prepareNewFileEncryption(EncryptParameter parameter) { + if (parameter != null && parameter.isTdePageAead()) { + ownedEncryptParameter = parameter.copy(); + try { + ownedEncryptParameter.claimForNewFile(); + } catch (RuntimeException | Error e) { + ownedEncryptParameter.close(); + throw e; + } + newFileContextClaimed = true; + setEncryptParam(ownedEncryptParameter); + } else { + setEncryptParam(parameter); + } + } + + private void closeAfterNewFileFailure(Throwable failure) { + try { + if (out != null) { + out.close(); + } + } catch (IOException | RuntimeException e) { + failure.addSuppressed(e); + } finally { + closeOwnedEncryptParameter(); + } + } + + private void closeOwnedEncryptParameter() { + if (ownedEncryptParameter != null) { + ownedEncryptParameter.close(); + } } /** for test only */ @@ -236,14 +290,28 @@ public TsFileIOWriter(File file, long maxMetadataSize, EncryptParameter param) } public void setEncryptParam(String encryptLevel, String encryptType, String encryptKey) { + if (encryptionHeaderWritten) { + throw new EncryptException(Messages.get("error.write.encryption_context_immutable")); + } this.encryptLevel = encryptLevel; this.encryptType = encryptType; this.encryptKey = encryptKey; + this.encryptParameter = new EncryptParameter(encryptType, null); } public void setEncryptParam(EncryptParameter param) { + if (encryptionHeaderWritten && encryptParameter != param) { + throw new EncryptException(Messages.get("error.write.encryption_context_immutable")); + } + this.encryptParameter = param; if (param == null) { setEncryptParam("0", "org.apache.tsfile.encrypt.UNENCRYPTED", null); + } else if (param.isTdePageAead()) { + // File-level encryption metadata is persisted in FileEncryptionHeader. Legacy footer fields + // stay explicitly unencrypted so wrapped key material is not duplicated. + encryptLevel = "0"; + encryptType = "org.apache.tsfile.encrypt.UNENCRYPTED"; + encryptKey = ""; } else { if (!Objects.equals(param.getType(), "UNENCRYPTED") && !Objects.equals(param.getType(), "org.apache.tsfile.encrypt.UNENCRYPTED")) { @@ -258,6 +326,34 @@ public void setEncryptParam(EncryptParameter param) { setEncryptParam("0", "org.apache.tsfile.encrypt.UNENCRYPTED", null); } } + this.encryptParameter = param; + } + + public EncryptParameter getEncryptParameter() { + return encryptParameter; + } + + protected void markExistingFileStarted(boolean hasEncryptionHeader) { + fileStarted = true; + encryptionHeaderWritten = hasEncryptionHeader; + } + + public void writeEncryptionHeaderIfNecessary() throws IOException { + if (encryptParameter == null || !encryptParameter.isTdePageAead() || encryptionHeaderWritten) { + return; + } + long baseHeaderSize = MAGIC_STRING_BYTES.length + Byte.BYTES; + if (!fileStarted || out.getPosition() != baseHeaderSize) { + throw new IOException(Messages.get("error.write.encryption_header_must_precede_data")); + } + if (!newFileContextClaimed) { + encryptParameter.claimForNewFile(); + newFileContextClaimed = true; + } + FileEncryptionHeader.serialize(encryptParameter, out.wrapAsStream()); + // Make the header visible to readers without a separate durability synchronization. + out.flush(); + encryptionHeaderWritten = true; } /** Add a custom property to the TsFile metadata. */ @@ -277,15 +373,19 @@ public void addFlushListener(FlushChunkMetadataListener listener) { * @throws IOException if an I/O error occurs. */ public void writeBytesToStream(PublicBAOS bytes) throws IOException { + writeEncryptionHeaderIfNecessary(); bytes.writeTo(out.wrapAsStream()); } protected void startFile() throws IOException { out.write(MAGIC_STRING_BYTES); out.write(VERSION_NUMBER_BYTE); + fileStarted = true; + writeEncryptionHeaderIfNecessary(); } public int startChunkGroup(IDeviceID deviceId) throws IOException { + writeEncryptionHeaderIfNecessary(); updateTableSize(deviceId); this.currentChunkGroupDeviceId = deviceId; if (logger.isDebugEnabled()) { @@ -346,6 +446,41 @@ public void startFlushChunk( int numOfPages, int mask) throws IOException { + startFlushChunk( + measurementId, + compressionCodecName, + tsDataType, + encodingType, + statistics, + dataSize, + numOfPages, + mask, + -1, + null); + } + + public void startFlushChunk( + String measurementId, + CompressionType compressionCodecName, + TSDataType tsDataType, + TSEncoding encodingType, + Statistics statistics, + int dataSize, + int numOfPages, + int mask, + long chunkOrdinal, + EncryptParameter chunkEncryptParameter) + throws IOException { + + boolean pageAead = encryptParameter != null && encryptParameter.isTdePageAead(); + if (pageAead != (chunkOrdinal >= 0)) { + throw new IOException(Messages.get("error.write.invalid_chunk_ordinal")); + } + if (pageAead && !encryptParameter.sharesPageAeadFileContext(chunkEncryptParameter)) { + throw new IOException(Messages.get("error.write.encrypted_chunk_context_mismatch")); + } + + writeEncryptionHeaderIfNecessary(); currentChunkMetadata = new ChunkMetadata( @@ -365,12 +500,15 @@ public void startFlushChunk( compressionCodecName, encodingType, numOfPages, - mask); - header.serializeTo(out.wrapAsStream()); + mask, + chunkOrdinal); + writeChunkHeader(header); } /** Write a whole chunk in another file into this file. Providing fast merge for IoTDB. */ public void writeChunk(Chunk chunk, ChunkMetadata chunkMetadata) throws IOException { + writeEncryptionHeaderIfNecessary(); + validateChunkEncryptionContext(chunk); ChunkHeader chunkHeader = chunk.getHeader(); currentChunkMetadata = new ChunkMetadata( @@ -381,7 +519,7 @@ public void writeChunk(Chunk chunk, ChunkMetadata chunkMetadata) throws IOExcept out.getPosition(), chunkMetadata.getStatistics()); currentChunkMetadata.setMask(chunkMetadata.getMask()); - chunkHeader.serializeTo(out.wrapAsStream()); + writeChunkHeader(chunkHeader); out.write(chunk.getData()); endCurrentChunk(); if (logger.isDebugEnabled()) { @@ -400,6 +538,7 @@ public void writeEmptyValueChunk( TSEncoding encodingType, Statistics statistics) throws IOException { + writeEncryptionHeaderIfNecessary(); currentChunkMetadata = new ChunkMetadata( measurementId, @@ -417,12 +556,17 @@ public void writeEmptyValueChunk( compressionType, encodingType, 0, - TsFileConstant.VALUE_COLUMN_MASK); - emptyChunkHeader.serializeTo(out.wrapAsStream()); + TsFileConstant.VALUE_COLUMN_MASK, + encryptParameter != null && encryptParameter.isTdePageAead() + ? encryptParameter.nextChunkOrdinal() + : -1); + writeChunkHeader(emptyChunkHeader); endCurrentChunk(); } public void writeChunk(Chunk chunk) throws IOException { + writeEncryptionHeaderIfNecessary(); + validateChunkEncryptionContext(chunk); ChunkHeader chunkHeader = chunk.getHeader(); currentChunkMetadata = new ChunkMetadata( @@ -436,11 +580,19 @@ public void writeChunk(Chunk chunk) throws IOException { (byte) (chunkHeader.getChunkType() & (TsFileConstant.TIME_COLUMN_MASK | TsFileConstant.VALUE_COLUMN_MASK))); - chunkHeader.serializeTo(out.wrapAsStream()); + writeChunkHeader(chunkHeader); out.write(chunk.getData()); endCurrentChunk(); } + private void writeChunkHeader(ChunkHeader header) throws IOException { + byte[] authentication = + encryptParameter != null && encryptParameter.isTdePageAead() + ? encryptParameter.authenticateChunk(out.getPosition(), header.getChunkOrdinal()) + : null; + header.serializeTo(out.wrapAsStream(), authentication); + } + /** end chunk and write some log. */ public void endCurrentChunk() { this.currentChunkMetadataSize += currentChunkMetadata.getRetainedSizeInBytes(); @@ -468,6 +620,7 @@ public void endFile() throws IOException { if (!canWrite) { return; } + writeEncryptionHeaderIfNecessary(); updateTableSize(null); checkInMemoryPathCount(); @@ -484,7 +637,11 @@ public void endFile() throws IOException { // flush page cache data to disk out.force(); // close file - out.close(); + try { + out.close(); + } finally { + closeOwnedEncryptParameter(); + } if (resourceLogger.isDebugEnabled() && file != null) { resourceLogger.debug("{} writer is closed.", file.getName()); @@ -689,9 +846,13 @@ public void reset() throws IOException { */ public void close() throws IOException { canWrite = false; - out.close(); - if (tempOutput != null) { - this.tempOutput.close(); + try { + out.close(); + if (tempOutput != null) { + this.tempOutput.close(); + } + } finally { + closeOwnedEncryptParameter(); } } @@ -712,12 +873,26 @@ public void setFile(File file) { } public void writePlanIndices() throws IOException { + writeEncryptionHeaderIfNecessary(); ReadWriteIOUtils.write(MetaMarker.OPERATION_INDEX_RANGE, out.wrapAsStream()); ReadWriteIOUtils.write(minPlanIndex, out.wrapAsStream()); ReadWriteIOUtils.write(maxPlanIndex, out.wrapAsStream()); out.flush(); } + private void validateChunkEncryptionContext(Chunk chunk) throws IOException { + EncryptParameter sourceParameter = chunk.getEncryptParam(); + boolean targetUsesPageAead = encryptParameter != null && encryptParameter.isTdePageAead(); + boolean sourceUsesPageAead = sourceParameter != null && sourceParameter.isTdePageAead(); + if (!targetUsesPageAead && !sourceUsesPageAead) { + if (chunk.getHeader().getChunkOrdinal() >= 0) { + throw new IOException(Messages.get("error.write.unencrypted_chunk_has_ordinal")); + } + return; + } + throw new IOException(Messages.get("error.write.encrypted_chunk_copy_unsupported")); + } + public void truncate(long offset) throws IOException { out.truncate(offset); } diff --git a/java/tsfile/src/test/java/org/apache/tsfile/encrypt/EncryptionProviderRegistryTest.java b/java/tsfile/src/test/java/org/apache/tsfile/encrypt/EncryptionProviderRegistryTest.java new file mode 100644 index 0000000000..96f46e571e --- /dev/null +++ b/java/tsfile/src/test/java/org/apache/tsfile/encrypt/EncryptionProviderRegistryTest.java @@ -0,0 +1,228 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.encrypt; + +import org.apache.tsfile.exception.encrypt.EncryptException; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.lang.reflect.Field; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class EncryptionProviderRegistryTest { + private static final String SLOW_ID = "registry-slow-provider"; + private static final String GOOD_ID = "registry-good-provider"; + private static final String BAD_ID = "registry-retry-provider"; + private static volatile CountDownLatch entered; + private static volatile CountDownLatch release; + private static volatile boolean failConstruction; + + @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Before + public void setUp() throws Exception { + resetDiscovery(); + entered = new CountDownLatch(1); + release = new CountDownLatch(1); + failConstruction = false; + } + + @After + public void tearDown() throws Exception { + release.countDown(); + for (String id : new String[] {SLOW_ID, GOOD_ID, BAD_ID}) { + EncryptionProviderRegistry.unregisterProvider(id); + } + resetDiscovery(); + } + + private static void resetDiscovery() throws Exception { + Field field = EncryptionProviderRegistry.class.getDeclaredField("servicesLoaded"); + field.setAccessible(true); + field.setBoolean(null, false); + } + + @Test + public void testConcurrentFirstUseWaitsForServiceRegistration() throws Exception { + try (URLClassLoader loader = serviceLoader(SlowProvider.class)) { + ExecutorService pool = Executors.newFixedThreadPool(2); + CountDownLatch secondStarted = new CountDownLatch(1); + try { + Future first = pool.submit(() -> createThrough(loader, SLOW_ID)); + assertTrue(entered.await(10, TimeUnit.SECONDS)); + Future second = + pool.submit( + () -> { + secondStarted.countDown(); + createThrough(loader, SLOW_ID); + }); + assertTrue(secondStarted.await(10, TimeUnit.SECONDS)); + Thread.sleep(100); + assertFalse(second.isDone()); + release.countDown(); + first.get(10, TimeUnit.SECONDS); + second.get(10, TimeUnit.SECONDS); + } finally { + release.countDown(); + pool.shutdownNow(); + assertTrue(pool.awaitTermination(10, TimeUnit.SECONDS)); + } + } + } + + @Test + public void testFailedDiscoveryIsRetryableAndContinuesRegistration() throws Exception { + failConstruction = true; + try (URLClassLoader loader = serviceLoader(RetryProvider.class, GoodProvider.class)) { + assertThrows(EncryptException.class, () -> createThrough(loader, GOOD_ID)); + Field field = EncryptionProviderRegistry.class.getDeclaredField("PROVIDERS"); + field.setAccessible(true); + assertTrue(((java.util.Map) field.get(null)).containsKey(GOOD_ID)); + failConstruction = false; + createThrough(loader, GOOD_ID); + createThrough(loader, BAD_ID); + } + } + + @Test + public void testUnsupportedChunkAuthenticationNamesImplementation() { + IEncrypt legacy = new UNENCRYPTED(null); + try (legacy) { + assertTrue( + assertThrows(EncryptException.class, () -> legacy.authenticateChunk(null)) + .getMessage() + .contains(legacy.getClass().getName())); + assertTrue( + assertThrows( + EncryptException.class, () -> legacy.verifyChunkAuthentication(new byte[0], null)) + .getMessage() + .contains(legacy.getClass().getName())); + } + } + + @Test + public void testLegacyOperationsAreRejectedEvenForPermissiveProvider() { + IEncrypt permissive = new UNENCRYPTED(null); + try (IEncrypt guarded = new PageAeadEncryption(permissive)) { + assertThrows(EncryptException.class, () -> guarded.getEncryptor().encrypt(new byte[1])); + assertThrows(EncryptException.class, () -> guarded.getEncryptor().encrypt(new byte[1], 0, 1)); + assertThrows(EncryptException.class, () -> guarded.getDecryptor().decrypt(new byte[1])); + assertThrows(EncryptException.class, () -> guarded.getDecryptor().decrypt(new byte[1], 0, 1)); + } + } + + private URLClassLoader serviceLoader(Class... providers) throws Exception { + Path root = temporaryFolder.newFolder().toPath(); + Path descriptor = root.resolve("META-INF/services/" + IEncryptProvider.class.getName()); + Files.createDirectories(descriptor.getParent()); + StringBuilder names = new StringBuilder(); + for (Class provider : providers) { + names.append(provider.getName()).append('\n'); + } + Files.write(descriptor, names.toString().getBytes(StandardCharsets.UTF_8)); + return new URLClassLoader( + new java.net.URL[] {root.toUri().toURL()}, getClass().getClassLoader()); + } + + private static void createThrough(ClassLoader loader, String providerId) { + ClassLoader original = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(loader); + try (EncryptParameter parameter = + EncryptParameter.pageAeadBuilder() + .key(new byte[16]) + .providerId(providerId) + .profileId(TestAeadEncryptionProvider.PROFILE_ID) + .keyId("test-key") + .keyVersion("1") + .wrappedDataKey(new byte[24]) + .fileCryptoId(new byte[16]) + .build(); + IEncrypt encrypt = EncryptionProviderRegistry.create(parameter)) { + assertEquals(28, encrypt.getPageBodyOverhead()); + } finally { + Thread.currentThread().setContextClassLoader(original); + } + } + + public static class SlowProvider implements IEncryptProvider { + public SlowProvider() { + entered.countDown(); + try { + if (!release.await(10, TimeUnit.SECONDS)) { + throw new IllegalStateException("Service registration probe timed out"); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IllegalStateException(e); + } + } + + @Override + public String getProviderId() { + return SLOW_ID; + } + + @Override + public IEncrypt create(EncryptParameter parameter) { + return TestAeadEncryptionProvider.INSTANCE.create(parameter); + } + } + + public static class GoodProvider implements IEncryptProvider { + @Override + public String getProviderId() { + return GOOD_ID; + } + + @Override + public IEncrypt create(EncryptParameter parameter) { + return TestAeadEncryptionProvider.INSTANCE.create(parameter); + } + } + + public static class RetryProvider extends GoodProvider { + public RetryProvider() { + if (failConstruction) { + throw new IllegalStateException("Injected service discovery failure"); + } + } + + @Override + public String getProviderId() { + return BAD_ID; + } + } +} diff --git a/java/tsfile/src/test/java/org/apache/tsfile/encrypt/PageCryptoContextTest.java b/java/tsfile/src/test/java/org/apache/tsfile/encrypt/PageCryptoContextTest.java new file mode 100644 index 0000000000..efd4f3a46f --- /dev/null +++ b/java/tsfile/src/test/java/org/apache/tsfile/encrypt/PageCryptoContextTest.java @@ -0,0 +1,426 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.encrypt; + +import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.file.metadata.enums.EncryptionType; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class PageCryptoContextTest { + + @BeforeClass + public static void setUpProvider() { + EncryptionProviderRegistry.registerProvider(TestAeadEncryptionProvider.INSTANCE); + } + + @AfterClass + public static void tearDownProvider() { + EncryptionProviderRegistry.unregisterProvider(TestAeadEncryptionProvider.PROVIDER_ID); + } + + @Before + public void resetProviderCount() { + TestAeadEncryptionProvider.resetCreateCount(); + } + + @Test + public void testAeadRejectsTamperingAndPageSwap() { + byte[] dataKey = new byte[16]; + Arrays.fill(dataKey, (byte) 0x5A); + byte[] fileCryptoId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + Arrays.fill(fileCryptoId, (byte) 0x2C); + EncryptParameter parameter = TestAeadEncryptionProvider.createParameter(dataKey, fileCryptoId); + byte[] plaintext = new byte[] {1, 3, 5, 7, 9, 11, 13, 15}; + + try { + IEncryptor encryptor = IEncryptor.getEncryptor(parameter); + IDecryptor decryptor = IDecryptor.getDecryptor(parameter); + PageCryptoContext page0 = + PageCryptoContext.forEncryption(parameter, plaintext.length, plaintext.length, 0, 0); + PageCryptoContext page1 = + PageCryptoContext.forEncryption(parameter, plaintext.length, plaintext.length, 1, 0); + PageCryptoContext anotherChunkPage0 = + PageCryptoContext.forEncryption(parameter, plaintext.length, plaintext.length, 0, 1); + + byte[] encryptedPage0 = encryptor.encryptPage(plaintext, 0, plaintext.length, page0); + byte[] encryptedPage1 = encryptor.encryptPage(plaintext, 0, plaintext.length, page1); + assertArrayEquals( + plaintext, decryptor.decryptPage(encryptedPage0, 0, encryptedPage0.length, page0)); + assertArrayEquals( + plaintext, decryptor.decryptPage(encryptedPage1, 0, encryptedPage1.length, page1)); + + assertThrows( + EncryptException.class, + () -> decryptor.decryptPage(encryptedPage0, 0, encryptedPage0.length, page1)); + assertThrows( + EncryptException.class, + () -> decryptor.decryptPage(encryptedPage0, 0, encryptedPage0.length, anotherChunkPage0)); + + encryptedPage0[encryptedPage0.length - 1] ^= 1; + assertThrows( + EncryptException.class, + () -> decryptor.decryptPage(encryptedPage0, 0, encryptedPage0.length, page0)); + + assertEquals(1, TestAeadEncryptionProvider.getCreateCount()); + } finally { + parameter.close(); + } + } + + @Test + public void testPaddedLengthFramingAndAuthenticatedCandidate() { + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createPaddedParameter(new byte[16], new byte[16])) { + for (int size : new int[] {0, 1, 15, 16, 17, 31, 32, 65536}) { + byte[] plaintext = new byte[size]; + PageCryptoContext write = PageCryptoContext.forEncryption(parameter, size, size, 0, 2); + byte[] body = IEncryptor.getEncryptor(parameter).encryptPage(plaintext, 0, size, write); + assertEquals(write.getEncryptedPageBodySize(), body.length); + ByteBuffer slice = ByteBuffer.allocateDirect(body.length + 9); + slice.position(5); + slice.put(body).flip().position(5); + PageCryptoContext read = PageCryptoContext.forDecryption(parameter, size, slice, 0, 2); + assertEquals(5, slice.position()); + assertEquals(size, read.getCompressedPlaintextSize()); + assertArrayEquals(write.getAssociatedData(), read.getAssociatedData()); + assertArrayEquals( + plaintext, IDecryptor.getDecryptor(parameter).decryptPage(body, 0, body.length, read)); + if (size > 1 && size % 16 != 0) { + ByteBuffer.wrap(body).putInt(size - 1); + PageCryptoContext modified = + PageCryptoContext.forDecryption(parameter, size, ByteBuffer.wrap(body), 0, 2); + assertThrows( + EncryptException.class, + () -> IDecryptor.getDecryptor(parameter).decryptPage(body, 0, body.length, modified)); + } + } + assertThrows( + EncryptException.class, + () -> PageCryptoContext.forDecryption(parameter, 1, ByteBuffer.allocate(3), 0, 2)); + assertThrows( + EncryptException.class, + () -> + PageCryptoContext.forDecryption( + parameter, 1, ByteBuffer.allocate(48).putInt(-1).flip(), 0, 2)); + } + } + + @Test + public void testChunkOrdinalAllocationSurvivesCopyAndRecovery() { + EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + try { + assertEquals(0, parameter.nextChunkOrdinal()); + try (EncryptParameter copy = parameter.copy()) { + assertEquals(1, copy.nextChunkOrdinal()); + parameter.resumeAfterChunkOrdinal(10); + assertEquals(11, copy.nextChunkOrdinal()); + } + assertEquals(12, parameter.nextChunkOrdinal()); + } finally { + parameter.close(); + } + } + + @Test + public void testRetainedReferencesShareProviderUntilLastClose() { + EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + int previousCloseCount = TestAeadEncryptionProvider.getCloseCount(); + try (EncryptParameter first = parameter.retain(); + EncryptParameter second = parameter.retain()) { + assertSame(first.getOrCreateFileEncrypt(), second.getOrCreateFileEncrypt()); + assertEquals(1, TestAeadEncryptionProvider.getCreateCount()); + parameter.close(); + first.close(); + assertEquals(previousCloseCount, TestAeadEncryptionProvider.getCloseCount()); + assertEquals(28, second.getPageBodyOverhead()); + assertThrows(EncryptException.class, parameter::retain); + assertThrows(EncryptException.class, first::getOrCreateFileEncrypt); + } finally { + parameter.close(); + } + assertEquals(previousCloseCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testRetainedReferenceCanInitializeAfterOriginalCloses() { + EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + try (EncryptParameter retained = parameter.retain()) { + parameter.close(); + byte[] plaintext = {1, 2, 3}; + PageCryptoContext context = PageCryptoContext.forEncryption(retained, 3, 3, 0, 0); + byte[] encrypted = IEncryptor.getEncryptor(retained).encryptPage(plaintext, 0, 3, context); + assertArrayEquals( + plaintext, + IDecryptor.getDecryptor(retained).decryptPage(encrypted, 0, encrypted.length, context)); + assertEquals(1, TestAeadEncryptionProvider.getCreateCount()); + } finally { + parameter.close(); + } + } + + @Test + public void testConcurrentRetainedOwnersCreateAndCloseProviderOnce() throws Exception { + EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + List owners = new ArrayList<>(); + ExecutorService executor = Executors.newFixedThreadPool(4); + CountDownLatch start = new CountDownLatch(1); + int previousCloseCount = TestAeadEncryptionProvider.getCloseCount(); + try { + for (int i = 0; i < 4; i++) { + owners.add(parameter.retain()); + } + parameter.close(); + List> results = new ArrayList<>(); + for (EncryptParameter owner : owners) { + results.add( + executor.submit( + () -> { + try (EncryptParameter reference = owner) { + start.await(); + return reference.getOrCreateFileEncrypt(); + } + })); + } + start.countDown(); + IEncrypt provider = results.get(0).get(10, TimeUnit.SECONDS); + for (Future result : results) { + assertSame(provider, result.get(10, TimeUnit.SECONDS)); + } + assertEquals(1, TestAeadEncryptionProvider.getCreateCount()); + assertEquals(previousCloseCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } finally { + start.countDown(); + executor.shutdownNow(); + for (EncryptParameter owner : owners) { + owner.close(); + } + parameter.close(); + } + } + + @Test + public void testCopyStillOwnsAnIndependentProvider() { + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + EncryptParameter copy = parameter.copy()) { + parameter.getPageBodyOverhead(); + copy.getPageBodyOverhead(); + assertEquals(2, TestAeadEncryptionProvider.getCreateCount()); + parameter.close(); + assertEquals(28, copy.getPageBodyOverhead()); + } + } + + @Test + public void testProviderLookupNormalizesIdLikeRegistration() { + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + EncryptParameter nonCanonical = + EncryptParameter.pageAeadBuilder() + .key(parameter.getKey()) + .providerId(" " + TestAeadEncryptionProvider.PROVIDER_ID + " ") + .profileId(parameter.getProfileId()) + .keyId(parameter.getKeyId()) + .keyVersion(parameter.getKeyVersion()) + .wrappedDataKey(parameter.getWrappedDataKey()) + .fileCryptoId(parameter.getFileCryptoId()) + .build()) { + assertEquals(28, nonCanonical.getPageBodyOverhead()); + } + } + + @Test + public void testPageAeadFileContextRequiresSharedAllocatorAndKey() { + byte[] fileId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + EncryptParameter parameter = TestAeadEncryptionProvider.createParameter(new byte[16], fileId); + EncryptParameter independent = TestAeadEncryptionProvider.createParameter(new byte[16], fileId); + try (EncryptParameter copy = parameter.copy()) { + assertTrue(parameter.sharesPageAeadFileContext(copy)); + assertFalse(parameter.sharesPageAeadFileContext(independent)); + copy.getKey()[0] ^= 1; + assertFalse(parameter.sharesPageAeadFileContext(copy)); + } finally { + parameter.close(); + independent.close(); + } + } + + @Test + public void testAssociatedDataCannotBeModifiedByProvider() { + EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + try { + PageCryptoContext context = PageCryptoContext.forEncryption(parameter, 8, 8, 0, 0); + byte[] expected = context.getAssociatedData(); + byte[] providerCopy = context.getAssociatedData(); + providerCopy[0] ^= 1; + assertArrayEquals(expected, context.getAssociatedData()); + } finally { + parameter.close(); + } + } + + @Test + public void testCachedAssociatedDataPreservesEncodingAndReferenceLifetime() throws Exception { + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter(new byte[16], new byte[16])) { + PageCryptoContext.forEncryption(parameter, 100, 80, 0, 0); + try (EncryptParameter retained = parameter.retain()) { + parameter.close(); + for (int page = 0; page < 100; page++) { + ByteArrayOutputStream expected = new ByteArrayOutputStream(); + DataOutputStream output = new DataOutputStream(expected); + output.writeInt(1); + for (String field : + new String[] { + retained.getProviderId(), retained.getProfileId(), + retained.getKeyId(), retained.getKeyVersion() + }) { + byte[] bytes = field.getBytes(TSFileConfig.STRING_CHARSET); + output.writeInt(bytes.length); + output.write(bytes); + } + output.writeInt(EncryptParameter.FILE_CRYPTO_ID_LENGTH); + output.write(retained.getFileCryptoId()); + output.writeLong(7); + output.writeInt(page); + output.writeInt(100); + output.writeInt(80); + PageCryptoContext context = PageCryptoContext.forEncryption(retained, 100, 80, page, 7); + assertArrayEquals(expected.toByteArray(), context.getAssociatedData()); + context.getAssociatedData()[0] ^= 1; + assertArrayEquals(expected.toByteArray(), context.getAssociatedData()); + assertArrayEquals( + expected.toByteArray(), + PageCryptoContext.forDecryption( + retained, 100, context.getEncryptedPageBodySize(), page, 7) + .getAssociatedData()); + } + } + } + } + + @Test + public void testLegacyCipherCannotSilentlyHandleAuthenticatedPages() { + EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + try { + PageCryptoContext context = PageCryptoContext.forEncryption(parameter, 1, 1, 0, 0); + IEncryptor legacyEncryptor = + new IEncryptor() { + @Override + public byte[] encrypt(byte[] data) { + return data; + } + + @Override + public byte[] encrypt(byte[] data, int offset, int size) { + throw new AssertionError("Legacy encryption must not be called for AEAD pages"); + } + + @Override + public EncryptionType getEncryptionType() { + return EncryptionType.NewWay; + } + }; + IDecryptor legacyDecryptor = + new IDecryptor() { + @Override + public byte[] decrypt(byte[] data) { + return data; + } + + @Override + public byte[] decrypt(byte[] data, int offset, int size) { + throw new AssertionError("Legacy decryption must not be called for AEAD pages"); + } + + @Override + public EncryptionType getEncryptionType() { + return EncryptionType.NewWay; + } + }; + byte[] page = {1}; + assertTrue( + assertThrows( + EncryptException.class, () -> legacyEncryptor.encryptPage(page, 0, 1, context)) + .getMessage() + .contains(legacyEncryptor.getClass().getName())); + assertTrue( + assertThrows( + EncryptException.class, () -> legacyDecryptor.decryptPage(page, 0, 1, context)) + .getMessage() + .contains(legacyDecryptor.getClass().getName())); + } finally { + parameter.close(); + } + } + + @Test + public void testUnregisterProviderTrimsId() { + EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + try { + EncryptionProviderRegistry.create(parameter); + EncryptionProviderRegistry.unregisterProvider( + " " + TestAeadEncryptionProvider.PROVIDER_ID + " "); + assertThrows(EncryptException.class, () -> EncryptionProviderRegistry.create(parameter)); + } finally { + EncryptionProviderRegistry.registerProvider(TestAeadEncryptionProvider.INSTANCE); + parameter.close(); + } + } +} diff --git a/java/tsfile/src/test/java/org/apache/tsfile/encrypt/TestAeadEncryptionProvider.java b/java/tsfile/src/test/java/org/apache/tsfile/encrypt/TestAeadEncryptionProvider.java new file mode 100644 index 0000000000..9ec770392a --- /dev/null +++ b/java/tsfile/src/test/java/org/apache/tsfile/encrypt/TestAeadEncryptionProvider.java @@ -0,0 +1,350 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.encrypt; + +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.file.metadata.enums.EncryptionType; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.SecretKey; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import java.security.GeneralSecurityException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; + +/** JDK-only AEAD provider used to exercise the generic TsFile encryption SPI. */ +public final class TestAeadEncryptionProvider implements IEncryptProvider { + + public static final TestAeadEncryptionProvider INSTANCE = new TestAeadEncryptionProvider(); + public static final String PROVIDER_ID = "test-aead-provider"; + public static final String PROFILE_ID = "AES_GCM_128_AES_WRAP_128_V1"; + public static final String MAC_PROFILE_ID = "TEST_AES_GCM_HMAC_SHA256_V1"; + public static final String PADDED_PROFILE_ID = "TEST_PADDED_AES_GCM_V1"; + + private static final int IV_LENGTH = 12; + private static final int TAG_LENGTH = 16; + + private static final byte[] TEST_KEY_ENCRYPTION_KEY = + new byte[] { + 0x10, 0x32, 0x54, 0x76, 0x01, 0x23, 0x45, 0x67, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x00 + }; + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + private static final AtomicInteger CREATE_COUNT = new AtomicInteger(); + private static final AtomicInteger CLOSE_COUNT = new AtomicInteger(); + + private TestAeadEncryptionProvider() {} + + @Override + public String getProviderId() { + return PROVIDER_ID; + } + + @Override + public IEncrypt create(EncryptParameter encryptParameter) { + if (!PROFILE_ID.equals(encryptParameter.getProfileId()) + && !MAC_PROFILE_ID.equals(encryptParameter.getProfileId()) + && !PADDED_PROFILE_ID.equals(encryptParameter.getProfileId())) { + throw new EncryptException("unsupported test encryption profile"); + } + CREATE_COUNT.incrementAndGet(); + return new TestAeadEncrypt( + resolveDataKey(encryptParameter), + MAC_PROFILE_ID.equals(encryptParameter.getProfileId()), + PADDED_PROFILE_ID.equals(encryptParameter.getProfileId())); + } + + public static EncryptParameter createParameter(byte[] dataKey, byte[] fileCryptoId) { + return createParameter(dataKey, fileCryptoId, PROFILE_ID); + } + + /** Test-only 32-byte proof profile; page bodies still use the existing AES-GCM test cipher. */ + public static EncryptParameter createMacParameter(byte[] dataKey, byte[] fileCryptoId) { + return createParameter(dataKey, fileCryptoId, MAC_PROFILE_ID); + } + + public static EncryptParameter createPaddedParameter(byte[] dataKey, byte[] fileCryptoId) { + return createParameter(dataKey, fileCryptoId, PADDED_PROFILE_ID); + } + + private static EncryptParameter createParameter( + byte[] dataKey, byte[] fileCryptoId, String profile) { + return EncryptParameter.pageAeadBuilder() + .key(dataKey) + .providerId(PROVIDER_ID) + .profileId(profile) + .keyId("test-key") + .keyVersion("1") + .wrappedDataKey(wrapDataKey(dataKey)) + .fileCryptoId(fileCryptoId) + .build(); + } + + public static int getCreateCount() { + return CREATE_COUNT.get(); + } + + public static int getCloseCount() { + return CLOSE_COUNT.get(); + } + + public static void resetCreateCount() { + CREATE_COUNT.set(0); + } + + private static byte[] resolveDataKey(EncryptParameter parameter) { + byte[] dataKey = parameter.getKey(); + if (dataKey != null) { + return Arrays.copyOf(dataKey, dataKey.length); + } + try { + Cipher cipher = Cipher.getInstance("AESWrap"); + cipher.init(Cipher.UNWRAP_MODE, new SecretKeySpec(TEST_KEY_ENCRYPTION_KEY, "AES")); + SecretKey unwrapped = + (SecretKey) cipher.unwrap(parameter.getWrappedDataKey(), "AES", Cipher.SECRET_KEY); + return unwrapped.getEncoded(); + } catch (GeneralSecurityException e) { + throw new EncryptException("test data key unwrap failed", e); + } + } + + private static byte[] wrapDataKey(byte[] dataKey) { + try { + Cipher cipher = Cipher.getInstance("AESWrap"); + cipher.init(Cipher.WRAP_MODE, new SecretKeySpec(TEST_KEY_ENCRYPTION_KEY, "AES")); + return cipher.wrap(new SecretKeySpec(dataKey, "AES")); + } catch (GeneralSecurityException e) { + throw new EncryptException("test data key wrap failed", e); + } + } + + private static final class TestAeadEncrypt implements IEncrypt { + + private final byte[] dataKey; + private final boolean macProof; + private final boolean padded; + + private TestAeadEncrypt(byte[] dataKey, boolean macProof, boolean padded) { + this.dataKey = dataKey; + this.macProof = macProof; + this.padded = padded; + } + + @Override + public IDecryptor getDecryptor() { + return new AeadDecryptor(dataKey, padded); + } + + @Override + public IEncryptor getEncryptor() { + return new AeadEncryptor(dataKey, padded); + } + + @Override + public int getPageBodyOverhead() { + return padded ? -1 : IV_LENGTH + TAG_LENGTH; + } + + @Override + public int getEncryptedPageBodySize(int plaintextSize) { + return padded + ? 32 + (plaintextSize / 16 + 1) * 16 + : IEncrypt.super.getEncryptedPageBodySize(plaintextSize); + } + + @Override + public int getPagePlaintextSize(java.nio.ByteBuffer body) { + if (!padded) { + return IEncrypt.super.getPagePlaintextSize(body); + } + if (body.remaining() < 48) { + throw new EncryptException("Truncated test padded page"); + } + return body.getInt(); + } + + @Override + public int getChunkAuthenticationSize() { + return macProof ? 32 : IV_LENGTH + TAG_LENGTH; + } + + @Override + public byte[] authenticateChunk(ChunkCryptoContext context) { + if (macProof) { + return chunkMac(context); + } + return new AeadEncryptor(dataKey) + .encryptWithAad(new byte[0], 0, 0, context.getAssociatedData()); + } + + @Override + public void verifyChunkAuthentication(byte[] authentication, ChunkCryptoContext context) { + if (macProof) { + if (!java.security.MessageDigest.isEqual(authentication, chunkMac(context))) { + throw new EncryptException("Test chunk MAC mismatch"); + } + return; + } + byte[] plaintext = + new AeadDecryptor(dataKey) + .decryptWithAad( + authentication, 0, authentication.length, context.getAssociatedData()); + if (plaintext.length != 0) { + throw new EncryptException("Unexpected chunk authentication plaintext"); + } + } + + private byte[] chunkMac(ChunkCryptoContext context) { + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(dataKey, "HmacSHA256")); + return mac.doFinal(context.getAssociatedData()); + } catch (GeneralSecurityException e) { + throw new EncryptException("Test chunk MAC failed", e); + } + } + + @Override + public void close() { + Arrays.fill(dataKey, (byte) 0); + CLOSE_COUNT.incrementAndGet(); + } + } + + private static final class AeadEncryptor implements IEncryptor { + + private final byte[] dataKey; + private final boolean padded; + + private AeadEncryptor(byte[] dataKey) { + this(dataKey, false); + } + + private AeadEncryptor(byte[] dataKey, boolean padded) { + this.dataKey = dataKey; + this.padded = padded; + } + + @Override + public byte[] encrypt(byte[] data) { + return encrypt(data, 0, data.length); + } + + @Override + public byte[] encrypt(byte[] data, int offset, int size) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] encryptPage( + byte[] data, int offset, int size, PageCryptoContext pageCryptoContext) { + if (!padded) { + return encryptWithAad(data, offset, size, pageCryptoContext.getAssociatedData()); + } + byte[] input = new byte[(size / 16 + 1) * 16]; + System.arraycopy(data, offset, input, 0, size); + byte[] encrypted = + encryptWithAad(input, 0, input.length, pageCryptoContext.getAssociatedData()); + return java.nio.ByteBuffer.allocate(4 + encrypted.length).putInt(size).put(encrypted).array(); + } + + private byte[] encryptWithAad(byte[] data, int offset, int size, byte[] associatedData) { + byte[] iv = new byte[IV_LENGTH]; + SECURE_RANDOM.nextBytes(iv); + try { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init( + Cipher.ENCRYPT_MODE, + new SecretKeySpec(dataKey, "AES"), + new GCMParameterSpec(TAG_LENGTH * Byte.SIZE, iv)); + cipher.updateAAD(associatedData); + byte[] ciphertext = cipher.doFinal(data, offset, size); + byte[] pageBody = new byte[iv.length + ciphertext.length]; + System.arraycopy(iv, 0, pageBody, 0, iv.length); + System.arraycopy(ciphertext, 0, pageBody, iv.length, ciphertext.length); + return pageBody; + } catch (GeneralSecurityException e) { + throw new EncryptException("test page encryption failed", e); + } + } + + @Override + public EncryptionType getEncryptionType() { + return EncryptionType.NewWay; + } + } + + private static final class AeadDecryptor implements IDecryptor { + + private final byte[] dataKey; + private final boolean padded; + + private AeadDecryptor(byte[] dataKey) { + this(dataKey, false); + } + + private AeadDecryptor(byte[] dataKey, boolean padded) { + this.dataKey = dataKey; + this.padded = padded; + } + + @Override + public byte[] decrypt(byte[] data) { + return decrypt(data, 0, data.length); + } + + @Override + public byte[] decrypt(byte[] data, int offset, int size) { + throw new UnsupportedOperationException(); + } + + @Override + public byte[] decryptPage( + byte[] data, int offset, int size, PageCryptoContext pageCryptoContext) { + if (!padded) { + return decryptWithAad(data, offset, size, pageCryptoContext.getAssociatedData()); + } + byte[] decoded = + decryptWithAad(data, offset + 4, size - 4, pageCryptoContext.getAssociatedData()); + return Arrays.copyOf(decoded, pageCryptoContext.getCompressedPlaintextSize()); + } + + private byte[] decryptWithAad(byte[] data, int offset, int size, byte[] associatedData) { + try { + GCMParameterSpec spec = + new GCMParameterSpec(TAG_LENGTH * Byte.SIZE, data, offset, IV_LENGTH); + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(dataKey, "AES"), spec); + cipher.updateAAD(associatedData); + return cipher.doFinal(data, offset + IV_LENGTH, size - IV_LENGTH); + } catch (GeneralSecurityException e) { + throw new EncryptException("test page decryption failed", e); + } + } + + @Override + public EncryptionType getEncryptionType() { + return EncryptionType.NewWay; + } + } +} diff --git a/java/tsfile/src/test/java/org/apache/tsfile/file/header/FileEncryptionHeaderTest.java b/java/tsfile/src/test/java/org/apache/tsfile/file/header/FileEncryptionHeaderTest.java new file mode 100644 index 0000000000..38ab61650e --- /dev/null +++ b/java/tsfile/src/test/java/org/apache/tsfile/file/header/FileEncryptionHeaderTest.java @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.file.header; + +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.TestAeadEncryptionProvider; +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.file.MetaMarker; +import org.apache.tsfile.utils.ReadWriteForEncodingUtils; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +public class FileEncryptionHeaderTest { + + @Test + public void testRoundTrip() throws IOException { + byte[] dataKey = new byte[16]; + byte[] fileCryptoId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + for (int i = 0; i < dataKey.length; i++) { + dataKey[i] = (byte) (i + 1); + fileCryptoId[i] = (byte) (0x40 + i); + } + EncryptParameter parameter = TestAeadEncryptionProvider.createParameter(dataKey, fileCryptoId); + + ByteArrayOutputStream output = new ByteArrayOutputStream(); + FileEncryptionHeader.serialize(parameter, output); + byte[] serialized = output.toByteArray(); + assertEquals(MetaMarker.ENCRYPTION_HEADER, serialized[0]); + assertFalse(contains(serialized, dataKey)); + + ByteArrayInputStream input = new ByteArrayInputStream(serialized); + assertEquals(MetaMarker.ENCRYPTION_HEADER, input.read()); + EncryptParameter restored = FileEncryptionHeader.deserialize(input); + + assertNull(restored.getType()); + assertNull(restored.getKey()); + assertEquals(parameter.getProviderId(), restored.getProviderId()); + assertEquals(parameter.getProfileId(), restored.getProfileId()); + assertEquals(parameter.getKeyId(), restored.getKeyId()); + assertEquals(parameter.getKeyVersion(), restored.getKeyVersion()); + assertArrayEquals(parameter.getFileCryptoId(), restored.getFileCryptoId()); + assertArrayEquals(parameter.getWrappedDataKey(), restored.getWrappedDataKey()); + } + + @Test + public void testLargeHeaderUsesExactSerializedSize() throws IOException { + try (EncryptParameter parameter = + EncryptParameter.pageAeadBuilder() + .providerId("provider") + .profileId("profile") + .keyId("k".repeat(300)) + .keyVersion("1") + .fileCryptoId(new byte[16]) + .wrappedDataKey(new byte[20000]) + .build()) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + int serializedSize = FileEncryptionHeader.serialize(parameter, output); + assertEquals(serializedSize, output.size()); + ByteArrayInputStream input = new ByteArrayInputStream(output.toByteArray()); + assertEquals(MetaMarker.ENCRYPTION_HEADER, input.read()); + int payloadSize = ReadWriteForEncodingUtils.readUnsignedVarInt(input); + assertEquals(payloadSize, input.available()); + input = new ByteArrayInputStream(output.toByteArray()); + input.read(); + try (EncryptParameter restored = FileEncryptionHeader.deserialize(input)) { + assertEquals(parameter.getKeyId(), restored.getKeyId()); + assertArrayEquals(parameter.getWrappedDataKey(), restored.getWrappedDataKey()); + assertEquals(0, input.available()); + } + } + } + + @Test + public void testOversizedHeaderDoesNotWriteOutput() { + try (EncryptParameter parameter = + EncryptParameter.pageAeadBuilder() + .providerId("provider") + .profileId("profile") + .keyId("key") + .keyVersion("1") + .fileCryptoId(new byte[16]) + .wrappedDataKey(new byte[FileEncryptionHeader.MAX_HEADER_SIZE]) + .build()) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + assertThrows(EncryptException.class, () -> FileEncryptionHeader.serialize(parameter, output)); + assertEquals(0, output.size()); + } + } + + @Test + public void testRejectInvalidFileCryptoId() { + EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter(new byte[16], new byte[8]); + assertThrows( + EncryptException.class, + () -> FileEncryptionHeader.serialize(parameter, new ByteArrayOutputStream())); + } + + private static boolean contains(byte[] data, byte[] target) { + for (int i = 0; i <= data.length - target.length; i++) { + boolean matches = true; + for (int j = 0; j < target.length; j++) { + if (data[i + j] != target[j]) { + matches = false; + break; + } + } + if (matches) { + return true; + } + } + return false; + } +} diff --git a/java/tsfile/src/test/java/org/apache/tsfile/read/PageAeadReaderLifecycleTest.java b/java/tsfile/src/test/java/org/apache/tsfile/read/PageAeadReaderLifecycleTest.java new file mode 100644 index 0000000000..1d2a497a30 --- /dev/null +++ b/java/tsfile/src/test/java/org/apache/tsfile/read/PageAeadReaderLifecycleTest.java @@ -0,0 +1,477 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.read; + +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.EncryptionProviderRegistry; +import org.apache.tsfile.encrypt.TestAeadEncryptionProvider; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.file.header.ChunkHeader; +import org.apache.tsfile.file.metadata.AlignedChunkMetadata; +import org.apache.tsfile.file.metadata.ChunkMetadata; +import org.apache.tsfile.file.metadata.IDeviceID; +import org.apache.tsfile.file.metadata.enums.TSEncoding; +import org.apache.tsfile.read.common.Chunk; +import org.apache.tsfile.read.common.Path; +import org.apache.tsfile.read.controller.CachedChunkLoaderImpl; +import org.apache.tsfile.read.controller.IChunkLoader; +import org.apache.tsfile.read.expression.QueryExpression; +import org.apache.tsfile.read.query.dataset.QueryDataSet; +import org.apache.tsfile.read.query.dataset.TreeResultSet; +import org.apache.tsfile.read.reader.BufferedTsFileInput; +import org.apache.tsfile.read.reader.IChunkReader; +import org.apache.tsfile.read.reader.chunk.AlignedChunkReader; +import org.apache.tsfile.read.reader.chunk.ChunkReader; +import org.apache.tsfile.read.reader.series.FileSeriesReader; +import org.apache.tsfile.read.reader.series.FileSeriesReaderByTimestamp; +import org.apache.tsfile.utils.TsPrimitiveType; +import org.apache.tsfile.write.TsFileWriter; +import org.apache.tsfile.write.record.TSRecord; +import org.apache.tsfile.write.record.datapoint.LongDataPoint; +import org.apache.tsfile.write.schema.MeasurementSchema; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class PageAeadReaderLifecycleTest { + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeClass + public static void registerProvider() { + EncryptionProviderRegistry.registerProvider(TestAeadEncryptionProvider.INSTANCE); + } + + @AfterClass + public static void unregisterProvider() { + EncryptionProviderRegistry.unregisterProvider(TestAeadEncryptionProvider.PROVIDER_ID); + } + + @Test + public void testPlaintextQueriesAreNotStronglyRetained() throws Exception { + File file = new File(temporaryFolder.getRoot(), "plain.tsfile"); + try (TsFileWriter writer = new TsFileWriter(file)) { + writer.registerTimeseries(new Path("d1"), new MeasurementSchema("s1", TSDataType.INT64)); + for (int i = 1; i <= 3; i++) { + writer.writeRecord(new TSRecord("d1", i).addTuple(new LongDataPoint("s1", i))); + } + } + try (TsFileReader reader = new TsFileReader(file)) { + for (int i = 0; i < 100; i++) { + QueryDataSet query = reader.query(queryExpression()); + assertTrue(query.hasNext()); + query.next(); + } + java.lang.reflect.Field field = TsFileReader.class.getDeclaredField("queryDataSets"); + field.setAccessible(true); + assertTrue(((java.util.Set) field.get(reader)).isEmpty()); + } + } + + @Test + public void testPaddedBodiesInAlignedAndNonAlignedQueries() throws Exception { + for (boolean aligned : new boolean[] {false, true}) { + File file = writeFile(aligned, true); + try (TsFileReader reader = new TsFileReader(file)) { + QueryDataSet query = reader.query(queryExpression()); + int count = 0; + while (query.hasNext()) { + assertEquals(++count * 10L, query.next().getFields().get(0).getLongV()); + } + assertEquals(3, count); + } + } + } + + @Test + public void testSeriesScanReleasesEachChunkAndCreatesOneProvider() throws Exception { + File file = writeFile(false); + int createCount = TestAeadEncryptionProvider.getCreateCount(); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + TrackingChunkLoader loader = new TrackingChunkLoader(sequence); + FileSeriesReader reader = + new FileSeriesReader( + loader, + new ArrayList<>(sequence.getChunkMetadataList(new Path("d1", "s1", true))), + null); + try { + int count = 0; + while (reader.hasNextBatch()) { + assertEquals(++count * 10L, reader.nextBatch().currentValue()); + } + assertEquals(3, count); + assertEquals(3, loader.loaded.size()); + assertAllDestroyed(loader.loaded); + assertEquals(createCount + 1, TestAeadEncryptionProvider.getCreateCount()); + } finally { + reader.close(); + } + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testTimestampReaderReleasesChunksOnSwitchAndEarlyClose() throws Exception { + File file = writeFile(false); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + TrackingChunkLoader loader = new TrackingChunkLoader(sequence); + try (FileSeriesReaderByTimestamp reader = + new FileSeriesReaderByTimestamp( + loader, new ArrayList<>(sequence.getChunkMetadataList(new Path("d1", "s1", true))))) { + assertEquals(10L, reader.getValueInTimestamp(1)); + assertFalse(loader.loaded.get(0).getEncryptParam().isDestroyed()); + assertEquals(20L, reader.getValueInTimestamp(2)); + assertTrue(loader.loaded.get(0).getEncryptParam().isDestroyed()); + } + assertAllDestroyed(loader.loaded); + } + } + + @Test + public void testTimestampReaderPreservesLoadFailureAndClosesRemainingChunks() throws Exception { + IChunkLoader loader = mock(IChunkLoader.class); + AlignedChunkMetadata aligned = mock(AlignedChunkMetadata.class); + ChunkMetadata timeMetadata = mock(ChunkMetadata.class); + ChunkMetadata firstMetadata = mock(ChunkMetadata.class); + ChunkMetadata secondMetadata = mock(ChunkMetadata.class); + Chunk timeChunk = mock(Chunk.class); + Chunk firstChunk = mock(Chunk.class); + IOException loadFailure = new IOException("load failed"); + IllegalStateException closeFailure = new IllegalStateException("close failed"); + when(aligned.getEndTime()).thenReturn(Long.MAX_VALUE); + when(aligned.getTimeChunkMetadata()).thenReturn(timeMetadata); + when(aligned.getValueChunkMetadataList()) + .thenReturn(Arrays.asList(firstMetadata, secondMetadata)); + when(loader.loadChunk(timeMetadata)).thenReturn(timeChunk); + when(loader.loadChunk(firstMetadata)).thenReturn(firstChunk); + when(loader.loadChunk(secondMetadata)).thenThrow(loadFailure); + doThrow(closeFailure).when(timeChunk).close(); + try (FileSeriesReaderByTimestamp reader = + new FileSeriesReaderByTimestamp(loader, Arrays.asList(aligned))) { + IOException actual = assertThrows(IOException.class, () -> reader.getValueInTimestamp(1)); + assertSame(loadFailure, actual); + assertEquals(1, actual.getSuppressed().length); + assertSame(closeFailure, actual.getSuppressed()[0]); + verify(timeChunk).close(); + verify(firstChunk).close(); + } + } + + @Test + public void testCachedChunkReaderSurvivesFileCloseAndReleasesProvider() throws Exception { + File file = writeFile(false); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + IChunkReader chunkReader; + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + CachedChunkLoaderImpl loader = new CachedChunkLoaderImpl(sequence); + chunkReader = + loader.getChunkReader( + sequence.getChunkMetadataList(new Path("d1", "s1", true)).get(0), null); + } + try { + assertEquals(10L, chunkReader.nextPageData().currentValue()); + assertEquals(closeCount, TestAeadEncryptionProvider.getCloseCount()); + } finally { + chunkReader.close(); + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testAlignedReaderClosesTimeAndAllValueContexts() throws Exception { + File file = writeFile(true); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + Chunk time; + List values = new ArrayList<>(); + AlignedChunkReader reader; + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + Map> metadata = + sequence.readChunkMetadataInDevice(IDeviceID.Factory.DEFAULT_FACTORY.create("d1")); + time = sequence.readMemChunk(metadata.get("").get(0)); + values.add(sequence.readMemChunk(metadata.get("s1").get(0))); + values.add(sequence.readMemChunk(metadata.get("s2").get(0))); + values.add(null); + reader = new AlignedChunkReader(time, values); + } + try { + assertTrue(reader.hasNextSatisfiedPage()); + reader.nextPageData(); + } finally { + reader.close(); + reader.close(); + } + assertTrue(time.getEncryptParam().isDestroyed()); + assertAllDestroyed(values); + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testMalformedChunkReaderConstructionReleasesContext() throws Exception { + File file = writeFile(false); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath()); + Chunk chunk = + sequence.readMemChunk( + sequence.getChunkMetadataList(new Path("d1", "s1", true)).get(0))) { + chunk.getData().limit(2); + assertThrows(RuntimeException.class, () -> new ChunkReader(chunk)); + assertTrue(chunk.getEncryptParam().isDestroyed()); + } + } + + @Test + public void testMalformedAlignedReaderConstructionReleasesAllContexts() throws Exception { + File file = writeFile(true); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + Map> metadata = + sequence.readChunkMetadataInDevice(IDeviceID.Factory.DEFAULT_FACTORY.create("d1")); + try (Chunk time = sequence.readMemChunk(metadata.get("").get(0)); + Chunk first = sequence.readMemChunk(metadata.get("s1").get(0)); + Chunk second = sequence.readMemChunk(metadata.get("s2").get(0))) { + time.getData().limit(2); + assertThrows( + IOException.class, + () -> new AlignedChunkReader(time, Arrays.asList(first, second, null))); + assertAllDestroyed(Arrays.asList(time, first, second)); + } + } + } + + @Test + public void testTreeResultSetEarlyCloseReleasesQueryWithoutClosingFile() throws Exception { + File file = writeFile(false); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + try (TsFileReader reader = new TsFileReader(file)) { + QueryDataSet query = reader.query(queryExpression()); + TreeResultSet result = new TreeResultSet(query, Arrays.asList("d1"), Arrays.asList("s1")); + assertTrue(result.next()); + result.close(); + result.close(); + assertTrue(query.isClosed()); + assertFalse(query.hasNext()); + try (QueryDataSet another = reader.query(queryExpression())) { + assertTrue(another.hasNext()); + assertEquals(10L, another.next().getFields().get(0).getLongV()); + } + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testFileReaderClosesAnAbandonedQuery() throws Exception { + File file = writeFile(false); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + QueryDataSet query; + try (TsFileReader reader = new TsFileReader(file)) { + query = reader.query(queryExpression()); + assertTrue(query.hasNext()); + } + assertTrue(query.isClosed()); + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testQueryLimitClosesRemainingChunkReferences() throws Exception { + File file = writeFile(false); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + try (TsFileReader reader = new TsFileReader(file)) { + QueryDataSet query = reader.query(queryExpression()); + query.setRowLimit(1); + assertTrue(query.hasNext()); + assertEquals(10L, query.next().getFields().get(0).getLongV()); + assertFalse(query.hasNext()); + assertTrue(query.isClosed()); + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testAlignedIteratorFailureClosesPreviouslyLoadedChunks() throws Exception { + File file = writeFile(true); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + try (FailingSequenceReader sequence = new FailingSequenceReader(file)) { + sequence.getEncryptParam().getPageBodyOverhead(); + Map> metadata = + sequence.readChunkMetadataInDevice(IDeviceID.Factory.DEFAULT_FACTORY.create("d1")); + AlignedChunkMetadata aligned = + new AlignedChunkMetadata( + metadata.get("").get(0), + Arrays.asList(metadata.get("s1").get(0), metadata.get("s2").get(0))); + TsFileAlignedSeriesReaderIterator iterator = + new TsFileAlignedSeriesReaderIterator( + sequence, + Arrays.asList(aligned), + Arrays.asList( + new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.PLAIN), + new MeasurementSchema("s2", TSDataType.INT64, TSEncoding.PLAIN))); + assertThrows(IOException.class, iterator::nextReader); + assertEquals(2, sequence.loaded.size()); + assertAllDestroyed(sequence.loaded); + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testAlignedRewriteReleasesTemporaryReferencesAndKeepsSources() throws Exception { + File file = writeFile(true); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + Map> metadata = + sequence.readChunkMetadataInDevice(IDeviceID.Factory.DEFAULT_FACTORY.create("d1")); + try (Chunk time = sequence.readMemChunk(metadata.get("").get(0)); + Chunk value = sequence.readMemChunk(metadata.get("s1").get(0)); + Chunk rewritten = value.rewrite(TSDataType.DOUBLE, time)) { + assertFalse(time.getEncryptParam().isDestroyed()); + assertFalse(value.getEncryptParam().isDestroyed()); + AlignedChunkReader chunkReader = new AlignedChunkReader(time, Arrays.asList(rewritten)); + TsPrimitiveType[] values = (TsPrimitiveType[]) chunkReader.nextPageData().currentValue(); + assertEquals(10.0, values[0].getDouble(), 0.0); + } + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testMetadataOffsetConstructorsRemainCompatibleAndLoadLazily() throws Exception { + File file = writeFile(false); + try (TsFileSequenceReader original = new TsFileSequenceReader(file.getPath())) { + ChunkMetadata metadata = original.getChunkMetadataList(new Path("d1", "s1", true)).get(0); + for (boolean withParameter : new boolean[] {false, true}) { + BufferedTsFileInput input = new BufferedTsFileInput(file.toPath()); + try (TsFileSequenceReader reader = + metadataOffsetReader( + input, + original.getFileMetadataPos(), + original.getTsFileMetadataSize(), + withParameter)) { + reader.position(metadata.getOffsetOfChunkHeader()); + ChunkHeader header = reader.readChunkHeader(reader.readMarker()); + assertTrue(header.getChunkOrdinal() >= 0); + try (Chunk chunk = reader.readMemChunk(metadata)) { + assertEquals(10L, new ChunkReader(chunk).nextPageData().currentValue()); + } + } + } + } + } + + // Deliberately has no throws clause: callers of the old public constructors must still compile. + private static TsFileSequenceReader metadataOffsetReader( + BufferedTsFileInput input, long position, int size, boolean withParameter) { + return withParameter + ? new TsFileSequenceReader(input, position, size, null) + : new TsFileSequenceReader(input, position, size); + } + + private File writeFile(boolean aligned) throws Exception { + return writeFile(aligned, false); + } + + private File writeFile(boolean aligned, boolean padded) throws Exception { + File file = + new File(temporaryFolder.getRoot(), "lifecycle-" + aligned + "-" + padded + ".tsfile"); + try (EncryptParameter parameter = + padded + ? TestAeadEncryptionProvider.createPaddedParameter(new byte[16], new byte[16]) + : TestAeadEncryptionProvider.createParameter(new byte[16], new byte[16]); + TsFileWriter writer = new TsFileWriter(file, parameter)) { + MeasurementSchema first = new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.PLAIN); + if (aligned) { + writer.registerAlignedTimeseries( + new Path("d1"), + Arrays.asList(first, new MeasurementSchema("s2", TSDataType.INT64, TSEncoding.PLAIN))); + } else { + writer.registerTimeseries(new Path("d1"), first); + } + for (int i = 1; i <= 3; i++) { + TSRecord record = new TSRecord("d1", i).addTuple(new LongDataPoint("s1", i * 10L)); + if (aligned) { + record.addTuple(new LongDataPoint("s2", i * 100L)); + } + writer.writeRecord(record); + writer.flush(); + } + } + return file; + } + + private static QueryExpression queryExpression() { + return QueryExpression.create(Arrays.asList(new Path("d1", "s1", true)), null); + } + + private static void assertAllDestroyed(List chunks) { + for (Chunk chunk : chunks) { + if (chunk != null) { + assertTrue(chunk.getEncryptParam().isDestroyed()); + } + } + } + + private static class TrackingChunkLoader extends CachedChunkLoaderImpl { + private final List loaded = new ArrayList<>(); + + private TrackingChunkLoader(TsFileSequenceReader reader) { + super(reader); + } + + @Override + public Chunk loadChunk(ChunkMetadata metadata) throws IOException { + Chunk chunk = super.loadChunk(metadata); + loaded.add(chunk); + return chunk; + } + } + + private static class FailingSequenceReader extends TsFileSequenceReader { + private final List loaded = new ArrayList<>(); + + private FailingSequenceReader(File file) throws IOException { + super(file.getPath()); + } + + @Override + public Chunk readMemChunk(ChunkMetadata metadata) throws IOException { + if (loaded.size() == 2) { + throw new IOException("Injected chunk read failure"); + } + Chunk chunk = super.readMemChunk(metadata); + loaded.add(chunk); + return chunk; + } + } +} diff --git a/java/tsfile/src/test/java/org/apache/tsfile/read/TsFileSequenceReaderHeaderTest.java b/java/tsfile/src/test/java/org/apache/tsfile/read/TsFileSequenceReaderHeaderTest.java new file mode 100644 index 0000000000..0530c316a1 --- /dev/null +++ b/java/tsfile/src/test/java/org/apache/tsfile/read/TsFileSequenceReaderHeaderTest.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.read; + +import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.TestAeadEncryptionProvider; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.file.MetaMarker; +import org.apache.tsfile.file.header.ChunkHeader; +import org.apache.tsfile.file.header.FileEncryptionHeader; +import org.apache.tsfile.file.metadata.enums.CompressionType; +import org.apache.tsfile.file.metadata.enums.TSEncoding; +import org.apache.tsfile.read.reader.BufferedTsFileInput; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.util.concurrent.atomic.AtomicLong; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class TsFileSequenceReaderHeaderTest { + + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + @Test + public void testPlainMarkerIsReadWithVersion() throws Exception { + ByteArrayOutputStream bytes = baseHeader(); + bytes.write(MetaMarker.SEPARATOR); + BufferedTsFileInput input = input(bytes); + AtomicLong ioBytes = new AtomicLong(); + try (TsFileSequenceReader reader = + new TsFileSequenceReader(input, false, false, ioBytes::addAndGet)) { + assertFalse(reader.hasFileEncryptionHeader()); + assertEquals(7, reader.getDataStartOffset()); + assertEquals(0, reader.position()); + assertEquals(2, ioBytes.get()); + verify(input, times(1)).read(any(ByteBuffer.class)); + verify(input, never()).position(7); + verify(input, never()).wrapAsInputStream(); + } + } + + @Test + public void testEncryptedMarkerIsReadWithVersion() throws Exception { + ByteArrayOutputStream bytes = baseHeader(); + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter(new byte[16], new byte[16])) { + FileEncryptionHeader.serialize(parameter, bytes); + } + BufferedTsFileInput input = input(bytes); + AtomicLong ioBytes = new AtomicLong(); + try (TsFileSequenceReader reader = + new TsFileSequenceReader(input, false, false, ioBytes::addAndGet)) { + assertTrue(reader.hasFileEncryptionHeader()); + assertEquals(bytes.size(), reader.getDataStartOffset()); + assertEquals(0, reader.position()); + assertEquals(bytes.size() - 6, ioBytes.get()); + verify(input, never()).position(7); + } + } + + @Test + public void testBaseHeaderWithoutMarker() throws Exception { + AtomicLong ioBytes = new AtomicLong(); + try (TsFileSequenceReader reader = + new TsFileSequenceReader(input(baseHeader()), false, false, ioBytes::addAndGet)) { + assertFalse(reader.hasFileEncryptionHeader()); + assertEquals(7, reader.getDataStartOffset()); + assertEquals(1, ioBytes.get()); + } + } + + @Test + public void testShortReadsStillPrefetchMarker() throws Exception { + ByteArrayOutputStream bytes = baseHeader(); + bytes.write(MetaMarker.SEPARATOR); + BufferedTsFileInput input = input(bytes); + doAnswer( + invocation -> { + ByteBuffer buffer = invocation.getArgument(0); + int limit = buffer.limit(); + buffer.limit(buffer.position() + 1); + try { + return invocation.callRealMethod(); + } finally { + buffer.limit(limit); + } + }) + .when(input) + .read(any(ByteBuffer.class)); + try (TsFileSequenceReader reader = new TsFileSequenceReader(input, false, false, null)) { + assertFalse(reader.hasFileEncryptionHeader()); + verify(input, times(2)).read(any(ByteBuffer.class)); + verify(input, never()).wrapAsInputStream(); + } + } + + @Test + public void testLazyHeaderLoadingPreservesPosition() throws Exception { + ByteArrayOutputStream bytes = baseHeader(); + bytes.write(MetaMarker.SEPARATOR); + BufferedTsFileInput input = input(bytes); + input.position(8); + try (TsFileSequenceReader reader = new TsFileSequenceReader(input, 8L, 0)) { + assertFalse(reader.hasFileEncryptionHeader()); + assertEquals(8, reader.position()); + } + } + + @Test + public void testChunkHeaderIncludesOrdinalInDescription() { + ChunkHeader header = + new ChunkHeader( + "s1", 10, TSDataType.INT64, CompressionType.LZ4, TSEncoding.PLAIN, 1, 0, 42); + assertTrue(header.toString().contains("chunkOrdinal=42")); + ChunkHeader plainHeader = + new ChunkHeader("s1", 10, TSDataType.INT64, CompressionType.LZ4, TSEncoding.PLAIN, 1); + assertTrue(plainHeader.toString().contains("chunkOrdinal=-1")); + } + + private ByteArrayOutputStream baseHeader() throws Exception { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + bytes.write(TSFileConfig.MAGIC_STRING.getBytes(TSFileConfig.STRING_CHARSET)); + bytes.write(TSFileConfig.VERSION_NUMBER); + return bytes; + } + + private BufferedTsFileInput input(ByteArrayOutputStream bytes) throws Exception { + File file = folder.newFile(); + Files.write(file.toPath(), bytes.toByteArray()); + return spy(new BufferedTsFileInput(file.toPath())); + } +} diff --git a/java/tsfile/src/test/java/org/apache/tsfile/read/UnClosedTsFileReaderTest.java b/java/tsfile/src/test/java/org/apache/tsfile/read/UnClosedTsFileReaderTest.java index 8e865413a1..5fadd06dca 100644 --- a/java/tsfile/src/test/java/org/apache/tsfile/read/UnClosedTsFileReaderTest.java +++ b/java/tsfile/src/test/java/org/apache/tsfile/read/UnClosedTsFileReaderTest.java @@ -20,6 +20,9 @@ package org.apache.tsfile.read; import org.apache.tsfile.constant.TestConstant; +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.EncryptionProviderRegistry; +import org.apache.tsfile.encrypt.TestAeadEncryptionProvider; import org.apache.tsfile.enums.TSDataType; import org.apache.tsfile.file.metadata.ChunkMetadata; import org.apache.tsfile.file.metadata.IDeviceID.Factory; @@ -30,6 +33,8 @@ import org.apache.tsfile.write.schema.MeasurementSchema; import org.apache.tsfile.write.writer.TsFileIOWriter; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; import java.io.File; @@ -41,6 +46,16 @@ public class UnClosedTsFileReaderTest { + @BeforeClass + public static void setUpEncryptionProvider() { + EncryptionProviderRegistry.registerProvider(TestAeadEncryptionProvider.INSTANCE); + } + + @AfterClass + public static void tearDownEncryptionProvider() { + EncryptionProviderRegistry.unregisterProvider(TestAeadEncryptionProvider.PROVIDER_ID); + } + @Test public void testRead() throws IOException { File file = new File(TestConstant.BASE_OUTPUT_PATH + File.separator + "test.tsfile"); @@ -71,4 +86,47 @@ public void testRead() throws IOException { file.delete(); } + + @Test + public void testReadEncryptedPagesUsingFileHeader() throws IOException { + File file = new File(TestConstant.BASE_OUTPUT_PATH + File.separator + "test-encrypted.tsfile"); + byte[] fileCryptoId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + fileCryptoId[0] = 1; + EncryptParameter encryptParameter = + TestAeadEncryptionProvider.createParameter(new byte[16], fileCryptoId); + + try { + TsFileIOWriter writer = new TsFileIOWriter(file, encryptParameter); + writer.startChunkGroup(Factory.DEFAULT_FACTORY.create("root.sg1.d1")); + ChunkWriterImpl chunkWriter = + new ChunkWriterImpl(new MeasurementSchema("s1", TSDataType.INT64), encryptParameter); + chunkWriter.write(1, 1L); + chunkWriter.sealCurrentPage(); + chunkWriter.write(2, 2L); + chunkWriter.sealCurrentPage(); + chunkWriter.writeToFileWriter(writer); + writer.endChunkGroup(); + writer.close(); + ChunkMetadata chunkMetadata = + writer.getChunkGroupMetadataList().get(0).getChunkMetadataList().get(0); + + try (UnClosedTsFileReader reader = new UnClosedTsFileReader(file.getAbsolutePath(), null)) { + assertEquals("test-key", reader.getEncryptParam().getKeyId()); + ChunkReader chunkReader = new ChunkReader(reader.readMemChunk(chunkMetadata)); + + BatchData firstPage = chunkReader.nextPageData(); + assertTrue(firstPage.hasCurrent()); + assertEquals(1, firstPage.currentTime()); + assertEquals(1L, firstPage.currentValue()); + + BatchData secondPage = chunkReader.nextPageData(); + assertTrue(secondPage.hasCurrent()); + assertEquals(2, secondPage.currentTime()); + assertEquals(2L, secondPage.currentValue()); + } + } finally { + encryptParameter.close(); + file.delete(); + } + } } diff --git a/java/tsfile/src/test/java/org/apache/tsfile/read/reader/block/PageAeadBlockReaderLifecycleTest.java b/java/tsfile/src/test/java/org/apache/tsfile/read/reader/block/PageAeadBlockReaderLifecycleTest.java new file mode 100644 index 0000000000..d02fef0c60 --- /dev/null +++ b/java/tsfile/src/test/java/org/apache/tsfile/read/reader/block/PageAeadBlockReaderLifecycleTest.java @@ -0,0 +1,387 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.read.reader.block; + +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.EncryptionProviderRegistry; +import org.apache.tsfile.encrypt.TestAeadEncryptionProvider; +import org.apache.tsfile.enums.ColumnCategory; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.file.metadata.ChunkMetadata; +import org.apache.tsfile.file.metadata.IChunkMetadata; +import org.apache.tsfile.file.metadata.IDeviceID; +import org.apache.tsfile.file.metadata.TableSchema; +import org.apache.tsfile.file.metadata.enums.TSEncoding; +import org.apache.tsfile.read.TsFileSequenceReader; +import org.apache.tsfile.read.common.Chunk; +import org.apache.tsfile.read.common.Path; +import org.apache.tsfile.read.controller.CachedChunkLoaderImpl; +import org.apache.tsfile.read.controller.IMetadataQuerier; +import org.apache.tsfile.read.controller.MetadataQuerierByFileImpl; +import org.apache.tsfile.read.query.dataset.TableResultSet; +import org.apache.tsfile.read.query.executor.TableQueryExecutor; +import org.apache.tsfile.read.query.executor.TableQueryExecutor.ColumnMapping; +import org.apache.tsfile.read.query.executor.TableQueryExecutor.TableQueryOrdering; +import org.apache.tsfile.read.query.executor.task.DeviceQueryTask; +import org.apache.tsfile.write.TsFileWriter; +import org.apache.tsfile.write.record.TSRecord; +import org.apache.tsfile.write.record.Tablet; +import org.apache.tsfile.write.record.datapoint.LongDataPoint; +import org.apache.tsfile.write.schema.MeasurementSchema; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +public class PageAeadBlockReaderLifecycleTest { + private static final List TABLE_COLUMNS = Arrays.asList("device_id", "s1", "s2"); + private static final List TABLE_TYPES = + Arrays.asList(TSDataType.STRING, TSDataType.INT64, TSDataType.INT64); + private static final List TABLE_CATEGORIES = + Arrays.asList(ColumnCategory.TAG, ColumnCategory.FIELD, ColumnCategory.FIELD); + private static final IDeviceID TREE_DEVICE = IDeviceID.Factory.DEFAULT_FACTORY.create("d1"); + + @Rule public final TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @BeforeClass + public static void registerProvider() { + EncryptionProviderRegistry.registerProvider(TestAeadEncryptionProvider.INSTANCE); + } + + @AfterClass + public static void unregisterProvider() { + EncryptionProviderRegistry.unregisterProvider(TestAeadEncryptionProvider.PROVIDER_ID); + } + + @Test + public void testPublicTableQueryPropagatesAuthenticationFailure() throws Exception { + File file = writeTableFile(1); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + TrackingChunkLoader loader = new TrackingChunkLoader(sequence); + loader.tamperTimeColumn = true; + TableQueryExecutor executor = + new TableQueryExecutor( + new MetadataQuerierByFileImpl(sequence), loader, TableQueryOrdering.DEVICE); + try (TableResultSet result = tableResult(executor)) { + Exception failure = assertThrows(Exception.class, result::next); + assertTrue(EncryptException.isEncryptionFailure(failure)); + } + assertAllDestroyed(loader.loaded); + } + } + + @Test + public void testPublicTableQueryPropagatesLaterValuePageFailure() throws Exception { + File file = writeTableFile(1); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + TrackingChunkLoader loader = new TrackingChunkLoader(sequence); + loader.tamperSecondColumn = true; + TableQueryExecutor executor = + new TableQueryExecutor( + new MetadataQuerierByFileImpl(sequence), loader, TableQueryOrdering.DEVICE); + try (TableResultSet result = tableResult(executor)) { + Exception failure = + assertThrows( + Exception.class, + () -> { + while (result.next()) { + result.getLong("s2"); + } + }); + assertTrue(EncryptException.isEncryptionFailure(failure)); + } + assertAllDestroyed(loader.loaded); + } + } + + @Test + public void testTableResultEarlyCloseReleasesContextsAndKeepsSharedLoaderOpen() throws Exception { + File file = writeTableFile(1); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + TrackingChunkLoader loader = new TrackingChunkLoader(sequence); + TableQueryExecutor executor = + new TableQueryExecutor( + new MetadataQuerierByFileImpl(sequence), loader, TableQueryOrdering.DEVICE); + try (TableResultSet result = tableResult(executor)) { + assertTrue(result.next()); + assertEquals(1L, result.getLong("s1")); + assertTrue(loader.loaded.stream().anyMatch(c -> !c.getEncryptParam().isDestroyed())); + result.close(); + result.close(); + assertAllDestroyed(loader.loaded); + assertEquals(closeCount, TestAeadEncryptionProvider.getCloseCount()); + } + // A result only owns its chunks, so another query can still use the same file loader. + try (TableResultSet another = tableResult(executor)) { + assertTrue(another.next()); + assertEquals(1L, another.getLong("s1")); + } + assertAllDestroyed(loader.loaded); + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testDeviceSwitchAndExhaustionReleaseAllContexts() throws Exception { + File file = writeTableFile(2); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + TrackingChunkLoader loader = new TrackingChunkLoader(sequence); + TableQueryExecutor executor = + new TableQueryExecutor( + new MetadataQuerierByFileImpl(sequence), loader, TableQueryOrdering.DEVICE); + try (TableResultSet result = tableResult(executor)) { + int rows = 0; + while (result.next()) { + rows++; + } + assertEquals(4096, rows); + assertAllDestroyed(loader.loaded); + } + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testSingleDeviceEarlyCloseReleasesEveryScalarColumn() throws Exception { + File file = writeScalarFile(); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + TrackingChunkLoader loader = new TrackingChunkLoader(sequence); + IMetadataQuerier metadata = scalarMetadata(sequence); + DeviceQueryTask task = scalarTask(metadata, loader); + try (SingleDeviceTsBlockReader reader = + new SingleDeviceTsBlockReader(task, metadata, loader, 1, null, null)) { + assertTrue(reader.hasNext()); + assertEquals(1, reader.next().getPositionCount()); + reader.close(); + reader.close(); + assertFalse(reader.hasNext()); + assertEquals(2, loader.loaded.size()); + assertAllDestroyed(loader.loaded); + assertTrue(sequence.fileSize() > 0); + } + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testLaterColumnLoadFailureReleasesPreviouslyInitializedColumn() throws Exception { + File file = writeScalarFile(); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + TrackingChunkLoader loader = new TrackingChunkLoader(sequence); + IOException failure = new IOException("Injected column load failure"); + loader.loadFailure = failure; + IMetadataQuerier metadata = scalarMetadata(sequence); + DeviceQueryTask task = scalarTask(metadata, loader); + assertSame( + failure, + assertThrows( + IOException.class, + () -> new SingleDeviceTsBlockReader(task, metadata, loader, 1, null, null))); + assertEquals(1, loader.loaded.size()); + assertAllDestroyed(loader.loaded); + assertTrue(sequence.fileSize() > 0); + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testLaterColumnDecodeFailureAlsoReleasesTheUnregisteredReader() throws Exception { + File file = writeScalarFile(); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + TrackingChunkLoader loader = new TrackingChunkLoader(sequence); + loader.tamperSecondColumn = true; + IMetadataQuerier metadata = scalarMetadata(sequence); + DeviceQueryTask task = scalarTask(metadata, loader); + assertThrows( + EncryptException.class, + () -> new SingleDeviceTsBlockReader(task, metadata, loader, 1, null, null)); + assertEquals(2, loader.loaded.size()); + assertAllDestroyed(loader.loaded); + assertTrue(sequence.fileSize() > 0); + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + @Test + public void testRuntimeColumnInitializationFailurePreservesCauseAndReleasesReaders() + throws Exception { + File file = writeScalarFile(); + int closeCount = TestAeadEncryptionProvider.getCloseCount(); + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath())) { + TrackingChunkLoader loader = new TrackingChunkLoader(sequence); + IMetadataQuerier metadata = scalarMetadata(sequence); + DeviceQueryTask original = scalarTask(metadata, loader); + ColumnMapping mapping = spy(original.getColumnMapping()); + IllegalStateException failure = new IllegalStateException("Injected column mapping failure"); + doThrow(failure).when(mapping).getColumnPos("s2"); + DeviceQueryTask task = + new DeviceQueryTask( + TREE_DEVICE, original.getColumnNames(), mapping, null, original.getTableSchema()); + assertSame( + failure, + assertThrows( + IllegalStateException.class, + () -> new SingleDeviceTsBlockReader(task, metadata, loader, 1, null, null))); + assertEquals(2, loader.loaded.size()); + assertAllDestroyed(loader.loaded); + } + assertEquals(closeCount + 1, TestAeadEncryptionProvider.getCloseCount()); + } + + private TableResultSet tableResult(TableQueryExecutor executor) throws Exception { + return new TableResultSet( + executor.query("t1", TABLE_COLUMNS, null, null, null), TABLE_COLUMNS, TABLE_TYPES, "t1"); + } + + private File writeTableFile(int devices) throws Exception { + File file = new File(temporaryFolder.getRoot(), "table-lifecycle.tsfile"); + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + TsFileWriter writer = new TsFileWriter(file, parameter)) { + writer.setGenerateTableSchema(true); + writer.registerTableSchema( + new TableSchema( + "t1", + Arrays.asList( + new MeasurementSchema("device_id", TSDataType.STRING), + new MeasurementSchema("s1", TSDataType.INT64), + new MeasurementSchema("s2", TSDataType.INT64)), + TABLE_CATEGORIES)); + for (int device = 0; device < devices; device++) { + Tablet tablet = new Tablet("t1", TABLE_COLUMNS, TABLE_TYPES, TABLE_CATEGORIES, 2048); + for (int row = 0; row < 2048; row++) { + tablet.addTimestamp(row, row + 1); + tablet.addValue("device_id", row, "device-" + device); + tablet.addValue("s1", row, (long) row + 1); + tablet.addValue("s2", row, (long) row + 101); + } + writer.writeTable(tablet); + } + } + return file; + } + + private File writeScalarFile() throws Exception { + File file = new File(temporaryFolder.getRoot(), "scalar-columns.tsfile"); + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + TsFileWriter writer = new TsFileWriter(file, parameter)) { + writer.registerTimeseries( + new Path("d1"), new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.PLAIN)); + writer.registerTimeseries( + new Path("d1"), new MeasurementSchema("s2", TSDataType.INT64, TSEncoding.PLAIN)); + for (int row = 1; row <= 3; row++) { + writer.writeRecord( + new TSRecord("d1", row) + .addTuple(new LongDataPoint("s1", row)) + .addTuple(new LongDataPoint("s2", row + 100L))); + } + } + return file; + } + + private IMetadataQuerier scalarMetadata(TsFileSequenceReader sequence) throws IOException { + IMetadataQuerier metadata = mock(IMetadataQuerier.class); + List first = + new ArrayList<>(sequence.getChunkMetadataList(new Path("d1", "s1", true))); + List second = + new ArrayList<>(sequence.getChunkMetadataList(new Path("d1", "s2", true))); + when(metadata.getChunkMetadataLists(eq(TREE_DEVICE), anySet(), isNull())) + .thenReturn(Arrays.asList(first, second)); + return metadata; + } + + private DeviceQueryTask scalarTask(IMetadataQuerier metadata, TrackingChunkLoader loader) + throws Exception { + List columns = Arrays.asList("s1", "s2"); + TableSchema schema = + new TableSchema( + "t1", + Arrays.asList( + new MeasurementSchema("s1", TSDataType.INT64), + new MeasurementSchema("s2", TSDataType.INT64)), + Arrays.asList(ColumnCategory.FIELD, ColumnCategory.FIELD)); + ColumnMapping mapping = + new TableQueryExecutor(metadata, loader, TableQueryOrdering.DEVICE).new ColumnMapping(); + mapping.add("s1", 0, schema); + mapping.add("s2", 1, schema); + return new DeviceQueryTask(TREE_DEVICE, columns, mapping, null, schema); + } + + private static void assertAllDestroyed(List chunks) { + for (Chunk chunk : chunks) { + assertTrue(chunk.getEncryptParam().isDestroyed()); + } + } + + private static class TrackingChunkLoader extends CachedChunkLoaderImpl { + private final List loaded = new ArrayList<>(); + private IOException loadFailure; + private boolean tamperSecondColumn; + private boolean tamperTimeColumn; + + private TrackingChunkLoader(TsFileSequenceReader sequence) { + super(sequence); + } + + @Override + public Chunk loadChunk(ChunkMetadata metadata) throws IOException { + if (loadFailure != null && "s2".equals(metadata.getMeasurementUid())) { + throw loadFailure; + } + Chunk chunk = super.loadChunk(metadata); + if ((tamperSecondColumn && "s2".equals(metadata.getMeasurementUid())) + || (tamperTimeColumn && "".equals(metadata.getMeasurementUid()))) { + byte[] data = chunk.getData().array(); + data[data.length - 1] ^= 1; + } + loaded.add(chunk); + return chunk; + } + } +} diff --git a/java/tsfile/src/test/java/org/apache/tsfile/utils/ReadWriteIOUtilsTest.java b/java/tsfile/src/test/java/org/apache/tsfile/utils/ReadWriteIOUtilsTest.java index 4322903ce9..0d1bd36674 100644 --- a/java/tsfile/src/test/java/org/apache/tsfile/utils/ReadWriteIOUtilsTest.java +++ b/java/tsfile/src/test/java/org/apache/tsfile/utils/ReadWriteIOUtilsTest.java @@ -27,7 +27,9 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataOutputStream; +import java.io.EOFException; import java.io.IOException; +import java.io.InputStream; import java.nio.BufferUnderflowException; import java.nio.ByteBuffer; import java.util.ArrayList; @@ -43,6 +45,35 @@ public class ReadWriteIOUtilsTest { protected static final int DEFAULT_BUFFER_SIZE = 4096; + @Test + public void testReadLongFromFragmentedStream() throws IOException { + long expected = 0x8123456789ABCDEFL; + byte[] encoded = ByteBuffer.allocate(Long.BYTES).putLong(expected).array(); + InputStream input = + new ByteArrayInputStream(encoded) { + private boolean zeroRead = true; + + @Override + public synchronized int read(byte[] bytes, int offset, int length) { + if (zeroRead) { + zeroRead = false; + return 0; + } + return super.read(bytes, offset, Math.min(length, 2)); + } + }; + Assert.assertEquals(expected, ReadWriteIOUtils.readLong(input)); + Assert.assertEquals(-1, input.read()); + } + + @Test + public void testReadLongRejectsEveryTruncatedLength() { + for (int length = 0; length < Long.BYTES; length++) { + InputStream input = new ByteArrayInputStream(new byte[length]); + Assert.assertThrows(EOFException.class, () -> ReadWriteIOUtils.readLong(input)); + } + } + @Test public void testReadStringRejectsOversizedLength() { for (int length : new int[] {2, Integer.MAX_VALUE}) { diff --git a/java/tsfile/src/test/java/org/apache/tsfile/write/ChunkPositionAuthenticationTest.java b/java/tsfile/src/test/java/org/apache/tsfile/write/ChunkPositionAuthenticationTest.java new file mode 100644 index 0000000000..80f4c6cfa9 --- /dev/null +++ b/java/tsfile/src/test/java/org/apache/tsfile/write/ChunkPositionAuthenticationTest.java @@ -0,0 +1,526 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.tsfile.write; + +import org.apache.tsfile.encrypt.ChunkCryptoContext; +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.EncryptionProviderRegistry; +import org.apache.tsfile.encrypt.IEncrypt; +import org.apache.tsfile.encrypt.IEncryptProvider; +import org.apache.tsfile.encrypt.TestAeadEncryptionProvider; +import org.apache.tsfile.encrypt.UNENCRYPTED; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.file.header.ChunkHeader; +import org.apache.tsfile.file.metadata.ChunkMetadata; +import org.apache.tsfile.file.metadata.IDeviceID; +import org.apache.tsfile.file.metadata.enums.CompressionType; +import org.apache.tsfile.file.metadata.enums.TSEncoding; +import org.apache.tsfile.file.metadata.statistics.Statistics; +import org.apache.tsfile.read.TsFileReader; +import org.apache.tsfile.read.TsFileSequenceReader; +import org.apache.tsfile.read.common.Chunk; +import org.apache.tsfile.read.common.Path; +import org.apache.tsfile.read.expression.QueryExpression; +import org.apache.tsfile.read.query.dataset.QueryDataSet; +import org.apache.tsfile.read.reader.LocalTsFileInput; +import org.apache.tsfile.utils.RamUsageEstimator; +import org.apache.tsfile.write.record.TSRecord; +import org.apache.tsfile.write.record.datapoint.LongDataPoint; +import org.apache.tsfile.write.schema.MeasurementSchema; +import org.apache.tsfile.write.writer.RestorableTsFileIOWriter; +import org.apache.tsfile.write.writer.TsFileIOWriter; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ChunkPositionAuthenticationTest { + @Rule public TemporaryFolder folder = new TemporaryFolder(); + + @BeforeClass + public static void registerProvider() { + EncryptionProviderRegistry.registerProvider(TestAeadEncryptionProvider.INSTANCE); + } + + @AfterClass + public static void unregisterProvider() { + EncryptionProviderRegistry.unregisterProvider(TestAeadEncryptionProvider.PROVIDER_ID); + } + + @Test + public void testProofBindsOffsetOrdinalAndFileIdentity() { + try (EncryptParameter parameter = parameter(); + EncryptParameter other = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[] {1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})) { + byte[] proof = parameter.authenticateChunk(100, 3); + assertEquals(parameter.getChunkAuthenticationSize(), proof.length); + parameter.verifyChunkAuthentication(proof, 100, 3); + assertThrows( + EncryptException.class, () -> parameter.verifyChunkAuthentication(proof, 101, 3)); + assertThrows( + EncryptException.class, () -> parameter.verifyChunkAuthentication(proof, 100, 4)); + assertThrows(EncryptException.class, () -> other.verifyChunkAuthentication(proof, 100, 3)); + byte[] corrupted = proof.clone(); + corrupted[0] ^= 1; + assertThrows( + EncryptException.class, () -> parameter.verifyChunkAuthentication(corrupted, 100, 3)); + corrupted[0] ^= 1; + corrupted[corrupted.length - 1] ^= 1; + assertThrows( + EncryptException.class, () -> parameter.verifyChunkAuthentication(corrupted, 100, 3)); + assertThrows( + EncryptException.class, () -> parameter.verifyChunkAuthentication(new byte[0], 100, 3)); + assertFalse(Arrays.equals(proof, parameter.authenticateChunk(100, 3))); + ChunkCryptoContext context = ChunkCryptoContext.forChunk(parameter, 100, 3); + byte[] aad = context.getAssociatedData(); + aad[0] ^= 1; + assertFalse(Arrays.equals(aad, context.getAssociatedData())); + assertThrows(EncryptException.class, () -> ChunkCryptoContext.forChunk(parameter, -1, 3)); + assertThrows(EncryptException.class, () -> ChunkCryptoContext.forChunk(parameter, 100, -1)); + parameter.close(); + assertThrows(EncryptException.class, () -> parameter.authenticateChunk(100, 3)); + } + } + + @Test + public void testUnsupportedProviderAndMissingHeaderProofFailClosed() throws Exception { + try (EncryptParameter parameter = parameter(); + IEncrypt unsupported = new UNENCRYPTED(null)) { + ChunkCryptoContext context = ChunkCryptoContext.forChunk(parameter, 100, 3); + assertThrows(EncryptException.class, () -> unsupported.authenticateChunk(context)); + assertThrows( + EncryptException.class, + () -> unsupported.verifyChunkAuthentication(new byte[28], context)); + ChunkHeader header = + new ChunkHeader( + "s1", 0, TSDataType.INT64, CompressionType.UNCOMPRESSED, TSEncoding.PLAIN, 0, 0, 3); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + assertThrows(EncryptException.class, () -> header.serializeTo(output)); + assertEquals(0, output.size()); + assertThrows(EncryptException.class, header::getSerializedSize); + ByteBuffer buffer = ByteBuffer.allocate(128); + assertThrows(EncryptException.class, () -> header.serializeTo(buffer)); + assertEquals(0, buffer.position()); + } + } + + @Test + public void testWholeChunkSwapRejectedByBothReadPathsAndQuery() throws Exception { + File file = writeFile(); + List metadata = metadata(file); + swapChunks(file, metadata); + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + assertThrows( + EncryptException.class, + () -> reader.readChunkHeader(metadata.get(0).getOffsetOfChunkHeader())); + reader.position(metadata.get(1).getOffsetOfChunkHeader()); + byte marker = reader.readMarker(); + assertThrows(EncryptException.class, () -> reader.readChunkHeader(marker)); + assertThrows(EncryptException.class, () -> reader.readMemChunk(metadata.get(0))); + } + Exception failure = + assertThrows( + Exception.class, + () -> { + try (TsFileReader reader = + new TsFileReader(new TsFileSequenceReader(file.getPath()))) { + QueryDataSet data = + reader.query( + QueryExpression.create( + Collections.singletonList(new Path("d1", "s1", true)), null)); + while (data.hasNext()) { + data.next(); + } + } + }); + assertTrue(EncryptException.isEncryptionFailure(failure)); + } + + @Test + public void testRecoveryDoesNotTruncateAwayAuthenticationFailure() throws Exception { + File file = writeFile(); + List metadata = metadata(file); + int end; + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + ChunkHeader last = reader.readChunkHeader(metadata.get(1).getOffsetOfChunkHeader()); + end = + Math.toIntExact( + metadata.get(1).getOffsetOfChunkHeader() + + last.getSerializedSize() + + last.getDataSize()); + } + swapChunks(file, metadata); + byte[] bytes = Arrays.copyOf(Files.readAllBytes(file.toPath()), end); + Files.write(file.toPath(), bytes); + IOException failure = assertThrows(IOException.class, () -> new RestorableTsFileIOWriter(file)); + assertTrue(EncryptException.isEncryptionFailure(failure)); + assertArrayEquals(bytes, Files.readAllBytes(file.toPath())); + } + + @Test + public void testIncompleteTailProofCanBeRecovered() throws Exception { + File file = writeFile(); + List metadata = metadata(file); + int end; + long dataStart; + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + ChunkHeader first = reader.readChunkHeader(metadata.get(0).getOffsetOfChunkHeader()); + end = + Math.toIntExact( + metadata.get(0).getOffsetOfChunkHeader() + + first.getSerializedSize() + - first.getChunkAuthenticationSize() + + 10); + dataStart = reader.getDataStartOffset(); + } + Files.write(file.toPath(), Arrays.copyOf(Files.readAllBytes(file.toPath()), end)); + try (RestorableTsFileIOWriter recovered = new RestorableTsFileIOWriter(file)) { + assertTrue(recovered.canWrite()); + assertEquals(dataStart, file.length()); + } + } + + @Test + public void testEmptyValueChunkHasPositionProof() throws Exception { + File file = new File(folder.getRoot(), "empty.tsfile"); + long offset; + try (EncryptParameter parameter = parameter(); + TsFileIOWriter writer = new TsFileIOWriter(file, parameter)) { + writer.startChunkGroup(IDeviceID.Factory.DEFAULT_FACTORY.create("d1")); + offset = writer.getPos(); + writer.writeEmptyValueChunk( + "s1", + CompressionType.UNCOMPRESSED, + TSDataType.INT64, + TSEncoding.PLAIN, + Statistics.getStatsByType(TSDataType.INT64)); + writer.endChunkGroup(); + writer.endFile(); + } + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + ChunkHeader header = reader.readChunkHeader(offset); + assertEquals(0, header.getDataSize()); + assertEquals(28, header.getChunkAuthentication().length); + ChunkHeader plain = + new ChunkHeader( + "s1", 0, TSDataType.INT64, CompressionType.UNCOMPRESSED, TSEncoding.PLAIN, 0); + try (Chunk encrypted = new Chunk(header, ByteBuffer.allocate(0)); + Chunk plaintext = new Chunk(plain, ByteBuffer.allocate(0))) { + assertEquals( + RamUsageEstimator.sizeOfByteArray(28), + encrypted.getRetainedSizeInBytes() - plaintext.getRetainedSizeInBytes()); + } + ByteBuffer buffer = ByteBuffer.allocate(header.getSerializedSize()); + assertEquals(header.getSerializedSize(), header.serializeTo(buffer)); + } + } + + @Test + public void testVariableProofHeaderRoundTripAndFragmentedReads() throws Exception { + for (int size : new int[] {28, 32, 128, ChunkCryptoContext.MAX_AUTHENTICATION_SIZE}) { + ChunkHeader source = + new ChunkHeader( + "s1", 0, TSDataType.INT64, CompressionType.UNCOMPRESSED, TSEncoding.PLAIN, 0, 0, 3); + byte[] proof = new byte[size]; + Arrays.fill(proof, (byte) 0x5A); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + int length = source.serializeTo(output, proof); + byte[] bytes = output.toByteArray(); + assertEquals(bytes.length, length); + assertEquals(ChunkHeader.getSerializedSize("s1", 0, size), length); + ByteArrayInputStream stream = + new ByteArrayInputStream(bytes, 1, bytes.length - 1) { + @Override + public synchronized int read(byte[] target, int offset, int count) { + return super.read(target, offset, Math.min(count, 2)); + } + }; + ChunkHeader sequential = ChunkHeader.deserializeFrom(stream, bytes[0], size); + assertArrayEquals(proof, sequential.getChunkAuthentication()); + assertEquals(length, sequential.getSerializedSize()); + assertEquals(0, stream.available()); + ByteBuffer buffer = ByteBuffer.allocate(length); + assertEquals(length, sequential.serializeTo(buffer)); + assertArrayEquals(bytes, buffer.array()); + File file = folder.newFile(); + Files.write(file.toPath(), bytes); + try (CloseableInput input = + new CloseableInput(file) { + @Override + public int read(ByteBuffer target, long position) throws IOException { + int limit = target.limit(); + target.limit(Math.min(limit, target.position() + 2)); + try { + return super.read(target, position); + } finally { + target.limit(limit); + } + } + }) { + input.position(2); + ChunkHeader positional = ChunkHeader.deserializeFrom(input, 0, null, size); + assertEquals(length, positional.getSerializedSize()); + assertArrayEquals(proof, positional.getChunkAuthentication()); + assertEquals(2, input.position()); + } + for (int truncatedLength : new int[] {length - size - 1, length - 1}) { + byte[] truncated = Arrays.copyOf(bytes, truncatedLength); + assertThrows( + EOFException.class, + () -> + ChunkHeader.deserializeFrom( + new ByteArrayInputStream(truncated, 1, truncated.length - 1), bytes[0], size)); + Files.write(file.toPath(), truncated); + try (CloseableInput input = new CloseableInput(file)) { + assertThrows(EOFException.class, () -> ChunkHeader.deserializeFrom(input, 0, null, size)); + } + } + } + } + + @Test + public void testInvalidOrMismatchedLengthIsRejectedBeforeProofRead() throws Exception { + ChunkHeader source = + new ChunkHeader( + "s1", 0, TSDataType.INT64, CompressionType.UNCOMPRESSED, TSEncoding.PLAIN, 0, 0, 3); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + source.serializeTo(output, new byte[32]); + byte[] prefix = Arrays.copyOf(output.toByteArray(), output.size() - 32); + for (int size : new int[] {-1, 0, 28, 33, ChunkCryptoContext.MAX_AUTHENTICATION_SIZE + 1}) { + ByteBuffer.wrap(prefix).putInt(prefix.length - Integer.BYTES, size); + assertThrows( + EncryptException.class, + () -> + ChunkHeader.deserializeFrom( + new ByteArrayInputStream(prefix, 1, prefix.length - 1), prefix[0], 32)); + File file = folder.newFile(); + Files.write(file.toPath(), prefix); + try (CloseableInput input = new CloseableInput(file)) { + assertThrows(EncryptException.class, () -> ChunkHeader.deserializeFrom(input, 0, null, 32)); + } + } + } + + @Test + public void testProviderMustDeclareBoundedProofSizeAndReturnMatchingProof() { + for (int declared : new int[] {-1, 0, ChunkCryptoContext.MAX_AUTHENTICATION_SIZE + 1, 32}) { + String id = "test-proof-size-" + declared; + EncryptionProviderRegistry.registerProvider( + new IEncryptProvider() { + @Override + public String getProviderId() { + return id; + } + + @Override + public IEncrypt create(EncryptParameter parameter) { + return new UNENCRYPTED(null) { + @Override + public int getChunkAuthenticationSize() { + return declared; + } + + @Override + public byte[] authenticateChunk(ChunkCryptoContext context) { + return new byte[28]; + } + }; + } + }); + try (EncryptParameter parameter = + EncryptParameter.pageAeadBuilder() + .providerId(id) + .profileId("test") + .fileCryptoId(new byte[16]) + .build()) { + if (declared != 32) { + assertThrows(EncryptException.class, parameter::getChunkAuthenticationSize); + } + assertThrows(EncryptException.class, () -> parameter.authenticateChunk(100, 0)); + assertThrows( + EncryptException.class, + () -> parameter.verifyChunkAuthentication(new byte[28], 100, 0)); + } finally { + EncryptionProviderRegistry.unregisterProvider(id); + } + } + } + + @Test + public void test32ByteProofSupportsQueryBothReadersAndRecovery() throws Exception { + File file = writeFile(true); + List metadata = metadata(file); + long dataEnd; + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + for (ChunkMetadata item : metadata) { + ChunkHeader header = reader.readChunkHeader(item.getOffsetOfChunkHeader()); + assertEquals(32, header.getChunkAuthenticationSize()); + reader.position(item.getOffsetOfChunkHeader()); + assertArrayEquals( + header.getChunkAuthentication(), + reader.readChunkHeader(reader.readMarker()).getChunkAuthentication()); + } + ChunkMetadata last = metadata.get(1); + ChunkHeader header = reader.readChunkHeader(last.getOffsetOfChunkHeader()); + dataEnd = last.getOffsetOfChunkHeader() + header.getSerializedSize() + header.getDataSize(); + } + try (TsFileReader reader = new TsFileReader(file)) { + QueryDataSet data = + reader.query( + QueryExpression.create(Collections.singletonList(new Path("d1", "s1", true)), null)); + int points = 0; + while (data.hasNext()) { + assertEquals(++points, data.next().getTimestamp()); + } + assertEquals(2, points); + } + // Keep the separator so recovery knows the final chunk group is complete. + Files.write(file.toPath(), Arrays.copyOf(Files.readAllBytes(file.toPath()), (int) dataEnd + 1)); + try (RestorableTsFileIOWriter recovered = new RestorableTsFileIOWriter(file)) { + assertTrue(recovered.canWrite()); + assertEquals(dataEnd, file.length()); + } + } + + @Test + public void test32ByteWholeChunkSwapFailsAuthentication() throws Exception { + File file = writeFile(true); + List metadata = metadata(file); + swapChunks(file, metadata); + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + assertThrows( + EncryptException.class, + () -> reader.readChunkHeader(metadata.get(0).getOffsetOfChunkHeader())); + reader.position(metadata.get(1).getOffsetOfChunkHeader()); + byte marker = reader.readMarker(); + assertThrows(EncryptException.class, () -> reader.readChunkHeader(marker)); + } + } + + @Test + public void testAlteredProofLengthCannotBeRecoveredAsTruncatedTail() throws Exception { + File file = writeFile(true); + List metadata = metadata(file); + int sizeOffset; + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + long offset = metadata.get(0).getOffsetOfChunkHeader(); + ChunkHeader header = reader.readChunkHeader(offset); + sizeOffset = (int) offset + header.getSerializedSize() - 32 - Integer.BYTES; + } + byte[] bytes = Arrays.copyOf(Files.readAllBytes(file.toPath()), sizeOffset + Integer.BYTES); + ByteBuffer.wrap(bytes).putInt(sizeOffset, 33); + Files.write(file.toPath(), bytes); + IOException failure = assertThrows(IOException.class, () -> new RestorableTsFileIOWriter(file)); + assertTrue(EncryptException.isEncryptionFailure(failure)); + assertArrayEquals(bytes, Files.readAllBytes(file.toPath())); + } + + @Test + public void testFileRenameKeepsPositionsValid() throws Exception { + File original = writeFile(); + File renamed = new File(folder.getRoot(), "renamed.tsfile"); + Files.move(original.toPath(), renamed.toPath()); + try (TsFileSequenceReader reader = new TsFileSequenceReader(renamed.getPath())) { + for (ChunkMetadata chunk : reader.getChunkMetadataList(new Path("d1", "s1", true))) { + reader.readChunkHeader(chunk.getOffsetOfChunkHeader()); + } + } + } + + private File writeFile() throws Exception { + return writeFile(false); + } + + private File writeFile(boolean macProof) throws Exception { + File file = new File(folder.getRoot(), "positions" + macProof + ".tsfile"); + try (EncryptParameter parameter = + macProof + ? TestAeadEncryptionProvider.createMacParameter(new byte[16], new byte[16]) + : parameter(); + TsFileWriter writer = new TsFileWriter(file, parameter)) { + writer.registerTimeseries( + new Path("d1"), + new MeasurementSchema( + "s1", TSDataType.INT64, TSEncoding.PLAIN, CompressionType.UNCOMPRESSED)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new LongDataPoint("s1", 11))); + writer.flush(); + writer.getIOWriter().writePlanIndices(); + writer.writeRecord(new TSRecord("d1", 2).addTuple(new LongDataPoint("s1", 22))); + writer.flush(); + } + return file; + } + + private List metadata(File file) throws IOException { + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + List result = reader.getChunkMetadataList(new Path("d1", "s1", true)); + assertEquals(2, result.size()); + return result; + } + } + + private void swapChunks(File file, List metadata) throws IOException { + int length; + int first = Math.toIntExact(metadata.get(0).getOffsetOfChunkHeader()); + int second = Math.toIntExact(metadata.get(1).getOffsetOfChunkHeader()); + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + ChunkHeader a = reader.readChunkHeader((long) first); + ChunkHeader b = reader.readChunkHeader((long) second); + length = a.getSerializedSize() + a.getDataSize(); + assertEquals(length, b.getSerializedSize() + b.getDataSize()); + } + byte[] bytes = Files.readAllBytes(file.toPath()); + byte[] saved = Arrays.copyOfRange(bytes, first, first + length); + System.arraycopy(bytes, second, bytes, first, length); + System.arraycopy(saved, 0, bytes, second, length); + Files.write(file.toPath(), bytes); + } + + private EncryptParameter parameter() { + return TestAeadEncryptionProvider.createParameter(new byte[16], new byte[16]); + } + + private static class CloseableInput extends LocalTsFileInput implements AutoCloseable { + private CloseableInput(File file) throws IOException { + super(file.toPath()); + } + } +} diff --git a/java/tsfile/src/test/java/org/apache/tsfile/write/TDEPageAeadTsFileTest.java b/java/tsfile/src/test/java/org/apache/tsfile/write/TDEPageAeadTsFileTest.java new file mode 100644 index 0000000000..6baa9eafc1 --- /dev/null +++ b/java/tsfile/src/test/java/org/apache/tsfile/write/TDEPageAeadTsFileTest.java @@ -0,0 +1,780 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.write; + +import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.common.conf.TSFileDescriptor; +import org.apache.tsfile.constant.TestConstant; +import org.apache.tsfile.encoding.encoder.PlainEncoder; +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.EncryptionProviderRegistry; +import org.apache.tsfile.encrypt.TestAeadEncryptionProvider; +import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.exception.encrypt.EncryptException; +import org.apache.tsfile.file.MetaMarker; +import org.apache.tsfile.file.header.ChunkHeader; +import org.apache.tsfile.file.header.PageHeader; +import org.apache.tsfile.file.metadata.ChunkMetadata; +import org.apache.tsfile.file.metadata.enums.CompressionType; +import org.apache.tsfile.file.metadata.enums.TSEncoding; +import org.apache.tsfile.read.TimeValuePair; +import org.apache.tsfile.read.TsFileReader; +import org.apache.tsfile.read.TsFileSequenceReader; +import org.apache.tsfile.read.common.BatchData; +import org.apache.tsfile.read.common.Chunk; +import org.apache.tsfile.read.common.Path; +import org.apache.tsfile.read.common.RowRecord; +import org.apache.tsfile.read.expression.QueryExpression; +import org.apache.tsfile.read.query.dataset.QueryDataSet; +import org.apache.tsfile.read.reader.BufferedTsFileInput; +import org.apache.tsfile.read.reader.IPageReader; +import org.apache.tsfile.read.reader.IPointReader; +import org.apache.tsfile.read.reader.TsFileLastReader; +import org.apache.tsfile.read.reader.chunk.ChunkReader; +import org.apache.tsfile.utils.Binary; +import org.apache.tsfile.utils.Pair; +import org.apache.tsfile.write.chunk.AlignedChunkWriterImpl; +import org.apache.tsfile.write.chunk.ChunkWriterImpl; +import org.apache.tsfile.write.chunk.TimeChunkWriter; +import org.apache.tsfile.write.chunk.ValueChunkWriter; +import org.apache.tsfile.write.record.TSRecord; +import org.apache.tsfile.write.record.Tablet; +import org.apache.tsfile.write.record.datapoint.LongDataPoint; +import org.apache.tsfile.write.schema.IMeasurementSchema; +import org.apache.tsfile.write.schema.MeasurementSchema; +import org.apache.tsfile.write.writer.LocalTsFileOutput; +import org.apache.tsfile.write.writer.TsFileIOWriter; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class TDEPageAeadTsFileTest { + + private final File file = + new File(TestConstant.BASE_OUTPUT_PATH + File.separator + "tde-page-aead.tsfile"); + + @BeforeClass + public static void setUpEncryptionProvider() { + EncryptionProviderRegistry.registerProvider(TestAeadEncryptionProvider.INSTANCE); + } + + @AfterClass + public static void tearDownEncryptionProvider() { + EncryptionProviderRegistry.unregisterProvider(TestAeadEncryptionProvider.PROVIDER_ID); + } + + @Before + public void setUp() { + if (!file.getParentFile().exists()) { + assertTrue(file.getParentFile().mkdirs()); + } + } + + @After + public void tearDown() { + if (file.exists()) { + assertTrue(file.delete()); + } + } + + @Test + public void testRawPageCopyIsRejectedBeforeChangingBuffers() throws Exception { + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH])) { + MeasurementSchema schema = new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.PLAIN); + ChunkWriterImpl scalar = new ChunkWriterImpl(schema, parameter); + TimeChunkWriter time = + new TimeChunkWriter( + "", + CompressionType.UNCOMPRESSED, + TSEncoding.PLAIN, + new PlainEncoder(TSDataType.INT64, 0), + parameter); + ValueChunkWriter value = + new ValueChunkWriter( + "s1", + CompressionType.UNCOMPRESSED, + TSDataType.INT64, + TSEncoding.PLAIN, + new PlainEncoder(TSDataType.INT64, 0), + parameter); + AlignedChunkWriterImpl aligned = new AlignedChunkWriterImpl(Arrays.asList(schema), parameter); + ByteBuffer data = ByteBuffer.wrap(new byte[] {1, 2, 3}); + PageHeader header = new PageHeader(3, 3, null); + assertThrows( + EncryptException.class, () -> scalar.writePageHeaderAndDataIntoBuff(data, header)); + assertThrows(EncryptException.class, () -> time.writePageHeaderAndDataIntoBuff(data, header)); + assertThrows( + EncryptException.class, () -> value.writePageHeaderAndDataIntoBuff(data, header)); + assertThrows( + EncryptException.class, () -> aligned.writePageHeaderAndDataIntoTimeBuff(data, header)); + assertThrows( + EncryptException.class, + () -> aligned.writePageHeaderAndDataIntoValueBuff(data, header, 0)); + assertEquals(0, data.position()); + } + } + + @Test + public void testIoWriterConstructorDoesNotDestroyCallerParameter() throws Exception { + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH])) { + try (TsFileWriter writer = new TsFileWriter(new TsFileIOWriter(file), parameter)) { + writer.registerTimeseries(new Path("d1"), new MeasurementSchema("s1", TSDataType.INT64)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new LongDataPoint("s1", 11))); + } + assertFalse(parameter.isDestroyed()); + } + } + + @Test + public void testNewFileCannotReuseAeadParameterOrItsCopy() throws Exception { + File second = new File(file.getParentFile(), "tde-reused-context.tsfile"); + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH])) { + try (TsFileWriter writer = new TsFileWriter(file, parameter)) { + writer.registerTimeseries(new Path("d1"), new MeasurementSchema("s1", TSDataType.INT64)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new LongDataPoint("s1", 11))); + } + long originalSize = file.length(); + assertThrows(EncryptException.class, () -> new TsFileWriter(file, parameter)); + assertEquals(originalSize, file.length()); + try (EncryptParameter copy = parameter.copy()) { + assertThrows(EncryptException.class, () -> new TsFileWriter(second, copy)); + } + assertFalse(second.exists()); + assertFalse(parameter.isDestroyed()); + } + } + + @Test + public void testTruncatedEncryptionHeaderReportsIOExceptionWithoutModifyingFile() + throws Exception { + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + TsFileIOWriter writer = new TsFileIOWriter(file, parameter)) { + writer.getIOWriterOut().flush(); + } + try (RandomAccessFile output = new RandomAccessFile(file, "rw")) { + output.setLength(output.length() - 5); + } + long truncatedSize = file.length(); + IOException failure = + assertThrows(IOException.class, () -> new TsFileSequenceReader(file.getPath(), false)); + assertTrue(EncryptException.isEncryptionFailure(failure)); + assertThrows( + IOException.class, () -> new org.apache.tsfile.write.writer.RestorableTsFileIOWriter(file)); + assertEquals(truncatedSize, file.length()); + } + + @Test + public void testUnknownFirstMarkerIsRejectedWithoutMovingLazyReader() throws Exception { + try (TsFileIOWriter writer = new TsFileIOWriter(file)) { + writer.endFile(); + } + try (RandomAccessFile output = new RandomAccessFile(file, "rw")) { + output.seek(TSFileConfig.MAGIC_STRING.getBytes(TSFileConfig.STRING_CHARSET).length + 1); + output.writeByte(0x7F); + } + assertThrows(IOException.class, () -> new TsFileSequenceReader(file.getPath(), false)); + BufferedTsFileInput input = new BufferedTsFileInput(file.toPath()); + try (TsFileSequenceReader reader = new TsFileSequenceReader(input, 0, 0)) { + input.position(3); + assertThrows(IOException.class, reader::hasFileEncryptionHeader); + assertEquals(3, input.position()); + } + } + + @Test + public void testEncryptionHeaderDoesNotForceOutput() throws Exception { + LocalTsFileOutput output = spy(new LocalTsFileOutput(new FileOutputStream(file))); + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + TsFileIOWriter writer = new TsFileIOWriter(output, parameter)) { + long dataStartOffset = output.getPosition(); + assertTrue(dataStartOffset > TSFileConfig.MAGIC_STRING.length() + Byte.BYTES); + writer.writeEncryptionHeaderIfNecessary(); + assertEquals(dataStartOffset, output.getPosition()); + verify(output, never()).force(); + verify(output).flush(); + + writer.getIOWriterOut().force(); + verify(output).force(); + } + } + + @Test + public void testAlignedMultiPageReadWrite() throws Exception { + TSFileConfig config = TSFileDescriptor.getInstance().getConfig(); + int previousMaxPointsInPage = config.getMaxNumberOfPointsInPage(); + config.setMaxNumberOfPointsInPage(1); + byte[] fileCryptoId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + fileCryptoId[0] = 1; + EncryptParameter encryptParameter = + TestAeadEncryptionProvider.createParameter(new byte[16], fileCryptoId); + + try { + List schemas = + Arrays.asList( + new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.RLE), + new MeasurementSchema("s2", TSDataType.INT64, TSEncoding.RLE)); + try (TsFileWriter writer = new TsFileWriter(file, encryptParameter)) { + writer.registerAlignedTimeseries(new Path("d1"), schemas); + for (int i = 1; i <= 3; i++) { + writer.writeRecord( + new TSRecord("d1", i) + .addTuple(new LongDataPoint("s1", i * 10L)) + .addTuple(new LongDataPoint("s2", i * 100L))); + } + } + + try (TsFileReader reader = new TsFileReader(new TsFileSequenceReader(file.getPath()))) { + QueryDataSet dataSet = + reader.query( + QueryExpression.create( + Arrays.asList(new Path("d1", "s1", true), new Path("d1", "s2", true)), null)); + for (int i = 1; i <= 3; i++) { + RowRecord record = dataSet.next(); + assertEquals(i, record.getTimestamp()); + assertEquals(i * 10L, record.getFields().get(0).getLongV()); + assertEquals(i * 100L, record.getFields().get(1).getLongV()); + } + assertFalse(dataSet.hasNext()); + } + } finally { + encryptParameter.close(); + config.setMaxNumberOfPointsInPage(previousMaxPointsInPage); + } + } + + @Test + public void testBufferedInputConstructorLoadsEncryptionHeader() throws Exception { + byte[] fileCryptoId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + fileCryptoId[0] = 1; + EncryptParameter encryptParameter = + TestAeadEncryptionProvider.createParameter(new byte[16], fileCryptoId); + + try { + try (TsFileWriter writer = new TsFileWriter(file, encryptParameter)) { + writer.registerTimeseries( + new Path("d1"), new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.RLE)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new LongDataPoint("s1", 1L))); + } + + try (TsFileSequenceReader reader = + new TsFileSequenceReader(new BufferedTsFileInput(file.toPath()), false, false, null)) { + reader.position(reader.getDataStartOffset()); + assertEquals(MetaMarker.CHUNK_GROUP_HEADER, reader.readMarker()); + } + } finally { + encryptParameter.close(); + } + } + + @Test + public void testMetadataOffsetConstructorLoadsEncryptionHeader() throws Exception { + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH])) { + try (TsFileWriter writer = new TsFileWriter(file, parameter)) { + writer.registerTimeseries( + new Path("d1"), new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.RLE)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new LongDataPoint("s1", 11L))); + } + try (TsFileSequenceReader original = new TsFileSequenceReader(file.getPath())) { + ChunkMetadata metadata = original.getChunkMetadataList(new Path("d1", "s1", true)).get(0); + try (TsFileSequenceReader reader = + new TsFileSequenceReader( + new BufferedTsFileInput(file.toPath()), + original.getFileMetadataPos(), + original.getTsFileMetadataSize())) { + assertTrue(reader.hasFileEncryptionHeader()); + original.position(metadata.getOffsetOfChunkHeader()); + reader.position(metadata.getOffsetOfChunkHeader()); + assertEquals( + original.readChunkHeader(original.readMarker()).getChunkOrdinal(), + reader.readChunkHeader(reader.readMarker()).getChunkOrdinal()); + } + } + } + } + + @Test + public void testMaterializedChunkSurvivesReaderClose() throws Exception { + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH])) { + try (TsFileWriter writer = new TsFileWriter(file, parameter)) { + writer.registerTimeseries( + new Path("d1"), new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.PLAIN)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new LongDataPoint("s1", 11L))); + } + Chunk chunk; + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + chunk = reader.readMemChunk(reader.getChunkMetadataList(new Path("d1", "s1", true)).get(0)); + } + try (Chunk ownedChunk = chunk) { + BatchData data = new ChunkReader(ownedChunk).nextPageData(); + IPointReader points = data.getBatchDataIterator(); + assertTrue(points.hasNextTimeValuePair()); + assertEquals(11L, points.nextTimeValuePair().getValue().getLong()); + } + assertTrue(chunk.getEncryptParam().isDestroyed()); + chunk.getData().rewind(); + assertThrows(EncryptException.class, () -> new ChunkReader(chunk).nextPageData()); + } + } + + @Test + public void testAlignedBlobLastPointDecryptsPage() throws Exception { + TSFileConfig config = TSFileDescriptor.getInstance().getConfig(); + int previousMaxPointsInPage = config.getMaxNumberOfPointsInPage(); + config.setMaxNumberOfPointsInPage(1); + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH])) { + List schemas = + Arrays.asList(new MeasurementSchema("s1", TSDataType.BLOB, TSEncoding.PLAIN)); + try (TsFileWriter writer = new TsFileWriter(file, parameter)) { + writer.registerAlignedTimeseries(new Path("d1"), schemas); + Tablet tablet = new Tablet("d1", schemas, 2); + for (int i = 0; i < 2; i++) { + tablet.addTimestamp(i, i + 1); + tablet.addValue(i, 0, ("value-" + i).getBytes(StandardCharsets.UTF_8)); + } + writer.writeTree(tablet); + } + try (TsFileLastReader reader = new TsFileLastReader(file.getPath(), true, false)) { + Pair>> last = reader.next(); + assertEquals("s1", last.right.get(1).left); + assertEquals(2L, last.right.get(1).right.getTimestamp()); + assertEquals( + new Binary("value-1", StandardCharsets.UTF_8), + last.right.get(1).right.getValue().getBinary()); + } + } finally { + config.setMaxNumberOfPointsInPage(previousMaxPointsInPage); + } + } + + @Test + public void testUnencryptedWriterRejectsChunkWithOrdinal() throws Exception { + try (TsFileIOWriter writer = new TsFileIOWriter(file)) { + ChunkHeader header = + new ChunkHeader( + "s1", 0, TSDataType.INT64, CompressionType.UNCOMPRESSED, TSEncoding.PLAIN, 1, 0, 0); + Chunk chunk = new Chunk(header, ByteBuffer.allocate(0)); + assertThrows(IOException.class, () -> writer.writeChunk(chunk)); + assertThrows(IOException.class, () -> writer.writeChunk(chunk, null)); + } + } + + @Test + public void testLegacyReadPageOverloadsRejectPageAeadBeforeReading() throws Exception { + EncryptParameter encryptParameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + try { + try (TsFileWriter writer = new TsFileWriter(file, encryptParameter)) { + writer.registerTimeseries( + new Path("d1"), + new MeasurementSchema( + "s1", TSDataType.INT64, TSEncoding.PLAIN, CompressionType.UNCOMPRESSED)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new LongDataPoint("s1", 11L))); + } + + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + ChunkMetadata metadata = reader.getChunkMetadataList(new Path("d1", "s1", true)).get(0); + reader.position(metadata.getOffsetOfChunkHeader()); + ChunkHeader chunkHeader = reader.readChunkHeader(reader.readMarker()); + PageHeader pageHeader = reader.readPageHeader(chunkHeader.getDataType(), false); + long pageBodyOffset = reader.position(); + + IOException noPageIndex = + assertThrows( + IOException.class, + () -> reader.readPage(pageHeader, chunkHeader.getCompressionType())); + assertTrue(noPageIndex.getMessage().contains("chunkOrdinal")); + assertEquals(pageBodyOffset, reader.position()); + + IOException noChunkOrdinal = + assertThrows( + IOException.class, + () -> reader.readPage(pageHeader, chunkHeader.getCompressionType(), 0)); + assertTrue(noChunkOrdinal.getMessage().contains("chunkOrdinal")); + assertEquals(pageBodyOffset, reader.position()); + + assertTrue( + reader + .readPage( + pageHeader, + chunkHeader.getCompressionType(), + 0, + chunkHeader.getChunkOrdinal()) + .remaining() + > 0); + } + } finally { + encryptParameter.close(); + } + } + + @Test + public void testChunkWriterReuseAllocatesDistinctOrdinals() throws Exception { + EncryptParameter encryptParameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + try { + try (TsFileWriter writer = new TsFileWriter(file, encryptParameter)) { + writer.registerTimeseries( + new Path("d1"), new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.RLE)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new LongDataPoint("s1", 11L))); + writer.flush(); + writer.writeRecord(new TSRecord("d1", 2).addTuple(new LongDataPoint("s1", 22L))); + } + + try (TsFileSequenceReader sequenceReader = new TsFileSequenceReader(file.getPath()); + TsFileReader reader = new TsFileReader(sequenceReader)) { + List chunks = + sequenceReader.getChunkMetadataList(new Path("d1", "s1", true)); + assertEquals(2, chunks.size()); + long firstOrdinal = + sequenceReader.readMemChunk(chunks.get(0)).getHeader().getChunkOrdinal(); + long secondOrdinal = + sequenceReader.readMemChunk(chunks.get(1)).getHeader().getChunkOrdinal(); + assertTrue(firstOrdinal >= 0); + assertNotEquals(firstOrdinal, secondOrdinal); + + QueryDataSet dataSet = + reader.query(QueryExpression.create(Arrays.asList(new Path("d1", "s1", true)), null)); + assertEquals(11L, dataSet.next().getFields().get(0).getLongV()); + assertEquals(22L, dataSet.next().getFields().get(0).getLongV()); + assertFalse(dataSet.hasNext()); + } + } finally { + encryptParameter.close(); + } + } + + @Test + public void testFlushAfterClearingPageWriter() throws Exception { + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + TsFileIOWriter writer = new TsFileIOWriter(file, parameter)) { + writer.startChunkGroup( + org.apache.tsfile.file.metadata.IDeviceID.Factory.DEFAULT_FACTORY.create("d1")); + ChunkWriterImpl chunkWriter = + new ChunkWriterImpl( + new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.RLE), parameter); + chunkWriter.write(1L, 42L); + chunkWriter.sealCurrentPage(); + chunkWriter.clearPageWriter(); + chunkWriter.writeToFileWriter(writer); + writer.endChunkGroup(); + writer.endFile(); + } + try (TsFileReader reader = new TsFileReader(new TsFileSequenceReader(file.getPath()))) { + QueryDataSet data = + reader.query(QueryExpression.create(Arrays.asList(new Path("d1", "s1", true)), null)); + assertEquals(42L, data.next().getFields().get(0).getLongV()); + assertFalse(data.hasNext()); + } + } + + @Test + public void testAlignedFlushAfterClearingPageWriters() throws Exception { + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + TsFileIOWriter writer = new TsFileIOWriter(file, parameter)) { + writer.startChunkGroup( + org.apache.tsfile.file.metadata.IDeviceID.Factory.DEFAULT_FACTORY.create("d1")); + TimeChunkWriter time = + new TimeChunkWriter( + "", + CompressionType.UNCOMPRESSED, + TSEncoding.PLAIN, + new PlainEncoder(TSDataType.INT64, 0), + parameter); + ValueChunkWriter value = + new ValueChunkWriter( + "s1", + CompressionType.UNCOMPRESSED, + TSDataType.INT64, + TSEncoding.PLAIN, + new PlainEncoder(TSDataType.INT64, 0), + parameter); + time.write(1L); + value.write(1L, 42L, false); + time.sealCurrentPage(); + value.sealCurrentPage(); + time.clearPageWriter(); + value.clearPageWriter(); + time.writeToFileWriter(writer); + value.writeToFileWriter(writer); + writer.endChunkGroup(); + writer.endFile(); + } + try (TsFileReader reader = new TsFileReader(new TsFileSequenceReader(file.getPath()))) { + QueryDataSet data = + reader.query(QueryExpression.create(Arrays.asList(new Path("d1", "s1", true)), null)); + assertEquals(42L, data.next().getFields().get(0).getLongV()); + assertFalse(data.hasNext()); + } + } + + @Test + public void testEncryptedChunkRewriteUsesNewOrdinal() throws Exception { + EncryptParameter encryptParameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + try { + try (TsFileWriter writer = new TsFileWriter(file, encryptParameter)) { + writer.registerTimeseries( + new Path("d1"), new MeasurementSchema("s1", TSDataType.INT64, TSEncoding.PLAIN)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new LongDataPoint("s1", 11L))); + } + + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + Chunk source = + reader.readMemChunk(reader.getChunkMetadataList(new Path("d1", "s1", true)).get(0)); + Chunk rewritten = source.rewrite(TSDataType.DOUBLE); + assertNotEquals( + source.getHeader().getChunkOrdinal(), rewritten.getHeader().getChunkOrdinal()); + for (IPageReader page : new ChunkReader(rewritten).loadPageReaderList()) { + BatchData data = page.getAllSatisfiedPageData(true); + IPointReader points = data.getBatchDataIterator(); + assertTrue(points.hasNextTimeValuePair()); + TimeValuePair point = points.nextTimeValuePair(); + assertEquals(1L, point.getTimestamp()); + assertEquals(11.0, point.getValue().getDouble(), 0.0); + assertFalse(points.hasNextTimeValuePair()); + } + } + } finally { + encryptParameter.close(); + } + } + + @Test + public void testSwappingEqualSizedPagesBetweenChunksFailsAuthentication() throws Exception { + EncryptParameter encryptParameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + try { + try (TsFileWriter writer = new TsFileWriter(file, encryptParameter)) { + writer.registerTimeseries( + new Path("d1"), + new MeasurementSchema( + "s1", TSDataType.INT64, TSEncoding.PLAIN, CompressionType.UNCOMPRESSED)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new LongDataPoint("s1", 11L))); + writer.flush(); + writer.writeRecord(new TSRecord("d1", 2).addTuple(new LongDataPoint("s1", 22L))); + } + + long[] pageBodyOffsets = new long[2]; + int[] pageBodySizes = new int[2]; + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + List chunks = reader.getChunkMetadataList(new Path("d1", "s1", true)); + assertEquals(2, chunks.size()); + for (int i = 0; i < chunks.size(); i++) { + Chunk chunk = reader.readMemChunk(chunks.get(i)); + ByteBuffer chunkData = chunk.getData(); + PageHeader pageHeader = PageHeader.deserializeFrom(chunkData, chunk.getChunkStatistic()); + pageBodyOffsets[i] = + chunks.get(i).getOffsetOfChunkHeader() + + chunk.getHeader().getSerializedSize() + + chunkData.position(); + pageBodySizes[i] = pageHeader.getCompressedSize(); + } + } + assertEquals(pageBodySizes[0], pageBodySizes[1]); + try (RandomAccessFile data = new RandomAccessFile(file, "rw")) { + byte[] firstPage = new byte[pageBodySizes[0]]; + byte[] secondPage = new byte[pageBodySizes[1]]; + data.seek(pageBodyOffsets[0]); + data.readFully(firstPage); + data.seek(pageBodyOffsets[1]); + data.readFully(secondPage); + data.seek(pageBodyOffsets[0]); + data.write(secondPage); + data.seek(pageBodyOffsets[1]); + data.write(firstPage); + } + + try (TsFileReader reader = new TsFileReader(new TsFileSequenceReader(file.getPath()))) { + assertThrows( + Exception.class, + () -> + reader + .query(QueryExpression.create(Arrays.asList(new Path("d1", "s1", true)), null)) + .next()); + } + } finally { + encryptParameter.close(); + } + } + + @Test + public void testRejectUnsafeEncryptedChunkReuse() throws Exception { + File targetFile = new File(file.getPath() + ".target"); + byte[] sourceFileCryptoId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + byte[] targetFileCryptoId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + sourceFileCryptoId[0] = 1; + targetFileCryptoId[0] = 2; + EncryptParameter sourceParameter = + TestAeadEncryptionProvider.createParameter(new byte[16], sourceFileCryptoId); + EncryptParameter targetParameter = + TestAeadEncryptionProvider.createParameter(new byte[16], targetFileCryptoId); + + try (TsFileIOWriter writer = new TsFileIOWriter(targetFile, targetParameter)) { + ChunkHeader header = + new ChunkHeader( + "s1", 0, TSDataType.INT64, CompressionType.UNCOMPRESSED, TSEncoding.PLAIN, 1); + Chunk sourceChunk = new Chunk(header, ByteBuffer.allocate(0), sourceParameter); + + assertThrows(IOException.class, () -> writer.writeChunk(sourceChunk)); + Chunk sameContextChunk = new Chunk(header, ByteBuffer.allocate(0), targetParameter); + assertThrows(IOException.class, () -> writer.writeChunk(sameContextChunk)); + assertThrows(IOException.class, () -> sourceChunk.mergeChunkByAppendPage(sourceChunk)); + } finally { + sourceParameter.close(); + targetParameter.close(); + if (targetFile.exists()) { + assertTrue(targetFile.delete()); + } + } + } + + @Test + public void testFileEncryptionContextCannotChangeAfterHeader() throws Exception { + EncryptParameter first = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + byte[] secondFileId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + secondFileId[0] = 1; + EncryptParameter second = + TestAeadEncryptionProvider.createParameter(new byte[16], secondFileId); + try (TsFileIOWriter writer = new TsFileIOWriter(file, first); + EncryptParameter firstCopy = first.copy()) { + EncryptParameter owned = writer.getEncryptParameter(); + writer.setEncryptParam(owned); + assertThrows(EncryptException.class, () -> writer.setEncryptParam(second)); + assertThrows(EncryptException.class, () -> writer.setEncryptParam(firstCopy)); + assertThrows( + EncryptException.class, + () -> writer.setEncryptParam("0", "org.apache.tsfile.encrypt.UNENCRYPTED", null)); + assertTrue(writer.getEncryptParameter() == owned); + assertFalse(first.isDestroyed()); + } finally { + first.close(); + second.close(); + } + } + + @Test + public void testChunkFromDifferentFileContextIsRejectedBeforeWriting() throws Exception { + EncryptParameter target = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + byte[] sourceFileId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + sourceFileId[0] = 1; + EncryptParameter source = + TestAeadEncryptionProvider.createParameter(new byte[16], sourceFileId); + try (TsFileIOWriter writer = new TsFileIOWriter(file, target)) { + ChunkWriterImpl chunk = + new ChunkWriterImpl(new MeasurementSchema("s1", TSDataType.INT64), source); + chunk.write(1, 11L); + long position = writer.getPos(); + assertThrows(IOException.class, () -> chunk.writeToFileWriter(writer)); + assertEquals(position, writer.getPos()); + + TimeChunkWriter timeChunk = + new TimeChunkWriter( + "", + CompressionType.UNCOMPRESSED, + TSEncoding.PLAIN, + new PlainEncoder(TSDataType.INT64, 0), + source); + timeChunk.write(1); + timeChunk.sealCurrentPage(); + assertThrows(IOException.class, () -> timeChunk.writeAllPagesOfChunkToTsFile(writer)); + assertEquals(position, writer.getPos()); + + ValueChunkWriter valueChunk = + new ValueChunkWriter( + "s1", + CompressionType.UNCOMPRESSED, + TSDataType.INT64, + TSEncoding.PLAIN, + new PlainEncoder(TSDataType.INT64, 0), + source); + valueChunk.write(1, 11L, false); + valueChunk.sealCurrentPage(); + assertThrows(IOException.class, () -> valueChunk.writeAllPagesOfChunkToTsFile(writer, null)); + assertEquals(position, writer.getPos()); + + assertThrows( + IOException.class, + () -> + writer.startFlushChunk( + "s1", + CompressionType.UNCOMPRESSED, + TSDataType.INT64, + TSEncoding.PLAIN, + null, + 0, + 0, + 0, + 0, + null)); + assertEquals(position, writer.getPos()); + } finally { + source.close(); + target.close(); + } + } +} diff --git a/java/tsfile/src/test/java/org/apache/tsfile/write/TsFileIOWriterTest.java b/java/tsfile/src/test/java/org/apache/tsfile/write/TsFileIOWriterTest.java index 51a8b80a0e..2e6e06e3ef 100644 --- a/java/tsfile/src/test/java/org/apache/tsfile/write/TsFileIOWriterTest.java +++ b/java/tsfile/src/test/java/org/apache/tsfile/write/TsFileIOWriterTest.java @@ -22,6 +22,7 @@ import org.apache.tsfile.common.conf.TSFileDescriptor; import org.apache.tsfile.common.constant.TsFileConstant; import org.apache.tsfile.constant.TestConstant; +import org.apache.tsfile.encrypt.EncryptParameter; import org.apache.tsfile.enums.ColumnCategory; import org.apache.tsfile.enums.TSDataType; import org.apache.tsfile.exception.write.WriteProcessException; @@ -112,6 +113,17 @@ public void after() { } } + @Test + public void testLegacyParameterSetterPreservesSuppliedKey() throws IOException { + EncryptParameter parameter = + new EncryptParameter("org.apache.tsfile.encrypt.UNENCRYPTED", new byte[] {1, 2, 3}); + try (TsFileIOWriter writer = new TsFileIOWriter(new File(FILE_PATH))) { + writer.setEncryptParam(parameter); + Assert.assertSame(parameter, writer.getEncryptParameter()); + Assert.assertArrayEquals(new byte[] {1, 2, 3}, writer.getEncryptParameter().getKey()); + } + } + @Test public void changeTypeCompressionTest() throws IOException, WriteProcessException { TSFileConfig config = TSFileDescriptor.getInstance().getConfig(); diff --git a/java/tsfile/src/test/java/org/apache/tsfile/write/page/PageBodyEncoderTest.java b/java/tsfile/src/test/java/org/apache/tsfile/write/page/PageBodyEncoderTest.java new file mode 100644 index 0000000000..60f5e8a7f0 --- /dev/null +++ b/java/tsfile/src/test/java/org/apache/tsfile/write/page/PageBodyEncoderTest.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.tsfile.write.page; + +import org.apache.tsfile.compress.ICompressor; +import org.apache.tsfile.compress.IUnCompressor; +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.EncryptionProviderRegistry; +import org.apache.tsfile.encrypt.IDecryptor; +import org.apache.tsfile.encrypt.PageCryptoContext; +import org.apache.tsfile.encrypt.TestAeadEncryptionProvider; +import org.apache.tsfile.file.metadata.enums.CompressionType; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.nio.ByteBuffer; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +public class PageBodyEncoderTest { + + @BeforeClass + public static void registerProvider() { + EncryptionProviderRegistry.registerProvider(TestAeadEncryptionProvider.INSTANCE); + } + + @AfterClass + public static void unregisterProvider() { + EncryptionProviderRegistry.unregisterProvider(TestAeadEncryptionProvider.PROVIDER_ID); + } + + @Test + public void testUncompressedBodyWrapsSliceWithoutCopying() throws Exception { + ByteBuffer input = slicedInput(); + ByteBuffer encoded = + PageBodyEncoder.encode( + input, + input.remaining(), + ICompressor.getCompressor(CompressionType.UNCOMPRESSED), + null, + 0, + 0); + assertSame(input.array(), encoded.array()); + assertEquals(input.remaining(), encoded.remaining()); + assertEquals(input.arrayOffset() + input.position(), encoded.position()); + assertArrayEquals(remainingBytes(input), remainingBytes(encoded)); + assertEquals(2, input.position()); + } + + @Test + public void testCompressedBodyUsesOnlySliceAndValidOutputBytes() throws Exception { + for (CompressionType compression : + new CompressionType[] {CompressionType.GZIP, CompressionType.LZ4}) { + ByteBuffer input = slicedInput(); + ByteBuffer encoded = + PageBodyEncoder.encode( + input, input.remaining(), ICompressor.getCompressor(compression), null, 0, 0); + byte[] restored = new byte[input.remaining()]; + assertEquals( + restored.length, + IUnCompressor.getUnCompressor(compression) + .uncompress( + encoded.array(), + encoded.arrayOffset() + encoded.position(), + encoded.remaining(), + restored, + 0)); + assertArrayEquals(remainingBytes(input), restored); + assertEquals(2, input.position()); + } + } + + @Test + public void testEncryptedBodyRoundTripForSlicedInput() throws Exception { + encryptedBodyRoundTrip(false); + encryptedBodyRoundTrip(true); + } + + private void encryptedBodyRoundTrip(boolean padded) throws Exception { + for (CompressionType compression : + new CompressionType[] { + CompressionType.UNCOMPRESSED, CompressionType.GZIP, CompressionType.LZ4 + }) { + ByteBuffer input = slicedInput(); + try (EncryptParameter parameter = + padded + ? TestAeadEncryptionProvider.createPaddedParameter(new byte[16], new byte[16]) + : TestAeadEncryptionProvider.createParameter(new byte[16], new byte[16])) { + ByteBuffer encoded = + PageBodyEncoder.encode( + input, input.remaining(), ICompressor.getCompressor(compression), parameter, 3, 7); + PageCryptoContext context = + PageCryptoContext.forDecryption( + parameter, input.remaining(), encoded.asReadOnlyBuffer(), 3, 7); + byte[] compressed = + IDecryptor.getDecryptor(parameter) + .decryptPage( + encoded.array(), + encoded.arrayOffset() + encoded.position(), + encoded.remaining(), + context); + byte[] restored = new byte[input.remaining()]; + assertEquals( + restored.length, + IUnCompressor.getUnCompressor(compression) + .uncompress(compressed, 0, compressed.length, restored, 0)); + assertArrayEquals(remainingBytes(input), restored); + assertEquals(2, input.position()); + } + } + } + + private static ByteBuffer slicedInput() { + byte[] bytes = new byte[140]; + for (int i = 0; i < bytes.length; i++) { + bytes[i] = (byte) i; + } + ByteBuffer parent = ByteBuffer.wrap(bytes); + parent.position(3); + parent.limit(135); + ByteBuffer slice = parent.slice(); + slice.position(2); + slice.limit(130); + return slice; + } + + private static byte[] remainingBytes(ByteBuffer buffer) { + byte[] bytes = new byte[buffer.remaining()]; + buffer.duplicate().get(bytes); + return bytes; + } +} diff --git a/java/tsfile/src/test/java/org/apache/tsfile/write/writer/ForceAppendTsFileWriterTest.java b/java/tsfile/src/test/java/org/apache/tsfile/write/writer/ForceAppendTsFileWriterTest.java index 68d05cefd7..223d68a393 100644 --- a/java/tsfile/src/test/java/org/apache/tsfile/write/writer/ForceAppendTsFileWriterTest.java +++ b/java/tsfile/src/test/java/org/apache/tsfile/write/writer/ForceAppendTsFileWriterTest.java @@ -18,7 +18,11 @@ */ package org.apache.tsfile.write.writer; +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.EncryptionProviderRegistry; +import org.apache.tsfile.encrypt.TestAeadEncryptionProvider; import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.file.metadata.ChunkMetadata; import org.apache.tsfile.file.metadata.enums.TSEncoding; import org.apache.tsfile.fileSystem.FSFactoryProducer; import org.apache.tsfile.fileSystem.fsFactory.FSFactory; @@ -34,15 +38,21 @@ import org.apache.tsfile.write.record.datapoint.FloatDataPoint; import org.apache.tsfile.write.schema.MeasurementSchema; +import org.junit.AfterClass; import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Test; import java.io.File; +import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -51,6 +61,16 @@ public class ForceAppendTsFileWriterTest { TsFileGeneratorForTest.getTestTsFilePath("root.sg1", 0, 0, 1); private static FSFactory fsFactory = FSFactoryProducer.getFSFactory(); + @BeforeClass + public static void setUpEncryptionProvider() { + EncryptionProviderRegistry.registerProvider(TestAeadEncryptionProvider.INSTANCE); + } + + @AfterClass + public static void tearDownEncryptionProvider() { + EncryptionProviderRegistry.unregisterProvider(TestAeadEncryptionProvider.PROVIDER_ID); + } + @Test public void test() throws Exception { File file = fsFactory.getFile(FILE_NAME); @@ -120,4 +140,90 @@ record = dataSet.next(); assertTrue(file.delete()); } + + @Test + public void testHeaderlessForceAppendRejectsPageAeadParameter() throws Exception { + File file = fsFactory.getFile(FILE_NAME + ".headerless"); + if (!file.getParentFile().exists()) { + assertTrue(file.getParentFile().mkdirs()); + } + try { + try (TsFileWriter writer = new TsFileWriter(file)) { + writer.registerTimeseries( + new Path("d1"), new MeasurementSchema("s1", TSDataType.FLOAT, TSEncoding.RLE)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new FloatDataPoint("s1", 5))); + } + long originalLength = file.length(); + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH])) { + assertThrows(IOException.class, () -> new ForceAppendTsFileWriter(file, parameter)); + } + assertEquals(originalLength, file.length()); + } finally { + if (file.exists()) { + assertTrue(file.delete()); + } + } + } + + @Test + public void testEncryptedForceAppend() throws Exception { + File file = fsFactory.getFile(FILE_NAME + ".encrypted"); + if (!file.getParentFile().exists()) { + Assert.assertTrue(file.getParentFile().mkdirs()); + } + byte[] fileCryptoId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + fileCryptoId[0] = 1; + EncryptParameter encryptParameter = + TestAeadEncryptionProvider.createParameter(new byte[16], fileCryptoId); + EncryptParameter appendParameter = + TestAeadEncryptionProvider.createParameter(new byte[16], fileCryptoId); + + try { + try (TsFileWriter writer = new TsFileWriter(file, encryptParameter)) { + writer.registerTimeseries( + new Path("d1"), new MeasurementSchema("s1", TSDataType.FLOAT, TSEncoding.RLE)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new FloatDataPoint("s1", 5))); + } + + ForceAppendTsFileWriter appendWriter = new ForceAppendTsFileWriter(file, appendParameter); + EncryptParameter ownedParameter = appendWriter.getEncryptParameter(); + Assert.assertNotSame(encryptParameter, ownedParameter); + appendWriter.doTruncate(); + try (TsFileWriter writer = new TsFileWriter(appendWriter, appendParameter)) { + writer.registerTimeseries( + new Path("d1"), new MeasurementSchema("s1", TSDataType.FLOAT, TSEncoding.RLE)); + writer.writeRecord(new TSRecord("d1", 2).addTuple(new FloatDataPoint("s1", 6))); + } + assertTrue(ownedParameter.isDestroyed()); + assertFalse(encryptParameter.isDestroyed()); + assertFalse(appendParameter.isDestroyed()); + + try (TsFileSequenceReader sequenceReader = new TsFileSequenceReader(file.getPath()); + TsFileReader reader = new TsFileReader(sequenceReader)) { + List chunks = + sequenceReader.getChunkMetadataList(new Path("d1", "s1", true)); + assertEquals(2, chunks.size()); + assertNotEquals( + sequenceReader.readMemChunk(chunks.get(0)).getHeader().getChunkOrdinal(), + sequenceReader.readMemChunk(chunks.get(1)).getHeader().getChunkOrdinal()); + QueryDataSet dataSet = + reader.query( + QueryExpression.create( + Collections.singletonList(new Path("d1", "s1", true)), null)); + RowRecord first = dataSet.next(); + assertEquals(1, first.getTimestamp()); + assertEquals(5.0f, first.getFields().get(0).getFloatV(), 0.001); + RowRecord second = dataSet.next(); + assertEquals(2, second.getTimestamp()); + assertEquals(6.0f, second.getFields().get(0).getFloatV(), 0.001); + assertFalse(dataSet.hasNext()); + } + } finally { + encryptParameter.close(); + appendParameter.close(); + file.delete(); + } + } } diff --git a/java/tsfile/src/test/java/org/apache/tsfile/write/writer/RestorableTsFileIOWriterTest.java b/java/tsfile/src/test/java/org/apache/tsfile/write/writer/RestorableTsFileIOWriterTest.java index 0e1279faeb..e1ffa067cf 100644 --- a/java/tsfile/src/test/java/org/apache/tsfile/write/writer/RestorableTsFileIOWriterTest.java +++ b/java/tsfile/src/test/java/org/apache/tsfile/write/writer/RestorableTsFileIOWriterTest.java @@ -20,6 +20,9 @@ package org.apache.tsfile.write.writer; import org.apache.tsfile.common.conf.TSFileConfig; +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.EncryptionProviderRegistry; +import org.apache.tsfile.encrypt.TestAeadEncryptionProvider; import org.apache.tsfile.enums.TSDataType; import org.apache.tsfile.exception.NotCompatibleTsFileException; import org.apache.tsfile.file.MetaMarker; @@ -44,8 +47,10 @@ import org.apache.tsfile.write.schema.MeasurementSchema; import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import java.io.File; @@ -58,6 +63,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; @SuppressWarnings("squid:S4042") // Suppress use java.nio.Files#delete warning public class RestorableTsFileIOWriterTest { @@ -67,6 +73,16 @@ public class RestorableTsFileIOWriterTest { private static final FSFactory fsFactory = FSFactoryProducer.getFSFactory(); File file = fsFactory.getFile(FILE_NAME); + @BeforeClass + public static void setUpEncryptionProvider() { + EncryptionProviderRegistry.registerProvider(TestAeadEncryptionProvider.INSTANCE); + } + + @AfterClass + public static void tearDownEncryptionProvider() { + EncryptionProviderRegistry.unregisterProvider(TestAeadEncryptionProvider.PROVIDER_ID); + } + @Before public void setUp() throws IOException { if (!file.getParentFile().exists()) { @@ -105,6 +121,155 @@ public void testOnlyHeadMagic() throws Exception { rWriter.close(); } + @Test + public void testHeaderlessRecoveryKeepsOnDiskFormatWithPageAeadParameter() throws Exception { + TsFileWriter writer = new TsFileWriter(file); + writer.getIOWriter().close(); + long originalLength = file.length(); + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH])) { + try (RestorableTsFileIOWriter recovered = + new RestorableTsFileIOWriter(file, false, parameter)) { + assertFalse(recovered.getEncryptParameter().isTdePageAead()); + assertTrue(recovered.canWrite()); + } + assertFalse(parameter.isDestroyed()); + } + assertEquals(originalLength, file.length()); + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath(), false)) { + assertFalse(reader.hasFileEncryptionHeader()); + } + } + + @Test + public void testPlaintextRecoveryAndAppendDoNotAdoptCallerAeadContext() throws Exception { + TsFileWriter original = new TsFileWriter(file); + original.registerTimeseries(new Path("d1"), new MeasurementSchema("s1", TSDataType.FLOAT)); + original.writeRecord(new TSRecord("d1", 1).addTuple(new FloatDataPoint("s1", 11))); + original.flush(); + original.getIOWriter().writePlanIndices(); + original.getIOWriter().close(); + try (EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH])) { + try (org.apache.tsfile.read.UnClosedTsFileReader reader = + new org.apache.tsfile.read.UnClosedTsFileReader(file.getPath(), parameter, null)) { + assertFalse(reader.getEncryptParam().isTdePageAead()); + } + RestorableTsFileIOWriter recovered = new RestorableTsFileIOWriter(file, parameter); + try (TsFileWriter writer = new TsFileWriter(recovered, parameter)) { + writer.writeRecord(new TSRecord("d1", 2).addTuple(new FloatDataPoint("s1", 22))); + } + assertFalse(parameter.isDestroyed()); + } + try (TsFileSequenceReader sequence = new TsFileSequenceReader(file.getPath()); + TsFileReader reader = new TsFileReader(sequence)) { + assertFalse(sequence.hasFileEncryptionHeader()); + QueryDataSet query = + reader.query( + QueryExpression.create( + java.util.Collections.singletonList(new Path("d1", "s1", true)), null)); + assertEquals(11.0f, query.next().getFields().get(0).getFloatV(), 0.0f); + assertEquals(22.0f, query.next().getFields().get(0).getFloatV(), 0.0f); + assertFalse(query.hasNext()); + } + } + + @Test + public void testEncryptedHeaderSurvivesUnclosedFile() throws Exception { + byte[] fileCryptoId = new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]; + for (int i = 0; i < fileCryptoId.length; i++) { + fileCryptoId[i] = (byte) (i + 1); + } + EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter(new byte[16], fileCryptoId); + TsFileWriter writer = new TsFileWriter(file, parameter); + long expectedDataStartOffset; + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath(), false)) { + expectedDataStartOffset = reader.getDataStartOffset(); + assertNotEquals( + TSFileConfig.MAGIC_STRING.getBytes().length + Byte.BYTES, expectedDataStartOffset); + assertEquals("test-key", reader.getEncryptParam().getKeyId()); + Assert.assertArrayEquals(fileCryptoId, reader.getEncryptParam().getFileCryptoId()); + } + writer.getIOWriter().close(); + writer.getIOWriter().getEncryptParameter().close(); + + RestorableTsFileIOWriter restorableWriter = new RestorableTsFileIOWriter(file, parameter); + assertEquals(expectedDataStartOffset, restorableWriter.getTruncatedSize()); + assertEquals("test-key", restorableWriter.getEncryptParameter().getKeyId()); + EncryptParameter ownedParameter = restorableWriter.getEncryptParameter(); + Assert.assertNotSame(parameter, ownedParameter); + try (TsFileWriter recoveredWriter = new TsFileWriter(restorableWriter, parameter)) { + // Closing writes a valid footer without replacing the file encryption header. + } + Assert.assertTrue(ownedParameter.isDestroyed()); + Assert.assertFalse(parameter.isDestroyed()); + parameter.close(); + } + + @Test + public void testEncryptedChunkOrdinalContinuesAfterRecovery() throws Exception { + EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + try { + TsFileWriter writer = new TsFileWriter(file, parameter); + writer.registerTimeseries( + new Path("d1"), new MeasurementSchema("s1", TSDataType.FLOAT, TSEncoding.RLE)); + writer.writeRecord(new TSRecord("d1", 1).addTuple(new FloatDataPoint("s1", 1.0f))); + writer.flush(); + writer.getIOWriter().writePlanIndices(); + ChunkMetadata firstMetadata = + writer.getIOWriter().getChunkGroupMetadataList().get(0).getChunkMetadataList().get(0); + long firstOrdinal; + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath(), false)) { + firstOrdinal = reader.readMemChunk(firstMetadata).getHeader().getChunkOrdinal(); + } + writer.getIOWriter().close(); + writer.getIOWriter().getEncryptParameter().close(); + + try (EncryptParameter recoveryParameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH])) { + RestorableTsFileIOWriter restorableWriter = + new RestorableTsFileIOWriter(file, recoveryParameter); + try (TsFileWriter recoveredWriter = new TsFileWriter(restorableWriter, recoveryParameter)) { + recoveredWriter.writeRecord( + new TSRecord("d1", 2).addTuple(new FloatDataPoint("s1", 2.0f))); + } + } + + try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath())) { + List chunks = reader.getChunkMetadataList(new Path("d1", "s1", true)); + assertEquals(2, chunks.size()); + assertEquals( + firstOrdinal, reader.readMemChunk(chunks.get(0)).getHeader().getChunkOrdinal()); + Assert.assertTrue( + reader.readMemChunk(chunks.get(1)).getHeader().getChunkOrdinal() > firstOrdinal); + } + } finally { + parameter.close(); + } + } + + @Test + public void testCloseDestroysOwnedTdeParameter() throws Exception { + EncryptParameter parameter = + TestAeadEncryptionProvider.createParameter( + new byte[16], new byte[EncryptParameter.FILE_CRYPTO_ID_LENGTH]); + RestorableTsFileIOWriter writer = new RestorableTsFileIOWriter(file, parameter); + EncryptParameter ownedParameter = writer.getEncryptParameter(); + + writer.close(); + + Assert.assertNotSame(parameter, ownedParameter); + Assert.assertTrue(ownedParameter.isDestroyed()); + Assert.assertFalse(parameter.isDestroyed()); + parameter.close(); + } + @Test public void testOnlyFirstMask() throws Exception { TsFileWriter writer = new TsFileWriter(file); diff --git a/java/tsfile/src/test/java/org/apache/tsfile/write/writer/TimePageWriterTest.java b/java/tsfile/src/test/java/org/apache/tsfile/write/writer/TimePageWriterTest.java index 878b9d7a76..c09ccf28fa 100644 --- a/java/tsfile/src/test/java/org/apache/tsfile/write/writer/TimePageWriterTest.java +++ b/java/tsfile/src/test/java/org/apache/tsfile/write/writer/TimePageWriterTest.java @@ -23,14 +23,24 @@ import org.apache.tsfile.encoding.decoder.PlainDecoder; import org.apache.tsfile.encoding.encoder.Encoder; import org.apache.tsfile.encoding.encoder.PlainEncoder; +import org.apache.tsfile.encrypt.EncryptParameter; +import org.apache.tsfile.encrypt.EncryptionProviderRegistry; +import org.apache.tsfile.encrypt.IDecryptor; +import org.apache.tsfile.encrypt.TestAeadEncryptionProvider; import org.apache.tsfile.enums.TSDataType; +import org.apache.tsfile.file.header.ChunkHeader; +import org.apache.tsfile.file.header.PageHeader; import org.apache.tsfile.file.metadata.enums.CompressionType; +import org.apache.tsfile.file.metadata.enums.TSEncoding; import org.apache.tsfile.file.metadata.statistics.TimeStatistics; +import org.apache.tsfile.read.reader.chunk.ChunkReader; import org.apache.tsfile.utils.PublicBAOS; import org.apache.tsfile.utils.ReadWriteForEncodingUtils; import org.apache.tsfile.utils.ReadWriteIOUtils; import org.apache.tsfile.write.page.TimePageWriter; +import org.junit.AfterClass; +import org.junit.BeforeClass; import org.junit.Test; import java.io.IOException; @@ -41,6 +51,16 @@ public class TimePageWriterTest { + @BeforeClass + public static void setUpEncryptionProvider() { + EncryptionProviderRegistry.registerProvider(TestAeadEncryptionProvider.INSTANCE); + } + + @AfterClass + public static void tearDownEncryptionProvider() { + EncryptionProviderRegistry.unregisterProvider(TestAeadEncryptionProvider.PROVIDER_ID); + } + @Test public void testWrite() { Encoder timeEncoder = new PlainEncoder(TSDataType.INT64, 0); @@ -168,4 +188,50 @@ public void testWritePageHeaderAndDataIntoBuffWithSnappy() { fail(); } } + + @Test + public void testWriteAndReadPageWithAeadEncryption() throws IOException { + Encoder timeEncoder = new PlainEncoder(TSDataType.INT64, 0); + ICompressor compressor = ICompressor.getCompressor(CompressionType.UNCOMPRESSED); + EncryptParameter encryptParameter = + TestAeadEncryptionProvider.createParameter(new byte[16], new byte[16]); + TimePageWriter pageWriter = new TimePageWriter(timeEncoder, compressor, encryptParameter); + long chunkOrdinal = encryptParameter.nextChunkOrdinal(); + pageWriter.setChunkOrdinal(chunkOrdinal); + PublicBAOS publicBAOS = new PublicBAOS(); + + try { + pageWriter.write(1L); + pageWriter.write(2L); + pageWriter.write(3L); + pageWriter.writePageHeaderAndDataIntoBuff(publicBAOS, false, 0); + + ByteBuffer pageBuffer = ByteBuffer.wrap(publicBAOS.getBuf(), 0, publicBAOS.size()); + PageHeader pageHeader = PageHeader.deserializeFrom(pageBuffer, TSDataType.VECTOR); + assertEquals(24, pageHeader.getUncompressedSize()); + assertEquals(52, pageHeader.getCompressedSize()); + + ChunkHeader chunkHeader = + new ChunkHeader( + "time", + pageHeader.getCompressedSize(), + TSDataType.INT64, + CompressionType.UNCOMPRESSED, + TSEncoding.PLAIN, + 1, + 0, + chunkOrdinal); + IDecryptor decryptor = IDecryptor.getDecryptor(encryptParameter); + ByteBuffer uncompressedPageData = + ChunkReader.deserializePageData( + pageHeader, pageBuffer, chunkHeader, decryptor, encryptParameter, 0); + + assertEquals(1L, ReadWriteIOUtils.readLong(uncompressedPageData)); + assertEquals(2L, ReadWriteIOUtils.readLong(uncompressedPageData)); + assertEquals(3L, ReadWriteIOUtils.readLong(uncompressedPageData)); + assertEquals(0, uncompressedPageData.remaining()); + } finally { + encryptParameter.close(); + } + } }