Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

br: add log backup/restore encryption support #55757

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions br/cmd/br/restore.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,14 @@ func runRestoreCommand(command *cobra.Command, cmdName string) error {

if err := task.RunRestore(GetDefaultContext(), tidbGlue, cmdName, &cfg); err != nil {
log.Error("failed to restore", zap.Error(err))
printWorkaroundOnFullRestoreError(command, err)
printWorkaroundOnFullRestoreError(err)
return errors.Trace(err)
}
return nil
}

// print workaround when we met not fresh or incompatible cluster error on full cluster restore
func printWorkaroundOnFullRestoreError(command *cobra.Command, err error) {
func printWorkaroundOnFullRestoreError(err error) {
if !errors.ErrorEqual(err, berrors.ErrRestoreNotFreshCluster) &&
!errors.ErrorEqual(err, berrors.ErrRestoreIncompatibleSys) {
return
Expand Down
2 changes: 1 addition & 1 deletion br/pkg/checkpoint/checkpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,7 @@ func walkCheckpointFile[K KeyType, V ValueType](
pastDureTime = checkpointData.DureTime
}
for _, meta := range checkpointData.RangeGroupMetas {
decryptContent, err := metautil.Decrypt(meta.RangeGroupsEncriptedData, cipher, meta.CipherIv)
decryptContent, err := utils.Decrypt(meta.RangeGroupsEncriptedData, cipher, meta.CipherIv)
if err != nil {
return errors.Trace(err)
}
Expand Down
4 changes: 2 additions & 2 deletions br/pkg/conn/conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,8 @@ func (mgr *Mgr) Close() {
mgr.PdController.Close()
}

// GetTS gets current ts from pd.
func (mgr *Mgr) GetTS(ctx context.Context) (uint64, error) {
// GetCurrentTsFromPd gets current ts from pd.
func (mgr *Mgr) GetCurrentTsFromPd(ctx context.Context) (uint64, error) {
p, l, err := mgr.GetPDClient().GetTS(ctx)
if err != nil {
return 0, errors.Trace(err)
Expand Down
15 changes: 15 additions & 0 deletions br/pkg/encryption/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")

go_library(
name = "encryption",
srcs = ["manager.go"],
importpath = "github.com/pingcap/tidb/br/pkg/encryption",
visibility = ["//visibility:public"],
deps = [
"//br/pkg/encryption/master_key",
"//br/pkg/utils",
"@com_github_pingcap_errors//:errors",
"@com_github_pingcap_kvproto//pkg/brpb",
"@com_github_pingcap_kvproto//pkg/encryptionpb",
],
)
84 changes: 84 additions & 0 deletions br/pkg/encryption/manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0.

package encryption

import (
"context"

"github.com/pingcap/errors"
backuppb "github.com/pingcap/kvproto/pkg/brpb"
"github.com/pingcap/kvproto/pkg/encryptionpb"
encryption "github.com/pingcap/tidb/br/pkg/encryption/master_key"
"github.com/pingcap/tidb/br/pkg/utils"
)

type Manager struct {
cipherInfo *backuppb.CipherInfo
masterKeyBackends *encryption.MultiMasterKeyBackend
encryptionMethod *encryptionpb.EncryptionMethod
}

func NewManager(cipherInfo *backuppb.CipherInfo, masterKeyConfigs *backuppb.MasterKeyConfig) (*Manager, error) {
// should never happen since config has default
if cipherInfo == nil || masterKeyConfigs == nil {
return nil, errors.New("cipherInfo or masterKeyConfigs is nil")
}

if cipherInfo.CipherType != encryptionpb.EncryptionMethod_PLAINTEXT && cipherInfo.CipherType != encryptionpb.EncryptionMethod_UNKNOWN {
return &Manager{
cipherInfo: cipherInfo,
masterKeyBackends: nil,
encryptionMethod: nil,
}, nil
}

if masterKeyConfigs.EncryptionType != encryptionpb.EncryptionMethod_PLAINTEXT && masterKeyConfigs.EncryptionType != encryptionpb.EncryptionMethod_UNKNOWN {
masterKeyBackends, err := encryption.NewMultiMasterKeyBackend(masterKeyConfigs.GetMasterKeys())
if err != nil {
return nil, errors.Trace(err)
}
return &Manager{
cipherInfo: nil,
masterKeyBackends: masterKeyBackends,
encryptionMethod: &masterKeyConfigs.EncryptionType,
}, nil
}
return nil, nil
}

func (m *Manager) Decrypt(ctx context.Context, content []byte, fileEncryptionInfo *encryptionpb.FileEncryptionInfo) ([]byte, error) {
switch mode := fileEncryptionInfo.Mode.(type) {
case *encryptionpb.FileEncryptionInfo_PlainTextDataKey:
if m.cipherInfo == nil {
return nil, errors.New("plaintext data key info is required but not set")
}
decryptedContent, err := utils.Decrypt(content, m.cipherInfo, fileEncryptionInfo.FileIv)
if err != nil {
return nil, errors.Annotate(err, "failed to decrypt content using plaintext data key")
}
return decryptedContent, nil
case *encryptionpb.FileEncryptionInfo_MasterKeyBased:
encryptedContents := fileEncryptionInfo.GetMasterKeyBased().DataKeyEncryptedContent
if encryptedContents == nil || len(encryptedContents) == 0 {
return nil, errors.New("should contain at least one encrypted data key")
}
// pick first one, the list is for future expansion of multiple encrypted data keys by different master key backend
encryptedContent := encryptedContents[0]
decryptedDataKey, err := m.masterKeyBackends.Decrypt(ctx, encryptedContent)
if err != nil {
return nil, errors.Annotate(err, "failed to decrypt data key using master key")
}

cipherInfo := backuppb.CipherInfo{
CipherType: fileEncryptionInfo.EncryptionMethod,
CipherKey: decryptedDataKey,
}
decryptedContent, err := utils.Decrypt(content, &cipherInfo, fileEncryptionInfo.FileIv)
if err != nil {
return nil, errors.Annotate(err, "failed to decrypt content using decrypted data key")
}
return decryptedContent, nil
default:
return nil, errors.Errorf("internal error: unsupported encryption mode type %T", mode)
}
}
44 changes: 44 additions & 0 deletions br/pkg/encryption/master_key/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")

go_library(
name = "master_key",
srcs = [
"common.go",
"file_backend.go",
"kms_backend.go",
"master_key.go",
"mem_backend.go",
"multi_master_key_backend.go",
],
importpath = "github.com/pingcap/tidb/br/pkg/encryption/master_key",
visibility = ["//visibility:public"],
deps = [
"//br/pkg/kms:aws",
"//br/pkg/utils",
"@com_github_pingcap_errors//:errors",
"@com_github_pingcap_kvproto//pkg/encryptionpb",
"@com_github_pingcap_log//:log",
"@org_uber_go_zap//:zap",
],
)

go_test(
name = "master_key_test",
timeout = "short",
srcs = [
"file_backend_test.go",
"kms_backend_test.go",
"mem_backend_test.go",
"multi_master_key_backend_test.go",
],
embed = [":master_key"],
flaky = True,
shard_count = 11,
deps = [
"@com_github_pingcap_errors//:errors",
"@com_github_pingcap_kvproto//pkg/encryptionpb",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//mock",
"@com_github_stretchr_testify//require",
],
)
31 changes: 31 additions & 0 deletions br/pkg/encryption/master_key/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0.

package encryption

import (
"crypto/rand"
"encoding/binary"
"time"
)

// must keep it same with the constants in TiKV implementation
const (
MetadataKeyMethod string = "method"
MetadataKeyIv string = "iv"
MetadataKeyAesGcmTag string = "aes_gcm_tag"
MetadataKeyKmsVendor string = "kms_vendor"
MetadataKeyKmsCiphertextKey string = "kms_ciphertext_key"
MetadataMethodAes256Gcm string = "aes256-gcm"
)

type IV [12]byte

func NewIV() IV {
var iv IV
binary.BigEndian.PutUint64(iv[:8], uint64(time.Now().UnixNano()))
// Fill the remaining 4 bytes with random data
if _, err := rand.Read(iv[8:]); err != nil {
panic(err) // Handle this error appropriately in production code
}
return iv
}
60 changes: 60 additions & 0 deletions br/pkg/encryption/master_key/file_backend.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0.

package encryption

import (
"context"
"encoding/hex"
"os"

"github.com/pingcap/errors"
"github.com/pingcap/kvproto/pkg/encryptionpb"
)

const AesGcmKeyLen = 32 // AES-256 key length

type FileBackend struct {
memCache *MemAesGcmBackend
}

func createFileBackend(keyPath string) (*FileBackend, error) {
// FileBackend uses AES-256-GCM
keyLen := AesGcmKeyLen

content, err := os.ReadFile(keyPath)
if err != nil {
return nil, errors.Annotate(err, "failed to read master key file from disk")
}

fileLen := len(content)
expectedLen := keyLen*2 + 1 // hex-encoded key + newline

if fileLen != expectedLen {
return nil, errors.Errorf("mismatch master key file size, expected %d, actual %d", expectedLen, fileLen)
}

if content[fileLen-1] != '\n' {
return nil, errors.Errorf("master key file should end with newline")
}

key, err := hex.DecodeString(string(content[:fileLen-1]))
if err != nil {
return nil, errors.Annotate(err, "failed to decode master key from file")
}

backend, err := NewMemAesGcmBackend(key)
if err != nil {
return nil, errors.Annotate(err, "failed to create MemAesGcmBackend")
}

return &FileBackend{memCache: backend}, nil
}

func (f *FileBackend) Encrypt(ctx context.Context, plaintext []byte) (*encryptionpb.EncryptedContent, error) {
iv := NewIV()
return f.memCache.EncryptContent(ctx, plaintext, iv)
}

func (f *FileBackend) Decrypt(ctx context.Context, content *encryptionpb.EncryptedContent) ([]byte, error) {
return f.memCache.DecryptContent(ctx, content)
}
105 changes: 105 additions & 0 deletions br/pkg/encryption/master_key/file_backend_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2024 PingCAP, Inc. Licensed under Apache-2.0.

package encryption

import (
"context"
"encoding/hex"
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TempKeyFile represents a temporary key file for testing
type TempKeyFile struct {
Path string
file *os.File
}

// Cleanup closes and removes the temporary file
func (tkf *TempKeyFile) Cleanup() {
if tkf.file != nil {
tkf.file.Close()
}
os.Remove(tkf.Path)
}

// createMasterKeyFile creates a temporary master key file for testing
func createMasterKeyFile() (*TempKeyFile, error) {
tempFile, err := os.CreateTemp("", "test_key_*.txt")
if err != nil {
return nil, err
}

_, err = tempFile.WriteString("c3d99825f2181f4808acd2068eac7441a65bd428f14d2aab43fefc0129091139\n")
if err != nil {
tempFile.Close()
os.Remove(tempFile.Name())
return nil, err
}

return &TempKeyFile{
Path: tempFile.Name(),
file: tempFile,
}, nil
}

func TestFileBackendAes256Gcm(t *testing.T) {
pt, err := hex.DecodeString("25431587e9ecffc7c37f8d6d52a9bc3310651d46fb0e3bad2726c8f2db653749")
require.NoError(t, err)
ct, err := hex.DecodeString("84e5f23f95648fa247cb28eef53abec947dbf05ac953734618111583840bd980")
require.NoError(t, err)
iv, err := hex.DecodeString("cafabd9672ca6c79a2fbdc22")
require.NoError(t, err)

tempKeyFile, err := createMasterKeyFile()
require.NoError(t, err)
defer tempKeyFile.Cleanup()

backend, err := createFileBackend(tempKeyFile.Path)
require.NoError(t, err)

ctx := context.Background()
encryptedContent, err := backend.memCache.EncryptContent(ctx, pt, IV(iv))
require.NoError(t, err)
assert.Equal(t, ct, encryptedContent.Content)

plaintext, err := backend.Decrypt(ctx, encryptedContent)
require.NoError(t, err)
assert.Equal(t, pt, plaintext)
}

func TestFileBackendAuthenticate(t *testing.T) {
pt := []byte{1, 2, 3}

tempKeyFile, err := createMasterKeyFile()
require.NoError(t, err)
defer tempKeyFile.Cleanup()

backend, err := createFileBackend(tempKeyFile.Path)
require.NoError(t, err)

ctx := context.Background()
encryptedContent, err := backend.Encrypt(ctx, pt)
require.NoError(t, err)

plaintext, err := backend.Decrypt(ctx, encryptedContent)
require.NoError(t, err)
assert.Equal(t, pt, plaintext)

// Test checksum mismatch
encryptedContent1 := *encryptedContent
encryptedContent1.Metadata[MetadataKeyAesGcmTag][0] ^= 0xFF
_, err = backend.Decrypt(ctx, &encryptedContent1)
assert.Error(t, err)
assert.Contains(t, err.Error(), wrongMasterKey)

// Test checksum not found
encryptedContent2 := *encryptedContent
delete(encryptedContent2.Metadata, MetadataKeyAesGcmTag)
_, err = backend.Decrypt(ctx, &encryptedContent2)
assert.Error(t, err)
assert.Contains(t, err.Error(), gcmTagNotFound)
}
Loading
Loading