├── .bulldozer.yml ├── .changelog.yml ├── .circleci └── config.yml ├── .excavator.yml ├── .github └── dependabot.yml ├── .gitignore ├── .palantir └── autorelease.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── changelog └── 1.0.0 │ └── pr-30.v2.yml └── src ├── deserializer.rs └── lib.rs /.bulldozer.yml: -------------------------------------------------------------------------------- 1 | # Excavator auto-updates this file. Please contribute improvements to the central template. 2 | 3 | version: 1 4 | merge: 5 | trigger: 6 | labels: ["merge when ready"] 7 | ignore: 8 | labels: ["do not merge"] 9 | method: squash 10 | options: 11 | squash: 12 | body: pull_request_body 13 | message_delimiter: ==COMMIT_MSG== 14 | delete_after_merge: true 15 | update: 16 | trigger: 17 | labels: ["update me"] 18 | -------------------------------------------------------------------------------- /.changelog.yml: -------------------------------------------------------------------------------- 1 | # Excavator auto-updates this file. Please contribute improvements to the central template. 2 | 3 | # This file is intentionally empty. The file's existence enables changelog-app and is empty to use the default configuration. 4 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | restore_registry: &RESTORE_REGISTRY 2 | restore_cache: 3 | key: registry 4 | save_registry: &SAVE_REGISTRY 5 | save_cache: 6 | key: registry-{{ .BuildNum }} 7 | paths: 8 | - /usr/local/cargo/registry/index 9 | deps_key: &DEPS_KEY 10 | key: deps-{{ checksum "~/rust-version" }}-{{ checksum "Cargo.lock" }} 11 | restore_deps: &RESTORE_DEPS 12 | restore_cache: 13 | <<: *DEPS_KEY 14 | save_deps: &SAVE_DEPS 15 | save_cache: 16 | <<: *DEPS_KEY 17 | paths: 18 | - target 19 | - /usr/local/cargo/registry/cache 20 | 21 | version: 2 22 | jobs: 23 | build: 24 | working_directory: ~/build 25 | docker: 26 | - image: rust:1.63.0 27 | environment: 28 | RUSTFLAGS: -D warnings 29 | steps: 30 | - checkout 31 | - run: rustup component add rustfmt clippy 32 | - *RESTORE_REGISTRY 33 | - run: cargo generate-lockfile 34 | - *SAVE_REGISTRY 35 | - run: rustc --version > ~/rust-version 36 | - *RESTORE_DEPS 37 | - run: cargo fmt -- --check 38 | - run: cargo clippy --all-targets 39 | - run: cargo test 40 | - *SAVE_DEPS 41 | circle-all: 42 | docker: 43 | - image: busybox:1.34.1 44 | resource_class: small 45 | steps: 46 | - run: 47 | command: echo "All required jobs finished successfully" 48 | workflows: 49 | version: 2 50 | build: 51 | jobs: 52 | - build: 53 | requires: [] 54 | filters: 55 | tags: 56 | only: /.*/ 57 | - circle-all: 58 | requires: 59 | - build 60 | filters: 61 | tags: 62 | only: /.*/ 63 | -------------------------------------------------------------------------------- /.excavator.yml: -------------------------------------------------------------------------------- 1 | # Excavator auto-updates this file. Please contribute improvements to the central template. 2 | 3 | auto-label: 4 | names: 5 | versions-props/upgrade-all: [ "merge when ready" ] 6 | circleci/manage-circleci: [ "merge when ready" ] 7 | tags: 8 | donotmerge: [ "do not merge" ] 9 | roomba: [ "merge when ready" ] 10 | automerge: [ "merge when ready" ] 11 | autorelease: [ "autorelease" ] 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "13:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /.palantir/autorelease.yml: -------------------------------------------------------------------------------- 1 | version: 3 2 | options: 3 | repo_type: RUST 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "serde-encrypted-value" 3 | version = "1.0.0" 4 | authors = ["Steven Fackler "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "A Serde Deserializer wrapper which transparently decrypts encrypted values" 8 | repository = "https://github.com/palantir/serde-encrypted-value" 9 | readme = "README.md" 10 | 11 | [dependencies] 12 | aes-gcm = "0.10.1" 13 | base64 = "0.22" 14 | rand = "0.9.0" 15 | serde = { version = "1.0", features = ["derive"] } 16 | serde_json = "1.0" 17 | typenum = "1.15.0" 18 | 19 | [dev-dependencies] 20 | tempfile = "3.0" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # serde-encrypted-value 2 | 3 | [Documentation](https://docs.rs/serde-encrypted-value) 4 | 5 | Serde deserializer which transparently decrypts embedded encrypted strings. 6 | 7 | Application configurations typically consist mostly of non-sensitive information, with a few 8 | bits of information that is sensitive such as authentication secrets or cookie encryption keys. 9 | Storing those sensitive values in an encrypted form at rest can defend against leakage when, 10 | for example, copy/pasting the config as long as the encryption key is not additionally leaked. 11 | 12 | It is compatible with https://github.com/palantir/encrypted-config-value, though unlike that 13 | library, serde-encrypted-value does not support RSA. 14 | 15 | ## Usage 16 | 17 | Assume we have a `conf/encrypted-config-value.key` file that looks like: 18 | 19 | ``` 20 | AES:NwQZdNWsFmYMCNSQlfYPDJtFBgPzY8uZlFhMCLnxNQE= 21 | ``` 22 | 23 | And a `conf/config.json` file that looks like: 24 | 25 | ```json 26 | { 27 | "secret_value": "${enc:5BBfGvf90H6bApwfxUjNdoKRW1W+GZCbhBuBpzEogVBmQZyWFFxcKyf+UPV5FOhrw/wrVZyoL3npoDfYjPQV/zg0W/P9cVOw}", 28 | "non_secret_value": "hello, world!" 29 | } 30 | ``` 31 | 32 | ```rust 33 | extern crate serde; 34 | extern crate serde_json; 35 | extern crate serde_encrypted_value; 36 | 37 | #[macro_use] 38 | extern crate serde_derive; 39 | 40 | use serde::Deserialize; 41 | use std::io::Read; 42 | use std::fs::File; 43 | 44 | #[derive(Deserialize)] 45 | struct Config { 46 | secret_value: String, 47 | non_secret_value: String, 48 | } 49 | 50 | fn main() { 51 | let key = "conf/encrypted-config-value.key"; 52 | let key = serde_encrypted_value::Key::from_file(key) 53 | .unwrap(); 54 | 55 | let mut config = vec![]; 56 | File::open("conf/config.json") 57 | .unwrap() 58 | .read_to_end(&mut config) 59 | .unwrap(); 60 | 61 | let mut deserializer = serde_json::Deserializer::from_slice(&config); 62 | let deserializer = serde_encrypted_value::Deserializer::new( 63 | &mut deserializer, key.as_ref()); 64 | let config = Config::deserialize(deserializer).unwrap(); 65 | 66 | assert_eq!(config.secret_value, "L/TqOWz7E4z0SoeiTYBrqbqu"); 67 | assert_eq!(config.non_secret_value, "hello, world!"); 68 | } 69 | ``` 70 | 71 | ## License 72 | 73 | This repository is made available under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0). 74 | -------------------------------------------------------------------------------- /changelog/1.0.0/pr-30.v2.yml: -------------------------------------------------------------------------------- 1 | type: improvement 2 | improvement: 3 | description: Update rand requirement from 0.8.5 to 0.9.0 4 | links: 5 | - https://github.com/palantir/serde-encrypted-value/pull/30 6 | -------------------------------------------------------------------------------- /src/deserializer.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Palantir Technologies, Inc. 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 | 15 | use serde::de; 16 | use std::fmt; 17 | 18 | use crate::Key; 19 | 20 | /// A deserializer which automatically decrypts strings. 21 | /// 22 | /// Encrypted strings should be formatted like `${enc:}`. 23 | pub struct Deserializer<'a, D, T> { 24 | deserializer: D, 25 | key: Option<&'a Key>, 26 | } 27 | 28 | impl<'a, 'de, D, T> Deserializer<'a, D, T> 29 | where 30 | D: de::Deserializer<'de>, 31 | { 32 | /// Creates a new `Deserializer` wrapping another deserializer and decrypting string values. 33 | /// 34 | /// If `key` is `None`, deserialization will fail if an encrypted string is encountered. 35 | pub fn new(deserializer: D, key: Option<&'a Key>) -> Deserializer<'a, D, T> { 36 | Deserializer { deserializer, key } 37 | } 38 | } 39 | 40 | macro_rules! forward_deserialize { 41 | ($name:ident) => {forward_deserialize!($name, );}; 42 | ($name:ident, $($arg:tt => $ty:ty),*) => { 43 | fn $name(self, $($arg: $ty,)* visitor: V) -> Result 44 | where V: de::Visitor<'de> 45 | { 46 | let visitor = Visitor { 47 | visitor, 48 | key: self.key, 49 | }; 50 | self.deserializer.$name($($arg,)* visitor) 51 | } 52 | } 53 | } 54 | 55 | impl<'de, D, T> de::Deserializer<'de> for Deserializer<'_, D, T> 56 | where 57 | D: de::Deserializer<'de>, 58 | { 59 | type Error = D::Error; 60 | 61 | forward_deserialize!(deserialize_any); 62 | forward_deserialize!(deserialize_bool); 63 | forward_deserialize!(deserialize_u8); 64 | forward_deserialize!(deserialize_u16); 65 | forward_deserialize!(deserialize_u32); 66 | forward_deserialize!(deserialize_u64); 67 | forward_deserialize!(deserialize_i8); 68 | forward_deserialize!(deserialize_i16); 69 | forward_deserialize!(deserialize_i32); 70 | forward_deserialize!(deserialize_i64); 71 | forward_deserialize!(deserialize_f32); 72 | forward_deserialize!(deserialize_f64); 73 | forward_deserialize!(deserialize_char); 74 | forward_deserialize!(deserialize_str); 75 | forward_deserialize!(deserialize_string); 76 | forward_deserialize!(deserialize_unit); 77 | forward_deserialize!(deserialize_option); 78 | forward_deserialize!(deserialize_seq); 79 | forward_deserialize!(deserialize_bytes); 80 | forward_deserialize!(deserialize_byte_buf); 81 | forward_deserialize!(deserialize_map); 82 | forward_deserialize!(deserialize_unit_struct, name => &'static str); 83 | forward_deserialize!(deserialize_newtype_struct, name => &'static str); 84 | forward_deserialize!(deserialize_tuple_struct, name => &'static str, len => usize); 85 | forward_deserialize!(deserialize_struct, 86 | name => &'static str, 87 | fields => &'static [&'static str]); 88 | forward_deserialize!(deserialize_identifier); 89 | forward_deserialize!(deserialize_tuple, len => usize); 90 | forward_deserialize!(deserialize_enum, 91 | name => &'static str, 92 | variants => &'static [&'static str]); 93 | forward_deserialize!(deserialize_ignored_any); 94 | } 95 | 96 | struct Visitor<'a, V, T> { 97 | visitor: V, 98 | key: Option<&'a Key>, 99 | } 100 | 101 | impl Visitor<'_, V, T> { 102 | fn expand_str(&self, s: &str) -> Result, E> 103 | where 104 | E: de::Error, 105 | { 106 | if s.starts_with("${enc:") && s.ends_with('}') { 107 | match self.key { 108 | Some(key) => match key.decrypt(&s[6..s.len() - 1]) { 109 | Ok(s) => Ok(Some(s)), 110 | Err(e) => Err(E::custom(e.to_string())), 111 | }, 112 | None => Err(E::custom("missing encryption key")), 113 | } 114 | } else { 115 | Ok(None) 116 | } 117 | } 118 | } 119 | 120 | macro_rules! forward_visit { 121 | ($name:ident, $ty:ty) => { 122 | fn $name(self, v: $ty) -> Result 123 | where 124 | E: de::Error, 125 | { 126 | self.visitor.$name(v) 127 | } 128 | }; 129 | } 130 | 131 | impl<'de, V, T> de::Visitor<'de> for Visitor<'_, V, T> 132 | where 133 | V: de::Visitor<'de>, 134 | { 135 | type Value = V::Value; 136 | 137 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 138 | self.visitor.expecting(formatter) 139 | } 140 | 141 | forward_visit!(visit_bool, bool); 142 | forward_visit!(visit_i8, i8); 143 | forward_visit!(visit_i16, i16); 144 | forward_visit!(visit_i32, i32); 145 | forward_visit!(visit_i64, i64); 146 | forward_visit!(visit_u8, u8); 147 | forward_visit!(visit_u16, u16); 148 | forward_visit!(visit_u32, u32); 149 | forward_visit!(visit_u64, u64); 150 | forward_visit!(visit_f32, f32); 151 | forward_visit!(visit_f64, f64); 152 | forward_visit!(visit_char, char); 153 | forward_visit!(visit_bytes, &[u8]); 154 | forward_visit!(visit_byte_buf, Vec); 155 | 156 | fn visit_str(self, v: &str) -> Result 157 | where 158 | E: de::Error, 159 | { 160 | match self.expand_str(v)? { 161 | Some(s) => self.visitor.visit_string(s), 162 | None => self.visitor.visit_str(v), 163 | } 164 | } 165 | 166 | fn visit_string(self, v: String) -> Result 167 | where 168 | E: de::Error, 169 | { 170 | match self.expand_str(&v)? { 171 | Some(s) => self.visitor.visit_string(s), 172 | None => self.visitor.visit_string(v), 173 | } 174 | } 175 | 176 | fn visit_borrowed_str(self, v: &'de str) -> Result 177 | where 178 | E: de::Error, 179 | { 180 | match self.expand_str(v)? { 181 | Some(s) => self.visitor.visit_string(s), 182 | None => self.visitor.visit_borrowed_str(v), 183 | } 184 | } 185 | 186 | fn visit_unit(self) -> Result 187 | where 188 | E: de::Error, 189 | { 190 | self.visitor.visit_unit() 191 | } 192 | 193 | fn visit_none(self) -> Result 194 | where 195 | E: de::Error, 196 | { 197 | self.visitor.visit_none() 198 | } 199 | 200 | fn visit_some(self, deserializer: D) -> Result 201 | where 202 | D: de::Deserializer<'de>, 203 | { 204 | let deserializer = Deserializer::new(deserializer, self.key); 205 | self.visitor.visit_some(deserializer) 206 | } 207 | 208 | fn visit_newtype_struct(self, deserializer: D) -> Result 209 | where 210 | D: de::Deserializer<'de>, 211 | { 212 | let deserializer = Deserializer::new(deserializer, self.key); 213 | self.visitor.visit_newtype_struct(deserializer) 214 | } 215 | 216 | fn visit_seq(self, visitor: V2) -> Result 217 | where 218 | V2: de::SeqAccess<'de>, 219 | { 220 | let visitor = Visitor { 221 | visitor, 222 | key: self.key, 223 | }; 224 | self.visitor.visit_seq(visitor) 225 | } 226 | 227 | fn visit_map(self, visitor: V2) -> Result 228 | where 229 | V2: de::MapAccess<'de>, 230 | { 231 | let visitor = Visitor { 232 | visitor, 233 | key: self.key, 234 | }; 235 | self.visitor.visit_map(visitor) 236 | } 237 | 238 | fn visit_enum(self, visitor: V2) -> Result 239 | where 240 | V2: de::EnumAccess<'de>, 241 | { 242 | let visitor = Visitor { 243 | visitor, 244 | key: self.key, 245 | }; 246 | self.visitor.visit_enum(visitor) 247 | } 248 | } 249 | 250 | impl<'de, V, T> de::SeqAccess<'de> for Visitor<'_, V, T> 251 | where 252 | V: de::SeqAccess<'de>, 253 | { 254 | type Error = V::Error; 255 | 256 | fn next_element_seed(&mut self, seed: S) -> Result, V::Error> 257 | where 258 | S: de::DeserializeSeed<'de>, 259 | { 260 | let seed = DeserializeSeed { 261 | seed, 262 | key: self.key, 263 | }; 264 | self.visitor.next_element_seed(seed) 265 | } 266 | 267 | fn size_hint(&self) -> Option { 268 | self.visitor.size_hint() 269 | } 270 | } 271 | 272 | impl<'de, V, T> de::MapAccess<'de> for Visitor<'_, V, T> 273 | where 274 | V: de::MapAccess<'de>, 275 | { 276 | type Error = V::Error; 277 | 278 | fn next_key_seed(&mut self, seed: S) -> Result, V::Error> 279 | where 280 | S: de::DeserializeSeed<'de>, 281 | { 282 | let seed = DeserializeSeed { 283 | seed, 284 | key: self.key, 285 | }; 286 | self.visitor.next_key_seed(seed) 287 | } 288 | 289 | fn next_value_seed(&mut self, seed: S) -> Result 290 | where 291 | S: de::DeserializeSeed<'de>, 292 | { 293 | let seed = DeserializeSeed { 294 | seed, 295 | key: self.key, 296 | }; 297 | self.visitor.next_value_seed(seed) 298 | } 299 | 300 | #[allow(clippy::type_complexity)] 301 | fn next_entry_seed( 302 | &mut self, 303 | kseed: K, 304 | vseed: V2, 305 | ) -> Result, V::Error> 306 | where 307 | K: de::DeserializeSeed<'de>, 308 | V2: de::DeserializeSeed<'de>, 309 | { 310 | let kseed = DeserializeSeed { 311 | seed: kseed, 312 | key: self.key, 313 | }; 314 | let vseed = DeserializeSeed { 315 | seed: vseed, 316 | key: self.key, 317 | }; 318 | self.visitor.next_entry_seed(kseed, vseed) 319 | } 320 | 321 | fn size_hint(&self) -> Option { 322 | self.visitor.size_hint() 323 | } 324 | } 325 | 326 | impl<'a, 'de, V, T> de::EnumAccess<'de> for Visitor<'a, V, T> 327 | where 328 | V: de::EnumAccess<'de>, 329 | { 330 | type Error = V::Error; 331 | type Variant = Visitor<'a, V::Variant, T>; 332 | 333 | #[allow(clippy::type_complexity)] 334 | fn variant_seed(self, seed: S) -> Result<(S::Value, Visitor<'a, V::Variant, T>), V::Error> 335 | where 336 | S: de::DeserializeSeed<'de>, 337 | { 338 | let seed = DeserializeSeed { 339 | seed, 340 | key: self.key, 341 | }; 342 | match self.visitor.variant_seed(seed) { 343 | Ok((value, variant)) => { 344 | let variant = Visitor { 345 | visitor: variant, 346 | key: self.key, 347 | }; 348 | Ok((value, variant)) 349 | } 350 | Err(e) => Err(e), 351 | } 352 | } 353 | } 354 | 355 | impl<'de, V, T> de::VariantAccess<'de> for Visitor<'_, V, T> 356 | where 357 | V: de::VariantAccess<'de>, 358 | { 359 | type Error = V::Error; 360 | 361 | fn unit_variant(self) -> Result<(), V::Error> { 362 | self.visitor.unit_variant() 363 | } 364 | 365 | fn newtype_variant_seed(self, seed: S) -> Result 366 | where 367 | S: de::DeserializeSeed<'de>, 368 | { 369 | let seed = DeserializeSeed { 370 | seed, 371 | key: self.key, 372 | }; 373 | self.visitor.newtype_variant_seed(seed) 374 | } 375 | 376 | fn tuple_variant(self, len: usize, visitor: V2) -> Result 377 | where 378 | V2: de::Visitor<'de>, 379 | { 380 | let visitor = Visitor { 381 | visitor, 382 | key: self.key, 383 | }; 384 | self.visitor.tuple_variant(len, visitor) 385 | } 386 | 387 | fn struct_variant( 388 | self, 389 | fields: &'static [&'static str], 390 | visitor: V2, 391 | ) -> Result 392 | where 393 | V2: de::Visitor<'de>, 394 | { 395 | let visitor = Visitor { 396 | visitor, 397 | key: self.key, 398 | }; 399 | self.visitor.struct_variant(fields, visitor) 400 | } 401 | } 402 | 403 | struct DeserializeSeed<'a, S, T> { 404 | seed: S, 405 | key: Option<&'a Key>, 406 | } 407 | 408 | impl<'de, S, T> de::DeserializeSeed<'de> for DeserializeSeed<'_, S, T> 409 | where 410 | S: de::DeserializeSeed<'de>, 411 | { 412 | type Value = S::Value; 413 | 414 | fn deserialize(self, deserializer: D) -> Result 415 | where 416 | D: de::Deserializer<'de>, 417 | { 418 | let deserializer = Deserializer::new(deserializer, self.key); 419 | self.seed.deserialize(deserializer) 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2017 Palantir Technologies, Inc. 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 | 15 | //! A Serde deserializer which transparently decrypts embedded encrypted strings. 16 | //! 17 | //! Application configurations typically consist mostly of non-sensitive information, with a few 18 | //! bits of information that is sensitive such as authentication secrets or cookie encryption keys. 19 | //! Storing those sensitive values in an encrypted form at rest can defend against leakage when, 20 | //! for example, copy/pasting the config as long as the encryption key is not additionally leaked. 21 | //! 22 | //! # Usage 23 | //! 24 | //! Assume we have a `conf/encrypted-config-value.key` file that looks like: 25 | //! 26 | //! ```not_rust 27 | //! AES:NwQZdNWsFmYMCNSQlfYPDJtFBgPzY8uZlFhMCLnxNQE= 28 | //! ``` 29 | //! 30 | //! And a `conf/config.json` file that looks like: 31 | //! 32 | //! ```json 33 | //! { 34 | //! "secret_value": "${enc:5BBfGvf90H6bApwfxUjNdoKRW1W+GZCbhBuBpzEogVBmQZyWFFxcKyf+UPV5FOhrw/wrVZyoL3npoDfYjPQV/zg0W/P9cVOw}", 35 | //! "non_secret_value": "hello, world!" 36 | //! } 37 | //! ``` 38 | //! 39 | //! ```no_run 40 | //! use serde::Deserialize; 41 | //! use std::fs; 42 | //! 43 | //! #[derive(Deserialize)] 44 | //! struct Config { 45 | //! secret_value: String, 46 | //! non_secret_value: String, 47 | //! } 48 | //! 49 | //! let key = "conf/encrypted-config-value.key"; 50 | //! let key = serde_encrypted_value::Key::from_file(key) 51 | //! .unwrap(); 52 | //! 53 | //! let config = fs::read("conf/config.json").unwrap(); 54 | //! 55 | //! let mut deserializer = serde_json::Deserializer::from_slice(&config); 56 | //! let deserializer = serde_encrypted_value::Deserializer::new( 57 | //! &mut deserializer, key.as_ref()); 58 | //! let config = Config::deserialize(deserializer).unwrap(); 59 | //! 60 | //! assert_eq!(config.secret_value, "L/TqOWz7E4z0SoeiTYBrqbqu"); 61 | //! assert_eq!(config.non_secret_value, "hello, world!"); 62 | //! ``` 63 | #![warn(missing_docs, clippy::all)] 64 | 65 | pub use crate::deserializer::Deserializer; 66 | use aes_gcm::aes::Aes256; 67 | use aes_gcm::{AeadInPlace, Aes256Gcm, KeyInit, Nonce}; 68 | use aes_gcm::{AesGcm, Tag}; 69 | use base64::display::Base64Display; 70 | use base64::engine::general_purpose::STANDARD; 71 | use base64::Engine; 72 | use rand::{CryptoRng, Rng}; 73 | use serde::{Deserialize, Serialize}; 74 | use std::error; 75 | use std::fmt; 76 | use std::fs; 77 | use std::io; 78 | use std::path::Path; 79 | use std::result; 80 | use std::str::FromStr; 81 | use std::string::FromUtf8Error; 82 | use typenum::U32; 83 | 84 | const KEY_PREFIX: &str = "AES:"; 85 | const KEY_LEN: usize = 32; 86 | const LEGACY_IV_LEN: usize = 32; 87 | const IV_LEN: usize = 12; 88 | const TAG_LEN: usize = 16; 89 | 90 | type LegacyAes256Gcm = AesGcm; 91 | 92 | mod deserializer; 93 | 94 | /// The reuslt type returned by this library. 95 | pub type Result = result::Result; 96 | 97 | #[derive(Debug)] 98 | enum ErrorCause { 99 | AesGcm(aes_gcm::Error), 100 | Io(io::Error), 101 | Base64(base64::DecodeError), 102 | Utf8(FromUtf8Error), 103 | BadPrefix, 104 | InvalidLength, 105 | KeyExhausted, 106 | } 107 | 108 | /// The error type returned by this library. 109 | #[derive(Debug)] 110 | pub struct Error(Box); 111 | 112 | impl fmt::Display for Error { 113 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 114 | match *self.0 { 115 | ErrorCause::AesGcm(ref e) => fmt::Display::fmt(e, fmt), 116 | ErrorCause::Io(ref e) => fmt::Display::fmt(e, fmt), 117 | ErrorCause::Base64(ref e) => fmt::Display::fmt(e, fmt), 118 | ErrorCause::Utf8(ref e) => fmt::Display::fmt(e, fmt), 119 | ErrorCause::BadPrefix => fmt.write_str("invalid key prefix"), 120 | ErrorCause::InvalidLength => fmt.write_str("invalid encrypted value component length"), 121 | ErrorCause::KeyExhausted => fmt.write_str("key cannot encrypt more than 2^64 values"), 122 | } 123 | } 124 | } 125 | 126 | impl error::Error for Error {} 127 | 128 | #[derive(Serialize, Deserialize)] 129 | #[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] 130 | enum EncryptedValue { 131 | Aes { 132 | mode: AesMode, 133 | #[serde(with = "serde_base64")] 134 | iv: Vec, 135 | #[serde(with = "serde_base64")] 136 | ciphertext: Vec, 137 | #[serde(with = "serde_base64")] 138 | tag: Vec, 139 | }, 140 | } 141 | 142 | mod serde_base64 { 143 | use base64::engine::general_purpose::STANDARD; 144 | use base64::Engine; 145 | use serde::de; 146 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 147 | 148 | pub fn serialize(buf: &[u8], s: S) -> Result 149 | where 150 | S: Serializer, 151 | { 152 | STANDARD.encode(buf).serialize(s) 153 | } 154 | 155 | pub fn deserialize<'a, D>(d: D) -> Result, D::Error> 156 | where 157 | D: Deserializer<'a>, 158 | { 159 | let s = String::deserialize(d)?; 160 | STANDARD 161 | .decode(&s) 162 | .map_err(|_| de::Error::invalid_value(de::Unexpected::Str(&s), &"a base64 string")) 163 | } 164 | } 165 | 166 | // Just some insurance that rand::rng is in fact a CSPRNG 167 | fn secure_rng() -> impl Rng + CryptoRng { 168 | rand::rng() 169 | } 170 | 171 | #[derive(Serialize, Deserialize)] 172 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 173 | enum AesMode { 174 | Gcm, 175 | } 176 | 177 | /// A marker type indicating that a key can only decrypt values. 178 | pub struct ReadOnly(()); 179 | 180 | /// A marker type indicating that a key can both encrypt and decrypt values. 181 | pub struct ReadWrite { 182 | // GCM IVs must never be reused for the same key, and unpredictability makes 183 | // certain precomputation-based attacks more difficult: 184 | // https://tools.ietf.org/html/rfc5084#section-4. To account for this, we 185 | // use basically the same approach as TLSv1.3 - a random nonce generated per 186 | // key that's XORed with a counter incremented per message: 187 | // https://tools.ietf.org/html/rfc8446#section-5.3 188 | iv: [u8; IV_LEN], 189 | counter: u64, 190 | } 191 | 192 | /// A key used to encrypt or decrypt values. It represents both an algorithm and a key. 193 | /// 194 | /// Keys which have been deserialized from a string or file cannot encrypt new values; only freshly 195 | /// created keys have that ability. This is indicated by the type parameter `T`. 196 | /// 197 | /// The canonical serialized representation of a `Key` is a string consisting of an algorithm 198 | /// identifier, followed by a `:`, followed by the base64 encoded bytes of the key. The `Display` 199 | /// and `FromStr` implementations serialize and deserialize in this format. 200 | /// 201 | /// The only algorithm currently supported is AES 256 GCM, which uses the identifier `AES`. 202 | pub struct Key { 203 | key: [u8; KEY_LEN], 204 | mode: T, 205 | } 206 | 207 | impl Key { 208 | /// Creates a random AES key. 209 | pub fn random_aes() -> Result> { 210 | Ok(Key { 211 | key: secure_rng().random(), 212 | mode: ReadWrite { 213 | iv: secure_rng().random(), 214 | counter: 0, 215 | }, 216 | }) 217 | } 218 | 219 | /// Encrypts a string with this key. 220 | pub fn encrypt(&mut self, value: &str) -> Result { 221 | let counter = self.mode.counter; 222 | self.mode.counter = match self.mode.counter.checked_add(1) { 223 | Some(v) => v, 224 | None => return Err(Error(Box::new(ErrorCause::KeyExhausted))), 225 | }; 226 | 227 | let mut iv = Nonce::from(self.mode.iv); 228 | for (i, byte) in counter.to_le_bytes().iter().enumerate() { 229 | iv[i] ^= *byte; 230 | } 231 | 232 | let mut ciphertext = value.as_bytes().to_vec(); 233 | let tag = Aes256Gcm::new(&self.key.into()) 234 | .encrypt_in_place_detached(&iv, &[], &mut ciphertext) 235 | .map_err(|e| Error(Box::new(ErrorCause::AesGcm(e))))?; 236 | 237 | let value = EncryptedValue::Aes { 238 | mode: AesMode::Gcm, 239 | iv: iv.to_vec(), 240 | ciphertext, 241 | tag: tag.to_vec(), 242 | }; 243 | 244 | let value = serde_json::to_string(&value).unwrap(); 245 | Ok(STANDARD.encode(value.as_bytes())) 246 | } 247 | } 248 | 249 | impl Key { 250 | /// A convenience function which deserializes a `Key` from a file. 251 | /// 252 | /// If the file does not exist, `None` is returned. Otherwise, the contents of the file are 253 | /// parsed via `Key`'s `FromStr` implementation. 254 | pub fn from_file

