Skip to main content

rocketmq_auth/secret_provider/
encrypted_file.rs

1// Copyright 2023 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::fmt;
16#[cfg(unix)]
17use std::fs;
18#[cfg(unix)]
19use std::fs::File;
20#[cfg(unix)]
21use std::io::Read;
22#[cfg(unix)]
23use std::io::Write;
24use std::path::Path;
25use std::path::PathBuf;
26#[cfg(unix)]
27use std::sync::Mutex;
28
29#[cfg(any(unix, test))]
30use aes_gcm::aead::Aead;
31#[cfg(any(unix, test))]
32use aes_gcm::aead::Generate;
33#[cfg(any(unix, test))]
34use aes_gcm::aead::KeyInit;
35#[cfg(any(unix, test))]
36use aes_gcm::aead::Nonce;
37#[cfg(any(unix, test))]
38use aes_gcm::aead::Payload;
39#[cfg(any(unix, test))]
40use aes_gcm::Aes256Gcm;
41use rocketmq_security_api::SecretAccess;
42use rocketmq_security_api::SecretMaterial;
43use rocketmq_security_api::SecretName;
44use rocketmq_security_api::SecretPersistence;
45use rocketmq_security_api::SecretProvider;
46use rocketmq_security_api::SecretProviderCapabilities;
47use rocketmq_security_api::SecretProviderId;
48use rocketmq_security_api::SecretVersion;
49use rocketmq_security_api::SecretVersioning;
50#[cfg(unix)]
51use rocketmq_security_api::SecurityContractViolation;
52use rocketmq_security_api::SecurityOperation;
53use rocketmq_security_api::SecurityProviderError;
54use rocketmq_security_api::SecurityProviderFailure;
55use rocketmq_security_api::VersionedSecret;
56
57use crate::AuthFailureKind;
58use crate::AuthOperation;
59use crate::AuthServiceError;
60use crate::AuthServiceResult;
61
62#[cfg(any(unix, test))]
63const ENVELOPE_MAGIC: &[u8; 8] = b"RMQSEC01";
64#[cfg(any(unix, test))]
65const NONCE_LENGTH: usize = 12;
66#[cfg(any(unix, test))]
67const LENGTH_FIELD: usize = 8;
68#[cfg(any(unix, test))]
69const ENVELOPE_PREFIX_LENGTH: usize = ENVELOPE_MAGIC.len() + NONCE_LENGTH + LENGTH_FIELD;
70#[cfg(any(unix, test))]
71const MAX_SECRET_LENGTH: usize = 1024 * 1024;
72#[cfg(any(unix, test))]
73const AUTH_TAG_LENGTH: usize = 16;
74
75/// AES-256-GCM local development adapter with owner-only, immutable version files.
76pub struct EncryptedFileSecretProvider {
77    id: SecretProviderId,
78    #[cfg(unix)]
79    root: PathBuf,
80    #[cfg(unix)]
81    key: SecretMaterial,
82    #[cfg(unix)]
83    write_lock: Mutex<()>,
84}
85
86impl EncryptedFileSecretProvider {
87    /// Opens or creates an owner-only provider root. Windows remains fail-closed until an ACL
88    /// verifier can prove the same owner-only contract.
89    ///
90    /// # Errors
91    ///
92    /// Returns a redacted provider error for invalid keys, permissions, paths, or unsupported ACLs.
93    pub fn new(id: SecretProviderId, root: impl Into<PathBuf>, key: SecretMaterial) -> AuthServiceResult<Self> {
94        if key.len() != 32 {
95            return Err(AuthServiceError::new(
96                AuthOperation::LoadSecret,
97                AuthFailureKind::InvalidInput,
98            ));
99        }
100        let root = root.into();
101        prepare_root(&root).map_err(auth_provider_error)?;
102        Ok(Self {
103            id,
104            #[cfg(unix)]
105            root,
106            #[cfg(unix)]
107            key,
108            #[cfg(unix)]
109            write_lock: Mutex::new(()),
110        })
111    }
112
113    #[cfg(unix)]
114    fn read_version(
115        &self,
116        name: &SecretName,
117        version: SecretVersion,
118    ) -> Result<VersionedSecret, SecurityProviderError> {
119        let path = version_path(&self.root.join(name.as_str()), version);
120        let envelope = read_restricted_file(&path)?;
121        let plaintext = decrypt_envelope(&self.key, name, version, &envelope)?;
122        let material = SecretMaterial::new(plaintext).map_err(|source| {
123            SecurityProviderError::caused_by(
124                SecurityProviderFailure::InvalidData,
125                SecurityOperation::ReadSecret,
126                source,
127            )
128        })?;
129        Ok(VersionedSecret::new(material, Some(version)))
130    }
131
132    #[cfg(unix)]
133    fn write_version(
134        &self,
135        name: &SecretName,
136        material: &SecretMaterial,
137        expected_version: Option<SecretVersion>,
138    ) -> Result<SecretVersion, SecurityProviderError> {
139        if material.len() > MAX_SECRET_LENGTH {
140            return Err(provider_error(
141                SecurityProviderFailure::InvalidData,
142                SecurityOperation::WriteSecret,
143            ));
144        }
145        let directory = self.root.join(name.as_str());
146        prepare_secret_directory(&directory)?;
147        let current = latest_version(&directory)?;
148        let next = match (current, expected_version) {
149            (None, None) => SecretVersion::new(1),
150            (Some(current), Some(expected)) if current == expected => SecretVersion::new(
151                current
152                    .get()
153                    .checked_add(1)
154                    .ok_or_else(|| provider_error(SecurityProviderFailure::Conflict, SecurityOperation::WriteSecret))?,
155            ),
156            _ => {
157                return Err(provider_error(
158                    SecurityProviderFailure::Conflict,
159                    SecurityOperation::WriteSecret,
160                ));
161            }
162        };
163        let envelope = encrypt_envelope(&self.key, name, next, material.expose_secret())?;
164        publish_version(&directory, next, &envelope)?;
165        Ok(next)
166    }
167}
168
169impl fmt::Debug for EncryptedFileSecretProvider {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        f.debug_struct("EncryptedFileSecretProvider")
172            .field("id", &self.id)
173            .field("root", &"[REDACTED]")
174            .field("key", &"[REDACTED]")
175            .finish()
176    }
177}
178
179impl SecretProvider for EncryptedFileSecretProvider {
180    fn id(&self) -> &SecretProviderId {
181        &self.id
182    }
183
184    fn capabilities(&self) -> SecretProviderCapabilities {
185        SecretProviderCapabilities::new(
186            SecretAccess::ReadWrite,
187            SecretPersistence::EncryptedLocalStorage,
188            SecretVersioning::Optimistic,
189        )
190    }
191
192    fn read(&self, name: &SecretName) -> Result<VersionedSecret, SecurityProviderError> {
193        #[cfg(windows)]
194        {
195            let _ = name;
196            Err(provider_error(
197                SecurityProviderFailure::Unsupported,
198                SecurityOperation::ReadSecret,
199            ))
200        }
201        #[cfg(unix)]
202        {
203            let directory = self.root.join(name.as_str());
204            check_restricted_directory(&directory)?;
205            let version = latest_version(&directory)?
206                .ok_or_else(|| provider_error(SecurityProviderFailure::NotFound, SecurityOperation::ReadSecret))?;
207            self.read_version(name, version)
208        }
209    }
210
211    fn write(
212        &self,
213        name: &SecretName,
214        material: SecretMaterial,
215        expected_version: Option<SecretVersion>,
216    ) -> Result<SecretVersion, SecurityProviderError> {
217        #[cfg(windows)]
218        {
219            let _ = (name, material, expected_version);
220            Err(provider_error(
221                SecurityProviderFailure::Unsupported,
222                SecurityOperation::WriteSecret,
223            ))
224        }
225        #[cfg(unix)]
226        {
227            let _guard = self
228                .write_lock
229                .lock()
230                .map_err(|_| provider_error(SecurityProviderFailure::Unavailable, SecurityOperation::WriteSecret))?;
231            self.write_version(name, &material, expected_version)
232        }
233    }
234}
235
236#[cfg(windows)]
237fn prepare_root(_root: &Path) -> Result<(), SecurityProviderError> {
238    Err(provider_error(
239        SecurityProviderFailure::Unsupported,
240        SecurityOperation::InspectSecret,
241    ))
242}
243
244#[cfg(unix)]
245fn prepare_root(root: &Path) -> Result<(), SecurityProviderError> {
246    use std::os::unix::fs::DirBuilderExt;
247
248    if !root.exists() {
249        let mut builder = fs::DirBuilder::new();
250        builder.recursive(true).mode(0o700);
251        builder.create(root).map_err(|source| {
252            SecurityProviderError::caused_by(
253                SecurityProviderFailure::Unavailable,
254                SecurityOperation::InspectSecret,
255                source,
256            )
257        })?;
258    }
259    check_restricted_directory(root)
260}
261
262#[cfg(unix)]
263fn prepare_secret_directory(directory: &Path) -> Result<(), SecurityProviderError> {
264    use std::os::unix::fs::DirBuilderExt;
265
266    if !directory.exists() {
267        let mut builder = fs::DirBuilder::new();
268        builder.mode(0o700);
269        match builder.create(directory) {
270            Ok(()) => {}
271            Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
272            Err(source) => {
273                return Err(SecurityProviderError::caused_by(
274                    SecurityProviderFailure::Unavailable,
275                    SecurityOperation::WriteSecret,
276                    source,
277                ));
278            }
279        }
280    }
281    check_restricted_directory(directory)
282}
283
284#[cfg(unix)]
285fn check_restricted_directory(path: &Path) -> Result<(), SecurityProviderError> {
286    use std::os::unix::fs::PermissionsExt;
287
288    let metadata = fs::symlink_metadata(path).map_err(|source| {
289        SecurityProviderError::caused_by(
290            SecurityProviderFailure::NotFound,
291            SecurityOperation::InspectSecret,
292            source,
293        )
294    })?;
295    if metadata.file_type().is_symlink() || !metadata.is_dir() {
296        return Err(SecurityProviderError::contract(
297            SecurityOperation::InspectSecret,
298            SecurityContractViolation::SecretStoragePermissionsInsecure,
299        ));
300    }
301    if metadata.permissions().mode() & 0o077 != 0 {
302        return Err(SecurityProviderError::contract(
303            SecurityOperation::InspectSecret,
304            SecurityContractViolation::SecretStoragePermissionsInsecure,
305        ));
306    }
307    Ok(())
308}
309
310#[cfg(unix)]
311fn latest_version(directory: &Path) -> Result<Option<SecretVersion>, SecurityProviderError> {
312    let entries = fs::read_dir(directory).map_err(|source| {
313        SecurityProviderError::caused_by(
314            SecurityProviderFailure::NotFound,
315            SecurityOperation::InspectSecret,
316            source,
317        )
318    })?;
319    let mut latest = None;
320    for entry in entries {
321        let entry = entry.map_err(|source| {
322            SecurityProviderError::caused_by(
323                SecurityProviderFailure::Unavailable,
324                SecurityOperation::InspectSecret,
325                source,
326            )
327        })?;
328        let file_name = entry.file_name();
329        let Some(file_name) = file_name.to_str() else {
330            return Err(provider_error(
331                SecurityProviderFailure::InvalidData,
332                SecurityOperation::InspectSecret,
333            ));
334        };
335        let Some(raw_version) = file_name.strip_suffix(".secret") else {
336            if file_name.ends_with(".tmp") {
337                continue;
338            }
339            return Err(provider_error(
340                SecurityProviderFailure::InvalidData,
341                SecurityOperation::InspectSecret,
342            ));
343        };
344        if raw_version.len() != 20 || !raw_version.bytes().all(|byte| byte.is_ascii_digit()) {
345            return Err(provider_error(
346                SecurityProviderFailure::InvalidData,
347                SecurityOperation::InspectSecret,
348            ));
349        }
350        let version = raw_version.parse::<u64>().map_err(|source| {
351            SecurityProviderError::caused_by(
352                SecurityProviderFailure::InvalidData,
353                SecurityOperation::InspectSecret,
354                source,
355            )
356        })?;
357        if version == 0 {
358            return Err(provider_error(
359                SecurityProviderFailure::InvalidData,
360                SecurityOperation::InspectSecret,
361            ));
362        }
363        let metadata = entry.metadata().map_err(|source| {
364            SecurityProviderError::caused_by(
365                SecurityProviderFailure::Unavailable,
366                SecurityOperation::InspectSecret,
367                source,
368            )
369        })?;
370        if !metadata.is_file() {
371            return Err(provider_error(
372                SecurityProviderFailure::InvalidData,
373                SecurityOperation::InspectSecret,
374            ));
375        }
376        let version = SecretVersion::new(version);
377        latest = Some(latest.map_or(version, |current: SecretVersion| current.max(version)));
378    }
379    Ok(latest)
380}
381
382#[cfg(unix)]
383fn version_path(directory: &Path, version: SecretVersion) -> PathBuf {
384    directory.join(format!("{:020}.secret", version.get()))
385}
386
387#[cfg(unix)]
388fn read_restricted_file(path: &Path) -> Result<Vec<u8>, SecurityProviderError> {
389    use std::os::unix::fs::PermissionsExt;
390
391    let link_metadata = fs::symlink_metadata(path).map_err(|source| {
392        SecurityProviderError::caused_by(SecurityProviderFailure::NotFound, SecurityOperation::ReadSecret, source)
393    })?;
394    if link_metadata.file_type().is_symlink() {
395        return Err(SecurityProviderError::contract(
396            SecurityOperation::ReadSecret,
397            SecurityContractViolation::SecretStoragePermissionsInsecure,
398        ));
399    }
400    let mut file = File::open(path).map_err(|source| {
401        SecurityProviderError::caused_by(
402            SecurityProviderFailure::Unavailable,
403            SecurityOperation::ReadSecret,
404            source,
405        )
406    })?;
407    let metadata = file.metadata().map_err(|source| {
408        SecurityProviderError::caused_by(
409            SecurityProviderFailure::Unavailable,
410            SecurityOperation::ReadSecret,
411            source,
412        )
413    })?;
414    if !metadata.is_file() || metadata.permissions().mode() & 0o077 != 0 {
415        return Err(SecurityProviderError::contract(
416            SecurityOperation::ReadSecret,
417            SecurityContractViolation::SecretStoragePermissionsInsecure,
418        ));
419    }
420    let maximum = ENVELOPE_PREFIX_LENGTH + MAX_SECRET_LENGTH + AUTH_TAG_LENGTH;
421    if metadata.len() > maximum as u64 {
422        return Err(provider_error(
423            SecurityProviderFailure::InvalidData,
424            SecurityOperation::ReadSecret,
425        ));
426    }
427    let mut envelope = Vec::with_capacity(metadata.len() as usize);
428    (&mut file)
429        .take(maximum as u64 + 1)
430        .read_to_end(&mut envelope)
431        .map_err(|source| {
432            SecurityProviderError::caused_by(
433                SecurityProviderFailure::Unavailable,
434                SecurityOperation::ReadSecret,
435                source,
436            )
437        })?;
438    if envelope.len() > maximum {
439        return Err(provider_error(
440            SecurityProviderFailure::InvalidData,
441            SecurityOperation::ReadSecret,
442        ));
443    }
444    Ok(envelope)
445}
446
447#[cfg(any(unix, test))]
448fn associated_data(name: &SecretName, version: SecretVersion) -> Vec<u8> {
449    let mut data = Vec::with_capacity(32 + name.as_str().len());
450    data.extend_from_slice(b"rocketmq-secret-envelope-v1\0");
451    data.extend_from_slice(name.as_str().as_bytes());
452    data.push(0);
453    data.extend_from_slice(&version.get().to_be_bytes());
454    data
455}
456
457#[cfg(any(unix, test))]
458fn encrypt_envelope(
459    key: &SecretMaterial,
460    name: &SecretName,
461    version: SecretVersion,
462    plaintext: &[u8],
463) -> Result<Vec<u8>, SecurityProviderError> {
464    let cipher = Aes256Gcm::new_from_slice(key.expose_secret()).map_err(|source| {
465        SecurityProviderError::caused_by(
466            SecurityProviderFailure::InvalidData,
467            SecurityOperation::EncryptSecret,
468            source,
469        )
470    })?;
471    let nonce = Nonce::<Aes256Gcm>::generate();
472    let aad = associated_data(name, version);
473    let ciphertext = cipher
474        .encrypt(
475            &nonce,
476            Payload {
477                msg: plaintext,
478                aad: &aad,
479            },
480        )
481        .map_err(|source| {
482            SecurityProviderError::caused_by(
483                SecurityProviderFailure::OperationFailed,
484                SecurityOperation::EncryptSecret,
485                source,
486            )
487        })?;
488    let mut envelope = Vec::with_capacity(ENVELOPE_PREFIX_LENGTH + ciphertext.len());
489    envelope.extend_from_slice(ENVELOPE_MAGIC);
490    envelope.extend_from_slice(&nonce);
491    envelope.extend_from_slice(&(ciphertext.len() as u64).to_be_bytes());
492    envelope.extend_from_slice(&ciphertext);
493    Ok(envelope)
494}
495
496#[cfg(any(unix, test))]
497fn decrypt_envelope(
498    key: &SecretMaterial,
499    name: &SecretName,
500    version: SecretVersion,
501    envelope: &[u8],
502) -> Result<Vec<u8>, SecurityProviderError> {
503    if envelope.len() < ENVELOPE_PREFIX_LENGTH + AUTH_TAG_LENGTH || &envelope[..ENVELOPE_MAGIC.len()] != ENVELOPE_MAGIC
504    {
505        return Err(provider_error(
506            SecurityProviderFailure::InvalidData,
507            SecurityOperation::DecryptSecret,
508        ));
509    }
510    let nonce_start = ENVELOPE_MAGIC.len();
511    let nonce_end = nonce_start + NONCE_LENGTH;
512    let length_end = nonce_end + LENGTH_FIELD;
513    let nonce = Nonce::<Aes256Gcm>::try_from(&envelope[nonce_start..nonce_end])
514        .map_err(|_| provider_error(SecurityProviderFailure::InvalidData, SecurityOperation::DecryptSecret))?;
515    let encoded_length = u64::from_be_bytes(
516        envelope[nonce_end..length_end]
517            .try_into()
518            .map_err(|_| provider_error(SecurityProviderFailure::InvalidData, SecurityOperation::DecryptSecret))?,
519    );
520    let ciphertext = &envelope[length_end..];
521    if encoded_length != ciphertext.len() as u64 || ciphertext.len() > MAX_SECRET_LENGTH + AUTH_TAG_LENGTH {
522        return Err(provider_error(
523            SecurityProviderFailure::InvalidData,
524            SecurityOperation::DecryptSecret,
525        ));
526    }
527    let cipher = Aes256Gcm::new_from_slice(key.expose_secret()).map_err(|source| {
528        SecurityProviderError::caused_by(
529            SecurityProviderFailure::InvalidData,
530            SecurityOperation::DecryptSecret,
531            source,
532        )
533    })?;
534    let aad = associated_data(name, version);
535    cipher
536        .decrypt(
537            &nonce,
538            Payload {
539                msg: ciphertext,
540                aad: &aad,
541            },
542        )
543        .map_err(|source| {
544            SecurityProviderError::caused_by(
545                SecurityProviderFailure::InvalidData,
546                SecurityOperation::DecryptSecret,
547                source,
548            )
549        })
550}
551
552#[cfg(unix)]
553fn publish_version(directory: &Path, version: SecretVersion, envelope: &[u8]) -> Result<(), SecurityProviderError> {
554    use std::os::unix::fs::OpenOptionsExt;
555    use std::os::unix::fs::PermissionsExt;
556
557    let nonce_start = ENVELOPE_MAGIC.len();
558    let nonce_end = nonce_start + NONCE_LENGTH;
559    let temporary_path = directory.join(format!(
560        ".{:020}-{}.tmp",
561        version.get(),
562        hex::encode(&envelope[nonce_start..nonce_end])
563    ));
564    let final_path = version_path(directory, version);
565    let mut options = fs::OpenOptions::new();
566    options.write(true).create_new(true).mode(0o600);
567    let mut file = options.open(&temporary_path).map_err(|source| {
568        SecurityProviderError::caused_by(
569            SecurityProviderFailure::Unavailable,
570            SecurityOperation::WriteSecret,
571            source,
572        )
573    })?;
574    let write_result = (|| {
575        file.write_all(envelope).map_err(|source| {
576            SecurityProviderError::caused_by(
577                SecurityProviderFailure::Unavailable,
578                SecurityOperation::WriteSecret,
579                source,
580            )
581        })?;
582        file.sync_all().map_err(|source| {
583            SecurityProviderError::caused_by(
584                SecurityProviderFailure::Unavailable,
585                SecurityOperation::SynchronizeSecret,
586                source,
587            )
588        })?;
589        file.set_permissions(fs::Permissions::from_mode(0o400))
590            .map_err(|source| {
591                SecurityProviderError::caused_by(
592                    SecurityProviderFailure::Unavailable,
593                    SecurityOperation::WriteSecret,
594                    source,
595                )
596            })?;
597        fs::hard_link(&temporary_path, &final_path).map_err(|source| {
598            if source.kind() == std::io::ErrorKind::AlreadyExists {
599                SecurityProviderError::caused_by(
600                    SecurityProviderFailure::Conflict,
601                    SecurityOperation::WriteSecret,
602                    source,
603                )
604            } else {
605                SecurityProviderError::caused_by(
606                    SecurityProviderFailure::Unavailable,
607                    SecurityOperation::WriteSecret,
608                    source,
609                )
610            }
611        })?;
612        sync_directory(directory)
613    })();
614    drop(file);
615    let _ = fs::remove_file(&temporary_path);
616    write_result
617}
618
619#[cfg(unix)]
620fn sync_directory(directory: &Path) -> Result<(), SecurityProviderError> {
621    File::open(directory)
622        .and_then(|file| file.sync_all())
623        .map_err(|source| {
624            SecurityProviderError::caused_by(
625                SecurityProviderFailure::Unavailable,
626                SecurityOperation::SynchronizeSecret,
627                source,
628            )
629        })
630}
631
632fn provider_error(kind: SecurityProviderFailure, operation: SecurityOperation) -> SecurityProviderError {
633    SecurityProviderError::new(kind, operation)
634}
635
636fn auth_provider_error(source: SecurityProviderError) -> AuthServiceError {
637    AuthServiceError::provider(AuthOperation::LoadSecret, source)
638}
639
640#[cfg(test)]
641mod tests {
642    use std::error::Error;
643
644    use super::*;
645
646    fn material(bytes: &[u8]) -> SecretMaterial {
647        SecretMaterial::new(bytes.to_vec()).unwrap()
648    }
649
650    #[test]
651    fn envelope_authenticates_name_version_and_ciphertext() {
652        let key = material(&[9; 32]);
653        let name = SecretName::new("broker-admin-key").unwrap();
654        let version = SecretVersion::new(7);
655        let mut envelope = encrypt_envelope(&key, &name, version, b"not-written-in-clear").unwrap();
656        assert!(!envelope.windows(20).any(|window| window == b"not-written-in-clear"));
657        assert_eq!(
658            decrypt_envelope(&key, &name, version, &envelope).unwrap(),
659            b"not-written-in-clear"
660        );
661
662        envelope[ENVELOPE_PREFIX_LENGTH] ^= 1;
663        assert_eq!(
664            decrypt_envelope(&key, &name, version, &envelope).unwrap_err().kind(),
665            SecurityProviderFailure::InvalidData
666        );
667        let other_name = SecretName::new("other-key").unwrap();
668        assert_eq!(
669            decrypt_envelope(&key, &other_name, version, &envelope)
670                .unwrap_err()
671                .kind(),
672            SecurityProviderFailure::InvalidData
673        );
674    }
675
676    #[test]
677    fn cipher_failures_retain_typed_sources_without_rendering_them() {
678        let name = SecretName::new("broker-admin-key").unwrap();
679        let version = SecretVersion::new(7);
680        let invalid_key = material(&[9; 31]);
681        let key_error = encrypt_envelope(&invalid_key, &name, version, b"secret")
682            .expect_err("an invalid AES-256 key length must fail");
683        assert_eq!(key_error.kind(), SecurityProviderFailure::InvalidData);
684        assert_eq!(key_error.operation(), SecurityOperation::EncryptSecret);
685        assert!(Error::source(&key_error)
686            .and_then(|source| source.downcast_ref::<aes_gcm::aead::common::InvalidLength>())
687            .is_some());
688
689        let key = material(&[9; 32]);
690        let mut envelope = encrypt_envelope(&key, &name, version, b"secret").unwrap();
691        envelope[ENVELOPE_PREFIX_LENGTH] ^= 1;
692        let decrypt_error = decrypt_envelope(&key, &name, version, &envelope)
693            .expect_err("modified ciphertext must fail authentication");
694        assert_eq!(decrypt_error.kind(), SecurityProviderFailure::InvalidData);
695        assert_eq!(decrypt_error.operation(), SecurityOperation::DecryptSecret);
696        assert!(Error::source(&decrypt_error)
697            .and_then(|source| source.downcast_ref::<aes_gcm::Error>())
698            .is_some());
699
700        for error in [&key_error, &decrypt_error] {
701            assert!(!error.to_string().contains("InvalidLength"));
702            assert!(!format!("{error:?}").contains("aead::Error"));
703        }
704    }
705}