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 } ;
1919use 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) ]
11381186mod 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