(path: P) -> Result>> 255 | where 256 | P: AsRef, 257 | { 258 | let s = match fs::read_to_string(path) { 259 | Ok(s) => s, 260 | Err(ref e) if e.kind() == io::ErrorKind::NotFound => return Ok(None), 261 | Err(e) => return Err(Error(Box::new(ErrorCause::Io(e)))), 262 | }; 263 | s.parse().map(Some) 264 | } 265 | } 266 | 267 | impl Key { 268 | /// Decrypts a string with this key. 269 | pub fn decrypt(&self, value: &str) -> Result { 270 | let value = STANDARD 271 | .decode(value) 272 | .map_err(|e| Error(Box::new(ErrorCause::Base64(e))))?; 273 | 274 | let (iv, mut ct, tag) = match serde_json::from_slice(&value) { 275 | Ok(EncryptedValue::Aes { 276 | mode: AesMode::Gcm, 277 | iv, 278 | ciphertext, 279 | tag, 280 | }) => { 281 | if iv.len() != IV_LEN || tag.len() != TAG_LEN { 282 | return Err(Error(Box::new(ErrorCause::InvalidLength))); 283 | } 284 | 285 | let mut iv_arr = [0; IV_LEN]; 286 | iv_arr.copy_from_slice(&iv); 287 | 288 | let mut tag_arr = [0; TAG_LEN]; 289 | tag_arr.copy_from_slice(&tag); 290 | 291 | (Iv::Standard(iv_arr), ciphertext, tag_arr) 292 | } 293 | Err(_) => { 294 | if value.len() < LEGACY_IV_LEN + TAG_LEN { 295 | return Err(Error(Box::new(ErrorCause::InvalidLength))); 296 | } 297 | 298 | let mut iv = [0; LEGACY_IV_LEN]; 299 | iv.copy_from_slice(&value[..LEGACY_IV_LEN]); 300 | 301 | let ct = value[LEGACY_IV_LEN..value.len() - TAG_LEN].to_vec(); 302 | 303 | let mut tag = [0; TAG_LEN]; 304 | tag.copy_from_slice(&value[value.len() - TAG_LEN..]); 305 | 306 | (Iv::Legacy(iv), ct, tag) 307 | } 308 | }; 309 | 310 | let tag = Tag::from(tag); 311 | 312 | match iv { 313 | Iv::Legacy(iv) => { 314 | let iv = Nonce::from(iv); 315 | 316 | LegacyAes256Gcm::new(&self.key.into()) 317 | .decrypt_in_place_detached(&iv, &[], &mut ct, &tag) 318 | .map_err(|e| Error(Box::new(ErrorCause::AesGcm(e))))?; 319 | } 320 | Iv::Standard(iv) => { 321 | let iv = Nonce::from(iv); 322 | 323 | Aes256Gcm::new(&self.key.into()) 324 | .decrypt_in_place_detached(&iv, &[], &mut ct, &tag) 325 | .map_err(|e| Error(Box::new(ErrorCause::AesGcm(e))))?; 326 | } 327 | }; 328 | 329 | let pt = String::from_utf8(ct).map_err(|e| Error(Box::new(ErrorCause::Utf8(e))))?; 330 | 331 | Ok(pt) 332 | } 333 | } 334 | 335 | impl fmt::Display for Key { 336 | fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { 337 | write!(fmt, "AES:{}", Base64Display::new(&self.key, &STANDARD)) 338 | } 339 | } 340 | 341 | impl FromStr for Key { 342 | type Err = Error; 343 | 344 | fn from_str(s: &str) -> Result> { 345 | if !s.starts_with(KEY_PREFIX) { 346 | return Err(Error(Box::new(ErrorCause::BadPrefix))); 347 | } 348 | 349 | let key = STANDARD 350 | .decode(&s[KEY_PREFIX.len()..]) 351 | .map_err(|e| Error(Box::new(ErrorCause::Base64(e))))?; 352 | 353 | if key.len() != KEY_LEN { 354 | return Err(Error(Box::new(ErrorCause::InvalidLength))); 355 | } 356 | 357 | let mut key_arr = [0; KEY_LEN]; 358 | key_arr.copy_from_slice(&key); 359 | 360 | Ok(Key { 361 | key: key_arr, 362 | mode: ReadOnly(()), 363 | }) 364 | } 365 | } 366 | 367 | enum Iv { 368 | Legacy([u8; LEGACY_IV_LEN]), 369 | Standard([u8; IV_LEN]), 370 | } 371 | 372 | #[cfg(test)] 373 | mod test { 374 | use serde::Deserialize; 375 | use std::fs::File; 376 | use std::io::Write; 377 | use tempfile::tempdir; 378 | 379 | use super::*; 380 | 381 | const KEY: &str = "AES:NwQZdNWsFmYMCNSQlfYPDJtFBgPzY8uZlFhMCLnxNQE="; 382 | 383 | #[test] 384 | fn from_file_aes() { 385 | let dir = tempdir().unwrap(); 386 | let path = dir.path().join("encrypted-config-value.key"); 387 | let mut key = File::create(&path).unwrap(); 388 | key.write_all(KEY.as_bytes()).unwrap(); 389 | 390 | assert!(Key::from_file(&path).unwrap().is_some()); 391 | } 392 | 393 | #[test] 394 | fn from_file_empty() { 395 | let dir = tempdir().unwrap(); 396 | let path = dir.path().join("encrypted-config-value.key"); 397 | 398 | assert!(Key::from_file(path).unwrap().is_none()); 399 | } 400 | 401 | #[test] 402 | fn decrypt_legacy() { 403 | let ct = 404 | "5BBfGvf90H6bApwfxUjNdoKRW1W+GZCbhBuBpzEogVBmQZyWFFxcKyf+UPV5FOhrw/wrVZyoL3npoDfYj\ 405 | PQV/zg0W/P9cVOw"; 406 | let pt = "L/TqOWz7E4z0SoeiTYBrqbqu"; 407 | 408 | let key = KEY.parse::>().unwrap(); 409 | let actual = key.decrypt(ct).unwrap(); 410 | assert_eq!(actual, pt); 411 | } 412 | 413 | #[test] 414 | fn decrypt() { 415 | let ct = 416 | "eyJ0eXBlIjoiQUVTIiwibW9kZSI6IkdDTSIsIml2IjoiUCtRQXM5aHo4VFJVOUpNLyIsImNpcGhlcnRle\ 417 | HQiOiJmUGpDaDVuMkR0cklPSVNXSklLcVQzSUtRNUtONVI3LyIsInRhZyI6ImlJRFIzYUtER1UyK1Brej\ 418 | NPSEdSL0E9PSJ9"; 419 | let pt = "L/TqOWz7E4z0SoeiTYBrqbqu"; 420 | 421 | let key = KEY.parse::>().unwrap(); 422 | let actual = key.decrypt(ct).unwrap(); 423 | assert_eq!(actual, pt); 424 | } 425 | 426 | #[test] 427 | fn encrypt_decrypt() { 428 | let mut key = Key::random_aes().unwrap(); 429 | let pt = "L/TqOWz7E4z0SoeiTYBrqbqu"; 430 | let ct = key.encrypt(pt).unwrap(); 431 | let actual = key.decrypt(&ct).unwrap(); 432 | assert_eq!(pt, actual); 433 | } 434 | 435 | #[test] 436 | fn unique_ivs() { 437 | let mut key = Key::random_aes().unwrap(); 438 | let pt = "L/TqOWz7E4z0SoeiTYBrqbqu"; 439 | let ct1 = key.encrypt(pt).unwrap(); 440 | let ct2 = key.encrypt(pt).unwrap(); 441 | assert_ne!(ct1, ct2); 442 | } 443 | 444 | #[test] 445 | fn deserializer() { 446 | #[derive(Deserialize, PartialEq, Debug)] 447 | struct Config { 448 | sub: Subconfig, 449 | } 450 | 451 | #[derive(Deserialize, PartialEq, Debug)] 452 | struct Subconfig { 453 | encrypted: Vec, 454 | plaintext: String, 455 | } 456 | 457 | let config = r#" 458 | { 459 | "sub": { 460 | "encrypted": [ 461 | "${enc:5BBfGvf90H6bApwfxUjNdoKRW1W+GZCbhBuBpzEogVBmQZyWFFxcKyf+UPV5FOhrw/wrVZyoL3npoDfYjPQV/zg0W/P9cVOw}" 462 | ], 463 | "plaintext": "${foobar}" 464 | } 465 | } 466 | "#; 467 | 468 | let key = KEY.parse().unwrap(); 469 | let mut deserializer = serde_json::Deserializer::from_str(config); 470 | let deserializer = Deserializer::new(&mut deserializer, Some(&key)); 471 | 472 | let config = Config::deserialize(deserializer).unwrap(); 473 | 474 | let expected = Config { 475 | sub: Subconfig { 476 | encrypted: vec!["L/TqOWz7E4z0SoeiTYBrqbqu".to_string()], 477 | plaintext: "${foobar}".to_string(), 478 | }, 479 | }; 480 | 481 | assert_eq!(config, expected); 482 | } 483 | } 484 | --------------------------------------------------------------------------------