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 2836b8c1a..54f9728be 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
@@ -1117,6 +1117,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 = This encryptor does not implement authenticated page encryption
+error.encrypt.page_aead_decrypt_unsupported = This decryptor 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
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 502df532e..b86387e0a 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
@@ -1117,6 +1117,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 = 此加密器未实现经过认证的 Page 加密
+error.encrypt.page_aead_decrypt_unsupported = 此解密器未实现经过认证的 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
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 f7adb5221..70cf0951c 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,116 @@
*/
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.util.Arrays;
+import java.util.Objects;
+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 transient volatile IEncrypt fileEncrypt;
+ 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();
+ }
+
+ 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;
+ }
+
+ public static Builder pageAeadBuilder() {
+ return new Builder();
+ }
+
+ /** Returns an independent parameter object with no initialized runtime provider state. */
+ public 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)
+ .build();
+ }
+
+ /** 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 +137,169 @@ 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);
+ }
+
+ 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;
+ }
+
+ /** 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);
+ }
+
+ IEncrypt getOrCreateFileEncrypt() {
+ if (destroyed) {
+ throw new EncryptException(Messages.get("error.encrypt.parameter_destroyed"));
+ }
+ IEncrypt current = fileEncrypt;
+ if (current == null) {
+ synchronized (this) {
+ if (destroyed) {
+ throw new EncryptException(Messages.get("error.encrypt.parameter_destroyed"));
+ }
+ current = fileEncrypt;
+ if (current == null) {
+ current = EncryptionProviderRegistry.create(this);
+ fileEncrypt = current;
+ }
+ }
+ }
+ return current;
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return destroyed;
+ }
+
+ @Override
+ public void destroy() {
+ IEncrypt current;
+ synchronized (this) {
+ if (destroyed) {
+ return;
+ }
+ destroyed = true;
+ current = fileEncrypt;
+ fileEncrypt = null;
+ if (key != null && isTdePageAead()) {
+ Arrays.fill(key, (byte) 0);
+ }
+ }
+ if (current != null) {
+ current.close();
+ }
+ }
+
+ @Override
+ public void close() {
+ destroy();
+ }
+
+ private static byte[] copy(byte[] value) {
+ return value == null ? null : Arrays.copyOf(value, value.length);
+ }
+
+ 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 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;
+ }
+
+ 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 7c8dbcd25..daa79d1dd 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
@@ -260,6 +260,12 @@ public static EncryptParameter getEncryptParameter() {
/** Get the second EncryptParameter object according to the given type and first key. */
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 +303,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 000000000..49a097cf7
--- /dev/null
+++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/EncryptionProviderRegistry.java
@@ -0,0 +1,77 @@
+/*
+ * 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.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/** Registry for file-level encryption providers identified by stable provider ids. */
+public final class EncryptionProviderRegistry {
+
+ private static final ConcurrentHashMap PROVIDERS =
+ new ConcurrentHashMap<>();
+ private static final AtomicBoolean SERVICES_LOADED = new AtomicBoolean();
+
+ 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);
+ 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));
+ }
+ return encrypt;
+ }
+
+ private static void loadServices() {
+ if (SERVICES_LOADED.compareAndSet(false, true)) {
+ for (IEncryptProvider provider : ServiceLoader.load(IEncryptProvider.class)) {
+ registerProvider(provider);
+ }
+ }
+ }
+}
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 de7735dae..464dbb507 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,10 @@ 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.get("error.encrypt.page_aead_decrypt_unsupported"));
+ }
+
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 0ea6bbef7..7f8b5e0a3 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
@@ -20,7 +20,7 @@
import java.util.concurrent.ConcurrentHashMap;
-public interface IEncrypt {
+public interface IEncrypt extends AutoCloseable {
static ConcurrentHashMap> encryptMap =
new ConcurrentHashMap<>();
@@ -30,4 +30,17 @@ 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;
+ }
+
+ @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 000000000..d10262f51
--- /dev/null
+++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/IEncryptProvider.java
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+public interface IEncryptProvider {
+
+ String getProviderId();
+
+ 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 7d60ecfc4..c5c16ae64 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,10 @@ 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.get("error.encrypt.page_aead_encrypt_unsupported"));
+ }
+
EncryptionType getEncryptionType();
}
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 000000000..d4c503b62
--- /dev/null
+++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/PageCryptoContext.java
@@ -0,0 +1,195 @@
+/*
+ * 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 = 2;
+
+ 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 =
+ Math.addExact(compressedPlaintextSize, parameter.getPageBodyOverhead());
+ 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 compressedPlaintextSize = encryptedPageBodySize - parameter.getPageBodyOverhead();
+ if (compressedPlaintextSize < 0) {
+ throw new EncryptException(
+ Messages.format(
+ "error.encrypt.page_body_too_short",
+ encryptedPageBodySize,
+ parameter.getPageBodyOverhead()));
+ }
+ return new PageCryptoContext(
+ parameter,
+ uncompressedSize,
+ compressedPlaintextSize,
+ encryptedPageBodySize,
+ pageIndex,
+ chunkOrdinal);
+ }
+
+ public int getUncompressedSize() {
+ return uncompressedSize;
+ }
+
+ /** 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.getFileCryptoId() == null
+ || parameter.getFileCryptoId().length != EncryptParameter.FILE_CRYPTO_ID_LENGTH) {
+ 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) {
+ byte[][] components =
+ new byte[][] {
+ bytes(parameter.getProviderId()),
+ bytes(parameter.getProfileId()),
+ bytes(parameter.getKeyId()),
+ bytes(parameter.getKeyVersion()),
+ parameter.getFileCryptoId()
+ };
+ int size = Integer.BYTES * (4 + components.length) + Long.BYTES;
+ 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);
+ }
+ buffer.putLong(chunkOrdinal);
+ buffer.putInt(pageIndex);
+ buffer.putInt(uncompressedSize);
+ buffer.putInt(compressedPlaintextSize);
+ return buffer.array();
+ }
+
+ private static byte[] bytes(String value) {
+ return value == null ? new byte[0] : value.getBytes(TSFileConfig.STRING_CHARSET);
+ }
+}
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 5cca70594..ce8bf8b37 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 a03209fdc..12e19460e 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
@@ -34,6 +34,7 @@
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;
@@ -63,6 +64,7 @@ public class ChunkHeader {
private final TSDataType dataType;
private final CompressionType compressionType;
private final TSEncoding encodingType;
+ private final long chunkOrdinal;
// the following fields do not need to be serialized.
private int numOfPages;
@@ -87,16 +89,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),
+ getSerializedSize(measurementID, dataSize, chunkOrdinal >= 0),
dataType,
compressionType,
- encoding);
+ encoding,
+ chunkOrdinal);
this.numOfPages = numOfPages;
}
@@ -117,6 +132,25 @@ public ChunkHeader(
encoding);
}
+ public ChunkHeader(
+ byte chunkType,
+ String measurementID,
+ int dataSize,
+ TSDataType dataType,
+ CompressionType compressionType,
+ TSEncoding encoding,
+ long chunkOrdinal) {
+ this(
+ chunkType,
+ measurementID,
+ dataSize,
+ getSerializedSize(measurementID, dataSize, chunkOrdinal >= 0),
+ dataType,
+ compressionType,
+ encoding,
+ chunkOrdinal);
+ }
+
public ChunkHeader(
byte chunkType,
String measurementID,
@@ -125,12 +159,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 +194,10 @@ public static int getSerializedSize(String measurementID, int dataSize) {
+ TSEncoding.getSerializedSize(); // encodingType
}
+ public static int getSerializedSize(String measurementID, int dataSize, boolean pageAead) {
+ return getSerializedSize(measurementID, dataSize) + (pageAead ? Long.BYTES : 0);
+ }
+
/**
* The estimated serialized size of chunk header. Only used when we don't know the actual dataSize
* attribute
@@ -176,13 +227,34 @@ 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 {
// 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;
+ if (pageAead) {
+ byte[] ordinalBytes = inputStream.readNBytes(Long.BYTES);
+ if (ordinalBytes.length != Long.BYTES) {
+ throw new EOFException();
+ }
+ chunkOrdinal = ByteBuffer.wrap(ordinalBytes).getLong();
+ }
+ return new ChunkHeader(
+ chunkType,
+ measurementID,
+ dataSize,
+ getSerializedSize(measurementID, dataSize, pageAead),
+ dataType,
+ type,
+ encoding,
+ chunkOrdinal);
}
/**
@@ -194,7 +266,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,6 +280,12 @@ 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 {
// only 6 bytes, no need to call ioSizeRecorder.accept alone, combine into the remaining read
// operation
@@ -226,7 +304,8 @@ public static ChunkHeader deserializeFrom(
+ 1 // uVarInt dataSize
+ TSDataType.getSerializedSize() // dataType
+ CompressionType.getSerializedSize() // compressionType
- + TSEncoding.getSerializedSize();
+ + TSEncoding.getSerializedSize()
+ + (pageAead ? Long.BYTES : 0);
buffer = ByteBuffer.allocate(remainingBytes);
if (ioSizeRecorder != null) {
@@ -242,9 +321,23 @@ public static ChunkHeader deserializeFrom(
TSDataType dataType = ReadWriteIOUtils.readDataType(buffer);
CompressionType type = ReadWriteIOUtils.readCompressionType(buffer);
TSEncoding encoding = ReadWriteIOUtils.readEncoding(buffer);
+ long chunkOrdinal = -1;
+ if (pageAead) {
+ if (buffer.remaining() < Long.BYTES) {
+ throw new EOFException();
+ }
+ chunkOrdinal = buffer.getLong();
+ }
int chunkHeaderSize = alreadyReadLength + buffer.position();
return new ChunkHeader(
- chunkType, measurementID, dataSize, chunkHeaderSize, dataType, type, encoding);
+ chunkType,
+ measurementID,
+ dataSize,
+ chunkHeaderSize,
+ dataType,
+ type,
+ encoding,
+ chunkOrdinal);
}
/**
@@ -269,6 +362,10 @@ public String getMeasurementID() {
return measurementID;
}
+ public long getChunkOrdinal() {
+ return chunkOrdinal;
+ }
+
public void setMeasurementID(String measurementID) {
this.measurementID = measurementID;
}
@@ -312,6 +409,9 @@ 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);
+ }
return length;
}
@@ -329,6 +429,9 @@ 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);
+ }
return length;
}
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 000000000..97e5016ff
--- /dev/null
+++ b/java/tsfile/src/main/java/org/apache/tsfile/file/header/FileEncryptionHeader.java
@@ -0,0 +1,178 @@
+/*
+ * 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 = 2;
+ public static final int MAX_HEADER_SIZE = 1024 * 1024;
+
+ private FileEncryptionHeader() {}
+
+ public static int serialize(EncryptParameter parameter, OutputStream outputStream)
+ throws IOException {
+ validate(parameter);
+
+ PublicBAOS payload = new PublicBAOS();
+ 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(parameter.getFileCryptoId(), payload);
+ payloadSize += writeBytes(parameter.getWrappedDataKey(), 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/TsFileSequenceReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/TsFileSequenceReader.java
index 2eea4089f..ea82ce82b 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,6 +30,7 @@
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;
@@ -40,6 +41,7 @@
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;
@@ -114,6 +116,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 +152,11 @@ public class TsFileSequenceReader implements AutoCloseable {
private EncryptParameter dataEncryptParam = null;
+ private EncryptParameter fileEncryptionParam;
+ private long maxChunkOrdinal = -1;
+
+ private long dataStartOffset = BASE_FILE_HEADER_SIZE;
+
/**
* 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 +178,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 +325,7 @@ public TsFileSequenceReader(String file, boolean loadMetadataSize, LongConsumer
try {
loadFileVersion(ioSizeRecorder);
+ loadFileEncryptionHeader(ioSizeRecorder);
if (loadMetadataSize) {
loadMetadataSize(ioSizeRecorder);
}
@@ -379,6 +390,7 @@ public TsFileSequenceReader(TsFileInput input, boolean loadMetadataSize) throws
this.tsFileInput = input;
this.file = input.getFilePath();
try {
+ loadFileEncryptionHeader(null);
if (loadMetadataSize) { // NOTE no autoRepair here
loadMetadataSize();
}
@@ -403,6 +415,7 @@ public TsFileSequenceReader(
try {
loadFileVersion(ioSizeRecorder);
+ loadFileEncryptionHeader(ioSizeRecorder);
if (loadMetadataSize) {
loadMetadataSize(ioSizeRecorder);
}
@@ -428,17 +441,26 @@ public TsFileSequenceReader(
* of the input to the current position
* @param fileMetadataSize the byte size of the file metadata in the input
*/
- public TsFileSequenceReader(TsFileInput input, long fileMetadataPos, int fileMetadataSize) {
+ public TsFileSequenceReader(TsFileInput input, long fileMetadataPos, int fileMetadataSize)
+ throws IOException {
this.tsFileInput = input;
+ this.file = input.getFilePath();
this.fileMetadataPos = fileMetadataPos;
this.fileMetadataSize = fileMetadataSize;
+ try {
+ loadFileEncryptionHeader(null);
+ } catch (IOException | RuntimeException e) {
+ tsFileInput.close();
+ throw e;
+ }
}
public TsFileSequenceReader(
TsFileInput input,
long fileMetadataPos,
int fileMetadataSize,
- EncryptParameter firstEncryptParam) {
+ EncryptParameter firstEncryptParam)
+ throws IOException {
this(input, fileMetadataPos, fileMetadataSize);
this.firstEncryptParam = firstEncryptParam;
}
@@ -465,6 +487,42 @@ 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;
+ }
+ ByteBuffer marker = ByteBuffer.allocate(Byte.BYTES);
+ if (tsFileInput.read(marker, BASE_FILE_HEADER_SIZE) != Byte.BYTES) {
+ return;
+ }
+ marker.flip();
+ if (marker.get() != MetaMarker.ENCRYPTION_HEADER) {
+ return;
+ }
+
+ tsFileInput.position(BASE_FILE_HEADER_SIZE + Byte.BYTES);
+ fileEncryptionParam = FileEncryptionHeader.deserialize(tsFileInput.wrapAsInputStream());
+ dataStartOffset = tsFileInput.position();
+ if (ioSizeRecorder != null) {
+ ioSizeRecorder.accept(dataStartOffset - BASE_FILE_HEADER_SIZE);
+ }
+ } finally {
+ tsFileInput.position(savedPosition);
+ }
+ }
+
+ public long getDataStartOffset() {
+ return dataStartOffset;
+ }
+
+ public boolean hasFileEncryptionHeader() {
+ return fileEncryptionParam != null;
+ }
+
private void configDeserializer() {
if (fileVersion == TSFileConfig.VERSION_NUMBER_V3) {
deserializeConfig = CompatibilityUtils.v3DeserializeConfig;
@@ -672,6 +730,9 @@ public EncryptParameter getEncryptParam() throws IOException {
public EncryptParameter getEncryptParam(LongConsumer ioSizeRecorder) throws IOException {
if (dataEncryptParam != null) {
return dataEncryptParam;
+ } else if (fileEncryptionParam != null) {
+ dataEncryptParam = fileEncryptionParam;
+ return dataEncryptParam;
} else {
if (fileMetadataSize != 0) {
readFileMetadata(ioSizeRecorder);
@@ -2035,7 +2096,11 @@ public void readPlanIndex() throws IOException {
*/
public ChunkHeader readChunkHeader(byte chunkType) throws IOException {
try {
- return ChunkHeader.deserializeFrom(tsFileInput.wrapAsInputStream(), chunkType);
+ ChunkHeader header =
+ ChunkHeader.deserializeFrom(
+ tsFileInput.wrapAsInputStream(), chunkType, fileEncryptionParam != null);
+ recordChunkOrdinal(header);
+ return header;
} catch (StopReadTsFileByInterruptException e) {
throw e;
} catch (Throwable t) {
@@ -2054,7 +2119,11 @@ public ChunkHeader readChunkHeader(byte chunkType) throws IOException {
private ChunkHeader readChunkHeader(long position, LongConsumer ioSizeRecorder)
throws IOException {
try {
- return ChunkHeader.deserializeFrom(tsFileInput, position, ioSizeRecorder);
+ ChunkHeader header =
+ ChunkHeader.deserializeFrom(
+ tsFileInput, position, ioSizeRecorder, fileEncryptionParam != null);
+ recordChunkOrdinal(header);
+ return header;
} catch (StopReadTsFileByInterruptException e) {
throw e;
} catch (Throwable t) {
@@ -2064,6 +2133,17 @@ private ChunkHeader readChunkHeader(long position, LongConsumer ioSizeRecorder)
}
}
+ private void recordChunkOrdinal(ChunkHeader header) throws IOException {
+ if (fileEncryptionParam != null && header.getChunkOrdinal() < 0) {
+ throw new IOException(Messages.get("error.write.invalid_chunk_ordinal"));
+ }
+ maxChunkOrdinal = Math.max(maxChunkOrdinal, header.getChunkOrdinal());
+ }
+
+ public long getMaxChunkOrdinal() {
+ return maxChunkOrdinal;
+ }
+
/**
* notice, this function will modify channel's position.
*
@@ -2098,7 +2178,8 @@ public ByteBuffer readChunk(long position, int dataSize, LongConsumer ioSizeReco
/**
* read memory chunk.
*
- * @return -chunk
+ * @return a chunk whose page-AEAD context remains valid after this reader closes; close the chunk
+ * after use
*/
public Chunk readMemChunk(long offset) throws IOException {
return readMemChunk(offset, null);
@@ -2125,7 +2206,8 @@ public Chunk readMemChunk(long offset, LongConsumer ioSizeRecorder) throws IOExc
* read memory chunk.
*
* @param metaData -given chunk meta data
- * @return -chunk
+ * @return a chunk whose page-AEAD context remains valid after this reader closes; close the chunk
+ * after use
*/
public Chunk readMemChunk(ChunkMetadata metaData) throws IOException {
try {
@@ -2151,7 +2233,7 @@ public Chunk readMemChunk(ChunkMetadata metaData) throws IOException {
* read memory chunk.
*
* @param chunkCacheKey given key of chunk LRUCache
- * @return chunk
+ * @return a chunk with an independent page-AEAD context; close it after use
*/
public Chunk readMemChunk(CachedChunkLoaderImpl.ChunkCacheKey chunkCacheKey) throws IOException {
ChunkHeader header = readChunkHeader(chunkCacheKey.getOffsetOfChunkHeader(), null);
@@ -2235,17 +2317,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.remaining(),
+ 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 +2424,13 @@ 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();
+ }
+ }
}
public String getFileName() {
@@ -2437,7 +2571,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 +2658,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 =
@@ -2699,7 +2838,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 +2915,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 230a2e164..fcf11c9d6 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 d41ad14a3..da2bdccd1 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,8 @@
import static org.apache.tsfile.utils.RamUsageEstimator.sizeOfByteArray;
-/** used in query. */
-public class Chunk {
+/** A materialized chunk. Close it after use to destroy its independent page-AEAD context. */
+public class Chunk implements AutoCloseable {
private static final long INSTANCE_SIZE =
RamUsageEstimator.shallowSizeOfInstance(Chunk.class)
@@ -88,7 +88,7 @@ public Chunk(
this.chunkData = buffer;
this.deleteIntervalList = deleteIntervalList;
this.chunkStatistic = chunkStatistic;
- this.encryptParam = encryptParam;
+ this.encryptParam = copyPageAeadParameter(encryptParam);
}
public Chunk(ChunkHeader header, ByteBuffer buffer) {
@@ -100,7 +100,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 = copyPageAeadParameter(encryptParam);
+ }
+
+ private static EncryptParameter copyPageAeadParameter(EncryptParameter parameter) {
+ return parameter != null && parameter.isTdePageAead() ? parameter.copy() : parameter;
+ }
+
+ @Override
+ public void close() {
+ if (encryptParam != null && encryptParam.isTdePageAead()) {
+ encryptParam.close();
+ }
}
public EncryptParameter getEncryptParam() {
@@ -124,6 +135,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
@@ -220,6 +235,10 @@ 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(
@@ -270,7 +289,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,6 +305,9 @@ 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(
@@ -317,7 +340,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 92925ea82..43ef003c8 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/reader/TsFileLastReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/TsFileLastReader.java
index 8700a850e..0b30ea634 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/chunk/AbstractAlignedChunkReader.java b/java/tsfile/src/main/java/org/apache/tsfile/read/reader/chunk/AbstractAlignedChunkReader.java
index acc9789e4..ab7a77ff4 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
@@ -90,12 +90,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 +111,8 @@ private boolean isSinglePageChunk() {
private AbstractAlignedPageReader deserializeFromSinglePageChunk(
Statistics extends Serializable> timeChunkStatistics,
- List> valueChunkStatisticsList)
+ List> valueChunkStatisticsList,
+ int pageIndex)
throws IOException {
PageHeader timePageHeader =
PageHeader.deserializeFrom(timeChunkDataBuffer, timeChunkStatistics);
@@ -132,12 +136,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 +163,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 +198,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 +249,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 b555a25e1..77e8b18d6 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;
@@ -81,7 +82,9 @@ public ChunkReader(Chunk chunk, long readStopTime) {
private void initAllPageReaders(Statistics extends Serializable> 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 +103,7 @@ private void initAllPageReaders(Statistics extends Serializable> chunkStatisti
if (pageDeleted(pageHeader)) {
skipCurrentPage(pageHeader);
} else {
- pageReaderList.add(constructPageReader(pageHeader));
+ pageReaderList.add(constructPageReader(pageHeader, currentPageIndex));
}
}
}
@@ -141,7 +144,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 +154,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 +229,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(),
+ 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 +299,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 7b01a67f5..cfc185504 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,7 +23,9 @@
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;
@@ -39,11 +41,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 +59,67 @@ 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) {
+ this(data, offset, unCompressor, encryptParam, pageIndex, -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(),
+ compressedPageBodyLength,
+ 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 =
+ java.util.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/utils/TsFileSketchTool.java b/java/tsfile/src/main/java/org/apache/tsfile/utils/TsFileSketchTool.java
index a20c0e888..a04c13aa5 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 5521a4f72..b931c3b73 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, copyEncryptParameter(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, copyEncryptParameter(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, copyEncryptParameter(firstEncryptParam)),
schema,
TSFileDescriptor.getInstance().getConfig(),
firstEncryptParam);
@@ -190,7 +190,11 @@ 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, copyEncryptParameter(firstEncryptParam)),
+ schema,
+ conf,
+ firstEncryptParam);
}
/**
@@ -231,23 +235,39 @@ 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();
+ EncryptParameter effectiveEncryptParameter =
+ fileEncryptParameter != null && fileEncryptParameter.isTdePageAead()
+ ? fileEncryptParameter
+ : firstEncryptParam;
+ if (effectiveEncryptParameter != null && effectiveEncryptParameter.isTdePageAead()) {
+ this.secondEncryptParam = effectiveEncryptParameter;
+ fileWriter.setEncryptParam(effectiveEncryptParameter);
+ 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", "");
+ }
}
}
+ private static EncryptParameter copyEncryptParameter(EncryptParameter parameter) {
+ return parameter == null ? null : parameter.copy();
+ }
+
public void setChunkGroupSizeThreshold(long chunkGroupSizeThreshold) {
this.chunkGroupSizeThreshold = chunkGroupSizeThreshold;
}
@@ -729,8 +749,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 7d7912b14..2eba37f3a 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,8 @@ public void writeToFileWriter(
sizeWithoutStatistic = 0;
firstPageStatistics = null;
this.statistics = Statistics.getStatsByType(measurementSchema.getType());
+ this.chunkOrdinal = nextChunkOrdinal();
+ pageWriter.setChunkOrdinal(chunkOrdinal);
}
@Override
@@ -380,7 +394,8 @@ 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)
+ (long) pageBuffer.size();
}
@@ -505,7 +520,9 @@ private void writeAllPagesOfChunkToTsFile(
statistics,
pageBuffer.size(),
numOfPages,
- 0);
+ 0,
+ chunkOrdinal,
+ encryptParam);
long dataOffset = writer.getPos();
@@ -542,6 +559,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 392a0cf45..0796162b3 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
@@ -262,6 +274,8 @@ public void writeToFileWriter(TsFileIOWriter tsfileWriter) throws IOException {
sizeWithoutStatistic = 0;
firstPageStatistics = null;
this.statistics = new TimeStatistics();
+ this.chunkOrdinal = nextChunkOrdinal();
+ pageWriter.setChunkOrdinal(chunkOrdinal);
}
public long estimateMaxSeriesMemSize() {
@@ -276,7 +290,7 @@ 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)
+ (long) pageBuffer.size();
}
@@ -322,7 +336,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 382df6e80..c7709daf4 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
@@ -314,6 +326,8 @@ public void writeToFileWriter(
sizeWithoutStatistic = 0;
firstPageStatistics = null;
this.statistics = Statistics.getStatsByType(dataType);
+ this.chunkOrdinal = nextChunkOrdinal();
+ pageWriter.setChunkOrdinal(chunkOrdinal);
}
public long estimateMaxSeriesMemSize() {
@@ -335,11 +349,11 @@ 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);
}
// return the serialized size of the chunk header + all pages
- return ChunkHeader.getSerializedSize(measurementId, pageBuffer.size())
+ return ChunkHeader.getSerializedSize(measurementId, pageBuffer.size(), chunkOrdinal >= 0)
+ (long) pageBuffer.size();
}
@@ -385,6 +399,10 @@ public int getNumOfPages() {
return numOfPages;
}
+ public long getChunkOrdinal() {
+ return chunkOrdinal;
+ }
+
public TSDataType getDataType() {
return dataType;
}
@@ -421,7 +439,9 @@ public void writeAllPagesOfChunkToTsFile(
statistics,
0,
0,
- TsFileConstant.VALUE_COLUMN_MASK);
+ TsFileConstant.VALUE_COLUMN_MASK,
+ chunkOrdinal,
+ encryptParam);
writer.endCurrentChunk();
return;
}
@@ -435,7 +455,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/EncodedPageBody.java b/java/tsfile/src/main/java/org/apache/tsfile/write/page/EncodedPageBody.java
new file mode 100644
index 000000000..6089de625
--- /dev/null
+++ b/java/tsfile/src/main/java/org/apache/tsfile/write/page/EncodedPageBody.java
@@ -0,0 +1,48 @@
+/*
+ * 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;
+
+class EncodedPageBody {
+
+ private final byte[] data;
+ private final int offset;
+ private final int size;
+
+ EncodedPageBody(byte[] data) {
+ this(data, 0, data.length);
+ }
+
+ EncodedPageBody(byte[] data, int offset, int size) {
+ this.data = data;
+ this.offset = offset;
+ this.size = size;
+ }
+
+ byte[] getData() {
+ return data;
+ }
+
+ int getOffset() {
+ return offset;
+ }
+
+ int size() {
+ return size;
+ }
+}
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 000000000..0122936d3
--- /dev/null
+++ b/java/tsfile/src/main/java/org/apache/tsfile/write/page/PageBodyEncoder.java
@@ -0,0 +1,84 @@
+/*
+ * 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() {}
+
+ static EncodedPageBody encode(
+ ByteBuffer pageData,
+ int uncompressedSize,
+ ICompressor compressor,
+ EncryptParameter encryptParameter,
+ int pageIndex,
+ long chunkOrdinal)
+ throws IOException {
+ byte[] plaintext;
+ int plaintextOffset = 0;
+ int compressedSize;
+
+ if (compressor.getType().equals(CompressionType.UNCOMPRESSED)) {
+ plaintext = pageData.array();
+ plaintextOffset = pageData.position();
+ compressedSize = uncompressedSize;
+ } else if (compressor.getType().equals(CompressionType.GZIP)) {
+ plaintext = compressor.compress(pageData.array(), pageData.position(), uncompressedSize);
+ compressedSize = plaintext.length;
+ } else {
+ plaintext = new byte[compressor.getMaxBytesForCompression(uncompressedSize)];
+ compressedSize =
+ compressor.compress(pageData.array(), pageData.position(), 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 new EncodedPageBody(plaintext, plaintextOffset, compressedSize);
+ } else {
+ encryptedPageBody = encryptor.encrypt(plaintext, plaintextOffset, compressedSize);
+ }
+ return new EncodedPageBody(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 d479ad8d4..2c6f35680 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);
- }
+ EncodedPageBody pageBody =
+ PageBodyEncoder.encode(
+ pageData, uncompressedSize, compressor, encryptParam, pageIndex, chunkOrdinal);
+ int pageBodySize = pageBody.size();
// 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.getData(), pageBody.getOffset(), 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 e09e66cdf..d75bda019 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);
- }
+ EncodedPageBody pageBody =
+ PageBodyEncoder.encode(
+ pageData, uncompressedSize, compressor, encryptParam, pageIndex, chunkOrdinal);
+ int pageBodySize = pageBody.size();
// 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.getData(), pageBody.getOffset(), 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 f7f375759..265186f9f 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);
- }
+ EncodedPageBody pageBody =
+ PageBodyEncoder.encode(
+ pageData, uncompressedSize, compressor, encryptParam, pageIndex, chunkOrdinal);
+ int pageBodySize = pageBody.size();
// 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.getData(), pageBody.getOffset(), 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 cf7e3b651..eb13b63ed 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,10 @@ 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, copyEncryptParameter(firstEncryptParam)),
+ chunkGroupSizeThreshold,
+ firstEncryptParam);
}
@TsFileApi
@@ -123,23 +126,39 @@ 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;
+ fileWriter.setEncryptParam(effectiveEncryptParameter);
+ 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", "");
+ }
}
}
+ private static EncryptParameter copyEncryptParameter(EncryptParameter parameter) {
+ return parameter == null ? null : parameter.copy();
+ }
+
protected IChunkGroupWriter tryToInitialGroupWriter(
IDeviceID deviceId, boolean isAligned, boolean isTableModel) throws IOException {
IChunkGroupWriter groupWriter = groupWriters.get(deviceId);
@@ -273,7 +292,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 1f627d693..cc94ea857 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 09866b3eb..be8429915 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,45 @@ 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)) {
+ if (!reader.hasFileEncryptionHeader()
+ && this.param != null
+ && this.param.isTdePageAead()) {
+ throw new IOException(Messages.get("error.write.aead_append_requires_file_header"));
+ }
+ EncryptParameter recoveredParameter = reader.getEncryptParam();
+ if (recoveredParameter != null && recoveredParameter.isTdePageAead()) {
+ if (this.param != null && this.param.isTdePageAead()) {
+ this.param.close();
+ }
+ this.param = 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 +187,7 @@ public RestorableTsFileIOWriter(File file, boolean truncate, EncryptParameter pa
}
}
} catch (Exception e) {
- out.close();
+ closeAfterFailedInitialization(e);
throw e;
}
}
@@ -209,6 +231,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 93ddec569..ab94f3642 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,12 @@ public class TsFileIOWriter implements AutoCloseable {
protected String encryptKey;
+ protected EncryptParameter encryptParameter;
+
+ private boolean fileStarted;
+
+ private boolean encryptionHeaderWritten;
+
private final List flushListeners = new ArrayList<>();
protected String currentTable;
@@ -236,14 +244,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")) {
@@ -260,6 +282,29 @@ public void setEncryptParam(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"));
+ }
+ FileEncryptionHeader.serialize(encryptParameter, out.wrapAsStream());
+ // A recoverable encrypted file must persist its wrapped data key before any encrypted page.
+ out.force();
+ encryptionHeaderWritten = true;
+ }
+
/** Add a custom property to the TsFile metadata. */
public void addTsFileProperty(String key, byte[] value) {
tsFileProperties.put(key, value);
@@ -277,15 +322,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 +395,64 @@ public void startFlushChunk(
int numOfPages,
int mask)
throws IOException {
+ startFlushChunk(
+ measurementId,
+ compressionCodecName,
+ tsDataType,
+ encodingType,
+ statistics,
+ dataSize,
+ numOfPages,
+ mask,
+ -1);
+ }
+
+ public void startFlushChunk(
+ String measurementId,
+ CompressionType compressionCodecName,
+ TSDataType tsDataType,
+ TSEncoding encodingType,
+ Statistics extends Serializable> statistics,
+ int dataSize,
+ int numOfPages,
+ int mask,
+ long chunkOrdinal)
+ throws IOException {
+ startFlushChunk(
+ measurementId,
+ compressionCodecName,
+ tsDataType,
+ encodingType,
+ statistics,
+ dataSize,
+ numOfPages,
+ mask,
+ chunkOrdinal,
+ null);
+ }
+
+ public void startFlushChunk(
+ String measurementId,
+ CompressionType compressionCodecName,
+ TSDataType tsDataType,
+ TSEncoding encodingType,
+ Statistics extends Serializable> 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 +472,15 @@ public void startFlushChunk(
compressionCodecName,
encodingType,
numOfPages,
- mask);
+ mask,
+ chunkOrdinal);
header.serializeTo(out.wrapAsStream());
}
/** 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(
@@ -400,6 +510,7 @@ public void writeEmptyValueChunk(
TSEncoding encodingType,
Statistics extends Serializable> statistics)
throws IOException {
+ writeEncryptionHeaderIfNecessary();
currentChunkMetadata =
new ChunkMetadata(
measurementId,
@@ -417,12 +528,17 @@ public void writeEmptyValueChunk(
compressionType,
encodingType,
0,
- TsFileConstant.VALUE_COLUMN_MASK);
+ TsFileConstant.VALUE_COLUMN_MASK,
+ encryptParameter != null && encryptParameter.isTdePageAead()
+ ? encryptParameter.nextChunkOrdinal()
+ : -1);
emptyChunkHeader.serializeTo(out.wrapAsStream());
endCurrentChunk();
}
public void writeChunk(Chunk chunk) throws IOException {
+ writeEncryptionHeaderIfNecessary();
+ validateChunkEncryptionContext(chunk);
ChunkHeader chunkHeader = chunk.getHeader();
currentChunkMetadata =
new ChunkMetadata(
@@ -468,6 +584,7 @@ public void endFile() throws IOException {
if (!canWrite) {
return;
}
+ writeEncryptionHeaderIfNecessary();
updateTableSize(null);
checkInMemoryPathCount();
@@ -712,12 +829,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/PageCryptoContextTest.java b/java/tsfile/src/test/java/org/apache/tsfile/encrypt/PageCryptoContextTest.java
new file mode 100644
index 000000000..f37952e51
--- /dev/null
+++ b/java/tsfile/src/test/java/org/apache/tsfile/encrypt/PageCryptoContextTest.java
@@ -0,0 +1,212 @@
+/*
+ * 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.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import java.util.Arrays;
+
+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 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 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 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 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};
+ assertThrows(EncryptException.class, () -> legacyEncryptor.encryptPage(page, 0, 1, context));
+ assertThrows(EncryptException.class, () -> legacyDecryptor.decryptPage(page, 0, 1, context));
+ } 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 000000000..1e56df2d8
--- /dev/null
+++ b/java/tsfile/src/test/java/org/apache/tsfile/encrypt/TestAeadEncryptionProvider.java
@@ -0,0 +1,227 @@
+/*
+ * 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.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";
+
+ 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 TestAeadEncryptionProvider() {}
+
+ @Override
+ public String getProviderId() {
+ return PROVIDER_ID;
+ }
+
+ @Override
+ public IEncrypt create(EncryptParameter encryptParameter) {
+ if (!PROFILE_ID.equals(encryptParameter.getProfileId())) {
+ throw new EncryptException("unsupported test encryption profile");
+ }
+ CREATE_COUNT.incrementAndGet();
+ return new TestAeadEncrypt(resolveDataKey(encryptParameter));
+ }
+
+ public static EncryptParameter createParameter(byte[] dataKey, byte[] fileCryptoId) {
+ return EncryptParameter.pageAeadBuilder()
+ .key(dataKey)
+ .providerId(PROVIDER_ID)
+ .profileId(PROFILE_ID)
+ .keyId("test-key")
+ .keyVersion("1")
+ .wrappedDataKey(wrapDataKey(dataKey))
+ .fileCryptoId(fileCryptoId)
+ .build();
+ }
+
+ public static int getCreateCount() {
+ return CREATE_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 TestAeadEncrypt(byte[] dataKey) {
+ this.dataKey = dataKey;
+ }
+
+ @Override
+ public IDecryptor getDecryptor() {
+ return new AeadDecryptor(dataKey);
+ }
+
+ @Override
+ public IEncryptor getEncryptor() {
+ return new AeadEncryptor(dataKey);
+ }
+
+ @Override
+ public int getPageBodyOverhead() {
+ return IV_LENGTH + TAG_LENGTH;
+ }
+
+ @Override
+ public void close() {
+ Arrays.fill(dataKey, (byte) 0);
+ }
+ }
+
+ private static final class AeadEncryptor implements IEncryptor {
+
+ private final byte[] dataKey;
+
+ private AeadEncryptor(byte[] dataKey) {
+ this.dataKey = dataKey;
+ }
+
+ @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) {
+ 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(pageCryptoContext.getAssociatedData());
+ 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 AeadDecryptor(byte[] dataKey) {
+ this.dataKey = dataKey;
+ }
+
+ @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) {
+ 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(pageCryptoContext.getAssociatedData());
+ 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 000000000..82bd6a33c
--- /dev/null
+++ b/java/tsfile/src/test/java/org/apache/tsfile/file/header/FileEncryptionHeaderTest.java
@@ -0,0 +1,94 @@
+/*
+ * 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.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 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/UnClosedTsFileReaderTest.java b/java/tsfile/src/test/java/org/apache/tsfile/read/UnClosedTsFileReaderTest.java
index 8e865413a..5fadd06dc 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/write/TDEPageAeadTsFileTest.java b/java/tsfile/src/test/java/org/apache/tsfile/write/TDEPageAeadTsFileTest.java
new file mode 100644
index 000000000..abe940c43
--- /dev/null
+++ b/java/tsfile/src/test/java/org/apache/tsfile/write/TDEPageAeadTsFileTest.java
@@ -0,0 +1,572 @@
+/*
+ * 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.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.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.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;
+
+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 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, List>> 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 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()) {
+ writer.setEncryptParam(first);
+ 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() == first);
+ } 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));
+ assertEquals(position, writer.getPos());
+ } finally {
+ source.close();
+ target.close();
+ }
+ }
+}
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 68d05cefd..223d68a39 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 0e1279fae..b2a370eac 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.assertThrows;
@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,116 @@ public void testOnlyHeadMagic() throws Exception {
rWriter.close();
}
+ @Test
+ public void testHeaderlessRecoveryRejectsPageAeadParameter() 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])) {
+ assertThrows(IOException.class, () -> new RestorableTsFileIOWriter(file, false, parameter));
+ }
+ assertEquals(originalLength, file.length());
+ try (TsFileSequenceReader reader = new TsFileSequenceReader(file.getPath(), false)) {
+ assertFalse(reader.hasFileEncryptionHeader());
+ }
+ }
+
+ @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 878b9d7a7..c09ccf28f 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();
+ }
+ }
}