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 98909f7a6..5c5ad94be 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
@@ -1111,6 +1111,34 @@ 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_context_invalid_file_id = Page crypto context requires a 16-byte file crypto id
+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 can only be copied within the same file encryption context
+
# === 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 a1a437cfb..2298e9acc 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
@@ -1111,6 +1111,34 @@ 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_context_invalid_file_id = page crypto context 需要 16 字节的 file crypto id
+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 = 仅允许在相同 file encryption context 中复制 encrypted chunk
+
# === 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..e5a1c8197 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,83 @@
*/
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;
+
+/**
+ * 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 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;
+ }
+
+ 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);
+ }
+
+ 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)
+ .build();
}
public byte[] getKey() {
@@ -35,4 +104,146 @@ 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;
+ }
+
+ 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 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;
+ }
+
+ 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 42ccec8f7..dcb712ef0 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
@@ -248,6 +248,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);
}
@@ -285,6 +291,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..76091a3c4
--- /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);
+ }
+ }
+
+ 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 b906e5b72..014c75501 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) {
+ return decrypt(data, offset, size);
+ }
+
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 2c0b6cf2e..325b9131b 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) {
+ return encrypt(data, offset, size);
+ }
+
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..b1bfd45c3
--- /dev/null
+++ b/java/tsfile/src/main/java/org/apache/tsfile/encrypt/PageCryptoContext.java
@@ -0,0 +1,171 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tsfile.encrypt;
+
+import org.apache.tsfile.common.conf.TSFileConfig;
+import org.apache.tsfile.exception.encrypt.EncryptException;
+import org.apache.tsfile.i18n.Messages;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+
+/**
+ * Canonical context for page-level authenticated encryption.
+ *
+ *
Encryption and decryption construct the same associated data from persisted file encryption
+ * metadata and stable page fields. Providers should pass {@link #getAssociatedData()} to their AEAD
+ * implementation unchanged.
+ */
+public final class PageCryptoContext {
+
+ private static final int ASSOCIATED_DATA_VERSION = 1;
+
+ private 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) {
+ validate(parameter, uncompressedSize, compressedPlaintextSize, pageIndex);
+ this.uncompressedSize = uncompressedSize;
+ this.compressedPlaintextSize = compressedPlaintextSize;
+ this.encryptedPageBodySize = encryptedPageBodySize;
+ this.pageIndex = pageIndex;
+ this.associatedData =
+ buildAssociatedData(parameter, uncompressedSize, compressedPlaintextSize, pageIndex);
+ }
+
+ public static PageCryptoContext forEncryption(
+ EncryptParameter parameter,
+ int uncompressedSize,
+ int compressedPlaintextSize,
+ int pageIndex) {
+ validate(parameter, uncompressedSize, compressedPlaintextSize, pageIndex);
+ int encryptedPageBodySize =
+ Math.addExact(compressedPlaintextSize, parameter.getPageBodyOverhead());
+ return new PageCryptoContext(
+ parameter, uncompressedSize, compressedPlaintextSize, encryptedPageBodySize, pageIndex);
+ }
+
+ public static PageCryptoContext forDecryption(
+ EncryptParameter parameter, int uncompressedSize, int encryptedPageBodySize, int pageIndex) {
+ validate(parameter, uncompressedSize, 0, pageIndex);
+ 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);
+ }
+
+ 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;
+ }
+
+ public byte[] getAssociatedData() {
+ return Arrays.copyOf(associatedData, associatedData.length);
+ }
+
+ private static void validate(
+ EncryptParameter parameter,
+ int uncompressedSize,
+ int compressedPlaintextSize,
+ int pageIndex) {
+ 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 (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) {
+ 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);
+ 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.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/FileEncryptionHeader.java b/java/tsfile/src/main/java/org/apache/tsfile/file/header/FileEncryptionHeader.java
new file mode 100644
index 000000000..1e5590f2f
--- /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 = 1;
+ 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 97dcddc0d..acc5f5c9f 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;
@@ -113,6 +115,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();
@@ -146,6 +151,10 @@ public class TsFileSequenceReader implements AutoCloseable {
private EncryptParameter dataEncryptParam = null;
+ private EncryptParameter fileEncryptionParam;
+
+ 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
@@ -167,7 +176,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;
}
@@ -314,6 +323,7 @@ public TsFileSequenceReader(String file, boolean loadMetadataSize, LongConsumer
try {
loadFileVersion(ioSizeRecorder);
+ loadFileEncryptionHeader(ioSizeRecorder);
if (loadMetadataSize) {
loadMetadataSize(ioSizeRecorder);
}
@@ -378,6 +388,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();
}
@@ -440,6 +451,38 @@ 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;
+ }
+
private void configDeserializer() {
if (fileVersion == TSFileConfig.VERSION_NUMBER_V3) {
deserializeConfig = CompatibilityUtils.v3DeserializeConfig;
@@ -642,6 +685,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);
@@ -2205,17 +2251,48 @@ 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 {
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);
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)
+ throws IOException {
+ if (encryptParameter != null && encryptParameter.isTdePageAead()) {
+ PageCryptoContext pageCryptoContext =
+ PageCryptoContext.forDecryption(
+ encryptParameter, header.getUncompressedSize(), buffer.remaining(), pageIndex);
+ 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;
}
@@ -2266,7 +2343,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() {
@@ -2407,7 +2490,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;
}
@@ -2722,7 +2805,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;
}
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 bf83a4539..47f14bc8f 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
@@ -124,6 +124,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
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..0d4de4d34 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,8 @@ private AbstractAlignedPageReader constructAlignedPageReader(
valueChunkDataBufferList.get(i).array(),
currentPagePosition,
IUnCompressor.getUnCompressor(valueChunkHeader.getCompressionType()),
- encryptParam);
+ encryptParam,
+ pageIndex);
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..cbb0892bc 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,11 @@ private PageReader constructPageReader(PageHeader pageHeader) {
new PageReader(
pageHeader,
new LazyLoadPageData(
- chunkDataBuffer.array(), currentPagePosition, unCompressor, encryptParam),
+ chunkDataBuffer.array(),
+ currentPagePosition,
+ unCompressor,
+ encryptParam,
+ pageIndex),
chunkHeader.getDataType(),
chunkHeader.calculateDecoderForNonTimeChunk(),
getConfiguredTimeDecoder(),
@@ -221,7 +228,53 @@ 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)
+ throws IOException {
+ int compressedPageBodyLength = pageHeader.getCompressedSize();
+ byte[] uncompressedPageData = new byte[pageHeader.getUncompressedSize()];
+ try {
+ PageCryptoContext pageCryptoContext =
+ PageCryptoContext.forDecryption(
+ encryptParameter,
+ pageHeader.getUncompressedSize(),
+ compressedPageBodyLength,
+ pageIndex);
+ 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 +296,25 @@ 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);
+ } 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..a74c9bc8b 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,14 @@ public class LazyLoadPageData {
private final EncryptParameter encryptParam;
+ private final int pageIndex;
+
public LazyLoadPageData(byte[] data, int offset, IUnCompressor unCompressor) {
this.chunkData = data;
this.pageDataOffset = offset;
this.unCompressor = unCompressor;
this.encryptParam = EncryptUtils.getEncryptParameter();
+ this.pageIndex = -1;
}
public LazyLoadPageData(
@@ -52,17 +57,51 @@ public LazyLoadPageData(
this.pageDataOffset = offset;
this.unCompressor = unCompressor;
this.encryptParam = encryptParam;
+ this.pageIndex = -1;
+ }
+
+ public LazyLoadPageData(
+ byte[] data,
+ int offset,
+ IUnCompressor unCompressor,
+ EncryptParameter encryptParam,
+ int pageIndex) {
+ this.chunkData = data;
+ this.pageDataOffset = offset;
+ this.unCompressor = unCompressor;
+ this.encryptParam = encryptParam;
+ this.pageIndex = pageIndex;
}
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);
+ 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..1e56b5e68 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
@@ -322,17 +322,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
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..d19a8e566 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
@@ -184,17 +184,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
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 9ade788c1..97c07441b 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
@@ -209,7 +209,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();
@@ -218,10 +219,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
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..c464acae9
--- /dev/null
+++ b/java/tsfile/src/main/java/org/apache/tsfile/write/page/PageBodyEncoder.java
@@ -0,0 +1,83 @@
+/*
+ * 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)
+ 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);
+ 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..d714284ff 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
@@ -231,28 +226,21 @@ 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);
+ int pageBodySize = pageBody.size();
// write the page header to IOWriter
int sizeWithoutStatistic = 0;
@@ -260,41 +248,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;
}
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..ce8feb865 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
@@ -114,28 +109,21 @@ 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);
+ int pageBodySize = pageBody.size();
// write the page header to IOWriter
int sizeWithoutStatistic = 0;
@@ -143,41 +131,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;
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 525c3199a..189804bcb 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.PublicBAOS;
@@ -37,8 +34,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
@@ -272,6 +267,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) {
@@ -281,22 +282,9 @@ 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);
+ int pageBodySize = pageBody.size();
// write the page header to IOWriter
int sizeWithoutStatistic = 0;
@@ -304,39 +292,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;
}
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 94be1fdc1..e2ce2ce03 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);
@@ -272,6 +291,10 @@ public void close() {
fileWriter.endFile();
} catch (IOException e) {
LOG.warn(Messages.get("log.write.close_file_exception"), e);
+ } 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..ff7789569 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,19 +59,26 @@ 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)) {
+ EncryptParameter recoveredParameter = reader.getEncryptParam();
+ if (recoveredParameter != null && recoveredParameter.isTdePageAead()) {
+ ownedEncryptParameter = recoveredParameter.copy();
+ setEncryptParam(ownedEncryptParameter);
+ }
+ markExistingFileStarted(recoveredParameter != null && recoveredParameter.isTdePageAead());
+
// this tsfile is not complete
if (!reader.isComplete()) {
throw new TsFileNotCompleteException(
@@ -88,6 +96,9 @@ public ForceAppendTsFileWriter(File file, EncryptParameter param) throws IOExcep
ChunkGroupMetadata chunkGroupMetadata = new ChunkGroupMetadata(device, chunkMetadataList);
chunkGroupMetadataList.add(chunkGroupMetadata);
}
+ } catch (IOException | RuntimeException e) {
+ closeAfterFailedInitialization(e);
+ throw e;
}
}
@@ -98,4 +109,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..314a3c242 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,21 +126,35 @@ public RestorableTsFileIOWriter(File file, boolean truncate, EncryptParameter pa
}
this.file = file;
this.out = FSFactoryProducer.getFileOutputFactory().getTsFileOutput(file.getPath(), true);
- this.param = param;
- setEncryptParam(param);
+ this.param = param != null && param.isTdePageAead() ? param.copy() : param;
+ setEncryptParam(this.param);
// file doesn't exist
if (file.length() == 0) {
- startFile();
- crashed = true;
- canWrite = true;
- return;
+ try {
+ startFile();
+ crashed = true;
+ canWrite = true;
+ return;
+ } catch (IOException | RuntimeException e) {
+ closeAfterFailedInitialization(e);
+ throw e;
+ }
}
try {
if (file.exists()) {
try (TsFileSequenceReader reader =
new TsFileSequenceReader(file.getAbsolutePath(), param, false)) {
+ EncryptParameter recoveredParameter = reader.getEncryptParam();
+ if (recoveredParameter != null && recoveredParameter.isTdePageAead()) {
+ if (this.param != null && this.param.isTdePageAead()) {
+ this.param.close();
+ }
+ this.param = recoveredParameter.copy();
+ setEncryptParam(this.param);
+ }
+ markExistingFileStarted(recoveredParameter != null && recoveredParameter.isTdePageAead());
schema.setEnabledUpdateSchema(false);
truncatedSize = reader.selfCheck(schema, chunkGroupMetadataList, true);
minPlanIndex = reader.getMinPlanIndex();
@@ -165,7 +179,7 @@ public RestorableTsFileIOWriter(File file, boolean truncate, EncryptParameter pa
}
}
} catch (Exception e) {
- out.close();
+ closeAfterFailedInitialization(e);
throw e;
}
}
@@ -209,6 +223,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 df6d99b20..9f0db83f3 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
@@ -29,6 +29,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.metadata.ChunkGroupMetadata;
import org.apache.tsfile.file.metadata.ChunkMetadata;
import org.apache.tsfile.file.metadata.IChunkMetadata;
@@ -62,6 +63,7 @@
import java.io.Serializable;
import java.util.ArrayDeque;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -135,6 +137,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;
@@ -225,11 +233,19 @@ public void setEncryptParam(String encryptLevel, String encryptType, String encr
this.encryptLevel = encryptLevel;
this.encryptType = encryptType;
this.encryptKey = encryptKey;
+ this.encryptParameter = new EncryptParameter(encryptType, null);
}
public void setEncryptParam(EncryptParameter param) {
+ 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")) {
@@ -246,6 +262,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;
+ }
+
public void addFlushListener(FlushChunkMetadataListener listener) {
flushListeners.add(listener);
}
@@ -258,15 +297,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()) {
@@ -328,6 +371,8 @@ public void startFlushChunk(
int mask)
throws IOException {
+ writeEncryptionHeaderIfNecessary();
+
currentChunkMetadata =
new ChunkMetadata(
measurementId,
@@ -352,6 +397,8 @@ public void startFlushChunk(
/** 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(
@@ -380,6 +427,7 @@ public void writeEmptyValueChunk(
TSEncoding encodingType,
Statistics extends Serializable> statistics)
throws IOException {
+ writeEncryptionHeaderIfNecessary();
currentChunkMetadata =
new ChunkMetadata(
measurementId,
@@ -403,6 +451,8 @@ public void writeEmptyValueChunk(
}
public void writeChunk(Chunk chunk) throws IOException {
+ writeEncryptionHeaderIfNecessary();
+ validateChunkEncryptionContext(chunk);
ChunkHeader chunkHeader = chunk.getHeader();
currentChunkMetadata =
new ChunkMetadata(
@@ -435,6 +485,7 @@ public void endFile() throws IOException {
if (!canWrite) {
return;
}
+ writeEncryptionHeaderIfNecessary();
updateTableSize(null);
checkInMemoryPathCount();
@@ -670,12 +721,33 @@ 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) {
+ return;
+ }
+ if (!targetUsesPageAead
+ || !sourceUsesPageAead
+ || !Objects.equals(encryptParameter.getProviderId(), sourceParameter.getProviderId())
+ || !Objects.equals(encryptParameter.getProfileId(), sourceParameter.getProfileId())
+ || !Objects.equals(encryptParameter.getKeyId(), sourceParameter.getKeyId())
+ || !Objects.equals(encryptParameter.getKeyVersion(), sourceParameter.getKeyVersion())
+ || !Arrays.equals(encryptParameter.getFileCryptoId(), sourceParameter.getFileCryptoId())
+ || !Arrays.equals(
+ encryptParameter.getWrappedDataKey(), sourceParameter.getWrappedDataKey())) {
+ throw new IOException(Messages.get("error.write.encrypted_chunk_context_mismatch"));
+ }
+ }
+
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..0dcf2bcb1
--- /dev/null
+++ b/java/tsfile/src/test/java/org/apache/tsfile/encrypt/PageCryptoContextTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tsfile.encrypt;
+
+import org.apache.tsfile.exception.encrypt.EncryptException;
+
+import org.junit.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.assertThrows;
+
+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);
+ PageCryptoContext page1 =
+ PageCryptoContext.forEncryption(parameter, plaintext.length, plaintext.length, 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));
+
+ encryptedPage0[encryptedPage0.length - 1] ^= 1;
+ assertThrows(
+ EncryptException.class,
+ () -> decryptor.decryptPage(encryptedPage0, 0, encryptedPage0.length, page0));
+
+ assertEquals(1, TestAeadEncryptionProvider.getCreateCount());
+ } finally {
+ 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..a5f32a95e
--- /dev/null
+++ b/java/tsfile/src/test/java/org/apache/tsfile/write/TDEPageAeadTsFileTest.java
@@ -0,0 +1,162 @@
+/*
+ * 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.encrypt.EncryptParameter;
+import org.apache.tsfile.encrypt.EncryptionProviderRegistry;
+import org.apache.tsfile.encrypt.TestAeadEncryptionProvider;
+import org.apache.tsfile.enums.TSDataType;
+import org.apache.tsfile.file.header.ChunkHeader;
+import org.apache.tsfile.file.metadata.enums.CompressionType;
+import org.apache.tsfile.file.metadata.enums.TSEncoding;
+import org.apache.tsfile.read.TsFileReader;
+import org.apache.tsfile.read.TsFileSequenceReader;
+import org.apache.tsfile.read.common.Chunk;
+import org.apache.tsfile.read.common.Path;
+import org.apache.tsfile.read.common.RowRecord;
+import org.apache.tsfile.read.expression.QueryExpression;
+import org.apache.tsfile.read.query.dataset.QueryDataSet;
+import org.apache.tsfile.write.record.TSRecord;
+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.nio.ByteBuffer;
+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.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 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));
+ assertThrows(IOException.class, () -> sourceChunk.mergeChunkByAppendPage(sourceChunk));
+ } finally {
+ sourceParameter.close();
+ targetParameter.close();
+ if (targetFile.exists()) {
+ assertTrue(targetFile.delete());
+ }
+ }
+ }
+}
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..311dd4e10 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,6 +18,9 @@
*/
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.enums.TSEncoding;
import org.apache.tsfile.fileSystem.FSFactoryProducer;
@@ -34,11 +37,14 @@
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.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import static org.junit.Assert.assertEquals;
@@ -51,6 +57,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 +136,53 @@ record = dataSet.next();
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);
+
+ 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, encryptParameter);
+ EncryptParameter ownedParameter = appendWriter.getEncryptParameter();
+ Assert.assertNotSame(encryptParameter, ownedParameter);
+ appendWriter.doTruncate();
+ try (TsFileWriter writer = new TsFileWriter(appendWriter, encryptParameter)) {
+ 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());
+
+ try (TsFileReader reader = new TsFileReader(new TsFileSequenceReader(file.getPath()))) {
+ 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();
+ 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..f0c770b0f 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;
@@ -67,6 +72,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 +120,55 @@ public void testOnlyHeadMagic() throws Exception {
rWriter.close();
}
+ @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 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..23100e114 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,46 @@ 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);
+ 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);
+ 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();
+ }
+ }
}