From d639116adbc6b6b6aa206d69d7f0272c1e720875 Mon Sep 17 00:00:00 2001 From: Andy Yang Date: Tue, 23 May 2023 21:05:34 -0400 Subject: [PATCH] builtins: implement encrypt and decrypt pgcrypto functions This patch implements `encrypt`, `encrypt_iv`, `decrypt`, and `decrypt_iv` from pgcrypto. These functions require an enterprise license on a CCL distribution. Release note (enterprise change): The pgcrypto functions `encrypt`, `encrypt_iv`, `decrypt`, and `decrypt_iv` are now implemented. These functions require an enterprise license on a CCL distribution. --- docs/generated/sql/functions.md | 36 +++++ pkg/BUILD.bazel | 4 + pkg/ccl/BUILD.bazel | 1 + pkg/ccl/ccl_init.go | 1 + .../testdata/logic_test/pgcrypto_builtins | 136 ++++++++++++++++ .../tests/3node-tenant/generated_test.go | 7 + .../tests/fakedist-disk/BUILD.bazel | 2 +- .../tests/fakedist-disk/generated_test.go | 7 + .../tests/fakedist-vec-off/BUILD.bazel | 2 +- .../tests/fakedist-vec-off/generated_test.go | 7 + .../logictestccl/tests/fakedist/BUILD.bazel | 2 +- .../tests/fakedist/generated_test.go | 7 + .../local-legacy-schema-changer/BUILD.bazel | 2 +- .../generated_test.go | 7 + .../tests/local-mixed-22.2-23.1/BUILD.bazel | 2 +- .../local-mixed-22.2-23.1/generated_test.go | 7 + .../tests/local-vec-off/BUILD.bazel | 2 +- .../tests/local-vec-off/generated_test.go | 7 + pkg/ccl/logictestccl/tests/local/BUILD.bazel | 2 +- .../tests/local/generated_test.go | 7 + pkg/ccl/pgcryptoccl/BUILD.bazel | 38 +++++ pkg/ccl/pgcryptoccl/main_test.go | 31 ++++ pkg/ccl/pgcryptoccl/pgcryptoccl.go | 69 ++++++++ pkg/ccl/pgcryptoccl/pgcryptoccl_test.go | 60 +++++++ .../pgcryptoccl/pgcryptocipherccl/BUILD.bazel | 2 + .../pgcryptoccl/pgcryptocipherccl/cipher.go | 153 ++++++++++++++++++ .../pgcryptocipherccl/cipher_test.go | 129 +++++++++++++++ .../pgcryptoccl/pgcryptocipherccl/padding.go | 10 +- .../testdata/logic_test/pgcrypto_builtins | 20 +++ pkg/sql/sem/builtins/BUILD.bazel | 1 + pkg/sql/sem/builtins/fixed_oids.go | 4 + pkg/sql/sem/builtins/pgcrypto/BUILD.bazel | 13 ++ pkg/sql/sem/builtins/pgcrypto/pgcrypto.go | 47 ++++++ pkg/sql/sem/builtins/pgcrypto_builtins.go | 118 ++++++++++++++ 34 files changed, 933 insertions(+), 10 deletions(-) create mode 100644 pkg/ccl/logictestccl/testdata/logic_test/pgcrypto_builtins create mode 100644 pkg/ccl/pgcryptoccl/BUILD.bazel create mode 100644 pkg/ccl/pgcryptoccl/main_test.go create mode 100644 pkg/ccl/pgcryptoccl/pgcryptoccl.go create mode 100644 pkg/ccl/pgcryptoccl/pgcryptoccl_test.go create mode 100644 pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher.go create mode 100644 pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher_test.go create mode 100644 pkg/sql/sem/builtins/pgcrypto/BUILD.bazel create mode 100644 pkg/sql/sem/builtins/pgcrypto/pgcrypto.go diff --git a/docs/generated/sql/functions.md b/docs/generated/sql/functions.md index 31147c9f2f1c..1d4fb35607ed 100644 --- a/docs/generated/sql/functions.md +++ b/docs/generated/sql/functions.md @@ -388,10 +388,46 @@ crypt(password: string, salt: string) → string

Generates a hash based on a password and salt. The hash algorithm and number of rounds if applicable are encoded in the salt.

Immutable +decrypt(data: bytes, key: bytes, type: string) → bytes

Decrypt data with key using the cipher method specified by type.

+

The cipher type must have the format <algorithm>[-<mode>][/pad:<padding>] where:

+ +

This function requires an enterprise license on a CCL distribution.

+
Immutable +decrypt_iv(data: bytes, key: bytes, iv: bytes, type: string) → bytes

Decrypt data with key using the cipher method specified by type. If the mode is CBC, the provided iv will be used. Otherwise, it will be ignored.

+

The cipher type must have the format <algorithm>[-<mode>][/pad:<padding>] where:

+ +

This function requires an enterprise license on a CCL distribution.

+
Immutable digest(data: bytes, type: string) → bytes

Computes a binary hash of the given data. type is the algorithm to use (md5, sha1, sha224, sha256, sha384, or sha512).

Immutable digest(data: string, type: string) → bytes

Computes a binary hash of the given data. type is the algorithm to use (md5, sha1, sha224, sha256, sha384, or sha512).

Immutable +encrypt(data: bytes, key: bytes, type: string) → bytes

Encrypt data with key using the cipher method specified by type.

+

The cipher type must have the format <algorithm>[-<mode>][/pad:<padding>] where:

+ +

This function requires an enterprise license on a CCL distribution.

+
Immutable +encrypt_iv(data: bytes, key: bytes, iv: bytes, type: string) → bytes

Encrypt data with key using the cipher method specified by type. If the mode is CBC, the provided iv will be used. Otherwise, it will be ignored.

+

The cipher type must have the format <algorithm>[-<mode>][/pad:<padding>] where:

+ +

This function requires an enterprise license on a CCL distribution.

+
Immutable gen_salt(type: string) → string

Generates a salt for input into the crypt function using the default number of rounds.

Volatile gen_salt(type: string, iter_count: int) → string

Generates a salt for input into the crypt function using iter_count number of rounds.

