本页面介绍如何在 Cloud SQL 上执行客户端加密功能。
概览
客户端加密功能是在将数据写入 Cloud SQL 之前加密数据的行为。您可以以只有应用能解密的方式加密 Cloud SQL 数据。
如需启用客户端加密功能,您有以下选择:
- 使用存储在 Cloud Key Management Service (Cloud KMS) 中的加密密钥。
- 使用在应用中本地存储的加密密钥。
在本主题中,我们将介绍如何使用第一个选项,这种方式提供最无缝的密钥管理选项。我们在 Cloud KMS 中创建加密键,并使用 Google 的开源加密库 Tink 实现信封加密。
为什么需要客户端加密?
如果您要保护列级别 1 中的 Cloud SQL 数据,则需要客户端加密。假设您有一个包含姓名和信用卡号的表。您希望授予用户对该表的访问权限,但又不希望他们查看信用卡号。您可以使用客户端加密来加密卡号。只要用户未被授予对 Cloud KMS 中加密密钥的访问权限,其便无法读取信用卡信息。
使用 Cloud KMS 创建密钥
借助 Cloud KMS,您可以在 Google Cloud Platform 上创建和管理密钥。
Cloud KMS 支持许多不同的密钥类型。对于客户端加密,您需要创建对称密钥。
如需向您的应用授予对 Cloud KMS 中键的访问权限,您需要向应用使用的服务账号授予 cloudkms.cryptoKeyEncrypterDecrypter
角色。在 gcloud 中,您可以使用以下命令执行此操作:
gcloud kms keys add-iam-policy-binding key \ --keyring=key-ring \ --location=location \ --member=serviceAccount:service-account-name@example.domain.com \ --role=roles/cloudkms.cryptoKeyEncrypterDecrypter
虽然您可以使用 KMS 密钥直接加密数据,但我们在此处使用更为灵活的解决方案—信封加密。借助此方案,我们就可以加密长度超过 64KB 的消息,这是 Cloud Key Management Service API 可以支持的最大消息大小。
Cloud KMS 信封加密
在信封加密中,KMS 密钥充当密钥加密密钥 (KEK)。也就是说,该加密密钥可用于加密数据加密密钥 (DEK),而 DEK 则可用于加密实际数据。
在 Cloud KMS 中创建 KEK 之后,如要对每条消息进行加密,您需要执行以下操作:
- 在本地生成数据加密密钥 (DEK)。
- 在本地使用此 DEK 对消息进行加密。
- 调用 Cloud KMS 使用 KEK 加密(封装)DEK。
- 存储加密后的数据和封装后的 DEK。
在本主题中,我们使用 Tink,而不是从头开始实施信封加密。
Tink
Tink 是一个多语言的跨平台库,提供高级加密 API。如需使用 Tink 的信封加密对数据进行加密,请为 Tink 提供一个指向 Cloud KMS 中的 KEK 的密钥 URI,并提供允许 Tink 使用 KEK 的凭据。Tink 生成 DEK、加密数据、封装 DEK,然后返回包含加密数据和封装 DEK 的单个密文。
Tink 支持在 C++、Java、Go 和 Python 中使用 AEAD API 进行信封加密:
public interface Aead{
byte[] encrypt(final byte[] plaintext, final byte[] associatedData)
throws…
byte[] decrypt(final byte[] ciphertext, final byte[] associatedData)
throws…
}
除了普通的消息/密文参数,加密和解密方法还支持可选关联的数据。此参数可用于将密文与部分数据关联。例如,假设您有一个包含字段 user-id
和字段 encrypted-medical-history
的数据库。在这种情况下,在加密医疗记录时,字段 user-id
可能会用作关联数据。这样可确保攻击者无法将一个用户的医疗记录转移到另一个用户。它还用于在运行查询时验证数据行是否正确。
示例
在本部分中,我们将详细介绍使用客户端加密的选票信息数据库的示例代码。该示例代码演示了如何执行以下操作:
- 创建数据库表和连接池
- 设置 Tink 以进行信封加密
- 使用 Tink 的信封加密和 Cloud KMS 中的 KEK 加密和解密数据
准备工作
按照以下这些说明创建 Cloud SQL 实例。请记下您创建的连接字符串、数据库用户和数据库密码。
按照这些说明为您的应用创建数据库。 记下数据库名称。
按照这些说明为您的应用创建 KMS 密钥。复制已创建键的资源名称。
按照这些说明创建具有“Cloud SQL Client”权限的服务账号。
按照这些说明,将该键的“Cloud KMS CryptoKey Encrypter/Decrypter”权限添加到您的服务账号。
创建一个连接池并在数据库中创建一个新表。
Java
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.sql.DataSource;
public class CloudSqlConnectionPool {
public static DataSource createConnectionPool(String dbUser, String dbPass, String dbName,
String instanceConnectionName) {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(String.format("jdbc:mysql:///%s", dbName));
config.setUsername(dbUser);
config.setPassword(dbPass);
config.addDataSourceProperty("socketFactory", "com.google.cloud.sql.mysql.SocketFactory");
config.addDataSourceProperty("cloudSqlInstance", instanceConnectionName);
DataSource pool = new HikariDataSource(config);
return pool;
}
public static void createTable(DataSource pool, String tableName) throws SQLException {
// Safely attempt to create the table schema.
try (Connection conn = pool.getConnection()) {
String stmt = String.format("CREATE TABLE IF NOT EXISTS %s ( "
+ "vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, team CHAR(6) NOT NULL,"
+ "voter_email VARBINARY(255), PRIMARY KEY (vote_id) );", tableName);
try (PreparedStatement createTableStatement = conn.prepareStatement(stmt);) {
createTableStatement.execute();
}
}
}
}
Python
import sqlalchemy
def init_tcp_connection_engine(
db_user: str, db_pass: str, db_name: str, db_host: str
) -> sqlalchemy.engine.base.Engine:
"""
Creates a connection to the database using tcp socket.
"""
# Remember - storing secrets in plaintext is potentially unsafe. Consider using
# something like https://cloud.google.com/secret-manager/docs/overview to help keep
# secrets secret.
# Extract host and port from db_host
host_args = db_host.split(":")
db_hostname, db_port = host_args[0], int(host_args[1])
pool = sqlalchemy.create_engine(
# Equivalent URL:
# mysql+pymysql://<db_user>:<db_pass>@<db_host>:<db_port>/<db_name>
sqlalchemy.engine.url.URL.create(
drivername="mysql+pymysql",
username=db_user, # e.g. "my-database-user"
password=db_pass, # e.g. "my-database-password"
host=db_hostname, # e.g. "127.0.0.1"
port=db_port, # e.g. 3306
database=db_name, # e.g. "my-database-name"
),
)
print("Created TCP connection pool")
return pool
def init_unix_connection_engine(
db_user: str,
db_pass: str,
db_name: str,
instance_connection_name: str,
db_socket_dir: str,
) -> sqlalchemy.engine.base.Engine:
"""
Creates a connection to the database using unix socket.
"""
# Remember - storing secrets in plaintext is potentially unsafe. Consider using
# something like https://cloud.google.com/secret-manager/docs/overview to help keep
# secrets secret.
pool = sqlalchemy.create_engine(
# Equivalent URL:
# mysql+pymysql://<db_user>:<db_pass>@/<db_name>?unix_socket=<socket_path>/<cloud_sql_instance_name>
sqlalchemy.engine.url.URL.create(
drivername="mysql+pymysql",
username=db_user, # e.g. "my-database-user"
password=db_pass, # e.g. "my-database-password"
database=db_name, # e.g. "my-database-name"
query={"unix_socket": f"{db_socket_dir}/{instance_connection_name}"},
),
)
print("Created Unix socket connection pool")
return pool
def init_db(
db_user: str,
db_pass: str,
db_name: str,
table_name: str,
instance_connection_name: str = None,
db_socket_dir: str = None,
db_host: str = None,
) -> sqlalchemy.engine.base.Engine:
"""Starts a connection to the database and creates voting table if it doesn't exist."""
if db_host:
db = init_tcp_connection_engine(db_user, db_pass, db_name, db_host)
else:
db = init_unix_connection_engine(
db_user, db_pass, db_name, instance_connection_name, db_socket_dir
)
# Create tables (if they don't already exist)
with db.connect() as conn:
conn.execute(
f"CREATE TABLE IF NOT EXISTS {table_name} "
"( vote_id SERIAL NOT NULL, time_cast timestamp NOT NULL, "
"team CHAR(6) NOT NULL, voter_email VARBINARY(255), "
"PRIMARY KEY (vote_id) );"
)
print(f"Created table {table_name} in db {db_name}")
return db
使用 Tink 初始化信包 AEAD 基元。
Java
import com.google.crypto.tink.Aead;
import com.google.crypto.tink.KmsClient;
import com.google.crypto.tink.aead.AeadConfig;
import com.google.crypto.tink.aead.AeadKeyTemplates;
import com.google.crypto.tink.aead.KmsEnvelopeAead;
import com.google.crypto.tink.integration.gcpkms.GcpKmsClient;
import java.security.GeneralSecurityException;
public class CloudKmsEnvelopeAead {
public static Aead get(String kmsUri) throws GeneralSecurityException {
AeadConfig.register();
// Create a new KMS Client
KmsClient client = new GcpKmsClient().withDefaultCredentials();
// Create an AEAD primitive using the Cloud KMS key
Aead gcpAead = client.getAead(kmsUri);
// Create an envelope AEAD primitive.
// This key should only be used for client-side encryption to ensure authenticity and integrity
// of data.
return new KmsEnvelopeAead(AeadKeyTemplates.AES128_GCM, gcpAead);
}
}
Python
import logging
import tink
from tink import aead
from tink.integration import gcpkms
logger = logging.getLogger(__name__)
def init_tink_env_aead(key_uri: str, credentials: str) -> tink.aead.KmsEnvelopeAead:
"""
Initiates the Envelope AEAD object using the KMS credentials.
"""
aead.register()
try:
gcp_client = gcpkms.GcpKmsClient(key_uri, credentials)
gcp_aead = gcp_client.get_aead(key_uri)
except tink.TinkError as e:
logger.error("Error initializing GCP client: %s", e)
raise e
# Create envelope AEAD primitive using AES256 GCM for encrypting the data
# This key should only be used for client-side encryption to ensure authenticity and integrity
# of data.
key_template = aead.aead_key_templates.AES256_GCM
env_aead = aead.KmsEnvelopeAead(key_template, gcp_aead)
print(f"Created envelope AEAD Primitive using KMS URI: {key_uri}")
return env_aead
加密数据并将其插入数据库。
Java
import com.google.crypto.tink.Aead;
import java.security.GeneralSecurityException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import javax.sql.DataSource;
public class EncryptAndInsertData {
public static void main(String[] args) throws GeneralSecurityException, SQLException {
// Saving credentials in environment variables is convenient, but not secure - consider a more
// secure solution such as Cloud Secret Manager to help keep secrets safe.
String dbUser = System.getenv("DB_USER"); // e.g. "root", "mysql"
String dbPass = System.getenv("DB_PASS"); // e.g. "mysupersecretpassword"
String dbName = System.getenv("DB_NAME"); // e.g. "votes_db"
String instanceConnectionName =
System.getenv("INSTANCE_CONNECTION_NAME"); // e.g. "project-name:region:instance-name"
String kmsUri = System.getenv("CLOUD_KMS_URI"); // e.g. "gcp-kms://projects/...path/to/key
// Tink uses the "gcp-kms://" prefix for paths to keys stored in Google Cloud KMS. For more
// info on creating a KMS key and getting its path, see
// https://cloud.google.com/kms/docs/quickstart
String team = "TABS";
String tableName = "votes";
String email = "hello@example.com";
// Initialize database connection pool and create table if it does not exist
// See CloudSqlConnectionPool.java for setup details
DataSource pool =
CloudSqlConnectionPool.createConnectionPool(dbUser, dbPass, dbName, instanceConnectionName);
CloudSqlConnectionPool.createTable(pool, tableName);
// Initialize envelope AEAD
// See CloudKmsEnvelopeAead.java for setup details
Aead envAead = CloudKmsEnvelopeAead.get(kmsUri);
encryptAndInsertData(pool, envAead, tableName, team, email);
}
public static void encryptAndInsertData(
DataSource pool, Aead envAead, String tableName, String team, String email)
throws GeneralSecurityException, SQLException {
try (Connection conn = pool.getConnection()) {
String stmt =
String.format(
"INSERT INTO %s (team, time_cast, voter_email) VALUES (?, ?, ?);", tableName);
try (PreparedStatement voteStmt = conn.prepareStatement(stmt); ) {
voteStmt.setString(1, team);
voteStmt.setTimestamp(2, new Timestamp(new Date().getTime()));
// Use the envelope AEAD primitive to encrypt the email, using the team name as
// associated data. This binds the encryption of the email to the team name, preventing
// associating an encrypted email in one row with a team name in another row.
byte[] encryptedEmail = envAead.encrypt(email.getBytes(), team.getBytes());
voteStmt.setBytes(3, encryptedEmail);
// Finally, execute the statement. If it fails, an error will be thrown.
voteStmt.execute();
System.out.println(String.format("Successfully inserted row into table %s", tableName));
}
}
}
}
Python
import datetime
import logging
import os
import sqlalchemy
import tink
from .cloud_kms_env_aead import init_tink_env_aead
from .cloud_sql_connection_pool import init_db
logger = logging.getLogger(__name__)
def main() -> None:
"""
Connects to the database, encrypts and inserts some data.
"""
db_user = os.environ["DB_USER"] # e.g. "root", "mysql"
db_pass = os.environ["DB_PASS"] # e.g. "mysupersecretpassword"
db_name = os.environ["DB_NAME"] # e.g. "votes_db"
# Set if connecting using TCP:
db_host = os.environ["DB_HOST"] # e.g. "127.0.0.1"
# Set if connecting using Unix sockets:
db_socket_dir = os.environ.get("DB_SOCKET_DIR", "/cloudsql")
instance_connection_name = os.environ["INSTANCE_CONNECTION_NAME"]
# e.g. "project-name:region:instance-name"
credentials = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS", "")
key_uri = "gcp-kms://" + os.environ["GCP_KMS_URI"]
# e.g. "gcp-kms://projects/...path/to/key
# Tink uses the "gcp-kms://" prefix for paths to keys stored in Google
# Cloud KMS. For more info on creating a KMS key and getting its path, see
# https://cloud.google.com/kms/docs/quickstart
table_name = "votes"
team = "TABS"
email = "hello@example.com"
env_aead = init_tink_env_aead(key_uri, credentials)
db = init_db(
db_user,
db_pass,
db_name,
table_name,
instance_connection_name,
db_socket_dir,
db_host,
)
encrypt_and_insert_data(db, env_aead, table_name, team, email)
def encrypt_and_insert_data(
db: sqlalchemy.engine.base.Engine,
env_aead: tink.aead.KmsEnvelopeAead,
table_name: str,
team: str,
email: str,
) -> None:
"""
Inserts a vote into the database with email address previously encrypted using
a KmsEnvelopeAead object.
"""
time_cast = datetime.datetime.now(tz=datetime.timezone.utc)
# Use the envelope AEAD primitive to encrypt the email, using the team name as
# associated data. Encryption with associated data ensures authenticity
# (who the sender is) and integrity (the data has not been tampered with) of that
# data, but not its secrecy. (see RFC 5116 for more info)
encrypted_email = env_aead.encrypt(email.encode(), team.encode())
# Verify that the team is one of the allowed options
if team != "TABS" and team != "SPACES":
logger.error(f"Invalid team specified: {team}")
return
# Preparing a statement before hand can help protect against injections.
stmt = sqlalchemy.text(
f"INSERT INTO {table_name} (time_cast, team, voter_email)"
" VALUES (:time_cast, :team, :voter_email)"
)
# Using a with statement ensures that the connection is always released
# back into the pool at the end of statement (even if an error occurs)
with db.connect() as conn:
conn.execute(stmt, time_cast=time_cast, team=team, voter_email=encrypted_email)
print(f"Vote successfully cast for '{team}' at time {time_cast}!")
查询数据库并解密存储的数据。
Java
import com.google.crypto.tink.Aead;
import java.security.GeneralSecurityException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import javax.sql.DataSource;
public class QueryAndDecryptData {
public static void main(String[] args) throws GeneralSecurityException, SQLException {
// Saving credentials in environment variables is convenient, but not secure - consider a more
// secure solution such as Cloud Secret Manager to help keep secrets safe.
String dbUser = System.getenv("DB_USER"); // e.g. "root", "mysql"
String dbPass = System.getenv("DB_PASS"); // e.g. "mysupersecretpassword"
String dbName = System.getenv("DB_NAME"); // e.g. "votes_db"
String instanceConnectionName =
System.getenv("INSTANCE_CONNECTION_NAME"); // e.g. "project-name:region:instance-name"
String kmsUri = System.getenv("CLOUD_KMS_URI"); // e.g. "gcp-kms://projects/...path/to/key
// Tink uses the "gcp-kms://" prefix for paths to keys stored in Google Cloud KMS. For more
// info on creating a KMS key and getting its path, see
// https://cloud.google.com/kms/docs/quickstart
String tableName = "votes123";
// Initialize database connection pool and create table if it does not exist
// See CloudSqlConnectionPool.java for setup details
DataSource pool =
CloudSqlConnectionPool.createConnectionPool(dbUser, dbPass, dbName, instanceConnectionName);
CloudSqlConnectionPool.createTable(pool, tableName);
// Initialize envelope AEAD
// See CloudKmsEnvelopeAead.java for setup details
Aead envAead = CloudKmsEnvelopeAead.get(kmsUri);
// Insert row into table to test
// See EncryptAndInsert.java for setup details
EncryptAndInsertData.encryptAndInsertData(
pool, envAead, tableName, "SPACES", "hello@example.com");
queryAndDecryptData(pool, envAead, tableName);
}
public static void queryAndDecryptData(DataSource pool, Aead envAead, String tableName)
throws GeneralSecurityException, SQLException {
try (Connection conn = pool.getConnection()) {
String stmt =
String.format(
"SELECT team, time_cast, voter_email FROM %s ORDER BY time_cast DESC LIMIT 5",
tableName);
try (PreparedStatement voteStmt = conn.prepareStatement(stmt); ) {
ResultSet voteResults = voteStmt.executeQuery();
System.out.println("Team\tTime Cast\tEmail");
while (voteResults.next()) {
String team = voteResults.getString(1);
Timestamp timeCast = voteResults.getTimestamp(2);
// Use the envelope AEAD primitive to encrypt the email, using the team name as
// associated data. This binds the encryption of the email to the team name, preventing
// associating an encrypted email in one row with a team name in another row.
String email = new String(envAead.decrypt(voteResults.getBytes(3), team.getBytes()));
System.out.println(String.format("%s\t%s\t%s", team, timeCast, email));
}
}
}
}
}
Python
import os
import sqlalchemy
import tink
from .cloud_kms_env_aead import init_tink_env_aead
from .cloud_sql_connection_pool import init_db
from .encrypt_and_insert_data import encrypt_and_insert_data
def main() -> None:
"""
Connects to the database, inserts encrypted data and retrieves encrypted data.
"""
db_user = os.environ["DB_USER"] # e.g. "root", "mysql"
db_pass = os.environ["DB_PASS"] # e.g. "mysupersecretpassword"
db_name = os.environ["DB_NAME"] # e.g. "votes_db"
# Set if connecting using TCP:
db_host = os.environ["DB_HOST"] # e.g. "127.0.0.1"
# Set if connecting using Unix sockets:
db_socket_dir = os.environ.get("DB_SOCKET_DIR", "/cloudsql")
instance_connection_name = os.environ["INSTANCE_CONNECTION_NAME"]
# e.g. "project-name:region:instance-name"
credentials = os.environ.get("GOOGLE_APPLICATION_CREDENTIALS", "")
key_uri = "gcp-kms://" + os.environ["GCP_KMS_URI"]
# e.g. "gcp-kms://projects/...path/to/key
# Tink uses the "gcp-kms://" prefix for paths to keys stored in Google
# Cloud KMS. For more info on creating a KMS key and getting its path, see
# https://cloud.google.com/kms/docs/quickstart
table_name = "votes"
team = "TABS"
email = "hello@example.com"
env_aead = init_tink_env_aead(key_uri, credentials)
db = init_db(
db_user,
db_pass,
db_name,
table_name,
instance_connection_name,
db_socket_dir,
db_host,
)
encrypt_and_insert_data(db, env_aead, table_name, team, email)
query_and_decrypt_data(db, env_aead, table_name)
def query_and_decrypt_data(
db: sqlalchemy.engine.base.Engine,
env_aead: tink.aead.KmsEnvelopeAead,
table_name: str,
) -> list[tuple[str]]:
"""
Retrieves data from the database and decrypts it using the KmsEnvelopeAead object.
"""
with db.connect() as conn:
# Execute the query and fetch all results
recent_votes = conn.execute(
f"SELECT team, time_cast, voter_email FROM {table_name} "
"ORDER BY time_cast DESC LIMIT 5"
).fetchall()
print("Team\tEmail\tTime Cast")
output = []
for row in recent_votes:
team = row[0]
# Use the envelope AEAD primitive to decrypt the email, using the team name as
# associated data. Encryption with associated data ensures authenticity
# (who the sender is) and integrity (the data has not been tampered with) of that
# data, but not its secrecy. (see RFC 5116 for more info)
email = env_aead.decrypt(row[2], team.encode()).decode()
time_cast = row[1]
# Print recent votes
print(f"{team}\t{email}\t{time_cast}")
output.append((team, email, time_cast))
return output