Skip to content

Commit 5dc1c5a

Browse files
Supporting CPK (Customer Provided Keys) in Azure Blob Storage requests (#742)
* Supporting CPK (Customer Provided Keys) in Azure Blob Storage requests * Fix Azure CPK copy source authorization * Apply #[cfg(feature = reqwest)] to tests --------- Co-authored-by: Kevin Liu <kevin.jq.liu@gmail.com>
1 parent dd0afcb commit 5dc1c5a

5 files changed

Lines changed: 813 additions & 41 deletions

File tree

src/azure/builder.rs

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
// specific language governing permissions and limitations
1616
// under the License.
1717

18-
use crate::azure::client::{AzureClient, AzureConfig};
18+
use crate::azure::client::{AzureClient, AzureConfig, AzureEncryptionHeaders};
1919
use crate::azure::credential::{
2020
AzureAccessKey, AzureCliCredential, ClientSecretOAuthProvider, FabricTokenOAuthProvider,
2121
ImdsManagedIdentityProvider, WorkloadIdentityOAuthProvider,
@@ -85,6 +85,11 @@ enum Error {
8585
#[error("Missing component in SAS query pair")]
8686
MissingSasComponent {},
8787

88+
#[error("Invalid encryption key: {source}")]
89+
InvalidEncryptionKey {
90+
source: Box<dyn std::error::Error + Send + Sync + 'static>,
91+
},
92+
8893
#[error("Configuration key: '{}' is not known.", key)]
8994
UnknownConfigurationKey { key: String },
9095
}
@@ -178,6 +183,8 @@ pub struct MicrosoftAzureBuilder {
178183
fabric_session_token: Option<String>,
179184
/// Fabric cluster identifier
180185
fabric_cluster_identifier: Option<String>,
186+
/// Base64-encoded 256-bit customer-provided encryption key
187+
encryption_key: Option<String>,
181188
/// The [`HttpConnector`] to use
182189
http_connector: Option<Arc<dyn HttpConnector>>,
183190
}
@@ -380,6 +387,13 @@ pub enum AzureConfigKey {
380387
/// - `fabric_cluster_identifier`
381388
FabricClusterIdentifier,
382389

390+
/// Base64-encoded customer-provided encryption key
391+
///
392+
/// Supported keys:
393+
/// - `azure_storage_encryption_key`
394+
/// - `encryption_key`
395+
EncryptionKey,
396+
383397
/// Client options
384398
Client(ClientConfigKey),
385399
}
@@ -410,6 +424,7 @@ impl AsRef<str> for AzureConfigKey {
410424
Self::FabricWorkloadHost => "azure_fabric_workload_host",
411425
Self::FabricSessionToken => "azure_fabric_session_token",
412426
Self::FabricClusterIdentifier => "azure_fabric_cluster_identifier",
427+
Self::EncryptionKey => "azure_storage_encryption_key",
413428
Self::Client(key) => key.as_ref(),
414429
}
415430
}
@@ -466,6 +481,7 @@ impl FromStr for AzureConfigKey {
466481
"azure_fabric_cluster_identifier" | "fabric_cluster_identifier" => {
467482
Ok(Self::FabricClusterIdentifier)
468483
}
484+
"azure_storage_encryption_key" | "encryption_key" => Ok(Self::EncryptionKey),
469485
// Backwards compatibility
470486
"azure_allow_http" => Ok(Self::Client(ClientConfigKey::AllowHttp)),
471487
_ => match s.strip_prefix("azure_").unwrap_or(s).parse() {
@@ -594,6 +610,7 @@ impl MicrosoftAzureBuilder {
594610
AzureConfigKey::FabricClusterIdentifier => {
595611
self.fabric_cluster_identifier = Some(value.into())
596612
}
613+
AzureConfigKey::EncryptionKey => self.encryption_key = Some(value.into()),
597614
};
598615
self
599616
}
@@ -635,6 +652,7 @@ impl MicrosoftAzureBuilder {
635652
AzureConfigKey::FabricWorkloadHost => self.fabric_workload_host.clone(),
636653
AzureConfigKey::FabricSessionToken => self.fabric_session_token.clone(),
637654
AzureConfigKey::FabricClusterIdentifier => self.fabric_cluster_identifier.clone(),
655+
AzureConfigKey::EncryptionKey => self.encryption_key.clone(),
638656
}
639657
}
640658

@@ -938,6 +956,25 @@ impl MicrosoftAzureBuilder {
938956
self
939957
}
940958

959+
/// Set the customer-provided encryption key (CPK) used to encrypt blob content.
960+
///
961+
/// `key` must be a base64-encoded 256-bit AES key (the decoded value must be
962+
/// exactly 32 bytes). The same key must be supplied on every subsequent read,
963+
/// write, or copy of any blob created with it; if the key is lost or omitted
964+
/// the data is unrecoverable. CPK material is sent to Azure on every request,
965+
/// so the configured endpoint must use HTTPS.
966+
///
967+
/// Only a subset of Blob storage operations support CPK
968+
/// (see the [Azure documentation][cpk-ops]). When CPK is enabled, `copy`
969+
/// switches from the asynchronous `Copy Blob` API to `Put Blob From URL`,
970+
/// which is synchronous and limits the source blob to 5,000 MiB.
971+
///
972+
/// [cpk-ops]: https://learn.microsoft.com/en-us/azure/storage/blobs/encryption-customer-provided-keys#blob-storage-operations-supporting-customer-provided-keys
973+
pub fn with_encryption_key(mut self, key: impl Into<String>) -> Self {
974+
self.encryption_key = Some(key.into());
975+
self
976+
}
977+
941978
/// The [`HttpConnector`] to use
942979
///
943980
/// On non-WASM32 platforms uses [`reqwest`] by default, on WASM32 platforms must be provided
@@ -1079,6 +1116,16 @@ impl MicrosoftAzureBuilder {
10791116
(false, url, credential, account_name)
10801117
};
10811118

1119+
let encryption_headers =
1120+
AzureEncryptionHeaders::try_new(self.encryption_key).map_err(|source| {
1121+
Error::InvalidEncryptionKey {
1122+
source: match source {
1123+
crate::Error::Generic { source, .. } => source,
1124+
other => Box::new(other),
1125+
},
1126+
}
1127+
})?;
1128+
10821129
let config = AzureConfig {
10831130
account,
10841131
is_emulator,
@@ -1089,6 +1136,7 @@ impl MicrosoftAzureBuilder {
10891136
client_options: self.client_options,
10901137
service: storage_url,
10911138
credentials: auth,
1139+
encryption_headers,
10921140
};
10931141

10941142
let http_client = http.connect(&config.client_options)?;
@@ -1137,6 +1185,8 @@ pub fn split_sas(sas: &str) -> Result<Vec<(String, String)>> {
11371185
#[cfg(test)]
11381186
mod tests {
11391187
use super::*;
1188+
use base64::Engine;
1189+
use base64::prelude::BASE64_STANDARD;
11401190
use std::collections::HashMap;
11411191

11421192
#[test]
@@ -1357,6 +1407,59 @@ mod tests {
13571407
}
13581408
}
13591409

1410+
#[test]
1411+
fn azure_encryption_key_roundtrip() {
1412+
let key = BASE64_STANDARD.encode([7_u8; 32]);
1413+
let builder = MicrosoftAzureBuilder::new().with_encryption_key(&key);
1414+
1415+
assert_eq!(
1416+
builder
1417+
.get_config_value(&AzureConfigKey::EncryptionKey)
1418+
.as_deref(),
1419+
Some(key.as_str())
1420+
);
1421+
}
1422+
1423+
#[test]
1424+
fn azure_encryption_key_rejects_malformed_base64() {
1425+
let err = MicrosoftAzureBuilder::new()
1426+
.with_account("account")
1427+
.with_container_name("container")
1428+
.with_access_key(EMULATOR_ACCOUNT_KEY)
1429+
.with_encryption_key("not-base64!!!")
1430+
.build()
1431+
.unwrap_err();
1432+
1433+
let msg = err.to_string();
1434+
assert!(msg.contains("Invalid encryption key") || msg.contains("Invalid byte"));
1435+
}
1436+
1437+
#[test]
1438+
fn azure_encryption_key_rejects_short_decoded_key() {
1439+
let err = MicrosoftAzureBuilder::new()
1440+
.with_account("account")
1441+
.with_container_name("container")
1442+
.with_access_key(EMULATOR_ACCOUNT_KEY)
1443+
.with_encryption_key(BASE64_STANDARD.encode([7_u8; 31]))
1444+
.build()
1445+
.unwrap_err();
1446+
1447+
assert!(err.to_string().contains("must decode to 32 bytes, got 31"));
1448+
}
1449+
1450+
#[test]
1451+
fn azure_encryption_key_rejects_long_decoded_key() {
1452+
let err = MicrosoftAzureBuilder::new()
1453+
.with_account("account")
1454+
.with_container_name("container")
1455+
.with_access_key(EMULATOR_ACCOUNT_KEY)
1456+
.with_encryption_key(BASE64_STANDARD.encode([7_u8; 33]))
1457+
.build()
1458+
.unwrap_err();
1459+
1460+
assert!(err.to_string().contains("must decode to 32 bytes, got 33"));
1461+
}
1462+
13601463
#[test]
13611464
fn azure_test_config_from_map() {
13621465
let azure_client_id = "object_store:fake_access_key_id";

0 commit comments

Comments
 (0)