diff --git a/pkg/BUILD.bazel b/pkg/BUILD.bazel index 22aaf629fe34..019991a9508a 100644 --- a/pkg/BUILD.bazel +++ b/pkg/BUILD.bazel @@ -67,6 +67,7 @@ ALL_TESTS = [ "//pkg/ccl/oidcccl:oidcccl_test", "//pkg/ccl/partitionccl:partitionccl_test", "//pkg/ccl/pgcryptoccl/pgcryptocipherccl:pgcryptocipherccl_test", + "//pkg/ccl/pgcryptoccl:pgcryptoccl_test", "//pkg/ccl/schemachangerccl:schemachangerccl_test", "//pkg/ccl/serverccl/adminccl:adminccl_test", "//pkg/ccl/serverccl/diagnosticsccl:diagnosticsccl_test", @@ -871,6 +872,8 @@ GO_TARGETS = [ "//pkg/ccl/partitionccl:partitionccl_test", "//pkg/ccl/pgcryptoccl/pgcryptocipherccl:pgcryptocipherccl", "//pkg/ccl/pgcryptoccl/pgcryptocipherccl:pgcryptocipherccl_test", + "//pkg/ccl/pgcryptoccl:pgcryptoccl", + "//pkg/ccl/pgcryptoccl:pgcryptoccl_test", "//pkg/ccl/schemachangerccl:schemachangerccl", "//pkg/ccl/schemachangerccl:schemachangerccl_test", "//pkg/ccl/serverccl/adminccl:adminccl_test", @@ -2041,6 +2044,7 @@ GO_TARGETS = [ "//pkg/sql/sem/asof:asof", "//pkg/sql/sem/builtins/builtinconstants:builtinconstants", "//pkg/sql/sem/builtins/builtinsregistry:builtinsregistry", + "//pkg/sql/sem/builtins/pgcrypto:pgcrypto", "//pkg/sql/sem/builtins/pgformat:pgformat", "//pkg/sql/sem/builtins/pgformat:pgformat_test", "//pkg/sql/sem/builtins:builtins", diff --git a/pkg/ccl/BUILD.bazel b/pkg/ccl/BUILD.bazel index 8664f2b167f6..b86474e903d9 100644 --- a/pkg/ccl/BUILD.bazel +++ b/pkg/ccl/BUILD.bazel @@ -20,6 +20,7 @@ go_library( "//pkg/ccl/multitenantccl", "//pkg/ccl/oidcccl", "//pkg/ccl/partitionccl", + "//pkg/ccl/pgcryptoccl", "//pkg/ccl/storageccl", "//pkg/ccl/storageccl/engineccl", "//pkg/ccl/streamingccl/streamingest", diff --git a/pkg/ccl/ccl_init.go b/pkg/ccl/ccl_init.go index f266b2ec31d3..4a224d5cd10c 100644 --- a/pkg/ccl/ccl_init.go +++ b/pkg/ccl/ccl_init.go @@ -26,6 +26,7 @@ import ( _ "github.com/cockroachdb/cockroach/pkg/ccl/multitenantccl" _ "github.com/cockroachdb/cockroach/pkg/ccl/oidcccl" _ "github.com/cockroachdb/cockroach/pkg/ccl/partitionccl" + _ "github.com/cockroachdb/cockroach/pkg/ccl/pgcryptoccl" _ "github.com/cockroachdb/cockroach/pkg/ccl/storageccl" _ "github.com/cockroachdb/cockroach/pkg/ccl/storageccl/engineccl" _ "github.com/cockroachdb/cockroach/pkg/ccl/streamingccl/streamingest" diff --git a/pkg/ccl/logictestccl/testdata/logic_test/pgcrypto_builtins b/pkg/ccl/logictestccl/testdata/logic_test/pgcrypto_builtins new file mode 100644 index 000000000000..37c40273eead --- /dev/null +++ b/pkg/ccl/logictestccl/testdata/logic_test/pgcrypto_builtins @@ -0,0 +1,136 @@ +subtest encrypt_decrypt_aes_128 + +query T +SELECT encrypt('abc', '16_byte_long_key', 'aes')::STRING +---- +\x0026cd6206cfd92140883b75c098d613 + +query T +SELECT decrypt('\x0026cd6206cfd92140883b75c098d613', '16_byte_long_key', 'aes') +---- +abc + +subtest end + +subtest encrypt_decrypt_aes_192 + +query T +SELECT encrypt('abc', '24_byte_looooooooong_key', 'aes')::STRING +---- +\x6c42e2269a65d605ecd98b2aeb8eb4e9 + +query T +SELECT decrypt('\x6c42e2269a65d605ecd98b2aeb8eb4e9', '24_byte_looooooooong_key', 'aes') +---- +abc + +subtest end + +subtest encrypt_decrypt_aes_256 + +query T +SELECT encrypt('abc', '32_byte_looooooooooooooooong_key', 'aes')::STRING +---- +\xb368f7d6adcd73633dc37696b068cfda + +query T +SELECT decrypt('\xb368f7d6adcd73633dc37696b068cfda', '32_byte_looooooooooooooooong_key', 'aes') +---- +abc + +subtest end + +subtest encrypt_decrypt_aes_multi_block_data + +query T +SELECT encrypt('abcdefghijklmnopqrstuvwxyz', 'key', 'aes')::STRING +---- +\x4649e8618af65b2b50aa73ec9cfc102c95fcbbaf04cb8a82333e493dc97060f3 + +query T +SELECT decrypt('\x4649e8618af65b2b50aa73ec9cfc102c95fcbbaf04cb8a82333e493dc97060f3', 'key', 'aes') +---- +abcdefghijklmnopqrstuvwxyz + +subtest end + +subtest encrypt_decrypt_aes_no_padding + +query T +SELECT encrypt('16byte_long_data', 'key', 'aes/pad:none')::STRING +---- +\x043db9c657e2a2cd693b4239a3d8a1cb + +query T +SELECT decrypt('\x043db9c657e2a2cd693b4239a3d8a1cb', 'key', 'aes/pad:none') +---- +16byte_long_data + +subtest end + +subtest encrypt_decrypt_iv_aes + +query T +SELECT encrypt_iv('abc', 'key', '123', 'aes')::STRING +---- +\x91b4ef63852013c8da53829da662b871 + +query T +SELECT decrypt_iv('\x91b4ef63852013c8da53829da662b871', 'key', '123', 'aes') +---- +abc + +subtest end + +subtest encrypt_error + +query error pgcode 0A000 Blowfish is insecure and not supported +SELECT encrypt('abc', 'key', 'bf') + +query error pgcode 0A000 ECB mode is insecure and not supported +SELECT encrypt('abc', 'key', 'aes-ecb') + +query error pgcode 22023 cipher method has wrong format: "aes/pad=pkcs" +SELECT encrypt('abc', 'key', 'aes/pad=pkcs') + +query error pgcode 22023 cipher method has invalid algorithm: "fakealgo" +SELECT encrypt('abc', 'key', 'fakealgo') + +query error pgcode 22023 cipher method has invalid mode: "ctr" +SELECT encrypt('abc', 'key', 'aes-ctr') + +query error pgcode 22023 cipher method has invalid padding: "zero" +SELECT encrypt('abc', 'key', 'aes/pad:zero') + +query error pgcode 22023 data has length 3, which is not a multiple of block size 16 +SELECT encrypt('abc', 'key', 'aes/pad:none') + +subtest end + +subtest decrypt_error + +query error pgcode 0A000 Blowfish is insecure and not supported +SELECT decrypt('abc', 'key', 'bf') + +query error pgcode 0A000 ECB mode is insecure and not supported +SELECT decrypt('abc', 'key', 'aes-ecb') + +query error pgcode 22023 cipher method has wrong format: "aes/pad=pkcs" +SELECT decrypt('abc', 'key', 'aes/pad=pkcs') + +query error pgcode 22023 cipher method has invalid algorithm: "fakealgo" +SELECT decrypt('abc', 'key', 'fakealgo') + +query error pgcode 22023 cipher method has invalid mode: "ctr" +SELECT decrypt('abc', 'key', 'aes-ctr') + +query error pgcode 22023 cipher method has invalid padding: "zero" +SELECT decrypt('abc', 'key', 'aes/pad:zero') + +query error pgcode 22023 data has length 3, which is not a multiple of block size 16 +SELECT decrypt('abc', 'key', 'aes') + +query error pgcode 22023 data has length 3, which is not a multiple of block size 16 +SELECT decrypt('abc', 'key', 'aes/pad:none') + +subtest end diff --git a/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go b/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go index 96e5db025e1a..c1002b805cad 100644 --- a/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go +++ b/pkg/ccl/logictestccl/tests/3node-tenant/generated_test.go @@ -2515,6 +2515,13 @@ func TestTenantLogicCCL_partitioning_enum( runCCLLogicTest(t, "partitioning_enum") } +func TestTenantLogicCCL_pgcrypto_builtins( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "pgcrypto_builtins") +} + func TestTenantLogicCCL_redact_descriptor( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/fakedist-disk/BUILD.bazel b/pkg/ccl/logictestccl/tests/fakedist-disk/BUILD.bazel index c29772179ee6..a1d3e4458c89 100644 --- a/pkg/ccl/logictestccl/tests/fakedist-disk/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/fakedist-disk/BUILD.bazel @@ -15,7 +15,7 @@ go_test( exec_properties = { "Pool": "large", }, - shard_count = 5, + shard_count = 6, tags = [ "ccl_test", "cpu:2", diff --git a/pkg/ccl/logictestccl/tests/fakedist-disk/generated_test.go b/pkg/ccl/logictestccl/tests/fakedist-disk/generated_test.go index bec6322f598d..ed15a067a5b3 100644 --- a/pkg/ccl/logictestccl/tests/fakedist-disk/generated_test.go +++ b/pkg/ccl/logictestccl/tests/fakedist-disk/generated_test.go @@ -92,6 +92,13 @@ func TestCCLLogic_partitioning_enum( runCCLLogicTest(t, "partitioning_enum") } +func TestCCLLogic_pgcrypto_builtins( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "pgcrypto_builtins") +} + func TestCCLLogic_redact_descriptor( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/fakedist-vec-off/BUILD.bazel b/pkg/ccl/logictestccl/tests/fakedist-vec-off/BUILD.bazel index 0c31c6a344e6..81a39eea27ed 100644 --- a/pkg/ccl/logictestccl/tests/fakedist-vec-off/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/fakedist-vec-off/BUILD.bazel @@ -15,7 +15,7 @@ go_test( exec_properties = { "Pool": "large", }, - shard_count = 5, + shard_count = 6, tags = [ "ccl_test", "cpu:2", diff --git a/pkg/ccl/logictestccl/tests/fakedist-vec-off/generated_test.go b/pkg/ccl/logictestccl/tests/fakedist-vec-off/generated_test.go index b19cc51d7498..662396114099 100644 --- a/pkg/ccl/logictestccl/tests/fakedist-vec-off/generated_test.go +++ b/pkg/ccl/logictestccl/tests/fakedist-vec-off/generated_test.go @@ -92,6 +92,13 @@ func TestCCLLogic_partitioning_enum( runCCLLogicTest(t, "partitioning_enum") } +func TestCCLLogic_pgcrypto_builtins( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "pgcrypto_builtins") +} + func TestCCLLogic_redact_descriptor( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/fakedist/BUILD.bazel b/pkg/ccl/logictestccl/tests/fakedist/BUILD.bazel index 6eaa8c258a4e..afccaf838f1d 100644 --- a/pkg/ccl/logictestccl/tests/fakedist/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/fakedist/BUILD.bazel @@ -15,7 +15,7 @@ go_test( exec_properties = { "Pool": "large", }, - shard_count = 6, + shard_count = 7, tags = [ "ccl_test", "cpu:2", diff --git a/pkg/ccl/logictestccl/tests/fakedist/generated_test.go b/pkg/ccl/logictestccl/tests/fakedist/generated_test.go index edd36915ffbf..4781d36ae881 100644 --- a/pkg/ccl/logictestccl/tests/fakedist/generated_test.go +++ b/pkg/ccl/logictestccl/tests/fakedist/generated_test.go @@ -99,6 +99,13 @@ func TestCCLLogic_partitioning_enum( runCCLLogicTest(t, "partitioning_enum") } +func TestCCLLogic_pgcrypto_builtins( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "pgcrypto_builtins") +} + func TestCCLLogic_redact_descriptor( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel index 95fd285ad9dc..70830210bbe7 100644 --- a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/BUILD.bazel @@ -15,7 +15,7 @@ go_test( exec_properties = { "Pool": "large", }, - shard_count = 5, + shard_count = 6, tags = [ "ccl_test", "cpu:1", diff --git a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go index c9c128ef12b0..74fb8bc7ccb8 100644 --- a/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go +++ b/pkg/ccl/logictestccl/tests/local-legacy-schema-changer/generated_test.go @@ -92,6 +92,13 @@ func TestCCLLogic_partitioning_enum( runCCLLogicTest(t, "partitioning_enum") } +func TestCCLLogic_pgcrypto_builtins( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "pgcrypto_builtins") +} + func TestCCLLogic_redact_descriptor( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/local-mixed-22.2-23.1/BUILD.bazel b/pkg/ccl/logictestccl/tests/local-mixed-22.2-23.1/BUILD.bazel index 27609dc54134..2f058fbe96f7 100644 --- a/pkg/ccl/logictestccl/tests/local-mixed-22.2-23.1/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/local-mixed-22.2-23.1/BUILD.bazel @@ -15,7 +15,7 @@ go_test( exec_properties = { "Pool": "large", }, - shard_count = 6, + shard_count = 7, tags = [ "ccl_test", "cpu:1", diff --git a/pkg/ccl/logictestccl/tests/local-mixed-22.2-23.1/generated_test.go b/pkg/ccl/logictestccl/tests/local-mixed-22.2-23.1/generated_test.go index 94295f92a63f..071231559fe3 100644 --- a/pkg/ccl/logictestccl/tests/local-mixed-22.2-23.1/generated_test.go +++ b/pkg/ccl/logictestccl/tests/local-mixed-22.2-23.1/generated_test.go @@ -99,6 +99,13 @@ func TestCCLLogic_partitioning_enum( runCCLLogicTest(t, "partitioning_enum") } +func TestCCLLogic_pgcrypto_builtins( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "pgcrypto_builtins") +} + func TestCCLLogic_redact_descriptor( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/local-vec-off/BUILD.bazel b/pkg/ccl/logictestccl/tests/local-vec-off/BUILD.bazel index c8cedae0a43b..c2be802cf341 100644 --- a/pkg/ccl/logictestccl/tests/local-vec-off/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/local-vec-off/BUILD.bazel @@ -15,7 +15,7 @@ go_test( exec_properties = { "Pool": "large", }, - shard_count = 5, + shard_count = 6, tags = [ "ccl_test", "cpu:1", diff --git a/pkg/ccl/logictestccl/tests/local-vec-off/generated_test.go b/pkg/ccl/logictestccl/tests/local-vec-off/generated_test.go index ecbe0cd32845..eae4fccbd6fb 100644 --- a/pkg/ccl/logictestccl/tests/local-vec-off/generated_test.go +++ b/pkg/ccl/logictestccl/tests/local-vec-off/generated_test.go @@ -92,6 +92,13 @@ func TestCCLLogic_partitioning_enum( runCCLLogicTest(t, "partitioning_enum") } +func TestCCLLogic_pgcrypto_builtins( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "pgcrypto_builtins") +} + func TestCCLLogic_redact_descriptor( t *testing.T, ) { diff --git a/pkg/ccl/logictestccl/tests/local/BUILD.bazel b/pkg/ccl/logictestccl/tests/local/BUILD.bazel index 657b45774fac..0f8a15b77c95 100644 --- a/pkg/ccl/logictestccl/tests/local/BUILD.bazel +++ b/pkg/ccl/logictestccl/tests/local/BUILD.bazel @@ -15,7 +15,7 @@ go_test( exec_properties = { "Pool": "large", }, - shard_count = 19, + shard_count = 20, tags = [ "ccl_test", "cpu:1", diff --git a/pkg/ccl/logictestccl/tests/local/generated_test.go b/pkg/ccl/logictestccl/tests/local/generated_test.go index c5917fc1efdf..dd66a1be3062 100644 --- a/pkg/ccl/logictestccl/tests/local/generated_test.go +++ b/pkg/ccl/logictestccl/tests/local/generated_test.go @@ -169,6 +169,13 @@ func TestCCLLogic_partitioning_index( runCCLLogicTest(t, "partitioning_index") } +func TestCCLLogic_pgcrypto_builtins( + t *testing.T, +) { + defer leaktest.AfterTest(t)() + runCCLLogicTest(t, "pgcrypto_builtins") +} + func TestCCLLogic_redact_descriptor( t *testing.T, ) { diff --git a/pkg/ccl/pgcryptoccl/BUILD.bazel b/pkg/ccl/pgcryptoccl/BUILD.bazel new file mode 100644 index 000000000000..07993ab2f685 --- /dev/null +++ b/pkg/ccl/pgcryptoccl/BUILD.bazel @@ -0,0 +1,38 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "pgcryptoccl", + srcs = ["pgcryptoccl.go"], + importpath = "github.com/cockroachdb/cockroach/pkg/ccl/pgcryptoccl", + visibility = ["//visibility:public"], + deps = [ + "//pkg/ccl/pgcryptoccl/pgcryptocipherccl", + "//pkg/ccl/utilccl", + "//pkg/sql/sem/builtins/pgcrypto", + "//pkg/sql/sem/eval", + ], +) + +go_test( + name = "pgcryptoccl_test", + srcs = [ + "main_test.go", + "pgcryptoccl_test.go", + ], + args = ["-test.timeout=295s"], + tags = ["ccl_test"], + deps = [ + "//pkg/base", + "//pkg/ccl", + "//pkg/security/securityassets", + "//pkg/security/securitytest", + "//pkg/server", + "//pkg/testutils", + "//pkg/testutils/serverutils", + "//pkg/testutils/testcluster", + "//pkg/util/leaktest", + "//pkg/util/log", + "//pkg/util/randutil", + "@com_github_stretchr_testify//require", + ], +) diff --git a/pkg/ccl/pgcryptoccl/main_test.go b/pkg/ccl/pgcryptoccl/main_test.go new file mode 100644 index 000000000000..205ff2318720 --- /dev/null +++ b/pkg/ccl/pgcryptoccl/main_test.go @@ -0,0 +1,31 @@ +// Copyright 2023 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package pgcryptoccl_test + +import ( + "os" + "testing" + + "github.com/cockroachdb/cockroach/pkg/security/securityassets" + "github.com/cockroachdb/cockroach/pkg/security/securitytest" + "github.com/cockroachdb/cockroach/pkg/server" + "github.com/cockroachdb/cockroach/pkg/testutils/serverutils" + "github.com/cockroachdb/cockroach/pkg/testutils/testcluster" + "github.com/cockroachdb/cockroach/pkg/util/randutil" +) + +//go:generate ../../../util/leaktest/add-leaktest.sh *_test.go + +func TestMain(m *testing.M) { + securityassets.SetLoader(securitytest.EmbeddedAssets) + randutil.SeedForTests() + serverutils.InitTestServerFactory(server.TestServerFactory) + serverutils.InitTestClusterFactory(testcluster.TestClusterFactory) + os.Exit(m.Run()) +} diff --git a/pkg/ccl/pgcryptoccl/pgcryptoccl.go b/pkg/ccl/pgcryptoccl/pgcryptoccl.go new file mode 100644 index 000000000000..5c5d31b1ad5a --- /dev/null +++ b/pkg/ccl/pgcryptoccl/pgcryptoccl.go @@ -0,0 +1,69 @@ +// Copyright 2023 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package pgcryptoccl + +import ( + "context" + + "github.com/cockroachdb/cockroach/pkg/ccl/pgcryptoccl/pgcryptocipherccl" + "github.com/cockroachdb/cockroach/pkg/ccl/utilccl" + "github.com/cockroachdb/cockroach/pkg/sql/sem/builtins/pgcrypto" + "github.com/cockroachdb/cockroach/pkg/sql/sem/eval" +) + +func init() { + pgcrypto.Decrypt = decrypt + pgcrypto.DecryptIV = decryptIV + pgcrypto.Encrypt = encrypt + pgcrypto.EncryptIV = encryptIV +} + +func checkEnterpriseEnabledForCipherFunctions(evalCtx *eval.Context) error { + return utilccl.CheckEnterpriseEnabled( + evalCtx.Settings, + evalCtx.ClusterID, + "pgcrypto cipher functions", + ) +} + +func decrypt( + _ context.Context, evalCtx *eval.Context, data []byte, key []byte, cipherType string, +) ([]byte, error) { + if err := checkEnterpriseEnabledForCipherFunctions(evalCtx); err != nil { + return nil, err + } + return pgcryptocipherccl.Decrypt(data, key, nil /* iv */, cipherType) +} + +func decryptIV( + _ context.Context, evalCtx *eval.Context, data []byte, key []byte, iv []byte, cipherType string, +) ([]byte, error) { + if err := checkEnterpriseEnabledForCipherFunctions(evalCtx); err != nil { + return nil, err + } + return pgcryptocipherccl.Decrypt(data, key, iv, cipherType) +} + +func encrypt( + _ context.Context, evalCtx *eval.Context, data []byte, key []byte, cipherType string, +) ([]byte, error) { + if err := checkEnterpriseEnabledForCipherFunctions(evalCtx); err != nil { + return nil, err + } + return pgcryptocipherccl.Encrypt(data, key, nil /* iv */, cipherType) +} + +func encryptIV( + _ context.Context, evalCtx *eval.Context, data []byte, key []byte, iv []byte, cipherType string, +) ([]byte, error) { + if err := checkEnterpriseEnabledForCipherFunctions(evalCtx); err != nil { + return nil, err + } + return pgcryptocipherccl.Encrypt(data, key, iv, cipherType) +} diff --git a/pkg/ccl/pgcryptoccl/pgcryptoccl_test.go b/pkg/ccl/pgcryptoccl/pgcryptoccl_test.go new file mode 100644 index 000000000000..f709f74874be --- /dev/null +++ b/pkg/ccl/pgcryptoccl/pgcryptoccl_test.go @@ -0,0 +1,60 @@ +// Copyright 2023 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package pgcryptoccl_test + +import ( + "context" + "testing" + + "github.com/cockroachdb/cockroach/pkg/base" + "github.com/cockroachdb/cockroach/pkg/ccl" + "github.com/cockroachdb/cockroach/pkg/testutils" + "github.com/cockroachdb/cockroach/pkg/testutils/testcluster" + "github.com/cockroachdb/cockroach/pkg/util/leaktest" + "github.com/cockroachdb/cockroach/pkg/util/log" + "github.com/stretchr/testify/require" +) + +func TestCipherFunctionEnterpriseLicense(t *testing.T) { + defer leaktest.AfterTest(t)() + defer log.Scope(t).Close(t) + + ctx := context.Background() + tc := testcluster.StartTestCluster(t, 1, base.TestClusterArgs{}) + defer tc.Stopper().Stop(ctx) + + db := tc.ServerConn(0) + defer db.Close() + + for name, query := range map[string]string{ + "decrypt": `SELECT decrypt('\xdb5f149a7caf0cd275ca18c203a212c9', 'key', 'aes')`, + "decrypt_iv": `SELECT decrypt_iv('\x91b4ef63852013c8da53829da662b871', 'key', '123', 'aes')`, + "encrypt": `SELECT encrypt('abc', 'key', 'aes')`, + "encrypt_iv": `SELECT encrypt_iv('abc', 'key', '123', 'aes')`, + } { + t.Run(name, func(t *testing.T) { + testutils.RunTrueAndFalse(t, "enterprise_license", func(t *testing.T, hasLicense bool) { + if hasLicense { + defer ccl.TestingEnableEnterprise()() + } else { + defer ccl.TestingDisableEnterprise()() + } + + rows, err := db.QueryContext(ctx, query) + + if hasLicense { + require.NoError(t, err) + require.NoError(t, rows.Close()) + } else { + require.ErrorContains(t, err, "use of pgcrypto cipher functions requires an enterprise license") + } + }) + }) + } +} diff --git a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/BUILD.bazel b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/BUILD.bazel index 08028d31d0dc..d703ca8517e6 100644 --- a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/BUILD.bazel +++ b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/BUILD.bazel @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "pgcryptocipherccl", srcs = [ + "cipher.go", "cipher_method.go", "doc.go", "padding.go", @@ -21,6 +22,7 @@ go_test( name = "pgcryptocipherccl_test", srcs = [ "cipher_method_test.go", + "cipher_test.go", "padding_test.go", ], args = ["-test.timeout=295s"], diff --git a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher.go b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher.go new file mode 100644 index 000000000000..5320b3b87208 --- /dev/null +++ b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher.go @@ -0,0 +1,153 @@ +// Copyright 2023 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package pgcryptocipherccl + +import ( + "crypto/aes" + "crypto/cipher" + + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/errors" +) + +// Encrypt returns the ciphertext obtained by running the encryption +// algorithm for the specified cipher type with the provided key and +// initialization vector over the provided data. +func Encrypt(data []byte, key []byte, iv []byte, cipherType string) ([]byte, error) { + method, err := parseCipherMethod(cipherType) + if err != nil { + return nil, err + } + block, err := newCipher(method, key) + if err != nil { + return nil, err + } + blockSize := block.BlockSize() + data, err = padData(method, data, blockSize) + if err != nil { + return nil, err + } + err = validateDataLength(data, blockSize) + if err != nil { + return nil, err + } + return encrypt(method, block, iv, data) +} + +// Decrypt returns the plaintext obtained by running the decryption +// algorithm for the specified cipher type with the provided key and +// initialization vector over the provided data. +func Decrypt(data []byte, key []byte, iv []byte, cipherType string) ([]byte, error) { + method, err := parseCipherMethod(cipherType) + if err != nil { + return nil, err + } + block, err := newCipher(method, key) + if err != nil { + return nil, err + } + blockSize := block.BlockSize() + err = validateDataLength(data, blockSize) + if err != nil { + return nil, err + } + data, err = decrypt(method, block, iv, data) + if err != nil { + return nil, err + } + return unpadData(method, data) +} + +func newCipher(method cipherMethod, key []byte) (cipher.Block, error) { + switch a := method.algorithm; a { + case aesCipher: + switch l := len(key); { + case l >= 32: + key = zeroPadOrTruncate(key, 32) + case l >= 24: + key = zeroPadOrTruncate(key, 24) + default: + key = zeroPadOrTruncate(key, 16) + } + return aes.NewCipher(key) + default: + return nil, errors.Newf("cannot create new cipher for unknown algorithm: %d", a) + } +} + +func padData(method cipherMethod, data []byte, blockSize int) ([]byte, error) { + switch p := method.padding; p { + case pkcsPadding: + return pkcsPad(data, blockSize) + case noPadding: + return data, nil + default: + return nil, errors.Newf("cannot pad for unknown padding: %d", p) + } +} + +func unpadData(method cipherMethod, data []byte) ([]byte, error) { + switch p := method.padding; p { + case pkcsPadding: + return pkcsUnpad(data) + case noPadding: + return data, nil + default: + return nil, errors.Newf("cannot unpad for unknown padding: %d", p) + } +} + +func validateDataLength(data []byte, blockSize int) error { + if len(data)%blockSize != 0 { + // TODO(yang): Not sure if this is the right pgcode since Postgres + // returns pgcode.ExternalRoutineException. + return pgerror.Newf( + pgcode.InvalidParameterValue, + `data has length %d, which is not a multiple of block size %d`, + len(data), blockSize, + ) + } + return nil +} + +func encrypt(method cipherMethod, block cipher.Block, iv []byte, data []byte) ([]byte, error) { + switch m := method.mode; m { + case cbcMode: + ret := make([]byte, len(data)) + iv = zeroPadOrTruncate(iv, block.BlockSize()) + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ret, data) + return ret, nil + default: + return nil, errors.Newf("cannot encrypt for unknown mode: %d", m) + } +} + +func decrypt(method cipherMethod, block cipher.Block, iv []byte, data []byte) ([]byte, error) { + switch m := method.mode; m { + case cbcMode: + ret := make([]byte, len(data)) + iv = zeroPadOrTruncate(iv, block.BlockSize()) + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(ret, data) + return ret, nil + default: + return nil, errors.Newf("cannot encrypt for unknown mode: %d", m) + } +} + +func zeroPadOrTruncate(data []byte, size int) []byte { + if len(data) >= size { + return data[:size] + } + paddedData := make([]byte, size) + copy(paddedData, data) + return paddedData +} diff --git a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher_test.go b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher_test.go new file mode 100644 index 000000000000..d8368fb04a88 --- /dev/null +++ b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/cipher_test.go @@ -0,0 +1,129 @@ +// Copyright 2023 The Cockroach Authors. +// +// Licensed as a CockroachDB Enterprise file under the Cockroach Community +// License (the "License"); you may not use this file except in compliance with +// the License. You may obtain a copy of the License at +// +// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt + +package pgcryptocipherccl_test + +import ( + "testing" + + "github.com/cockroachdb/cockroach/pkg/ccl/pgcryptoccl/pgcryptocipherccl" + "github.com/stretchr/testify/require" +) + +func TestEncrypt(t *testing.T) { + for name, tc := range cipherTestCases { + t.Run(name, func(t *testing.T) { + res, err := pgcryptocipherccl.Encrypt(tc.plaintext, tc.key, tc.iv, tc.cipherType) + require.NoError(t, err) + require.Equal(t, tc.ciphertext, res) + }) + } +} + +func TestDecrypt(t *testing.T) { + for name, tc := range cipherTestCases { + t.Run(name, func(t *testing.T) { + res, err := pgcryptocipherccl.Decrypt(tc.ciphertext, tc.key, tc.iv, tc.cipherType) + require.NoError(t, err) + require.Equal(t, tc.plaintext, res) + }) + } +} + +func FuzzEncryptDecryptAES(f *testing.F) { + for _, tc := range cipherTestCases { + f.Add(tc.plaintext, tc.key, tc.iv) + } + f.Fuzz(func(t *testing.T, plaintext []byte, key []byte, iv []byte) { + ciphertext, err := pgcryptocipherccl.Encrypt(plaintext, key, iv, "aes") + require.NoError(t, err) + decryptedCiphertext, err := pgcryptocipherccl.Decrypt(ciphertext, key, iv, "aes") + require.NoError(t, err) + require.Equal(t, plaintext, decryptedCiphertext) + }) +} + +func BenchmarkEncrypt(b *testing.B) { + for name, tc := range cipherTestCases { + b.Run(name, func(b *testing.B) { + benchmarkEncrypt(b, tc.plaintext, tc.key, tc.iv, tc.cipherType) + }) + } +} + +func BenchmarkDecrypt(b *testing.B) { + for name, tc := range cipherTestCases { + b.Run(name, func(*testing.B) { + benchmarkDecrypt(b, tc.ciphertext, tc.key, tc.iv, tc.cipherType) + }) + } +} + +func benchmarkEncrypt(b *testing.B, data []byte, key []byte, iv []byte, cipherType string) { + for n := 0; n < b.N; n++ { + _, err := pgcryptocipherccl.Encrypt(data, key, iv, cipherType) + require.NoError(b, err) + } +} + +func benchmarkDecrypt(b *testing.B, data []byte, key []byte, iv []byte, cipherType string) { + for n := 0; n < b.N; n++ { + _, err := pgcryptocipherccl.Decrypt(data, key, iv, cipherType) + require.NoError(b, err) + } +} + +// Note: The encrypted values were manually checked against the results of +// the equivalent function on Postgres 15.3. +var cipherTestCases = map[string]struct { + plaintext []byte + key []byte + iv []byte + cipherType string + ciphertext []byte +}{ + "AES-128": { + plaintext: []byte("abc"), + key: []byte("16_byte_long_key"), + cipherType: "aes", + ciphertext: []byte{0x0, 0x26, 0xcd, 0x62, 0x6, 0xcf, 0xd9, 0x21, 0x40, 0x88, 0x3b, 0x75, 0xc0, 0x98, 0xd6, 0x13}, + }, + "AES-128 with IV": { + plaintext: []byte("abc"), + key: []byte("16_byte_long_key"), + iv: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, + cipherType: "aes/pad:pkcs", + ciphertext: []byte{0xf8, 0x3e, 0xf0, 0x88, 0x83, 0x9b, 0x2f, 0x19, 0x1c, 0x42, 0x5, 0xa2, 0xe, 0x62, 0x69, 0xd3}, + }, + "AES-192": { + plaintext: []byte("UCVBgQEhwVxXrABksapmqglhTGNWRRnGnUUQHtNanYuyIDrQgHvQanwkfXRvTnJLsMauOfdlLBuJRJkzaGpnVRyENKGwFfauXLJD"), + key: []byte("24_byte_looooooooong_key"), + cipherType: "aes", + ciphertext: []byte{0x44, 0x68, 0x34, 0xe4, 0x3c, 0xb1, 0x76, 0x3, 0xa2, 0xb2, 0x3d, 0x25, 0x13, 0xa7, 0x84, 0xa3, 0x6b, 0xae, 0x79, 0x3, 0x72, 0x74, 0x57, 0x1c, 0x32, 0xcd, 0x29, 0x4a, 0xd, 0x6e, 0xe, 0xf9, 0x27, 0xee, 0x0, 0x76, 0xdc, 0xa4, 0x60, 0xb0, 0x9, 0x0, 0xbc, 0x11, 0x75, 0x4f, 0xee, 0x61, 0x22, 0x52, 0xa6, 0x7, 0x25, 0xa, 0x4f, 0x86, 0xd6, 0x20, 0x56, 0xb6, 0xae, 0xee, 0xc, 0xcd, 0x66, 0x4, 0x88, 0x9e, 0x7f, 0x39, 0xaf, 0x64, 0xce, 0x8b, 0x59, 0x60, 0xc1, 0x74, 0x4d, 0xb2, 0x16, 0xb0, 0x10, 0xea, 0x9c, 0x1, 0x49, 0x6c, 0x42, 0x9f, 0xd5, 0xf5, 0x81, 0x6, 0xf6, 0x4f, 0x21, 0x12, 0xf4, 0xdb, 0xc2, 0x90, 0x74, 0x2a, 0xa3, 0xd1, 0x66, 0x91, 0x5d, 0x97, 0x60, 0x64}, + }, + "AES-192 with IV": { + plaintext: []byte("UCVBgQEhwVxXrABksapmqglhTGNWRRnGnUUQHtNanYuyIDrQgHvQanwkfXRvTnJLsMauOfdlLBuJRJkzaGpnVRyENKGwFfauXLJD"), + key: []byte("24_byte_looooooooong_key"), + iv: []byte{152, 0, 68, 172}, + cipherType: "aes", + ciphertext: []byte{0xd2, 0x84, 0x24, 0x69, 0x11, 0x17, 0xec, 0x5c, 0x1c, 0xbe, 0x2d, 0x34, 0x67, 0xb7, 0xf, 0xa9, 0x94, 0xeb, 0x11, 0x97, 0xe4, 0x60, 0x72, 0xe7, 0xf1, 0x49, 0x9b, 0xc7, 0xc8, 0xc6, 0x7e, 0xad, 0x2b, 0xe2, 0x36, 0x6a, 0xd1, 0x20, 0x0, 0x4a, 0x28, 0x7a, 0x25, 0x86, 0x9a, 0x23, 0x9, 0xe9, 0xf1, 0x2c, 0x7e, 0xe, 0x3a, 0xec, 0xb9, 0x10, 0xf5, 0x35, 0xb6, 0xc2, 0xf6, 0x93, 0x16, 0x2, 0xd, 0x3b, 0x65, 0x63, 0x27, 0x29, 0xd2, 0xeb, 0xc8, 0xc0, 0xa0, 0x2e, 0x19, 0x22, 0xc4, 0x1e, 0x38, 0x73, 0xb0, 0x34, 0x69, 0xda, 0x52, 0x63, 0x9e, 0xaa, 0xa5, 0x93, 0x1, 0x37, 0xff, 0x9d, 0x4e, 0xf1, 0x7f, 0x72, 0x79, 0xe6, 0xad, 0x4b, 0x1f, 0x67, 0x4f, 0x2, 0xab, 0x17, 0x13, 0xcc}, + }, + "AES-256": { + plaintext: []byte("ghJxetyYQiLwdAtibf52bECQbA6QP0FsC0wDURcrR9DRZs7WChql2cJSunTh6rr6b5MM5YYgzWgXHvTxHaEIMiAuEXHsfcyInlxIyaHe2wS03PV6HZ1GKNbhksUx6NjKEoW5SmvmSngdlXSAOWwTYalUP6mKZm9BYHe57LQiHhiX76dKkqqVBvt16t6Hki3hRbqxdUH9JTB3sNAjoH6EjZKnH9h04M02IYncyJAhEpfDINkaerYMQ1Hbavpo0UHu"), + key: []byte("32_byte_looooooooooooooooong_key"), + cipherType: "aes/pad:none", + ciphertext: []byte{0xb1, 0x7e, 0x2f, 0xfd, 0x8d, 0xe2, 0x96, 0x5, 0xdc, 0xb1, 0x74, 0x5f, 0x59, 0x67, 0x2e, 0x30, 0x78, 0x6c, 0xdf, 0x2e, 0xd6, 0xbd, 0x23, 0xcd, 0xf2, 0x9b, 0x31, 0xd0, 0xa1, 0xa3, 0xd5, 0xd0, 0x4e, 0x25, 0x81, 0x25, 0x19, 0xdd, 0x4a, 0x67, 0xb4, 0xba, 0x71, 0x28, 0x2f, 0xa6, 0x31, 0x71, 0xf, 0xfc, 0xd6, 0x9a, 0xe3, 0xa6, 0x7e, 0xc7, 0x5d, 0xe7, 0xe5, 0xef, 0x8c, 0x90, 0xeb, 0x15, 0xfb, 0xa0, 0xd7, 0x30, 0x87, 0xf6, 0x5, 0xa9, 0x16, 0x86, 0xcc, 0xc0, 0x9f, 0x19, 0x14, 0xb8, 0x43, 0x72, 0xcb, 0x35, 0x9d, 0x18, 0xdc, 0xd2, 0xd6, 0xe4, 0x30, 0xc5, 0xc4, 0x8f, 0x4b, 0x98, 0xdd, 0x29, 0x53, 0xd9, 0x6, 0x1b, 0x48, 0xe9, 0x4a, 0x76, 0x36, 0xf5, 0x5a, 0x55, 0x5e, 0xd, 0x18, 0x57, 0xfe, 0xc2, 0x90, 0x5b, 0x82, 0x50, 0x7e, 0x14, 0xe1, 0xe0, 0xb0, 0x5e, 0x26, 0x44, 0x61, 0xe4, 0x22, 0x72, 0x7, 0x19, 0x60, 0xef, 0x6c, 0xb9, 0xfb, 0xa5, 0x9e, 0x5c, 0x7, 0xf6, 0x13, 0xc7, 0xa1, 0x97, 0x6b, 0xc, 0x1c, 0xaa, 0xaa, 0xcf, 0xd8, 0x6d, 0x39, 0x50, 0x57, 0x94, 0xce, 0x5d, 0xd, 0xf2, 0xd4, 0x2b, 0xbd, 0x71, 0xb4, 0xc6, 0x3b, 0x92, 0xb0, 0x5f, 0x3d, 0x56, 0xf5, 0x23, 0x6d, 0xaf, 0xb0, 0xbc, 0x62, 0xd5, 0xf0, 0x1e, 0x75, 0xad, 0xc2, 0x64, 0xbf, 0x66, 0xe7, 0xad, 0xab, 0xfd, 0xbf, 0xde, 0x72, 0xa4, 0x6, 0x39, 0xd2, 0xf4, 0x3, 0x5e, 0x27, 0x6, 0x98, 0x92, 0x7d, 0x2c, 0xe4, 0xac, 0x9e, 0xff, 0x41, 0xc1, 0xe7, 0xc1, 0x69, 0xbd, 0x8f, 0x5b, 0xc4, 0x1e, 0x36, 0x59, 0xa9, 0xb5, 0xbc, 0x64, 0x49, 0xfd, 0x78, 0x7f, 0xc, 0xad, 0x25, 0xae, 0x4b, 0xec, 0xb5, 0x4f, 0x97, 0x9e, 0x8c, 0xa6, 0x7b, 0x9, 0x2a, 0x76, 0xcd, 0x2d, 0x2e, 0x41}, + }, + "AES-256 with IV": { + plaintext: []byte("ghJxetyYQiLwdAtibf52bECQbA6QP0FsC0wDURcrR9DRZs7WChql2cJSunTh6rr6b5MM5YYgzWgXHvTxHaEIMiAuEXHsfcyInlxIyaHe2wS03PV6HZ1GKNbhksUx6NjKEoW5SmvmSngdlXSAOWwTYalUP6mKZm9BYHe57LQiHhiX76dKkqqVBvt16t6Hki3hRbqxdUH9JTB3sNAjoH6EjZKnH9h04M02IYncyJAhEpfDINkaerYMQ1Hbavpo0UHu"), + key: []byte("32_byte_looooooooooooooooong_key"), + iv: []byte{154, 201, 158, 144, 3, 25, 184, 208, 70, 90, 123, 45, 168, 27, 236, 25}, + cipherType: "aes", + ciphertext: []byte{0x8b, 0xb2, 0xd7, 0x52, 0xdd, 0xff, 0x46, 0xa, 0xb9, 0x67, 0x12, 0x57, 0xe0, 0xff, 0x61, 0x23, 0x7, 0x60, 0x2, 0xe2, 0x28, 0x53, 0x53, 0x7c, 0x3e, 0xa, 0xdf, 0x8, 0x6e, 0xbd, 0x54, 0x48, 0xb3, 0x82, 0xf7, 0x6a, 0xce, 0xc6, 0xc8, 0xb3, 0xda, 0x19, 0xc5, 0x81, 0x91, 0x11, 0xd8, 0xd0, 0x87, 0x7d, 0xc0, 0x29, 0xb4, 0xe4, 0x8d, 0x74, 0x93, 0x0, 0xeb, 0x34, 0xc9, 0xd2, 0xb1, 0xc9, 0x30, 0xbc, 0xd9, 0x67, 0x7b, 0xa4, 0xeb, 0x81, 0x32, 0x8c, 0xa9, 0x95, 0xb9, 0x7c, 0xfb, 0xc0, 0x5f, 0x87, 0x2c, 0x32, 0x43, 0x15, 0xb4, 0xd1, 0x9b, 0x25, 0x3c, 0xe, 0xaf, 0xd1, 0x56, 0x17, 0xac, 0x26, 0x51, 0x17, 0x36, 0xda, 0xf6, 0x13, 0x5d, 0xf8, 0x12, 0xf6, 0x1f, 0x4, 0x17, 0x5b, 0xa8, 0xf1, 0xb1, 0xda, 0xc, 0x38, 0x13, 0x10, 0xe7, 0xe0, 0xd7, 0xa8, 0x4a, 0x69, 0x83, 0x5e, 0xe7, 0xc0, 0xf7, 0x21, 0xdb, 0xe3, 0x9d, 0x84, 0xce, 0x8e, 0x50, 0x7, 0x44, 0xe4, 0xe, 0xdf, 0xde, 0x88, 0xa5, 0xee, 0xd7, 0xf4, 0x32, 0xa0, 0x60, 0x24, 0x9d, 0x8b, 0x5a, 0xdf, 0xbc, 0xca, 0x6c, 0x2c, 0x80, 0x40, 0x47, 0xdf, 0x3f, 0x9b, 0xbe, 0x86, 0x34, 0x2b, 0x79, 0xc1, 0x27, 0xcf, 0x4c, 0xda, 0x89, 0x6b, 0xd8, 0xcb, 0xf8, 0xcd, 0x57, 0x46, 0xbc, 0x54, 0x55, 0x21, 0xc3, 0x34, 0x5e, 0x4c, 0xc8, 0xc3, 0x21, 0x51, 0xce, 0x8b, 0x3e, 0xcb, 0x36, 0xdc, 0x32, 0x31, 0xdc, 0x27, 0xf8, 0x12, 0x77, 0x64, 0xa8, 0xb8, 0x6d, 0x3c, 0x42, 0x6a, 0xd9, 0x7c, 0xbf, 0x14, 0xc5, 0x58, 0xdb, 0x1d, 0x24, 0x74, 0xb5, 0x47, 0xce, 0x54, 0x50, 0x32, 0xfc, 0xb9, 0x9f, 0xd3, 0x45, 0xa2, 0x5, 0x5d, 0xa4, 0x8a, 0x57, 0x58, 0x10, 0xc7, 0x5e, 0x83, 0x9f, 0x2b, 0x64, 0xe3, 0xd4, 0x98, 0x15, 0xf7, 0x8a, 0xdf, 0xe7, 0xc7, 0x2, 0x27, 0xa4, 0xff, 0xa7, 0x98, 0x24, 0xa, 0xf0, 0x2d}, + }, +} diff --git a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/padding.go b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/padding.go index 02ed2282a45e..efd83d150699 100644 --- a/pkg/ccl/pgcryptoccl/pgcryptocipherccl/padding.go +++ b/pkg/ccl/pgcryptoccl/pgcryptocipherccl/padding.go @@ -23,10 +23,14 @@ func pkcsPad(data []byte, blockSize int) ([]byte, error) { return nil, errors.Newf("invalid block size for PKCS padding: %d", blockSize) } - paddingLen := blockSize - len(data)%blockSize - padding := bytes.Repeat([]byte{byte(paddingLen)}, paddingLen) + paddedData := make([]byte, len(data)) + copy(paddedData, data) - return append(data, padding...), nil + paddingSize := blockSize - len(data)%blockSize + padding := bytes.Repeat([]byte{byte(paddingSize)}, paddingSize) + paddedData = append(paddedData, padding...) + + return paddedData, nil } // pkcsUnpad removes the padding added by pkcsPad. diff --git a/pkg/sql/logictest/testdata/logic_test/pgcrypto_builtins b/pkg/sql/logictest/testdata/logic_test/pgcrypto_builtins index d4c2880c3c3a..f7f6d7391227 100644 --- a/pkg/sql/logictest/testdata/logic_test/pgcrypto_builtins +++ b/pkg/sql/logictest/testdata/logic_test/pgcrypto_builtins @@ -288,3 +288,23 @@ statement error pgcode 22023 invalid salt encoding SELECT crypt('password', '$2a$06$#kv6DxN3PpZo4YboQRrIVO') subtest end + +subtest ccl_functions + +skipif config 3node-tenant-default-configs +query error pgcode XXC01 encrypt can only be used with a CCL distribution +SELECT encrypt('abc', 'key', 'aes') + +skipif config 3node-tenant-default-configs +query error pgcode XXC01 encrypt_iv can only be used with a CCL distribution +SELECT encrypt_iv('abc', 'key', '123', 'aes') + +skipif config 3node-tenant-default-configs +query error pgcode XXC01 decrypt can only be used with a CCL distribution +SELECT decrypt('\xdb5f149a7caf0cd275ca18c203a212c9', 'key', 'aes') + +skipif config 3node-tenant-default-configs +query error pgcode XXC01 decrypt_iv can only be used with a CCL distribution +SELECT decrypt_iv('\x91b4ef63852013c8da53829da662b871', 'key', '123', 'aes') + +subtest end diff --git a/pkg/sql/sem/builtins/BUILD.bazel b/pkg/sql/sem/builtins/BUILD.bazel index 66584bddcc70..ffe2655a7721 100644 --- a/pkg/sql/sem/builtins/BUILD.bazel +++ b/pkg/sql/sem/builtins/BUILD.bazel @@ -81,6 +81,7 @@ go_library( "//pkg/sql/sem/asof", "//pkg/sql/sem/builtins/builtinconstants", "//pkg/sql/sem/builtins/builtinsregistry", + "//pkg/sql/sem/builtins/pgcrypto", "//pkg/sql/sem/builtins/pgformat", "//pkg/sql/sem/cast", "//pkg/sql/sem/catconstants", diff --git a/pkg/sql/sem/builtins/fixed_oids.go b/pkg/sql/sem/builtins/fixed_oids.go index 2fc94ba12763..5dff72bd7031 100644 --- a/pkg/sql/sem/builtins/fixed_oids.go +++ b/pkg/sql/sem/builtins/fixed_oids.go @@ -2453,6 +2453,10 @@ var builtinOidsArray = []string{ 2482: `bitmask_xor(a: varbit, b: string) -> varbit`, 2483: `bitmask_xor(a: string, b: varbit) -> varbit`, 2484: `oidvectortypes(vector: oidvector) -> string`, + 2485: `encrypt(data: bytes, key: bytes, type: string) -> bytes`, + 2486: `encrypt_iv(data: bytes, key: bytes, iv: bytes, type: string) -> bytes`, + 2487: `decrypt(data: bytes, key: bytes, type: string) -> bytes`, + 2488: `decrypt_iv(data: bytes, key: bytes, iv: bytes, type: string) -> bytes`, } var builtinOidsBySignature map[string]oid.Oid diff --git a/pkg/sql/sem/builtins/pgcrypto/BUILD.bazel b/pkg/sql/sem/builtins/pgcrypto/BUILD.bazel new file mode 100644 index 000000000000..f1b9992ee6bc --- /dev/null +++ b/pkg/sql/sem/builtins/pgcrypto/BUILD.bazel @@ -0,0 +1,13 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "pgcrypto", + srcs = ["pgcrypto.go"], + importpath = "github.com/cockroachdb/cockroach/pkg/sql/sem/builtins/pgcrypto", + visibility = ["//visibility:public"], + deps = [ + "//pkg/sql/pgwire/pgcode", + "//pkg/sql/pgwire/pgerror", + "//pkg/sql/sem/eval", + ], +) diff --git a/pkg/sql/sem/builtins/pgcrypto/pgcrypto.go b/pkg/sql/sem/builtins/pgcrypto/pgcrypto.go new file mode 100644 index 000000000000..54685d1303f2 --- /dev/null +++ b/pkg/sql/sem/builtins/pgcrypto/pgcrypto.go @@ -0,0 +1,47 @@ +// Copyright 2023 The Cockroach Authors. +// +// Use of this software is governed by the Business Source License +// included in the file licenses/BSL.txt. +// +// As of the Change Date specified in that file, in accordance with +// the Business Source License, use of this software will be governed +// by the Apache License, Version 2.0, included in the file +// licenses/APL.txt. + +package pgcrypto + +import ( + "context" + + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" + "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" + "github.com/cockroachdb/cockroach/pkg/sql/sem/eval" +) + +var Decrypt = func(_ context.Context, _ *eval.Context, data []byte, key []byte, cipherType string) ([]byte, error) { + return nil, pgerror.New( + pgcode.CCLRequired, + "decrypt can only be used with a CCL distribution", + ) +} + +var DecryptIV = func(_ context.Context, _ *eval.Context, data []byte, key []byte, iv []byte, cipherType string) ([]byte, error) { + return nil, pgerror.New( + pgcode.CCLRequired, + "decrypt_iv can only be used with a CCL distribution", + ) +} + +var Encrypt = func(_ context.Context, _ *eval.Context, data []byte, key []byte, cipherType string) ([]byte, error) { + return nil, pgerror.New( + pgcode.CCLRequired, + "encrypt can only be used with a CCL distribution", + ) +} + +var EncryptIV = func(_ context.Context, _ *eval.Context, data []byte, key []byte, iv []byte, cipherType string) ([]byte, error) { + return nil, pgerror.New( + pgcode.CCLRequired, + "encrypt_iv can only be used with a CCL distribution", + ) +} diff --git a/pkg/sql/sem/builtins/pgcrypto_builtins.go b/pkg/sql/sem/builtins/pgcrypto_builtins.go index 34ed29858f1b..02331b72226b 100644 --- a/pkg/sql/sem/builtins/pgcrypto_builtins.go +++ b/pkg/sql/sem/builtins/pgcrypto_builtins.go @@ -27,6 +27,7 @@ import ( "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgcode" "github.com/cockroachdb/cockroach/pkg/sql/pgwire/pgerror" "github.com/cockroachdb/cockroach/pkg/sql/sem/builtins/builtinconstants" + "github.com/cockroachdb/cockroach/pkg/sql/sem/builtins/pgcrypto" "github.com/cockroachdb/cockroach/pkg/sql/sem/eval" "github.com/cockroachdb/cockroach/pkg/sql/sem/tree" "github.com/cockroachdb/cockroach/pkg/sql/sem/volatility" @@ -42,6 +43,13 @@ func init() { } } +const cipherSupportedCipherTypeInfo = "The cipher type must have the format `[-][/pad:]` where:\n" + + "* `` is `aes`\n" + + "* `` is `cbc` (default)\n" + + "* `` is `pkcs` (default) or `none`" + +const cipherRequiresEnterpriseLicenseInfo = "This function requires an enterprise license on a CCL distribution." + var pgcryptoBuiltins = map[string]builtinDefinition{ "crypt": makeBuiltin( @@ -63,6 +71,61 @@ var pgcryptoBuiltins = map[string]builtinDefinition{ }, ), + "decrypt": makeBuiltin( + tree.FunctionProperties{Category: builtinconstants.CategoryCrypto}, + tree.Overload{ + Types: tree.ParamTypes{ + {Name: "data", Typ: types.Bytes}, + {Name: "key", Typ: types.Bytes}, + {Name: "type", Typ: types.String}, + }, + ReturnType: tree.FixedReturnType(types.Bytes), + Fn: func(ctx context.Context, evalCtx *eval.Context, args tree.Datums) (tree.Datum, error) { + data := []byte(tree.MustBeDBytes(args[0])) + key := []byte(tree.MustBeDBytes(args[1])) + cipherType := string(tree.MustBeDString(args[2])) + decryptedData, err := pgcrypto.Decrypt(ctx, evalCtx, data, key, cipherType) + if err != nil { + return nil, err + } + return tree.NewDBytes(tree.DBytes(decryptedData)), nil + }, + Info: "Decrypt `data` with `key` using the cipher method specified by `type`." + + "\n\n" + cipherSupportedCipherTypeInfo + + "\n\n" + cipherRequiresEnterpriseLicenseInfo, + Volatility: volatility.Immutable, + }, + ), + + "decrypt_iv": makeBuiltin( + tree.FunctionProperties{Category: builtinconstants.CategoryCrypto}, + tree.Overload{ + Types: tree.ParamTypes{ + {Name: "data", Typ: types.Bytes}, + {Name: "key", Typ: types.Bytes}, + {Name: "iv", Typ: types.Bytes}, + {Name: "type", Typ: types.String}, + }, + ReturnType: tree.FixedReturnType(types.Bytes), + Fn: func(ctx context.Context, evalCtx *eval.Context, args tree.Datums) (tree.Datum, error) { + data := []byte(tree.MustBeDBytes(args[0])) + key := []byte(tree.MustBeDBytes(args[1])) + iv := []byte(tree.MustBeDBytes(args[2])) + cipherType := string(tree.MustBeDString(args[3])) + decryptedData, err := pgcrypto.DecryptIV(ctx, evalCtx, data, key, iv, cipherType) + if err != nil { + return nil, err + } + return tree.NewDBytes(tree.DBytes(decryptedData)), nil + }, + Info: "Decrypt `data` with `key` using the cipher method specified by `type`. " + + "If the mode is CBC, the provided `iv` will be used. Otherwise, it will be ignored." + + "\n\n" + cipherSupportedCipherTypeInfo + + "\n\n" + cipherRequiresEnterpriseLicenseInfo, + Volatility: volatility.Immutable, + }, + ), + "digest": makeBuiltin( tree.FunctionProperties{Category: builtinconstants.CategoryCrypto}, tree.Overload{ @@ -105,6 +168,61 @@ var pgcryptoBuiltins = map[string]builtinDefinition{ }, ), + "encrypt": makeBuiltin( + tree.FunctionProperties{Category: builtinconstants.CategoryCrypto}, + tree.Overload{ + Types: tree.ParamTypes{ + {Name: "data", Typ: types.Bytes}, + {Name: "key", Typ: types.Bytes}, + {Name: "type", Typ: types.String}, + }, + ReturnType: tree.FixedReturnType(types.Bytes), + Fn: func(ctx context.Context, evalCtx *eval.Context, args tree.Datums) (tree.Datum, error) { + data := []byte(tree.MustBeDBytes(args[0])) + key := []byte(tree.MustBeDBytes(args[1])) + cipherType := string(tree.MustBeDString(args[2])) + encryptedData, err := pgcrypto.Encrypt(ctx, evalCtx, data, key, cipherType) + if err != nil { + return nil, err + } + return tree.NewDBytes(tree.DBytes(encryptedData)), nil + }, + Info: "Encrypt `data` with `key` using the cipher method specified by `type`." + + "\n\n" + cipherSupportedCipherTypeInfo + + "\n\n" + cipherRequiresEnterpriseLicenseInfo, + Volatility: volatility.Immutable, + }, + ), + + "encrypt_iv": makeBuiltin( + tree.FunctionProperties{Category: builtinconstants.CategoryCrypto}, + tree.Overload{ + Types: tree.ParamTypes{ + {Name: "data", Typ: types.Bytes}, + {Name: "key", Typ: types.Bytes}, + {Name: "iv", Typ: types.Bytes}, + {Name: "type", Typ: types.String}, + }, + ReturnType: tree.FixedReturnType(types.Bytes), + Fn: func(ctx context.Context, evalCtx *eval.Context, args tree.Datums) (tree.Datum, error) { + data := []byte(tree.MustBeDBytes(args[0])) + key := []byte(tree.MustBeDBytes(args[1])) + iv := []byte(tree.MustBeDBytes(args[2])) + cipherType := string(tree.MustBeDString(args[3])) + encryptedData, err := pgcrypto.EncryptIV(ctx, evalCtx, data, key, iv, cipherType) + if err != nil { + return nil, err + } + return tree.NewDBytes(tree.DBytes(encryptedData)), nil + }, + Info: "Encrypt `data` with `key` using the cipher method specified by `type`. " + + "If the mode is CBC, the provided `iv` will be used. Otherwise, it will be ignored." + + "\n\n" + cipherSupportedCipherTypeInfo + + "\n\n" + cipherRequiresEnterpriseLicenseInfo, + Volatility: volatility.Immutable, + }, + ), + "gen_random_uuid": generateRandomUUID4Impl(), "gen_salt": makeBuiltin(