├── .circleci └── config.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── aes128gcm.rs ├── aesgcm.rs ├── common.rs ├── crypto ├── holder.rs ├── mod.rs └── openssl.rs ├── error.rs ├── legacy.rs └── lib.rs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | commands: 4 | test-setup: 5 | parameters: 6 | rust-version: 7 | type: string 8 | default: "stable" 9 | steps: 10 | - checkout 11 | - run: 12 | name: Rust setup 13 | command: | 14 | rustup install <> 15 | rustup default <> 16 | rustc --version 17 | rust-tests: 18 | parameters: 19 | rust-version: 20 | type: string 21 | default: "stable" 22 | steps: 23 | - test-setup: 24 | rust-version: <> 25 | - run: 26 | name: Test 27 | command: cargo test --all --verbose 28 | 29 | jobs: 30 | Check Rust formatting: 31 | docker: 32 | - image: circleci/rust:latest 33 | auth: 34 | username: $DOCKER_USER 35 | password: $DOCKER_PASS 36 | 37 | steps: 38 | - checkout 39 | - run: rustup component add rustfmt 40 | - run: rustfmt --version 41 | - run: cargo fmt -- --check 42 | Rust tests - stable: 43 | docker: 44 | - image: circleci/rust:latest 45 | auth: 46 | username: $DOCKER_USER 47 | password: $DOCKER_PASS 48 | 49 | steps: 50 | - rust-tests 51 | Rust tests - beta: 52 | docker: 53 | - image: circleci/rust:latest 54 | auth: 55 | username: $DOCKER_USER 56 | password: $DOCKER_PASS 57 | steps: 58 | - rust-tests: 59 | rust-version: "beta" 60 | 61 | workflows: 62 | version: 2 63 | check-formating: 64 | jobs: 65 | - Check Rust formatting 66 | run-tests: 67 | jobs: 68 | - Rust tests - stable 69 | - Rust tests - beta 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /target 3 | **/*.rs.bk 4 | Cargo.lock 5 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ece" 3 | version = "2.4.1" 4 | authors = [ 5 | "Firefox Sync Team ", 6 | "JR Conlin ", 7 | ] 8 | license = "MPL-2.0" 9 | edition = "2021" 10 | repository = "https://github.com/mozilla/rust-ece" 11 | description = "Encrypted Content-Encoding for HTTP Rust implementation." 12 | keywords = ["http-ece", "web-push"] 13 | 14 | [dependencies] 15 | byteorder = "1.3" 16 | thiserror = "2.0" 17 | base64 = "0.22" 18 | hex = "0.4" 19 | hkdf = { version = "0.12", optional = true } 20 | lazy_static = { version = "1.5", optional = true } 21 | once_cell = "1.21" 22 | openssl = { version = "0.10", optional = true } 23 | serde = { version = "1.0", features = ["derive"], optional = true } 24 | sha2 = { version = "0.10", optional = true } 25 | 26 | [features] 27 | default = ["backend-openssl", "serializable-keys"] 28 | serializable-keys = ["serde"] 29 | backend-openssl = ["openssl", "lazy_static", "hkdf", "sha2"] 30 | backend-test-helper = [] 31 | 32 | [package.metadata.release] 33 | no-dev-version = true 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-ece   [![Build Status]][circleci] [![Latest Version]][crates.io] 2 | 3 | [Build Status]: https://circleci.com/gh/mozilla/rust-ece.svg?style=svg 4 | [circleci]: https://circleci.com/gh/mozilla/rust-ece 5 | [Latest Version]: https://img.shields.io/crates/v/ece.svg 6 | [crates.io]: https://crates.io/crates/ece 7 | 8 | *This crate has not been security reviewed yet, use at your own risk 9 | ([tracking issue](https://github.com/mozilla/rust-ece/issues/18))*. 10 | 11 | The [ece](https://crates.io/crates/ece) crate is a Rust implementation of Message Encryption for Web Push 12 | ([RFC8291](https://tools.ietf.org/html/rfc8291)) and the HTTP Encrypted Content-Encoding scheme 13 | ([RFC8188](https://tools.ietf.org/html/rfc8188)) on which it is based. 14 | 15 | It provides low-level cryptographic "plumbing" and is destined to be used by higher-level Web Push libraries, both on 16 | the server and the client side. It is a port of the [ecec](https://github.com/web-push-libs/ecec) C library. 17 | 18 | [Full Documentation](https://docs.rs/ece/) 19 | 20 | ## Implemented schemes 21 | 22 | This crate implements both the published Web Push Encryption scheme, and a legacy scheme from earlier drafts 23 | that is still widely used in the wild: 24 | 25 | * `aes128gcm`: the scheme described in [RFC8291](https://tools.ietf.org/html/rfc8291) and 26 | [RFC8188](https://tools.ietf.org/html/rfc8188) 27 | * `aesgcm`: the draft scheme described in 28 | [draft-ietf-webpush-encryption-04](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04) and 29 | [draft-ietf-httpbis-encryption-encoding-03](https://tools.ietf.org/html/draft-ietf-httpbis-encryption-encoding-03_) 30 | 31 | It does not support, and we have no plans to ever support, the obsolete `aesgcm128` scheme 32 | from [earlier drafts](https://tools.ietf.org/html/draft-thomson-http-encryption-02). 33 | 34 | ## Usage 35 | 36 | To receive messages via WebPush, the receiver must generate an EC keypair and a symmetric authentication secret, 37 | then distribute the public key and authentication secret to the sender: 38 | 39 | ```rust 40 | let (keypair, auth_secret) = ece::generate_keypair_and_auth_secret()?; 41 | let pubkey = keypair.pub_as_raw(); 42 | // Base64-encode the `pubkey` and `auth_secret` bytes and distribute them to the sender. 43 | ``` 44 | 45 | The sender can encrypt a Web Push message to the receiver's public key: 46 | 47 | ```rust 48 | let ciphertext = ece::encrypt(&pubkey, &auth_secret, b"payload")?; 49 | ``` 50 | 51 | And the receiver can decrypt it using their private key: 52 | 53 | ```rust 54 | let plaintext = ece::decrypt(&keypair, &auth_secret, &ciphertext)?; 55 | ``` 56 | 57 | That's pretty much all there is to it! It's up to the higher-level library to manage distributing the encrypted payload, 58 | typically by arranging for it to be included in a HTTP response with `Content-Encoding: aes128gcm` header. 59 | 60 | ### Legacy `aesgcm` encryption 61 | 62 | The legacy `aesgcm` scheme is more complicated, because it communicates some encryption parameters in HTTP header fields 63 | rather than as part of the encrypted payload. When used for encryption, the sender must deal with `Encryption` and 64 | `Crypto-Key` headers in addition to the ciphertext: 65 | 66 | ```rust 67 | let encrypted_block = ece::legacy::encrypt_aesgcm(pubkey, auth_secret, b"payload")?; 68 | for (header, &value) in encrypted_block.headers().iter() { 69 | // Set header to corresponding value 70 | } 71 | // Send encrypted_block.body() as the body 72 | ``` 73 | 74 | When receiving an `aesgcm` message, the receiver needs to parse encryption parameters from the `Encryption` 75 | and `Crypto-Key` fields: 76 | 77 | ```rust 78 | // Parse `rs`, `salt` and `dh` from the `Encryption` and `Crypto-Key` headers. 79 | // You'll need to consult the spec for how to do this; we might add some helpers one day. 80 | let encrypted_block = ece::AesGcmEncryptedBlock::new(dh, rs, salt, ciphertext); 81 | let plaintext = ece::legacy::decrypt_aesgcm(keypair, auth_secret, encrypted_block)?; 82 | ``` 83 | 84 | ### Unimplemented Features 85 | 86 | * We do not implement streaming encryption or decryption, although the ECE scheme is designed to permit it. 87 | * We only support encrypting or decrypting across multiple records for `aes128gcm`; messages using the 88 | legacy `aesgcm` scheme must fit in a single record. 89 | * We do not support customizing the record size parameter during encryption, but do check it during decryption. 90 | * The default record size is 4096 bytes. 91 | * We do not support customizing the number of padding bytes added during encryption. 92 | * We currently select the padding length at random for each encryption, but this is an implementation detail and 93 | should not be relied on. 94 | 95 | These restrictions might be lifted in future, if it turns out that we need them. 96 | 97 | ## Cryptographic backends 98 | 99 | This crate is designed to use pluggable backend implementations of low-level crypto primitives. different crypto 100 | backends. At the moment only [openssl](https://github.com/sfackler/rust-openssl) is supported. 101 | 102 | ## Release process 103 | 104 | We use [`cargo-release`](https://crates.io/crates/cargo-release) to manage releases. To cut a new release, 105 | make sure you have it installed and then: 106 | 107 | 1. Start a new branch for the release: 108 | * `git checkout -b release-vX.Y.Z` 109 | * `git push -u origin release-vX.Y.Z` 110 | 2. Run `cargo release --dry-run -vv [major|minor|patch]` and check that the things 111 | it's proposing to do seem sensible. 112 | 3. Run `cargo release [major|minor|patch]` to prepare, commit, tag and publish the release. 113 | 4. Make a PR from your `release-vX.Y.Z` branch to request it be merged to the main branch. 114 | -------------------------------------------------------------------------------- /src/aes128gcm.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | //! Web Push encryption using the AES128GCM encoding scheme ([RFC8591](https://tools.ietf.org/html/rfc8291)). 6 | //! 7 | //! This module is meant for advanced use. For simple encryption/decryption, use the crate's top-level 8 | //! [`encrypt`](crate::encrypt) and [`decrypt`](crate::decrypt) functions. 9 | 10 | use crate::{ 11 | common::*, 12 | crypto::{self, LocalKeyPair, RemotePublicKey}, 13 | error::*, 14 | Cryptographer, 15 | }; 16 | use byteorder::{BigEndian, ByteOrder}; 17 | 18 | // Each record has a 16 byte authentication tag and 1 padding delimiter byte. 19 | // Thus, a record size of less than 18 could never store any plaintext. 20 | const ECE_AES128GCM_MIN_RS: u32 = 18; 21 | const ECE_AES128GCM_HEADER_LENGTH: usize = 21; 22 | pub(crate) const ECE_AES128GCM_PAD_SIZE: usize = 1; 23 | 24 | const ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX: &str = "WebPush: info\0"; 25 | const ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH: usize = 144; // 14 (prefix len) + 65 (pub key len) * 2; 26 | 27 | const ECE_WEBPUSH_IKM_LENGTH: usize = 32; 28 | const ECE_AES128GCM_KEY_INFO: &str = "Content-Encoding: aes128gcm\0"; 29 | const ECE_AES128GCM_NONCE_INFO: &str = "Content-Encoding: nonce\0"; 30 | 31 | /// Encrypts a Web Push message using the "aes128gcm" scheme, with an explicit sender key. 32 | /// 33 | /// It is the caller's responsibility to ensure that this function is used correctly, 34 | /// where "correctly" means important cryptographic details like: 35 | /// 36 | /// * use a new ephemeral local keypair for each encryption 37 | /// * use a randomly-generated salt 38 | /// 39 | /// In general-purpose AES128GM ECE, the "keyid" field in the header may be up to 255 octects 40 | /// and provides a string that allows the application to find the right key material in some 41 | /// application-defined way. We only currently support the specific scheme used by WebPush, where 42 | /// the "keyid" is an ephemeral ECDH public key and always has a fixed length. 43 | /// 44 | pub(crate) fn encrypt( 45 | local_prv_key: &dyn LocalKeyPair, 46 | remote_pub_key: &dyn RemotePublicKey, 47 | auth_secret: &[u8], 48 | plaintext: &[u8], 49 | mut params: WebPushParams, 50 | ) -> Result> { 51 | let cryptographer = crypto::holder::get_cryptographer(); 52 | 53 | if plaintext.is_empty() { 54 | return Err(Error::ZeroPlaintext); 55 | } 56 | 57 | let salt = params.take_or_generate_salt(cryptographer)?; 58 | let (key, nonce) = derive_key_and_nonce( 59 | cryptographer, 60 | EceMode::Encrypt, 61 | local_prv_key, 62 | remote_pub_key, 63 | auth_secret, 64 | &salt, 65 | )?; 66 | 67 | // Encode the ephemeral public key in the "kid" header field. 68 | let keyid = local_prv_key.pub_as_raw()?; 69 | if keyid.len() != ECE_WEBPUSH_PUBLIC_KEY_LENGTH { 70 | return Err(Error::InvalidKeyLength); 71 | } 72 | 73 | let header = Header { 74 | salt: &salt, 75 | rs: params.rs, 76 | keyid: &keyid, 77 | }; 78 | 79 | let records = split_into_records(plaintext, params.pad_length, params.rs as usize)?; 80 | 81 | let mut ciphertext = vec![0; header.encoded_size() + records.total_ciphertext_size()]; 82 | let mut offset = 0; 83 | 84 | offset += header.write_into(&mut ciphertext); 85 | for record in records { 86 | offset += record.encrypt_into(cryptographer, &key, &nonce, &mut ciphertext[offset..])?; 87 | } 88 | assert!(offset == ciphertext.len()); 89 | 90 | Ok(ciphertext) 91 | } 92 | 93 | /// Decrypts a Web Push message encrypted using the "aes128gcm" scheme. 94 | /// 95 | pub(crate) fn decrypt( 96 | local_prv_key: &dyn LocalKeyPair, 97 | auth_secret: &[u8], 98 | ciphertext: &[u8], 99 | ) -> Result> { 100 | let cryptographer = crypto::holder::get_cryptographer(); 101 | if ciphertext.is_empty() { 102 | return Err(Error::ZeroCiphertext); 103 | } 104 | 105 | // Buffer into which to write the output. 106 | // This will avoid any reallocations because plaintext will always be smaller than ciphertext. 107 | // We could calculate a tighter bound if memory usage is an issue in future. 108 | let mut output = Vec::::with_capacity(ciphertext.len()); 109 | 110 | let header = Header::read_from(ciphertext)?; 111 | if ciphertext.len() == header.encoded_size() { 112 | return Err(Error::ZeroCiphertext); 113 | } 114 | 115 | // The `keyid` field must contain the serialized ephemeral public key. 116 | if header.keyid.len() != ECE_WEBPUSH_PUBLIC_KEY_LENGTH { 117 | return Err(Error::InvalidKeyLength); 118 | } 119 | let remote_pub_key = cryptographer.import_public_key(header.keyid)?; 120 | 121 | let (key, nonce) = derive_key_and_nonce( 122 | cryptographer, 123 | EceMode::Decrypt, 124 | local_prv_key, 125 | &*remote_pub_key, 126 | auth_secret, 127 | header.salt, 128 | )?; 129 | 130 | // We'll re-use this buffer as scratch space for decrypting each record. 131 | // This is nice for memory usage, but actually the main motivation is to have the decryption 132 | // output a `PlaintextRecord` struct, which holds a borrowed slice of plaintext. 133 | // TODO: pre-allocate the final output buffer, and let `decrypt_from` write directly into it. 134 | let mut plaintext_buffer = vec![0u8; (header.rs as usize) - ECE_TAG_LENGTH]; 135 | 136 | let records = ciphertext[header.encoded_size()..].chunks(header.rs as usize); 137 | 138 | let mut seen_final_record = false; 139 | for (sequence_number, ciphertext) in records.enumerate() { 140 | // The record marked as final must actually be the final record. 141 | // We check this inline in the loop because the loop consumes ownership of `records`, 142 | // which means we can't do a separate "did we consume all the records?" check after loop termination. 143 | // There's probably a way, but I didn't find it. 144 | if seen_final_record { 145 | return Err(Error::DecryptPadding); 146 | } 147 | let record = PlaintextRecord::decrypt_from( 148 | cryptographer, 149 | &key, 150 | &nonce, 151 | sequence_number, 152 | ciphertext, 153 | plaintext_buffer.as_mut_slice(), 154 | )?; 155 | if record.is_final { 156 | seen_final_record = true; 157 | } 158 | output.extend(record.plaintext) 159 | } 160 | if !seen_final_record { 161 | return Err(Error::DecryptTruncated); 162 | } 163 | 164 | Ok(output) 165 | } 166 | 167 | /// Encapsulates header data for aes128gcm encryption scheme. 168 | /// 169 | /// The header is always written at the start of the encrypted data, like so: 170 | /// 171 | /// ```txt 172 | /// +-----------+--------+-----------+---------------+ 173 | /// | salt (16) | rs (4) | idlen (1) | keyid (idlen) | 174 | /// +-----------+--------+-----------+---------------+ 175 | /// ``` 176 | /// 177 | /// To avoid copying data when parsing, this struct stores references to its 178 | /// field, borrowed from the underlying data. 179 | /// 180 | pub(crate) struct Header<'a> { 181 | salt: &'a [u8], 182 | rs: u32, 183 | keyid: &'a [u8], 184 | } 185 | 186 | impl<'a> Header<'a> { 187 | /// Read a `Header` from the data at the start of the given input buffer. 188 | /// 189 | fn read_from(input: &'a [u8]) -> Result> { 190 | if input.len() < ECE_AES128GCM_HEADER_LENGTH { 191 | return Err(Error::HeaderTooShort); 192 | } 193 | 194 | let keyid_len = input[ECE_AES128GCM_HEADER_LENGTH - 1] as usize; 195 | if input.len() < ECE_AES128GCM_HEADER_LENGTH + keyid_len { 196 | return Err(Error::HeaderTooShort); 197 | } 198 | 199 | let salt = &input[0..ECE_SALT_LENGTH]; 200 | let rs = BigEndian::read_u32(&input[ECE_SALT_LENGTH..]); 201 | if rs < ECE_AES128GCM_MIN_RS { 202 | return Err(Error::InvalidRecordSize); 203 | } 204 | let keyid = &input[ECE_AES128GCM_HEADER_LENGTH..ECE_AES128GCM_HEADER_LENGTH + keyid_len]; 205 | 206 | Ok(Header { salt, rs, keyid }) 207 | } 208 | 209 | /// Write this `Header` at the start of the given output buffer. 210 | /// 211 | /// This assumes that the buffer has sufficient space for the data, and will 212 | /// panic (via Rust's runtime safety checks) if it does not. 213 | /// 214 | /// Returns the number of bytes written. 215 | /// 216 | pub fn write_into(&self, output: &mut [u8]) -> usize { 217 | output[0..ECE_SALT_LENGTH].copy_from_slice(self.salt); 218 | BigEndian::write_u32(&mut output[ECE_SALT_LENGTH..], self.rs); 219 | output[ECE_AES128GCM_HEADER_LENGTH - 1] = self.keyid.len() as u8; 220 | output[ECE_AES128GCM_HEADER_LENGTH..ECE_AES128GCM_HEADER_LENGTH + self.keyid.len()] 221 | .copy_from_slice(self.keyid); 222 | self.encoded_size() 223 | } 224 | 225 | /// Get the size occupied by this header when written to the encrypted data. 226 | /// 227 | pub fn encoded_size(&self) -> usize { 228 | ECE_AES128GCM_HEADER_LENGTH + self.keyid.len() 229 | } 230 | } 231 | 232 | /// Struct representing an individual plaintext record. 233 | /// 234 | /// The encryption process splits up the input plaintext to fixed-size records, 235 | /// each of which is encrypted independently. This struct encapsulates all the 236 | /// data about a particular record. This diagram from the RFC may help you to 237 | /// visualize how this data gets encrypted: 238 | /// 239 | /// ```txt 240 | /// +-----------+ content 241 | /// | data | any length up to rs-17 octets 242 | /// +-----------+ 243 | /// | 244 | /// v 245 | /// +-----------+-----+ add a delimiter octet (0x01 or 0x02) 246 | /// | data | pad | then 0x00-valued octets to rs-16 247 | /// +-----------+-----+ (or less on the last record) 248 | /// | 249 | /// v 250 | /// +--------------------+ encrypt with AEAD_AES_128_GCM; 251 | /// | ciphertext | final size is rs; 252 | /// +--------------------+ the last record can be smaller 253 | /// ``` 254 | /// 255 | /// To avoid copying data when chunking a plaintext into multiple records, this struct 256 | /// stores a reference to its portion of the plaintext, borrowed from the underlying data. 257 | /// 258 | struct PlaintextRecord<'a> { 259 | /// The plaintext, to go at the start of the record. 260 | plaintext: &'a [u8], 261 | /// The amount of padding to be added to the end of the record. 262 | /// Always >= 1 in practice, because the first byte of padding is a delimiter. 263 | padding: usize, 264 | /// The position of this record in the overall sequence of records for some data. 265 | sequence_number: usize, 266 | /// Whether this is the final record in the data. 267 | is_final: bool, 268 | } 269 | 270 | impl<'a> PlaintextRecord<'a> { 271 | /// Decrypt a single record from the given ciphertext, into its corresponding plaintext. 272 | /// 273 | /// The caller must provide a buffer with sufficient space to store the decrypted plaintext, 274 | /// and this method will panic (via Rust's runtime safety checks) if there is insufficient 275 | /// space available. 276 | /// 277 | pub(crate) fn decrypt_from( 278 | cryptographer: &dyn Cryptographer, 279 | key: &[u8], 280 | nonce: &[u8], 281 | sequence_number: usize, 282 | ciphertext: &[u8], 283 | plaintext_buffer: &'a mut [u8], 284 | ) -> Result { 285 | if ciphertext.len() <= ECE_TAG_LENGTH { 286 | return Err(Error::BlockTooShort); 287 | } 288 | let iv = generate_iv_for_record(nonce, sequence_number); 289 | // It would be nice if we could decrypt directly into `plaintext_buffer` here, 290 | // but that will require some refactoring in the crypto backend. 291 | let padded_plaintext = cryptographer.aes_gcm_128_decrypt(key, &iv, ciphertext)?; 292 | // Scan backwards for the first non-zero byte from the end of the data, which delimits the padding. 293 | let padding_delimiter_idx = padded_plaintext 294 | .iter() 295 | .rposition(|&b| b != 0u8) 296 | .ok_or(Error::DecryptPadding)?; 297 | // The padding delimiter tells is whether this is the final record. 298 | let is_final = match padded_plaintext[padding_delimiter_idx] { 299 | 1 => false, 300 | 2 => true, 301 | _ => return Err(Error::DecryptPadding), 302 | }; 303 | // Everything before the padding delimiter is the plaintext. 304 | plaintext_buffer[0..padding_delimiter_idx] 305 | .copy_from_slice(&padded_plaintext[0..padding_delimiter_idx]); 306 | // That's it! 307 | Ok(PlaintextRecord { 308 | plaintext: &plaintext_buffer[0..padding_delimiter_idx], 309 | padding: padded_plaintext.len() - padding_delimiter_idx, 310 | sequence_number, 311 | is_final, 312 | }) 313 | } 314 | 315 | /// Encrypt this record into the given output buffer. 316 | /// 317 | /// The caller must provide a buffer with sufficient space to store the encrypted data, 318 | /// and this method will panic (via Rust's runtime safety checks) if there is insufficient 319 | /// space available. 320 | /// 321 | /// Returns the number of bytes written. 322 | /// 323 | pub(crate) fn encrypt_into( 324 | &self, 325 | cryptographer: &dyn Cryptographer, 326 | key: &[u8], 327 | nonce: &[u8], 328 | output: &mut [u8], 329 | ) -> Result { 330 | // We're going to use the output buffer as scratch space for padding the plaintext. 331 | // Since the ciphertext is always longer than the plaintext, there will definitely 332 | // be enough space. 333 | let padded_plaintext_len = self.plaintext.len() + self.padding; 334 | // Plaintext goes at the start of the buffer. 335 | output[0..self.plaintext.len()].copy_from_slice(self.plaintext); 336 | // The first byte of padding is always the delimiter. 337 | assert!(self.padding >= 1); 338 | output[self.plaintext.len()] = if self.is_final { 2 } else { 1 }; 339 | // And the rest of the padding is all zeroes. 340 | output[self.plaintext.len() + 1..padded_plaintext_len].fill(0); 341 | // Now we can encrypt! 342 | let iv = generate_iv_for_record(nonce, self.sequence_number); 343 | let ciphertext = 344 | cryptographer.aes_gcm_128_encrypt(key, &iv, &output[0..padded_plaintext_len])?; 345 | output[0..ciphertext.len()].copy_from_slice(&ciphertext); 346 | Ok(ciphertext.len()) 347 | } 348 | } 349 | 350 | /// Iterator returning record-sized chunks of plaintext + padding. 351 | /// 352 | /// Given a plaintext, an amount of padding data to add, and a target encrypted record 353 | /// size, this function returns an iterator of `PlaintextRecord` structs such that: 354 | /// 355 | /// * The encrypted size of each plaintext chunk plus its padding will be equal 356 | /// to the given record size, except for the final record which may be shorter. 357 | /// 358 | /// * Each record has at least one padding byte; if necessary, additional padding 359 | /// bytes will be inserted beyond what was requested by the caller in order 360 | /// to meet this requirement. (This ensures each record has enough room for the 361 | /// padding delimiter byte). 362 | /// 363 | /// * The plaintext is distributed as evenly as possible between records. Records 364 | /// consisting entirely of padding will only be produced in degenerate cases such 365 | /// as where the caller requested far more padding than available plaintext, or 366 | /// where the requested total size falls just beyond a record boundary. 367 | /// 368 | fn split_into_records( 369 | plaintext: &[u8], 370 | pad_length: usize, 371 | rs: usize, 372 | ) -> Result> { 373 | // Adjust for encryption overhead. 374 | if rs < ECE_AES128GCM_MIN_RS as usize { 375 | return Err(Error::InvalidRecordSize); 376 | } 377 | let rs = rs - ECE_TAG_LENGTH; 378 | // Ensure we have enough padding to give at least one byte of it to each record. 379 | // This is the only reason why we might expand the padding beyond what was requested. 380 | let mut min_num_records = plaintext.len() / (rs - 1); 381 | if plaintext.len() % (rs - 1) != 0 { 382 | min_num_records += 1; 383 | } 384 | let pad_length = std::cmp::max(pad_length, min_num_records); 385 | // Knowing the total data size, determines the number of records. 386 | let total_size = plaintext.len() + pad_length; 387 | let mut num_records = total_size / rs; 388 | let size_of_final_record = total_size % rs; 389 | if size_of_final_record > 0 { 390 | num_records += 1; 391 | } 392 | assert!( 393 | num_records >= min_num_records, 394 | "record chunking error: we miscalculated the minimum number of records ({} < {})", 395 | num_records, 396 | min_num_records, 397 | ); 398 | // Evenly distribute the plaintext between that many records. 399 | // There may of course be some leftover that won't distribute evenly. 400 | let plaintext_per_record = plaintext.len() / num_records; 401 | let mut extra_plaintext = plaintext.len() % num_records; 402 | // If the final record is very small, we might not be able to fit 403 | // the recommended number of plaintext bytes, so redistribute them. 404 | // (Remember, the final block must contain at least one padding byte). 405 | if size_of_final_record > 0 && plaintext_per_record > size_of_final_record - 1 { 406 | extra_plaintext += plaintext_per_record - (size_of_final_record - 1) 407 | } 408 | // And now we can iterate! 409 | Ok(PlaintextRecordIterator { 410 | plaintext, 411 | pad_length, 412 | plaintext_per_record, 413 | extra_plaintext, 414 | rs, 415 | sequence_number: 0, 416 | num_records, 417 | total_size, 418 | }) 419 | } 420 | 421 | /// The underlying iterator implementation for `split_into_records`. 422 | /// 423 | struct PlaintextRecordIterator<'a> { 424 | /// The plaintext that remains to be split. 425 | plaintext: &'a [u8], 426 | /// The amount of padding that remains to be split. 427 | pad_length: usize, 428 | /// The amount of plaintext to put in each record. 429 | plaintext_per_record: usize, 430 | /// The amount of leftover plaintext that could not be distributed evenly. 431 | extra_plaintext: usize, 432 | /// The total number of bytes that will be produced by this iterator. 433 | total_size: usize, 434 | /// The target unencrypted record size. 435 | rs: usize, 436 | /// The total number of records that will be produced. 437 | num_records: usize, 438 | /// The sequence number of the next record to be produced. 439 | sequence_number: usize, 440 | } 441 | 442 | impl PlaintextRecordIterator<'_> { 443 | pub(crate) fn total_ciphertext_size(&self) -> usize { 444 | self.total_size + self.num_records * ECE_TAG_LENGTH 445 | } 446 | } 447 | 448 | impl<'a> Iterator for PlaintextRecordIterator<'a> { 449 | type Item = PlaintextRecord<'a>; 450 | fn next(&mut self) -> Option { 451 | let records_remaining = self.num_records - self.sequence_number; 452 | // We stop iterating when we've produced all records. 453 | if records_remaining == 0 { 454 | assert!( 455 | self.plaintext.is_empty(), 456 | "record chunking error: the plaintext was not fully consumed" 457 | ); 458 | assert!( 459 | self.extra_plaintext == 0, 460 | "record chunking error: the extra plaintext was not fully consumed" 461 | ); 462 | assert!( 463 | self.pad_length == 0, 464 | "record chunking error: the padding was not fully consumed" 465 | ); 466 | return None; 467 | } 468 | // Allocate a chunk of plaintext to this record. 469 | // We target `plaintext_per_record` bytes per record, but it's a little 470 | // more complicated than that... 471 | let mut plaintext_share = self.plaintext_per_record; 472 | if plaintext_share > self.plaintext.len() { 473 | // ...because the final record is allowed to be smaller. 474 | assert!( 475 | records_remaining == 1, 476 | "record chunking error: the plaintext was consumed too early" 477 | ); 478 | plaintext_share = self.plaintext.len(); 479 | } else { 480 | // ...because non-final records need to consume any extra plaintext. 481 | if self.extra_plaintext > 0 { 482 | // The extra plaintext must be distributed as evenly as possible 483 | // amongst all but the final record. 484 | let mut extra_share = self.extra_plaintext / (records_remaining - 1); 485 | if self.extra_plaintext % (records_remaining - 1) != 0 { 486 | extra_share += 1; 487 | } 488 | plaintext_share += extra_share; 489 | self.extra_plaintext -= extra_share; 490 | } 491 | } 492 | let plaintext = &self.plaintext[0..plaintext_share]; 493 | self.plaintext = &self.plaintext[plaintext_share..]; 494 | // Fill the rest of the record with padding. 495 | let padding_share = std::cmp::min(self.pad_length, self.rs - plaintext_share); 496 | self.pad_length -= padding_share; 497 | assert!( 498 | padding_share > 0, 499 | "record chunking error: the padding was consumed too early" 500 | ); 501 | // Check where we are in the iteration. 502 | let sequence_number = self.sequence_number; 503 | self.sequence_number += 1; 504 | let is_final = self.sequence_number == self.num_records; 505 | assert!( 506 | is_final || plaintext.len() + padding_share == self.rs, 507 | "record chunking error: non-final record is too short" 508 | ); 509 | // That's a record! 510 | Some(PlaintextRecord { 511 | plaintext, 512 | padding: padding_share, 513 | sequence_number, 514 | is_final, 515 | }) 516 | } 517 | } 518 | 519 | /// Derives the "aes128gcm" decryption key and nonce given the receiver private 520 | /// key, sender public key, authentication secret, and sender salt. 521 | fn derive_key_and_nonce( 522 | cryptographer: &dyn Cryptographer, 523 | ece_mode: EceMode, 524 | local_prv_key: &dyn LocalKeyPair, 525 | remote_pub_key: &dyn RemotePublicKey, 526 | auth_secret: &[u8], 527 | salt: &[u8], 528 | ) -> Result { 529 | if auth_secret.len() != ECE_WEBPUSH_AUTH_SECRET_LENGTH { 530 | return Err(Error::InvalidAuthSecret); 531 | } 532 | if salt.len() != ECE_SALT_LENGTH { 533 | return Err(Error::InvalidSalt); 534 | } 535 | 536 | let shared_secret = cryptographer.compute_ecdh_secret(remote_pub_key, local_prv_key)?; 537 | let raw_remote_pub_key = remote_pub_key.as_raw()?; 538 | let raw_local_pub_key = local_prv_key.pub_as_raw()?; 539 | 540 | // The "aes128gcm" scheme includes the sender and receiver public keys in 541 | // the info string when deriving the Web Push IKM. 542 | let ikm_info = match ece_mode { 543 | EceMode::Encrypt => generate_info(&raw_remote_pub_key, &raw_local_pub_key), 544 | EceMode::Decrypt => generate_info(&raw_local_pub_key, &raw_remote_pub_key), 545 | }?; 546 | let ikm = cryptographer.hkdf_sha256( 547 | auth_secret, 548 | &shared_secret, 549 | &ikm_info, 550 | ECE_WEBPUSH_IKM_LENGTH, 551 | )?; 552 | let key = cryptographer.hkdf_sha256( 553 | salt, 554 | &ikm, 555 | ECE_AES128GCM_KEY_INFO.as_bytes(), 556 | ECE_AES_KEY_LENGTH, 557 | )?; 558 | let nonce = cryptographer.hkdf_sha256( 559 | salt, 560 | &ikm, 561 | ECE_AES128GCM_NONCE_INFO.as_bytes(), 562 | ECE_NONCE_LENGTH, 563 | )?; 564 | Ok((key, nonce)) 565 | } 566 | 567 | // The "aes128gcm" IKM info string is "WebPush: info\0", followed by the 568 | // receiver and sender public keys. 569 | fn generate_info( 570 | raw_recv_pub_key: &[u8], 571 | raw_sender_pub_key: &[u8], 572 | ) -> Result<[u8; ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH]> { 573 | let mut info = [0u8; ECE_WEBPUSH_AES128GCM_IKM_INFO_LENGTH]; 574 | let prefix = ECE_WEBPUSH_AES128GCM_IKM_INFO_PREFIX.as_bytes(); 575 | let mut offset = prefix.len(); 576 | info[0..offset].copy_from_slice(prefix); 577 | info[offset..offset + ECE_WEBPUSH_PUBLIC_KEY_LENGTH].copy_from_slice(raw_recv_pub_key); 578 | offset += ECE_WEBPUSH_PUBLIC_KEY_LENGTH; 579 | info[offset..].copy_from_slice(raw_sender_pub_key); 580 | Ok(info) 581 | } 582 | 583 | #[cfg(test)] 584 | mod tests { 585 | use super::*; 586 | 587 | #[test] 588 | fn test_split_into_records_17_0_20() { 589 | let records = split_into_records(&[0u8; 17], 0, 20 + ECE_TAG_LENGTH) 590 | .unwrap() 591 | .collect::>(); 592 | // Should fit comfortably into a single record. 593 | assert_eq!(records.len(), 1); 594 | assert_eq!(records[0].plaintext.len(), 17); 595 | assert_eq!(records[0].padding, 1); 596 | assert_eq!(records[0].sequence_number, 0); 597 | assert!(records[0].is_final); 598 | } 599 | 600 | #[test] 601 | fn test_split_into_records_15_0_6() { 602 | let records = split_into_records(&[0u8; 15], 0, 6 + ECE_TAG_LENGTH) 603 | .unwrap() 604 | .collect::>(); 605 | // Should fit exactly across three records. 606 | assert_eq!(records.len(), 3); 607 | 608 | assert_eq!(records[0].plaintext.len(), 5); 609 | assert_eq!(records[0].padding, 1); 610 | assert_eq!(records[0].sequence_number, 0); 611 | assert!(!records[0].is_final); 612 | 613 | assert_eq!(records[1].plaintext.len(), 5); 614 | assert_eq!(records[1].padding, 1); 615 | assert_eq!(records[1].sequence_number, 1); 616 | assert!(!records[1].is_final); 617 | 618 | assert_eq!(records[2].plaintext.len(), 5); 619 | assert_eq!(records[2].padding, 1); 620 | assert_eq!(records[2].sequence_number, 2); 621 | assert!(records[2].is_final); 622 | } 623 | 624 | fn split_and_summarize(payload_len: usize, padding: usize, rs: usize) -> Vec<(usize, usize)> { 625 | split_into_records(&vec![0u8; payload_len], padding, rs + ECE_TAG_LENGTH) 626 | .unwrap() 627 | .map(|record| (record.plaintext.len(), record.padding)) 628 | .collect() 629 | } 630 | 631 | #[test] 632 | fn test_split_into_records_8_2_3() { 633 | // Should expand to 4 bytes of padding, then return 4 equal records 634 | // with two bytes of plaintext and one byte of padding. 635 | assert_eq!( 636 | split_and_summarize(8, 2, 3), 637 | vec![(2, 1), (2, 1), (2, 1), (2, 1)] 638 | ); 639 | } 640 | 641 | #[test] 642 | fn test_split_into_records_8_0_8() { 643 | // Should expand to 2 bytes of padding, 2 records. 644 | // The last record is only size 2, so can only fit 1 plaintext byte. 645 | assert_eq!(split_and_summarize(8, 0, 8), vec![(7, 1), (1, 1)]); 646 | } 647 | 648 | #[test] 649 | fn test_split_into_records_24_6_8() { 650 | // Total length of 30, 4 records. 651 | // Ideally we'd have 6 bytes of plaintext in each, but the final record 652 | // is only length 6 so it can't hold more than 5 bytes of plaintext. 653 | assert_eq!( 654 | split_and_summarize(24, 6, 8), 655 | vec![(7, 1), (6, 2), (6, 2), (5, 1)] 656 | ); 657 | } 658 | 659 | #[test] 660 | fn test_split_into_records_8_6_3() { 661 | // Total length 14, 4 records, the last only 2 bytes long. 662 | // But we can still spread the plaintext so that there's some in each record. 663 | assert_eq!( 664 | split_and_summarize(8, 6, 3), 665 | vec![(2, 1), (2, 1), (2, 1), (1, 2), (1, 1)] 666 | ); 667 | } 668 | 669 | #[test] 670 | fn test_split_into_records_3_25_8() { 671 | // Total length of 28, meaning 4 records. 672 | // One of the records will have to be only padding. 673 | assert_eq!( 674 | split_and_summarize(3, 25, 8), 675 | vec![(1, 7), (1, 7), (1, 7), (0, 4)] 676 | ); 677 | } 678 | 679 | #[test] 680 | fn test_split_into_records_3_35_8() { 681 | // Total length of 38, meaning 5 records. 682 | // Two of the records will have to be only padding. 683 | assert_eq!( 684 | split_and_summarize(3, 35, 8), 685 | vec![(1, 7), (1, 7), (1, 7), (0, 8), (0, 6)] 686 | ); 687 | } 688 | 689 | #[test] 690 | fn test_split_into_records_19_6_8() { 691 | // Total length of 25, 4 records with the final record being only a single byte. 692 | // It therefore can only be padding. 693 | assert_eq!( 694 | split_and_summarize(19, 6, 8), 695 | vec![(7, 1), (6, 2), (6, 2), (0, 1)] 696 | ); 697 | } 698 | } 699 | -------------------------------------------------------------------------------- /src/aesgcm.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | * 5 | * This supports the now obsolete HTTP-ECE Draft 02 "aesgcm" content 6 | * type. There are a number of providers that still use this format, 7 | * and there's no real mechanism to return the client supported crypto 8 | * versions. 9 | * 10 | * */ 11 | 12 | //! Web Push encryption structure for the legacy AESGCM encoding scheme 13 | //! ([Web Push Encryption Draft 4](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04)) 14 | //! 15 | //! This module is meant for advanced use. For simple encryption/decryption, use the top-level 16 | //! [`encrypt_aesgcm`](crate::legacy::encrypt_aesgcm) and [`decrypt_aesgcm`](crate::legacy::decrypt_aesgcm) 17 | //! functions. 18 | 19 | use crate::{ 20 | common::*, 21 | crypto::{self, Cryptographer, LocalKeyPair, RemotePublicKey}, 22 | error::*, 23 | }; 24 | use base64::Engine; 25 | use byteorder::{BigEndian, ByteOrder}; 26 | 27 | pub(crate) const ECE_AESGCM_PAD_SIZE: usize = 2; 28 | 29 | const ECE_WEBPUSH_AESGCM_KEYPAIR_LENGTH: usize = 134; // (2 + Raw Key Length) * 2 30 | const ECE_WEBPUSH_AESGCM_AUTHINFO: &str = "Content-Encoding: auth\0"; 31 | 32 | // a DER prefixed key is "\04" + ECE_WEBPUSH_RAW_KEY_LENGTH 33 | const ECE_WEBPUSH_RAW_KEY_LENGTH: usize = 65; 34 | const ECE_WEBPUSH_IKM_LENGTH: usize = 32; 35 | 36 | /// Struct representing the result of encrypting with the "aesgcm" scheme. 37 | /// 38 | /// Since the "aesgcm" scheme needs to represent some data in HTTP headers and 39 | /// other data in the encoded body, we need to represent it with a structure 40 | /// rather than just with raw bytes. 41 | /// 42 | pub struct AesGcmEncryptedBlock { 43 | pub(crate) dh: Vec, 44 | pub(crate) salt: Vec, 45 | pub(crate) rs: u32, 46 | pub(crate) ciphertext: Vec, 47 | } 48 | 49 | impl AesGcmEncryptedBlock { 50 | fn aesgcm_rs(rs: u32) -> u32 { 51 | if rs > u32::MAX - ECE_TAG_LENGTH as u32 { 52 | return 0; 53 | } 54 | rs + ECE_TAG_LENGTH as u32 55 | } 56 | 57 | pub fn new( 58 | dh: &[u8], 59 | salt: &[u8], 60 | rs: u32, 61 | ciphertext: Vec, 62 | ) -> Result { 63 | Ok(AesGcmEncryptedBlock { 64 | dh: dh.to_owned(), 65 | salt: salt.to_owned(), 66 | rs: Self::aesgcm_rs(rs), 67 | ciphertext, 68 | }) 69 | } 70 | 71 | /// Return the headers Hash. 72 | /// If you're using VAPID, provide the `p256ecdsa` public key that signed the Json Web Token 73 | /// so it can be included in the `Crypto-Key` field. 74 | /// 75 | /// Disclaimer : You will need to manually add the Authorization field for VAPID containing the JSON Web Token 76 | pub fn headers(&self, vapid_public_key: Option<&[u8]>) -> Vec<(&'static str, String)> { 77 | let mut result = Vec::new(); 78 | let mut rs = "".to_owned(); 79 | let dh = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&self.dh); 80 | let crypto_key = match vapid_public_key { 81 | Some(public_key) => format!( 82 | "dh={}; p256ecdsa={}", 83 | dh, 84 | base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(public_key) 85 | ), 86 | None => format!("dh={}", dh), 87 | }; 88 | result.push(("Crypto-Key", crypto_key)); 89 | if self.rs > 0 { 90 | rs = format!(";rs={}", self.rs); 91 | } 92 | result.push(( 93 | "Encryption", 94 | format!( 95 | "salt={}{}", 96 | base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&self.salt), 97 | rs 98 | ), 99 | )); 100 | result 101 | } 102 | 103 | /// Encode the body as a String. 104 | pub fn body(&self) -> String { 105 | base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&self.ciphertext) 106 | } 107 | } 108 | 109 | /// Encrypts a Web Push message using the "aesgcm" scheme, with an explicit sender key. 110 | /// 111 | /// It is the caller's responsibility to ensure that this function is used correctly, 112 | /// where "correctly" means important cryptographic details like: 113 | /// 114 | /// * use a new ephemeral local keypair for each encryption 115 | /// * use a randomly-generated salt 116 | /// 117 | pub(crate) fn encrypt( 118 | local_prv_key: &dyn LocalKeyPair, 119 | remote_pub_key: &dyn RemotePublicKey, 120 | auth_secret: &[u8], 121 | plaintext: &[u8], 122 | mut params: WebPushParams, 123 | ) -> Result { 124 | // Check parameters, including doing the random salt thing. 125 | // Probably could move into the WebPushParams struct? 126 | let cryptographer = crypto::holder::get_cryptographer(); 127 | 128 | if plaintext.is_empty() { 129 | return Err(Error::ZeroPlaintext); 130 | } 131 | 132 | let salt = params.take_or_generate_salt(cryptographer)?; 133 | let (key, nonce) = derive_key_and_nonce( 134 | cryptographer, 135 | EceMode::Encrypt, 136 | local_prv_key, 137 | remote_pub_key, 138 | auth_secret, 139 | &salt, 140 | )?; 141 | 142 | // Each record must contain at least some padding, for recording the padding size. 143 | let pad_length = std::cmp::max(params.pad_length, ECE_AESGCM_PAD_SIZE); 144 | 145 | // For this legacy scheme, we only support encrypting a single record. 146 | // The record size in this scheme is the size of the plaintext plus padding, 147 | // and the scheme requires that the final block be of a size less than `rs`. 148 | if plaintext.len() + pad_length >= params.rs as usize { 149 | return Err(Error::PlaintextTooLong); 150 | } 151 | 152 | // Pad out the plaintext. 153 | // The first two bytes of padding are big-endian padding length, 154 | // followed by the rest of the padding as zero bytes, 155 | // followed by the plaintext. 156 | let mut padded_plaintext = vec![0; pad_length + plaintext.len()]; 157 | BigEndian::write_u16( 158 | &mut padded_plaintext, 159 | (pad_length - ECE_AESGCM_PAD_SIZE) as u16, 160 | ); 161 | padded_plaintext[pad_length..].copy_from_slice(plaintext); 162 | 163 | // Now we can encrypt it. 164 | let iv = generate_iv_for_record(&nonce, 0); 165 | let cryptographer = crypto::holder::get_cryptographer(); 166 | let ciphertext = cryptographer.aes_gcm_128_encrypt(&key, &iv, &padded_plaintext)?; 167 | 168 | // Encapsulate the crypto parameters in headers to return to caller. 169 | let raw_local_pub_key = local_prv_key.pub_as_raw()?; 170 | Ok(AesGcmEncryptedBlock { 171 | salt, 172 | dh: raw_local_pub_key, 173 | rs: params.rs, 174 | ciphertext, 175 | }) 176 | } 177 | 178 | /// Decrypts a Web Push message encrypted using the "aesgcm" scheme. 179 | /// 180 | pub(crate) fn decrypt( 181 | local_prv_key: &dyn LocalKeyPair, 182 | auth_secret: &[u8], 183 | block: &AesGcmEncryptedBlock, 184 | ) -> Result> { 185 | let cryptographer = crypto::holder::get_cryptographer(); 186 | 187 | let sender_key = cryptographer.import_public_key(&block.dh)?; 188 | 189 | let (key, nonce) = derive_key_and_nonce( 190 | cryptographer, 191 | EceMode::Decrypt, 192 | local_prv_key, 193 | &*sender_key, 194 | auth_secret, 195 | block.salt.as_ref(), 196 | )?; 197 | 198 | // We only support receipt of a single record for this legacy scheme. 199 | // Recall that the final block must be strictly less than `rs` in size. 200 | if block.ciphertext.len() - ECE_TAG_LENGTH >= block.rs as usize { 201 | return Err(Error::MultipleRecordsNotSupported); 202 | } 203 | if block.ciphertext.len() <= ECE_TAG_LENGTH + ECE_AESGCM_PAD_SIZE { 204 | return Err(Error::BlockTooShort); 205 | } 206 | 207 | let iv = generate_iv_for_record(&nonce, 0); 208 | let padded_plaintext = cryptographer.aes_gcm_128_decrypt(&key, &iv, &block.ciphertext)?; 209 | 210 | // The first two bytes are a big-endian u16 padding size, 211 | // then that many zero bytes, 212 | // then the plaintext. 213 | let num_padding_bytes = 214 | (((padded_plaintext[0] as u16) << 8) | padded_plaintext[1] as u16) as usize; 215 | if num_padding_bytes + 2 >= padded_plaintext.len() { 216 | return Err(Error::DecryptPadding); 217 | } 218 | if padded_plaintext[2..(2 + num_padding_bytes)] 219 | .iter() 220 | .any(|b| *b != 0u8) 221 | { 222 | return Err(Error::DecryptPadding); 223 | } 224 | 225 | Ok(padded_plaintext[(2 + num_padding_bytes)..].to_owned()) 226 | } 227 | 228 | /// Derives the "aesgcm" decryption key and nonce given the receiver private 229 | /// key, sender public key, authentication secret, and sender salt. 230 | fn derive_key_and_nonce( 231 | cryptographer: &dyn Cryptographer, 232 | ece_mode: EceMode, 233 | local_prv_key: &dyn LocalKeyPair, 234 | remote_pub_key: &dyn RemotePublicKey, 235 | auth_secret: &[u8], 236 | salt: &[u8], 237 | ) -> Result { 238 | if auth_secret.len() != ECE_WEBPUSH_AUTH_SECRET_LENGTH { 239 | return Err(Error::InvalidAuthSecret); 240 | } 241 | if salt.len() != ECE_SALT_LENGTH { 242 | return Err(Error::InvalidSalt); 243 | } 244 | 245 | let shared_secret = cryptographer.compute_ecdh_secret(remote_pub_key, local_prv_key)?; 246 | let raw_remote_pub_key = remote_pub_key.as_raw()?; 247 | let raw_local_pub_key = local_prv_key.pub_as_raw()?; 248 | 249 | let keypair = match ece_mode { 250 | EceMode::Encrypt => encode_keys(&raw_remote_pub_key, &raw_local_pub_key), 251 | EceMode::Decrypt => encode_keys(&raw_local_pub_key, &raw_remote_pub_key), 252 | }?; 253 | let keyinfo = generate_info("aesgcm", &keypair)?; 254 | let nonceinfo = generate_info("nonce", &keypair)?; 255 | let ikm = cryptographer.hkdf_sha256( 256 | auth_secret, 257 | &shared_secret, 258 | ECE_WEBPUSH_AESGCM_AUTHINFO.as_bytes(), 259 | ECE_WEBPUSH_IKM_LENGTH, 260 | )?; 261 | let key = cryptographer.hkdf_sha256(salt, &ikm, &keyinfo, ECE_AES_KEY_LENGTH)?; 262 | let nonce = cryptographer.hkdf_sha256(salt, &ikm, &nonceinfo, ECE_NONCE_LENGTH)?; 263 | Ok((key, nonce)) 264 | } 265 | 266 | // Encode the input keys for inclusion in key-derivation info string. 267 | fn encode_keys(raw_key1: &[u8], raw_key2: &[u8]) -> Result> { 268 | let mut combined = vec![0u8; ECE_WEBPUSH_AESGCM_KEYPAIR_LENGTH]; 269 | 270 | if raw_key1.len() > ECE_WEBPUSH_RAW_KEY_LENGTH || raw_key2.len() > ECE_WEBPUSH_RAW_KEY_LENGTH { 271 | return Err(Error::InvalidKeyLength); 272 | } 273 | // length prefix each key 274 | combined[0] = 0; 275 | combined[1] = 65; 276 | combined[2..67].copy_from_slice(raw_key1); 277 | combined[67] = 0; 278 | combined[68] = 65; 279 | combined[69..].copy_from_slice(raw_key2); 280 | Ok(combined) 281 | } 282 | 283 | // The "aesgcm" IKM info string is "WebPush: info", followed by the 284 | // receiver and sender public keys prefixed by their lengths. 285 | fn generate_info(encoding: &str, keypair: &[u8]) -> Result> { 286 | let info_str = format!("Content-Encoding: {}\0P-256\0", encoding); 287 | let offset = info_str.len(); 288 | let mut info = vec![0u8; offset + keypair.len()]; 289 | info[0..offset].copy_from_slice(info_str.as_bytes()); 290 | info[offset..offset + ECE_WEBPUSH_AESGCM_KEYPAIR_LENGTH].copy_from_slice(keypair); 291 | Ok(info) 292 | } 293 | -------------------------------------------------------------------------------- /src/common.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | //! This module implements the parts of ECE that are currently shared by all 6 | //! supported schemes, such as the actual AES-GCM encryption of a single record. 7 | //! It can't be used in isolation; you must instead provide a concrete instantiation 8 | //! of an ECE encryption scheme by implementing the `EncryptionScheme` trait. 9 | 10 | use crate::{crypto::Cryptographer, error::*}; 11 | use byteorder::{BigEndian, ByteOrder}; 12 | 13 | pub(crate) const ECE_AES_KEY_LENGTH: usize = 16; 14 | pub(crate) const ECE_NONCE_LENGTH: usize = 12; 15 | pub(crate) const ECE_SALT_LENGTH: usize = 16; 16 | pub(crate) const ECE_TAG_LENGTH: usize = 16; 17 | pub(crate) const ECE_WEBPUSH_PUBLIC_KEY_LENGTH: usize = 65; 18 | pub(crate) const ECE_WEBPUSH_AUTH_SECRET_LENGTH: usize = 16; 19 | pub(crate) const ECE_WEBPUSH_DEFAULT_RS: u32 = 4096; 20 | pub(crate) const ECE_WEBPUSH_DEFAULT_PADDING_BLOCK_SIZE: usize = 128; 21 | 22 | /// Parameters that control the details of the encryption process. 23 | /// 24 | /// These are the various configuration knobs that could potentially be 25 | /// tweaked when encrypting a given piece of data, packaged together 26 | /// in a struct for convenience. 27 | /// 28 | pub(crate) struct WebPushParams { 29 | /// The record size, for chunking the plaintext into multiple records. 30 | pub rs: u32, 31 | /// The total amount of padding to add to the plaintext before encryption. 32 | pub pad_length: usize, 33 | /// The salt to use when deriving keys. 34 | /// The recommended and default value is `None`, which causes a new random 35 | /// salt to be used for every encryption. Specifying a specific salt may 36 | /// be useful for testing purposes. 37 | pub salt: Option>, 38 | } 39 | 40 | impl WebPushParams { 41 | /// Convenience method for getting an appropriate salt value. 42 | /// 43 | /// If we have a pre-configured salt then it is returned, transferring ownership 44 | /// to ensure it is only used once. If we do not have a pre-configured salt then 45 | /// a new random one is generated. 46 | pub fn take_or_generate_salt(&mut self, cryptographer: &dyn Cryptographer) -> Result> { 47 | Ok(match self.salt.take() { 48 | Some(salt) => salt, 49 | None => { 50 | let mut salt = [0u8; ECE_SALT_LENGTH]; 51 | cryptographer.random_bytes(&mut salt)?; 52 | salt.to_vec() 53 | } 54 | }) 55 | } 56 | } 57 | 58 | impl Default for WebPushParams { 59 | fn default() -> Self { 60 | // Random salt, no padding, record size = 4096. 61 | Self { 62 | rs: ECE_WEBPUSH_DEFAULT_RS, 63 | pad_length: 0, 64 | salt: None, 65 | } 66 | } 67 | } 68 | 69 | impl WebPushParams { 70 | /// Create new parameters suitable for use with the given plaintext. 71 | /// 72 | /// This constructor tries to provide some sensible defaults for using 73 | /// ECE to encrypt the given plaintext, including: 74 | /// 75 | /// * padding it to a multiple of 128 bytes. 76 | /// * using a random salt 77 | /// 78 | pub(crate) fn new_for_plaintext(plaintext: &[u8], min_pad_length: usize) -> Self { 79 | // We want (plaintext.len() + pad_length) % BLOCK_SIZE == 0, but need to 80 | // accomodate the non-zero minimum padding added by the encryption process. 81 | let mut pad_length = ECE_WEBPUSH_DEFAULT_PADDING_BLOCK_SIZE 82 | - (plaintext.len() % ECE_WEBPUSH_DEFAULT_PADDING_BLOCK_SIZE); 83 | if pad_length < min_pad_length { 84 | pad_length += ECE_WEBPUSH_DEFAULT_PADDING_BLOCK_SIZE; 85 | } 86 | WebPushParams { 87 | pad_length, 88 | ..Default::default() 89 | } 90 | } 91 | } 92 | 93 | /// Flag to indicate whether we're encrypting or decrypting. 94 | /// Used when deriving keys. 95 | /// 96 | pub(crate) enum EceMode { 97 | Encrypt, 98 | Decrypt, 99 | } 100 | 101 | /// Convenience tuple for "key" and "nonce" pair. 102 | /// These are always derived as a pair. 103 | /// 104 | pub(crate) type KeyAndNonce = (Vec, Vec); 105 | 106 | /// Generates the AES-GCM IV to use for encrypting a single record. 107 | /// 108 | /// Each record in ECE is encrypted with a unique IV, that combines a "global" nonce 109 | /// for the whole data with with the record's sequence number. 110 | /// 111 | pub(crate) fn generate_iv_for_record(nonce: &[u8], counter: usize) -> [u8; ECE_NONCE_LENGTH] { 112 | let mut iv = [0u8; ECE_NONCE_LENGTH]; 113 | let offset = ECE_NONCE_LENGTH - 8; 114 | iv[0..offset].copy_from_slice(&nonce[0..offset]); 115 | // Combine the remaining unsigned 64-bit integer with the record sequence 116 | // number using XOR. See the "nonce derivation" section of the draft. 117 | let mask = BigEndian::read_u64(&nonce[offset..]); 118 | BigEndian::write_u64(&mut iv[offset..], mask ^ (counter as u64)); 119 | iv 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | 126 | #[test] 127 | fn test_pad_to_block_size() { 128 | const BLOCK_SIZE: usize = ECE_WEBPUSH_DEFAULT_PADDING_BLOCK_SIZE; 129 | assert_eq!( 130 | WebPushParams::new_for_plaintext(&[0; 0], 1).pad_length, 131 | BLOCK_SIZE 132 | ); 133 | assert_eq!( 134 | WebPushParams::new_for_plaintext(&[0; 1], 1).pad_length, 135 | BLOCK_SIZE - 1 136 | ); 137 | assert_eq!( 138 | WebPushParams::new_for_plaintext(&[0; BLOCK_SIZE - 2], 1).pad_length, 139 | 2 140 | ); 141 | assert_eq!( 142 | WebPushParams::new_for_plaintext(&[0; BLOCK_SIZE - 1], 1).pad_length, 143 | 1 144 | ); 145 | assert_eq!( 146 | WebPushParams::new_for_plaintext(&[0; BLOCK_SIZE], 1).pad_length, 147 | BLOCK_SIZE 148 | ); 149 | assert_eq!( 150 | WebPushParams::new_for_plaintext(&[0; BLOCK_SIZE + 1], 1).pad_length, 151 | BLOCK_SIZE - 1 152 | ); 153 | 154 | assert_eq!( 155 | WebPushParams::new_for_plaintext(&[0; 0], 2).pad_length, 156 | BLOCK_SIZE 157 | ); 158 | assert_eq!( 159 | WebPushParams::new_for_plaintext(&[0; 1], 2).pad_length, 160 | BLOCK_SIZE - 1 161 | ); 162 | assert_eq!( 163 | WebPushParams::new_for_plaintext(&[0; BLOCK_SIZE - 2], 2).pad_length, 164 | 2 165 | ); 166 | assert_eq!( 167 | WebPushParams::new_for_plaintext(&[0; BLOCK_SIZE - 1], 2).pad_length, 168 | BLOCK_SIZE + 1 169 | ); 170 | assert_eq!( 171 | WebPushParams::new_for_plaintext(&[0; BLOCK_SIZE], 2).pad_length, 172 | BLOCK_SIZE 173 | ); 174 | assert_eq!( 175 | WebPushParams::new_for_plaintext(&[0; BLOCK_SIZE + 1], 2).pad_length, 176 | BLOCK_SIZE - 1 177 | ); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/crypto/holder.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use super::Cryptographer; 6 | use once_cell::sync::OnceCell; 7 | 8 | static CRYPTOGRAPHER: OnceCell<&'static dyn Cryptographer> = OnceCell::new(); 9 | 10 | #[derive(Debug, thiserror::Error)] 11 | #[error("Cryptographer already initialized")] 12 | pub struct SetCryptographerError(()); 13 | 14 | /// Sets the global object that will be used for cryptographic operations. 15 | /// 16 | /// This is a convenience wrapper over [`set_cryptographer`], 17 | /// but takes a `Box` instead. 18 | #[cfg(not(feature = "backend-openssl"))] 19 | pub fn set_boxed_cryptographer(c: Box) -> Result<(), SetCryptographerError> { 20 | // Just leak the Box. It wouldn't be freed as a `static` anyway, and we 21 | // never allow this to be re-assigned (so it's not a meaningful memory leak). 22 | set_cryptographer(Box::leak(c)) 23 | } 24 | 25 | /// Sets the global object that will be used for cryptographic operations. 26 | /// 27 | /// This function may only be called once in the lifetime of a program. 28 | /// 29 | /// Any calls into this crate that perform cryptography prior to calling this 30 | /// function will panic. 31 | pub fn set_cryptographer(c: &'static dyn Cryptographer) -> Result<(), SetCryptographerError> { 32 | CRYPTOGRAPHER.set(c).map_err(|_| SetCryptographerError(())) 33 | } 34 | 35 | pub(crate) fn get_cryptographer() -> &'static dyn Cryptographer { 36 | autoinit_crypto(); 37 | *CRYPTOGRAPHER 38 | .get() 39 | .expect("`rust-ece` cryptographer not initialized!") 40 | } 41 | 42 | #[cfg(feature = "backend-openssl")] 43 | #[inline] 44 | fn autoinit_crypto() { 45 | let _ = set_cryptographer(&super::openssl::OpensslCryptographer); 46 | } 47 | 48 | #[cfg(not(feature = "backend-openssl"))] 49 | #[inline] 50 | fn autoinit_crypto() {} 51 | -------------------------------------------------------------------------------- /src/crypto/mod.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use crate::error::*; 6 | use std::any::Any; 7 | 8 | pub(crate) mod holder; 9 | #[cfg(feature = "backend-openssl")] 10 | mod openssl; 11 | 12 | #[cfg(not(feature = "backend-openssl"))] 13 | pub use holder::{set_boxed_cryptographer, set_cryptographer}; 14 | 15 | pub trait RemotePublicKey: Send + Sync + 'static { 16 | /// Export the key component in the 17 | /// binary uncompressed point representation. 18 | fn as_raw(&self) -> Result>; 19 | /// For downcasting purposes. 20 | fn as_any(&self) -> &dyn Any; 21 | } 22 | 23 | pub trait LocalKeyPair: Send + Sync + 'static { 24 | /// Export the public key component in the 25 | /// binary uncompressed point representation. 26 | fn pub_as_raw(&self) -> Result>; 27 | /// Export the raw components of the keypair. 28 | fn raw_components(&self) -> Result; 29 | /// For downcasting purposes. 30 | fn as_any(&self) -> &dyn Any; 31 | } 32 | 33 | #[derive(Clone, Debug, Eq, PartialEq)] 34 | #[cfg_attr( 35 | feature = "serializable-keys", 36 | derive(serde::Serialize, serde::Deserialize) 37 | )] 38 | #[derive(Default)] 39 | pub enum EcCurve { 40 | #[default] 41 | P256, 42 | } 43 | 44 | #[derive(Clone, Debug, Eq, PartialEq)] 45 | #[cfg_attr( 46 | feature = "serializable-keys", 47 | derive(serde::Serialize, serde::Deserialize) 48 | )] 49 | pub struct EcKeyComponents { 50 | // The curve is only kept in case the ECE standard changes in the future. 51 | curve: EcCurve, 52 | // The `d` value of the EC Key. 53 | private_key: Vec, 54 | // The uncompressed x,y-representation of the public component of the EC Key. 55 | public_key: Vec, 56 | } 57 | 58 | impl EcKeyComponents { 59 | pub fn new>>(private_key: T, public_key: T) -> Self { 60 | EcKeyComponents { 61 | private_key: private_key.into(), 62 | public_key: public_key.into(), 63 | curve: Default::default(), 64 | } 65 | } 66 | pub fn curve(&self) -> &EcCurve { 67 | &self.curve 68 | } 69 | /// The `d` value of the EC Key. 70 | pub fn private_key(&self) -> &[u8] { 71 | &self.private_key 72 | } 73 | /// The uncompressed x,y-representation of the public component of the EC Key. 74 | pub fn public_key(&self) -> &[u8] { 75 | &self.public_key 76 | } 77 | } 78 | 79 | pub trait Cryptographer: Send + Sync + 'static { 80 | /// Generate a random ephemeral local key pair. 81 | fn generate_ephemeral_keypair(&self) -> Result>; 82 | /// Import a local keypair from its raw components. 83 | fn import_key_pair(&self, components: &EcKeyComponents) -> Result>; 84 | /// Import the public key component in the binary uncompressed point representation. 85 | fn import_public_key(&self, raw: &[u8]) -> Result>; 86 | fn compute_ecdh_secret( 87 | &self, 88 | remote: &dyn RemotePublicKey, 89 | local: &dyn LocalKeyPair, 90 | ) -> Result>; 91 | fn hkdf_sha256(&self, salt: &[u8], secret: &[u8], info: &[u8], len: usize) -> Result>; 92 | /// Should return [ciphertext, auth_tag]. 93 | fn aes_gcm_128_encrypt(&self, key: &[u8], iv: &[u8], data: &[u8]) -> Result>; 94 | fn aes_gcm_128_decrypt( 95 | &self, 96 | key: &[u8], 97 | iv: &[u8], 98 | ciphertext_and_tag: &[u8], 99 | ) -> Result>; 100 | fn random_bytes(&self, dest: &mut [u8]) -> Result<()>; 101 | } 102 | 103 | /// Run a small suite of tests to check that a `Cryptographer` backend is working correctly. 104 | /// 105 | /// You should only use this is you're implementing a custom `Cryptographer` and want to check 106 | /// that it is working as intended. This function will panic if the tests fail. 107 | /// 108 | #[cfg(any(test, feature = "backend-test-helper"))] 109 | pub fn test_cryptographer(cryptographer: T) { 110 | use crate::{aes128gcm, common::WebPushParams}; 111 | 112 | // These are test data from the RFC. 113 | let plaintext = "When I grow up, I want to be a watermelon"; 114 | let ciphertext = hex::decode("0c6bfaadad67958803092d454676f397000010004104fe33f4ab0dea71914db55823f73b54948f41306d920732dbb9a59a53286482200e597a7b7bc260ba1c227998580992e93973002f3012a28ae8f06bbb78e5ec0ff297de5b429bba7153d3a4ae0caa091fd425f3b4b5414add8ab37a19c1bbb05cf5cb5b2a2e0562d558635641ec52812c6c8ff42e95ccb86be7cd").unwrap(); 115 | 116 | // First, a trial encryption. 117 | let private_key = 118 | hex::decode("c9f58f89813e9f8e872e71f42aa64e1757c9254dcc62b72ddc010bb4043ea11c").unwrap(); 119 | let public_key = hex::decode("04fe33f4ab0dea71914db55823f73b54948f41306d920732dbb9a59a53286482200e597a7b7bc260ba1c227998580992e93973002f3012a28ae8f06bbb78e5ec0f").unwrap(); 120 | let ec_key = EcKeyComponents::new(private_key, public_key); 121 | let local_key_pair = cryptographer.import_key_pair(&ec_key).unwrap(); 122 | 123 | let remote_pub_key = hex::decode("042571b2becdfde360551aaf1ed0f4cd366c11cebe555f89bcb7b186a53339173168ece2ebe018597bd30479b86e3c8f8eced577ca59187e9246990db682008b0e").unwrap(); 124 | let remote_pub_key = cryptographer.import_public_key(&remote_pub_key).unwrap(); 125 | let auth_secret = hex::decode("05305932a1c7eabe13b6cec9fda48882").unwrap(); 126 | 127 | let params = WebPushParams { 128 | rs: 4096, 129 | pad_length: 0, 130 | salt: Some(hex::decode("0c6bfaadad67958803092d454676f397").unwrap()), 131 | }; 132 | 133 | assert_eq!( 134 | aes128gcm::encrypt( 135 | &*local_key_pair, 136 | &*remote_pub_key, 137 | &auth_secret, 138 | plaintext.as_bytes(), 139 | params, 140 | ) 141 | .unwrap(), 142 | ciphertext 143 | ); 144 | 145 | // Now, a trial decryption. 146 | let private_key = 147 | hex::decode("ab5757a70dd4a53e553a6bbf71ffefea2874ec07a6b379e3c48f895a02dc33de").unwrap(); 148 | let public_key = hex::decode("042571b2becdfde360551aaf1ed0f4cd366c11cebe555f89bcb7b186a53339173168ece2ebe018597bd30479b86e3c8f8eced577ca59187e9246990db682008b0e").unwrap(); 149 | let ec_key = EcKeyComponents::new(private_key, public_key); 150 | let local_key_pair = cryptographer.import_key_pair(&ec_key).unwrap(); 151 | 152 | assert_eq!( 153 | aes128gcm::decrypt(&*local_key_pair, &auth_secret, ciphertext.as_ref(),).unwrap(), 154 | plaintext.as_bytes() 155 | ); 156 | } 157 | 158 | #[cfg(all(test, feature = "backend-openssl"))] 159 | mod tests { 160 | use super::*; 161 | 162 | // All of the tests in this crate exercise the default backend, so running this here 163 | // doesn't tell us anyting more about the default backend. Instead, it tells us whether 164 | // the `test_cryptographer` function is working correctly! 165 | #[test] 166 | fn test_default_cryptograher() { 167 | test_cryptographer(super::openssl::OpensslCryptographer); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/crypto/openssl.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | use crate::{ 6 | crypto::{Cryptographer, EcKeyComponents, LocalKeyPair, RemotePublicKey}, 7 | error::*, 8 | }; 9 | use base64::Engine; 10 | use hkdf::Hkdf; 11 | use lazy_static::lazy_static; 12 | use openssl::{ 13 | bn::{BigNum, BigNumContext}, 14 | derive::Deriver, 15 | ec::{EcGroup, EcKey, EcPoint, PointConversionForm}, 16 | nid::Nid, 17 | pkey::{PKey, Private, Public}, 18 | rand::rand_bytes, 19 | symm::{Cipher, Crypter, Mode}, 20 | }; 21 | use sha2::Sha256; 22 | use std::{any::Any, fmt}; 23 | 24 | lazy_static! { 25 | static ref GROUP_P256: EcGroup = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); 26 | } 27 | const AES_GCM_TAG_LENGTH: usize = 16; 28 | 29 | #[derive(Clone, Debug)] 30 | pub struct OpenSSLRemotePublicKey { 31 | raw_pub_key: Vec, 32 | } 33 | 34 | impl OpenSSLRemotePublicKey { 35 | fn from_raw(raw: &[u8]) -> Result { 36 | Ok(OpenSSLRemotePublicKey { 37 | raw_pub_key: raw.to_vec(), 38 | }) 39 | } 40 | 41 | fn to_pkey(&self) -> Result> { 42 | let mut bn_ctx = BigNumContext::new()?; 43 | let point = EcPoint::from_bytes(&GROUP_P256, &self.raw_pub_key, &mut bn_ctx)?; 44 | let ec = EcKey::from_public_key(&GROUP_P256, &point)?; 45 | PKey::from_ec_key(ec).map_err(std::convert::Into::into) 46 | } 47 | } 48 | 49 | impl RemotePublicKey for OpenSSLRemotePublicKey { 50 | fn as_raw(&self) -> Result> { 51 | Ok(self.raw_pub_key.to_vec()) 52 | } 53 | fn as_any(&self) -> &dyn Any { 54 | self 55 | } 56 | } 57 | 58 | #[derive(Clone)] 59 | pub struct OpenSSLLocalKeyPair { 60 | ec_key: EcKey, 61 | } 62 | 63 | impl fmt::Debug for OpenSSLLocalKeyPair { 64 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 65 | write!( 66 | f, 67 | "{:?}", 68 | base64::engine::general_purpose::URL_SAFE.encode(self.ec_key.private_key().to_vec()) 69 | ) 70 | } 71 | } 72 | 73 | impl OpenSSLLocalKeyPair { 74 | /// Generate a random local key pair using OpenSSL `ECKey::generate`. 75 | fn generate_random() -> Result { 76 | let ec_key = EcKey::generate(&GROUP_P256)?; 77 | Ok(OpenSSLLocalKeyPair { ec_key }) 78 | } 79 | 80 | fn to_pkey(&self) -> Result> { 81 | PKey::from_ec_key(self.ec_key.clone()).map_err(std::convert::Into::into) 82 | } 83 | 84 | fn from_raw_components(components: &EcKeyComponents) -> Result { 85 | let d = BigNum::from_slice(components.private_key())?; 86 | let mut bn_ctx = BigNumContext::new()?; 87 | let ec_point = EcPoint::from_bytes(&GROUP_P256, components.public_key(), &mut bn_ctx)?; 88 | let mut x = BigNum::new()?; 89 | let mut y = BigNum::new()?; 90 | ec_point.affine_coordinates_gfp(&GROUP_P256, &mut x, &mut y, &mut bn_ctx)?; 91 | let public_key = EcKey::from_public_key_affine_coordinates(&GROUP_P256, &x, &y)?; 92 | let private_key = EcKey::from_private_components(&GROUP_P256, &d, public_key.public_key())?; 93 | Ok(Self { 94 | ec_key: private_key, 95 | }) 96 | } 97 | } 98 | 99 | impl LocalKeyPair for OpenSSLLocalKeyPair { 100 | /// Export the public key component in the binary uncompressed point representation 101 | /// using OpenSSL `PointConversionForm::UNCOMPRESSED`. 102 | fn pub_as_raw(&self) -> Result> { 103 | let pub_key_point = self.ec_key.public_key(); 104 | let mut bn_ctx = BigNumContext::new()?; 105 | let uncompressed = 106 | pub_key_point.to_bytes(&GROUP_P256, PointConversionForm::UNCOMPRESSED, &mut bn_ctx)?; 107 | Ok(uncompressed) 108 | } 109 | 110 | fn raw_components(&self) -> Result { 111 | let private_key = self.ec_key.private_key(); 112 | Ok(EcKeyComponents::new( 113 | private_key.to_vec(), 114 | self.pub_as_raw()?, 115 | )) 116 | } 117 | 118 | fn as_any(&self) -> &dyn Any { 119 | self 120 | } 121 | } 122 | 123 | impl From> for OpenSSLLocalKeyPair { 124 | fn from(key: EcKey) -> OpenSSLLocalKeyPair { 125 | OpenSSLLocalKeyPair { ec_key: key } 126 | } 127 | } 128 | 129 | pub struct OpensslCryptographer; 130 | impl Cryptographer for OpensslCryptographer { 131 | fn generate_ephemeral_keypair(&self) -> Result> { 132 | Ok(Box::new(OpenSSLLocalKeyPair::generate_random()?)) 133 | } 134 | 135 | fn import_key_pair(&self, components: &EcKeyComponents) -> Result> { 136 | Ok(Box::new(OpenSSLLocalKeyPair::from_raw_components( 137 | components, 138 | )?)) 139 | } 140 | 141 | fn import_public_key(&self, raw: &[u8]) -> Result> { 142 | Ok(Box::new(OpenSSLRemotePublicKey::from_raw(raw)?)) 143 | } 144 | 145 | fn compute_ecdh_secret( 146 | &self, 147 | remote: &dyn RemotePublicKey, 148 | local: &dyn LocalKeyPair, 149 | ) -> Result> { 150 | let local_any = local.as_any(); 151 | let local = local_any.downcast_ref::().unwrap(); 152 | let private = local.to_pkey()?; 153 | let remote_any = remote.as_any(); 154 | let remote = remote_any.downcast_ref::().unwrap(); 155 | let public = remote.to_pkey()?; 156 | let mut deriver = Deriver::new(&private)?; 157 | deriver.set_peer(&public)?; 158 | let shared_key = deriver.derive_to_vec()?; 159 | Ok(shared_key) 160 | } 161 | 162 | fn hkdf_sha256(&self, salt: &[u8], secret: &[u8], info: &[u8], len: usize) -> Result> { 163 | let (_, hk) = Hkdf::::extract(Some(salt), secret); 164 | let mut okm = vec![0u8; len]; 165 | hk.expand(info, &mut okm).unwrap(); 166 | Ok(okm) 167 | } 168 | 169 | fn aes_gcm_128_encrypt(&self, key: &[u8], iv: &[u8], data: &[u8]) -> Result> { 170 | let cipher = Cipher::aes_128_gcm(); 171 | let mut c = Crypter::new(cipher, Mode::Encrypt, key, Some(iv))?; 172 | let mut out = vec![0u8; data.len() + cipher.block_size()]; 173 | let count = c.update(data, &mut out)?; 174 | let rest = c.finalize(&mut out[count..])?; 175 | let mut tag = vec![0u8; AES_GCM_TAG_LENGTH]; 176 | c.get_tag(&mut tag)?; 177 | out.truncate(count + rest); 178 | out.append(&mut tag); 179 | Ok(out) 180 | } 181 | 182 | fn aes_gcm_128_decrypt( 183 | &self, 184 | key: &[u8], 185 | iv: &[u8], 186 | ciphertext_and_tag: &[u8], 187 | ) -> Result> { 188 | let block_len = ciphertext_and_tag.len() - AES_GCM_TAG_LENGTH; 189 | let ciphertext = &ciphertext_and_tag[0..block_len]; 190 | let tag = &ciphertext_and_tag[block_len..]; 191 | let cipher = Cipher::aes_128_gcm(); 192 | let mut c = Crypter::new(cipher, Mode::Decrypt, key, Some(iv))?; 193 | let mut out = vec![0u8; ciphertext.len() + cipher.block_size()]; 194 | let count = c.update(ciphertext, &mut out)?; 195 | c.set_tag(tag)?; 196 | let rest = c.finalize(&mut out[count..])?; 197 | out.truncate(count + rest); 198 | Ok(out) 199 | } 200 | 201 | fn random_bytes(&self, dest: &mut [u8]) -> Result<()> { 202 | Ok(rand_bytes(dest)?) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | pub type Result = std::result::Result; 6 | 7 | #[derive(Debug, thiserror::Error)] 8 | pub enum Error { 9 | #[error("Invalid auth secret")] 10 | InvalidAuthSecret, 11 | 12 | #[error("Invalid salt")] 13 | InvalidSalt, 14 | 15 | #[error("Invalid key length")] 16 | InvalidKeyLength, 17 | 18 | #[error("Invalid record size")] 19 | InvalidRecordSize, 20 | 21 | #[error("Invalid header size (too short)")] 22 | HeaderTooShort, 23 | 24 | #[error("Truncated ciphertext")] 25 | DecryptTruncated, 26 | 27 | #[error("Zero-length ciphertext")] 28 | ZeroCiphertext, 29 | 30 | #[error("Zero-length plaintext")] 31 | ZeroPlaintext, 32 | 33 | #[error("Block too short")] 34 | BlockTooShort, 35 | 36 | #[error("Plaintext is too long to fit in a single block")] 37 | PlaintextTooLong, 38 | 39 | #[error("Decryption across multiple records is not supported")] 40 | MultipleRecordsNotSupported, 41 | 42 | #[error("Invalid decryption padding")] 43 | DecryptPadding, 44 | 45 | #[error("Invalid encryption padding")] 46 | EncryptPadding, 47 | 48 | #[error("Could not decode base64 entry")] 49 | DecodeError(#[from] base64::DecodeError), 50 | 51 | #[error("Crypto backend error")] 52 | CryptoError, 53 | 54 | #[cfg(feature = "backend-openssl")] 55 | #[error("OpenSSL error: {0}")] 56 | OpenSSLError(#[from] openssl::error::ErrorStack), 57 | } 58 | -------------------------------------------------------------------------------- /src/legacy.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | pub use crate::aesgcm::AesGcmEncryptedBlock; 6 | use crate::{aesgcm, common::WebPushParams, crypto::EcKeyComponents, error::*}; 7 | 8 | /// Encrypt a block using legacy AESGCM encoding. 9 | /// 10 | /// * `remote_pub` : The public key of the remote message recipient 11 | /// * `remote_auth` : The authentication secret of the remote message recipient 12 | /// * `data` : the data to encrypt 13 | /// 14 | /// You should only use this function if you know that you definitely need 15 | /// to use the legacy format. The [`encrypt`](crate::encrypt) function should 16 | /// be preferred where possible. 17 | /// 18 | pub fn encrypt_aesgcm( 19 | remote_pub: &[u8], 20 | remote_auth: &[u8], 21 | data: &[u8], 22 | ) -> Result { 23 | let cryptographer = crate::crypto::holder::get_cryptographer(); 24 | let remote_key = cryptographer.import_public_key(remote_pub)?; 25 | let local_key_pair = cryptographer.generate_ephemeral_keypair()?; 26 | let params = WebPushParams::new_for_plaintext(data, aesgcm::ECE_AESGCM_PAD_SIZE); 27 | aesgcm::encrypt(&*local_key_pair, &*remote_key, remote_auth, data, params) 28 | } 29 | 30 | /// Decrypt a block using legacy AESGCM encoding. 31 | /// 32 | /// * `components` : The public and private key components of the local message recipient 33 | /// * `auth` : The authentication secret of the remote message recipient 34 | /// * `data` : The encrypted data block 35 | /// 36 | /// You should only use this function if you know that you definitely need 37 | /// to use the legacy format. The [`decrypt`](crate::decrypt) function should 38 | /// be preferred where possible. 39 | /// 40 | pub fn decrypt_aesgcm( 41 | components: &EcKeyComponents, 42 | auth: &[u8], 43 | data: &AesGcmEncryptedBlock, 44 | ) -> Result> { 45 | let cryptographer = crate::crypto::holder::get_cryptographer(); 46 | let priv_key = cryptographer.import_key_pair(components).unwrap(); 47 | aesgcm::decrypt(&*priv_key, auth, data) 48 | } 49 | 50 | #[cfg(all(test, feature = "backend-openssl"))] 51 | mod aesgcm_tests { 52 | use super::*; 53 | use base64::Engine; 54 | use hex; 55 | 56 | #[derive(Debug)] 57 | struct AesGcmTestPayload { 58 | dh: String, 59 | salt: String, 60 | rs: u32, 61 | ciphertext: String, 62 | } 63 | 64 | #[allow(clippy::too_many_arguments)] 65 | fn try_encrypt( 66 | private_key: &str, 67 | public_key: &str, 68 | remote_pub_key: &str, 69 | auth_secret: &str, 70 | salt: &str, 71 | pad_length: usize, 72 | rs: u32, 73 | plaintext: &str, 74 | ) -> Result { 75 | let cryptographer = crate::crypto::holder::get_cryptographer(); 76 | let private_key = hex::decode(private_key).unwrap(); 77 | let public_key = hex::decode(public_key).unwrap(); 78 | let ec_key = EcKeyComponents::new(private_key, public_key); 79 | let local_key_pair = cryptographer.import_key_pair(&ec_key)?; 80 | let remote_pub_key = hex::decode(remote_pub_key).unwrap(); 81 | let remote_pub_key = cryptographer.import_public_key(&remote_pub_key).unwrap(); 82 | let auth_secret = hex::decode(auth_secret).unwrap(); 83 | let salt = Some(hex::decode(salt).unwrap()); 84 | let plaintext = plaintext.as_bytes(); 85 | let params = WebPushParams { 86 | rs, 87 | pad_length, 88 | salt, 89 | }; 90 | let encrypted_block = aesgcm::encrypt( 91 | &*local_key_pair, 92 | &*remote_pub_key, 93 | &auth_secret, 94 | plaintext, 95 | params, 96 | )?; 97 | Ok(AesGcmTestPayload { 98 | dh: hex::encode(encrypted_block.dh), 99 | salt: hex::encode(encrypted_block.salt), 100 | rs: encrypted_block.rs, 101 | ciphertext: hex::encode(encrypted_block.ciphertext), 102 | }) 103 | } 104 | 105 | fn try_decrypt( 106 | private_key: &str, 107 | public_key: &str, 108 | auth_secret: &str, 109 | payload: &AesGcmTestPayload, 110 | ) -> Result { 111 | let private_key = hex::decode(private_key).unwrap(); 112 | let public_key = hex::decode(public_key).unwrap(); 113 | let ec_key = EcKeyComponents::new(private_key, public_key); 114 | let plaintext = decrypt_aesgcm( 115 | &ec_key, 116 | &hex::decode(auth_secret).unwrap(), 117 | &AesGcmEncryptedBlock::new( 118 | &hex::decode(&payload.dh).unwrap(), 119 | &hex::decode(&payload.salt).unwrap(), 120 | payload.rs, 121 | hex::decode(&payload.ciphertext).unwrap(), 122 | )?, 123 | )?; 124 | Ok(String::from_utf8(plaintext).unwrap()) 125 | } 126 | 127 | #[test] 128 | fn test_e2e() { 129 | let (local_key, remote_key) = crate::generate_keys().unwrap(); 130 | let plaintext = b"There was a green mouse, running in the grass"; 131 | let mut auth_secret = vec![0u8; 16]; 132 | let cryptographer = crate::crypto::holder::get_cryptographer(); 133 | cryptographer.random_bytes(&mut auth_secret).unwrap(); 134 | let remote_public = cryptographer 135 | .import_public_key(&remote_key.pub_as_raw().unwrap()) 136 | .unwrap(); 137 | let params = WebPushParams::default(); 138 | let encrypted_block = aesgcm::encrypt( 139 | &*local_key, 140 | &*remote_public, 141 | &auth_secret, 142 | plaintext, 143 | params, 144 | ) 145 | .unwrap(); 146 | let decrypted = aesgcm::decrypt(&*remote_key, &auth_secret, &encrypted_block).unwrap(); 147 | assert_eq!(decrypted, plaintext.to_vec()); 148 | } 149 | 150 | #[test] 151 | fn test_conv_fn() -> Result<()> { 152 | let (local_key, auth) = crate::generate_keypair_and_auth_secret()?; 153 | let plaintext = b"There was a little ship that had never sailed"; 154 | let encoded = encrypt_aesgcm(&local_key.pub_as_raw()?, &auth, plaintext).unwrap(); 155 | let decoded = decrypt_aesgcm(&local_key.raw_components()?, &auth, &encoded)?; 156 | assert_eq!(decoded, plaintext.to_vec()); 157 | Ok(()) 158 | } 159 | 160 | #[test] 161 | fn try_encrypt_ietf_rfc() { 162 | // Test data from [IETF Web Push Encryption Draft 5](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04#section-5) 163 | let encrypted_block = try_encrypt( 164 | "9c249c7a4f90a448e638e953fab437f27673bdd3e5a9ad34672d22ea6d8e26f6", 165 | "04da110db6fce091a6f20e59e42171bab4aab17589d7522d7d71166152c4f3963b0989038d7b0811ce1aab161a4351bc06a917089e833e90eb5ad7568ff9ae8075", 166 | "042124063ccbf19dc2fa88b643ba04e6dd8da7ea7ba2c8c62e0f77a943f4c2fa914f6d44116c9fd1c40341c6a440cab3e2140a60e4378a5da735972de078005105", 167 | "476f6f20676f6f206727206a6f6f6221", 168 | "96781aadbc8a7cca22f59ef9c585e692", 169 | 0, 170 | 4096, 171 | "I am the walrus", 172 | ).unwrap(); 173 | assert_eq!( 174 | encrypted_block.ciphertext, 175 | "ea7a80414304f2136ac39277925f1ca55549ca55ca62a64e7ac7991bc52e78aa40" 176 | ); 177 | } 178 | 179 | #[test] 180 | fn test_decrypt_ietf_rfc() { 181 | // Test data from [IETF Web Push Encryption Draft 5](https://tools.ietf.org/html/draft-ietf-webpush-encryption-04#section-5) 182 | let plaintext = try_decrypt( 183 | "f455a5d79fd05100160da0f7937979d19059409e1abb6ec5d55e05d2e2d20ff3", 184 | "042124063ccbf19dc2fa88b643ba04e6dd8da7ea7ba2c8c62e0f77a943f4c2fa914f6d44116c9fd1c40341c6a440cab3e2140a60e4378a5da735972de078005105", 185 | "476f6f20676f6f206727206a6f6f6221", 186 | &AesGcmTestPayload { 187 | ciphertext : "ea7a80414304f2136ac39277925f1ca55549ca55ca62a64e7ac7991bc52e78aa40".to_owned(), 188 | salt : "96781aadbc8a7cca22f59ef9c585e692".to_owned(), 189 | dh : "04da110db6fce091a6f20e59e42171bab4aab17589d7522d7d71166152c4f3963b0989038d7b0811ce1aab161a4351bc06a917089e833e90eb5ad7568ff9ae8075".to_owned(), 190 | rs : 4096, 191 | } 192 | ).unwrap(); 193 | assert_eq!(plaintext, "I am the walrus"); 194 | } 195 | 196 | // We have some existing test data in b64, and some in hex, 197 | // and it's easy to make a second `try_decrypt` helper function 198 | // than to re-encode all the data. 199 | fn try_decrypt_b64( 200 | priv_key: &str, 201 | pub_key: &str, 202 | auth_secret: &str, 203 | block: &AesGcmEncryptedBlock, 204 | ) -> Result { 205 | // The AesGcmEncryptedBlock is composed from the `Crypto-Key` & `Encryption` headers, and post body 206 | // The Block will attempt to decode the base64 strings for dh & salt, so no additional action needed. 207 | // Since the body is most likely not encoded, it is expected to be a raw buffer of [u8] 208 | let priv_key_raw = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(priv_key)?; 209 | let pub_key_raw = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(pub_key)?; 210 | let ec_key = EcKeyComponents::new(priv_key_raw, pub_key_raw); 211 | let auth_secret = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(auth_secret)?; 212 | let plaintext = decrypt_aesgcm(&ec_key, &auth_secret, block)?; 213 | Ok(String::from_utf8(plaintext).unwrap()) 214 | } 215 | 216 | #[test] 217 | fn test_decode() { 218 | use base64::Engine; 219 | 220 | // generated the content using pywebpush, which verified against the client. 221 | let auth_raw = "LsuUOBKVQRY6-l7_Ajo-Ag"; 222 | let priv_key_raw = "yerDmA9uNFoaUnSt2TkWWLwPseG1qtzS2zdjUl8Z7tc"; 223 | let pub_key_raw = "BLBlTYure2QVhJCiDt4gRL0JNmUBMxtNB5B6Z1hDg5h-Epw6mVFV4whoYGBlWNY-ENR1FObkGFyMf7-6ZMHMAxw"; 224 | 225 | // Incoming Crypto-Key: dh= 226 | let dh = "BJvcyzf8ocm6F7lbFePebtXU7OHkmylXN9FL2g-yBHwUKqo6cD-FP1h5SHEQQ-xEgJl-F0xEEmSaEx2-qeJHYmk"; 227 | // Incoming Encryption: salt= 228 | let salt = "8qX1ZgkLD50LHgocZdPKZQ"; 229 | // Incoming Body (this is normally raw bytes. It's encoded here for presentation) 230 | let ciphertext = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode("8Vyes671P_VDf3G2e6MgY6IaaydgR-vODZZ7L0ZHbpCJNVaf_2omEms2tiPJiU22L3BoECKJixiOxihcsxWMjTgAcplbvfu1g6LWeP4j8dMAzJionWs7OOLif6jBKN6LGm4EUw9e26EBv9hNhi87-HaEGbfBMGcLvm1bql1F").unwrap(); 231 | let plaintext = "Amidst the mists and coldest frosts I thrust my fists against the\nposts and still demand to see the ghosts.\n"; 232 | 233 | let block = AesGcmEncryptedBlock::new( 234 | &base64::engine::general_purpose::URL_SAFE_NO_PAD 235 | .decode(dh) 236 | .unwrap(), 237 | &base64::engine::general_purpose::URL_SAFE_NO_PAD 238 | .decode(salt) 239 | .unwrap(), 240 | 4096, 241 | ciphertext, 242 | ) 243 | .unwrap(); 244 | 245 | let result = try_decrypt_b64(priv_key_raw, pub_key_raw, auth_raw, &block).unwrap(); 246 | 247 | assert!(result == plaintext) 248 | } 249 | 250 | #[test] 251 | fn test_decode_padding() { 252 | use base64::Engine; 253 | 254 | // generated the content using pywebpush, which verified against the client. 255 | let auth_raw = "LsuUOBKVQRY6-l7_Ajo-Ag"; 256 | let priv_key_raw = "yerDmA9uNFoaUnSt2TkWWLwPseG1qtzS2zdjUl8Z7tc"; 257 | let pub_key_raw = "BLBlTYure2QVhJCiDt4gRL0JNmUBMxtNB5B6Z1hDg5h-Epw6mVFV4whoYGBlWNY-ENR1FObkGFyMf7-6ZMHMAxw"; 258 | 259 | // Incoming Crypto-Key: dh= 260 | let dh = "BCX7KJ_1Em-LjeB56E2KDoMjKDhTaDhjv8c6dwbvZQZ_Gsfp3AT54x2zYUPcBwd1GVyGsk55ProJ98cFrVxrPz4"; 261 | // Incoming Encryption-Key: salt= 262 | let salt = "x2I2OZpSCoe-Cc5UW36Nng"; 263 | // Incoming Body (this is normally raw bytes. It's encoded here for presentation) 264 | let ciphertext = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode("Ua3-WW5kTbt11dBTiXBP6_hLBYhBNOtDFfue5QHMTd2DicL0wutDnt5z9pjRJ76w562egPq5qro95YLnsX0NWGmDQbsQ0Azds6jcBGsxHPt0p5GELAtR4AJj2OsB_LV7dTuGHN2SqsyXLARjTFN2wsF3xWhmuw").unwrap(); 265 | let plaintext = "Tabs are the real indent"; 266 | 267 | let block = AesGcmEncryptedBlock::new( 268 | &base64::engine::general_purpose::URL_SAFE_NO_PAD 269 | .decode(dh) 270 | .unwrap(), 271 | &base64::engine::general_purpose::URL_SAFE_NO_PAD 272 | .decode(salt) 273 | .unwrap(), 274 | 4096, 275 | ciphertext, 276 | ) 277 | .unwrap(); 278 | 279 | let result = try_decrypt_b64(priv_key_raw, pub_key_raw, auth_raw, &block).unwrap(); 280 | 281 | assert!(result == plaintext) 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | /* This Source Code Form is subject to the terms of the Mozilla Public 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this 3 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 | 5 | #![warn(rust_2018_idioms)] 6 | 7 | mod aes128gcm; 8 | mod aesgcm; 9 | mod common; 10 | pub mod crypto; 11 | mod error; 12 | pub mod legacy; 13 | 14 | pub use crate::{ 15 | crypto::{Cryptographer, EcKeyComponents, LocalKeyPair, RemotePublicKey}, 16 | error::*, 17 | }; 18 | 19 | use crate::{ 20 | aes128gcm::ECE_AES128GCM_PAD_SIZE, 21 | common::{WebPushParams, ECE_WEBPUSH_AUTH_SECRET_LENGTH}, 22 | }; 23 | 24 | /// Generate a local ECE key pair and authentication secret. 25 | /// 26 | pub fn generate_keypair_and_auth_secret( 27 | ) -> Result<(Box, [u8; ECE_WEBPUSH_AUTH_SECRET_LENGTH])> { 28 | let cryptographer = crypto::holder::get_cryptographer(); 29 | let local_key_pair = cryptographer.generate_ephemeral_keypair()?; 30 | let mut auth_secret = [0u8; ECE_WEBPUSH_AUTH_SECRET_LENGTH]; 31 | cryptographer.random_bytes(&mut auth_secret)?; 32 | Ok((local_key_pair, auth_secret)) 33 | } 34 | 35 | /// Encrypt a block using the AES128GCM encryption scheme. 36 | /// 37 | /// * `remote_pub` : The public key of the remote message recipient 38 | /// * `remote_auth` : The authentication secret of the remote message recipient 39 | /// * `data` : The data to encrypt 40 | /// 41 | /// For the equivalent function using legacy AESGCM encryption scheme 42 | /// use [`legacy::encrypt_aesgcm`](crate::legacy::encrypt_aesgcm). 43 | /// 44 | pub fn encrypt(remote_pub: &[u8], remote_auth: &[u8], data: &[u8]) -> Result> { 45 | let cryptographer = crypto::holder::get_cryptographer(); 46 | let remote_key = cryptographer.import_public_key(remote_pub)?; 47 | let local_key_pair = cryptographer.generate_ephemeral_keypair()?; 48 | let params = WebPushParams::new_for_plaintext(data, ECE_AES128GCM_PAD_SIZE); 49 | aes128gcm::encrypt(&*local_key_pair, &*remote_key, remote_auth, data, params) 50 | } 51 | 52 | /// Decrypt a block using the AES128GCM encryption scheme. 53 | /// 54 | /// * `components` : The public and private key components of the local message recipient 55 | /// * `auth` : The authentication secret of the remote message recipient 56 | /// * `data` : The encrypted data block 57 | /// 58 | /// For the equivalent function using legacy AESGCM encryption scheme 59 | /// use [`legacy::decrypt_aesgcm`](crate::legacy::decrypt_aesgcm). 60 | /// 61 | pub fn decrypt(components: &EcKeyComponents, auth: &[u8], data: &[u8]) -> Result> { 62 | let cryptographer = crypto::holder::get_cryptographer(); 63 | let priv_key = cryptographer.import_key_pair(components).unwrap(); 64 | aes128gcm::decrypt(&*priv_key, auth, data) 65 | } 66 | 67 | /// Generate a pair of keys; useful for writing tests. 68 | /// 69 | #[cfg(all(test, feature = "backend-openssl"))] 70 | fn generate_keys() -> Result<(Box, Box)> { 71 | let cryptographer = crypto::holder::get_cryptographer(); 72 | let local_key = cryptographer.generate_ephemeral_keypair()?; 73 | let remote_key = cryptographer.generate_ephemeral_keypair()?; 74 | Ok((local_key, remote_key)) 75 | } 76 | 77 | #[cfg(all(test, feature = "backend-openssl"))] 78 | mod aes128gcm_tests { 79 | use super::common::ECE_TAG_LENGTH; 80 | use super::*; 81 | 82 | #[allow(clippy::too_many_arguments)] 83 | fn try_encrypt( 84 | private_key: &str, 85 | public_key: &str, 86 | remote_pub_key: &str, 87 | auth_secret: &str, 88 | salt: &str, 89 | pad_length: usize, 90 | rs: u32, 91 | plaintext: &str, 92 | ) -> Result { 93 | let cryptographer = crypto::holder::get_cryptographer(); 94 | let private_key = hex::decode(private_key).unwrap(); 95 | let public_key = hex::decode(public_key).unwrap(); 96 | let ec_key = EcKeyComponents::new(private_key, public_key); 97 | let local_key_pair = cryptographer.import_key_pair(&ec_key)?; 98 | let remote_pub_key = hex::decode(remote_pub_key).unwrap(); 99 | let remote_pub_key = cryptographer.import_public_key(&remote_pub_key).unwrap(); 100 | let auth_secret = hex::decode(auth_secret).unwrap(); 101 | let salt = Some(hex::decode(salt).unwrap()); 102 | let plaintext = plaintext.as_bytes(); 103 | let params = WebPushParams { 104 | rs, 105 | pad_length, 106 | salt, 107 | }; 108 | let ciphertext = aes128gcm::encrypt( 109 | &*local_key_pair, 110 | &*remote_pub_key, 111 | &auth_secret, 112 | plaintext, 113 | params, 114 | )?; 115 | Ok(hex::encode(ciphertext)) 116 | } 117 | 118 | fn try_decrypt( 119 | private_key: &str, 120 | public_key: &str, 121 | auth_secret: &str, 122 | payload: &str, 123 | ) -> Result { 124 | let private_key = hex::decode(private_key).unwrap(); 125 | let public_key = hex::decode(public_key).unwrap(); 126 | let ec_key = EcKeyComponents::new(private_key, public_key); 127 | let plaintext = decrypt( 128 | &ec_key, 129 | &hex::decode(auth_secret).unwrap(), 130 | &hex::decode(payload).unwrap(), 131 | )?; 132 | Ok(String::from_utf8(plaintext).unwrap()) 133 | } 134 | 135 | #[test] 136 | fn test_keygen() { 137 | let cryptographer = crypto::holder::get_cryptographer(); 138 | cryptographer.generate_ephemeral_keypair().unwrap(); 139 | } 140 | 141 | #[test] 142 | fn test_e2e_through_public_api() { 143 | let (remote_key, auth_secret) = generate_keypair_and_auth_secret().unwrap(); 144 | let plaintext = b"When I grow up, I want to be a watermelon"; 145 | let ciphertext = 146 | encrypt(&remote_key.pub_as_raw().unwrap(), &auth_secret, plaintext).unwrap(); 147 | let decrypted = decrypt( 148 | &remote_key.raw_components().unwrap(), 149 | &auth_secret, 150 | &ciphertext, 151 | ) 152 | .unwrap(); 153 | assert_eq!(decrypted, plaintext.to_vec()); 154 | } 155 | 156 | #[test] 157 | fn test_e2e_large_plaintext() { 158 | let (remote_key, auth_secret) = generate_keypair_and_auth_secret().unwrap(); 159 | let plaintext = [0; 5000]; 160 | let ciphertext = 161 | encrypt(&remote_key.pub_as_raw().unwrap(), &auth_secret, &plaintext).unwrap(); 162 | let decrypted = decrypt( 163 | &remote_key.raw_components().unwrap(), 164 | &auth_secret, 165 | &ciphertext, 166 | ) 167 | .unwrap(); 168 | assert_eq!(decrypted, plaintext.to_vec()); 169 | } 170 | 171 | #[test] 172 | fn test_e2e_with_different_record_sizes_and_padding() { 173 | let (local_key, remote_key) = generate_keys().unwrap(); 174 | let plaintext = b"When I grow up, I want to be a watermelon"; 175 | let mut auth_secret = vec![0u8; 16]; 176 | let cryptographer = crypto::holder::get_cryptographer(); 177 | cryptographer.random_bytes(&mut auth_secret).unwrap(); 178 | let remote_public = cryptographer 179 | .import_public_key(&remote_key.pub_as_raw().unwrap()) 180 | .unwrap(); 181 | let plen = plaintext.len(); 182 | // Try a variety of different record sizes. The numbers here aren't particularly deeply 183 | // considered, just a selection of numbers that might be interesting. (Although they did 184 | // trigger a bunch of interesting edge-cases during development, which is re-assuring). 185 | for plaintext_rs in &[2, 3, 7, 8, plen - 1, plen, plen + 1, 1024, 8192] { 186 | let rs = (*plaintext_rs + ECE_TAG_LENGTH) as u32; 187 | // Try a variety of padding lengths. Again, not deeply considered numbers. 188 | for pad_length in &[0, 1, 2, 8, 37, 127, 128] { 189 | let pad_length = *pad_length; 190 | let params = WebPushParams { 191 | rs, 192 | pad_length, 193 | ..WebPushParams::default() 194 | }; 195 | let ciphertext = aes128gcm::encrypt( 196 | &*local_key, 197 | &*remote_public, 198 | &auth_secret, 199 | plaintext, 200 | params, 201 | ) 202 | .unwrap(); 203 | let decrypted = 204 | aes128gcm::decrypt(&*remote_key, &auth_secret, &ciphertext).unwrap(); 205 | assert_eq!(decrypted, plaintext.to_vec()); 206 | } 207 | } 208 | } 209 | 210 | #[test] 211 | fn test_conv_fn() -> Result<()> { 212 | let (local_key, auth) = generate_keypair_and_auth_secret()?; 213 | let plaintext = b"Mary had a little lamb, with some nice mint jelly"; 214 | let encoded = encrypt(&local_key.pub_as_raw()?, &auth, plaintext).unwrap(); 215 | let decoded = decrypt(&local_key.raw_components()?, &auth, &encoded)?; 216 | assert_eq!(decoded, plaintext.to_vec()); 217 | Ok(()) 218 | } 219 | 220 | #[test] 221 | fn try_encrypt_ietf_rfc() { 222 | let ciphertext = try_encrypt( 223 | "c9f58f89813e9f8e872e71f42aa64e1757c9254dcc62b72ddc010bb4043ea11c", 224 | "04fe33f4ab0dea71914db55823f73b54948f41306d920732dbb9a59a53286482200e597a7b7bc260ba1c227998580992e93973002f3012a28ae8f06bbb78e5ec0f", 225 | "042571b2becdfde360551aaf1ed0f4cd366c11cebe555f89bcb7b186a53339173168ece2ebe018597bd30479b86e3c8f8eced577ca59187e9246990db682008b0e", 226 | "05305932a1c7eabe13b6cec9fda48882", 227 | "0c6bfaadad67958803092d454676f397", 228 | 0, 229 | 4096, 230 | "When I grow up, I want to be a watermelon", 231 | ).unwrap(); 232 | assert_eq!(ciphertext, "0c6bfaadad67958803092d454676f397000010004104fe33f4ab0dea71914db55823f73b54948f41306d920732dbb9a59a53286482200e597a7b7bc260ba1c227998580992e93973002f3012a28ae8f06bbb78e5ec0ff297de5b429bba7153d3a4ae0caa091fd425f3b4b5414add8ab37a19c1bbb05cf5cb5b2a2e0562d558635641ec52812c6c8ff42e95ccb86be7cd"); 233 | } 234 | 235 | #[test] 236 | fn test_decrypt_ietf_rfc() { 237 | let plaintext = try_decrypt( 238 | "ab5757a70dd4a53e553a6bbf71ffefea2874ec07a6b379e3c48f895a02dc33de", 239 | "042571b2becdfde360551aaf1ed0f4cd366c11cebe555f89bcb7b186a53339173168ece2ebe018597bd30479b86e3c8f8eced577ca59187e9246990db682008b0e", 240 | "05305932a1c7eabe13b6cec9fda48882", 241 | "0c6bfaadad67958803092d454676f397000010004104fe33f4ab0dea71914db55823f73b54948f41306d920732dbb9a59a53286482200e597a7b7bc260ba1c227998580992e93973002f3012a28ae8f06bbb78e5ec0ff297de5b429bba7153d3a4ae0caa091fd425f3b4b5414add8ab37a19c1bbb05cf5cb5b2a2e0562d558635641ec52812c6c8ff42e95ccb86be7cd" 242 | ).unwrap(); 243 | assert_eq!(plaintext, "When I grow up, I want to be a watermelon"); 244 | } 245 | 246 | #[test] 247 | fn test_decrypt_rs_18_pad_0() { 248 | let plaintext = try_decrypt( 249 | "27433fab8970b3cb5284b61183efb46286562cd2a7330d8cae960911a5571d0c", 250 | "04515d4326355652399da24b2be9241e633b5cf14faf0cf3a6fd60317b954c0a2f4848548004b27b0cf7480bc810c6bec03a8fb79c8ea00fc8b05e00f8834563ef", 251 | "d65a04df95f2db5e604839f717dcde79", 252 | "7caebdbc20938ee340a946f1bd4f68f100000012410437cfdb5223d9f95eaa02f6ed940ff22eaf05b3622e949dc3ce9f335e6ef9b26aeaacca0f74080a8b364592f2ccc6d5eddd43004b70b91887d144d9fa93f16c3bc7ea68f4fd547a94eca84b16e138a6080177" 253 | ).unwrap(); 254 | assert_eq!(plaintext, "1"); 255 | } 256 | 257 | #[test] 258 | fn test_decrypt_missing_header_block() { 259 | let err = try_decrypt( 260 | "1be83f38332ef09681faf3f307b1ff2e10cab78cc7cdab683ac0ee92ac3f6ee1", 261 | "04dba991ca215343f36bdd3e857cafde3d18bf57f1835b2833bad414f0884162051ac96a0b24490037d07cf528e4e18e100a1a64eb744748544bf1e220dabacf2c", 262 | "3471bb98481e02533bf39542bcf3dba4", 263 | "45b74d2b69be9b074de3b35aa87e7c15611d", 264 | ) 265 | .unwrap_err(); 266 | match err { 267 | Error::HeaderTooShort => {} 268 | _ => panic!("Unexpected error {:?}", err), 269 | }; 270 | } 271 | 272 | #[test] 273 | fn test_decrypt_truncated_sender_key() { 274 | let err = try_decrypt( 275 | "ce88e8e0b3057a4752eb4c8fa931eb621c302da5ad03b81af459cf6735560cae", 276 | "04a325d99084c40de0ce722a042c448d94a32691721ca79e3cf745e78c69886194b02cea19224176795a9d4dbbb2073af2ccd6fa6f0a4c7c4968556be502a3ba81", 277 | "5c31e0d96d9a139899ac0969d359f740", 278 | "de5b696b87f1a15cb6adebdd79d6f99e000000120100b6bc1826c37c9f73dd6b4859c2b505181952", 279 | ) 280 | .unwrap_err(); 281 | match err { 282 | Error::InvalidKeyLength => {} 283 | _ => panic!("Unexpected error {:?}", err), 284 | }; 285 | } 286 | 287 | #[test] 288 | fn test_decrypt_truncated_auth_secret() { 289 | let err = try_decrypt( 290 | "60c7636a517de7039a0ac2d0e3064400794c78e7e049398129a227cee0f9a801", 291 | "04fdd04128a85c05896d7f81fe118bdcb887b9f3c1ff4183adc4c824d128607300e986b2dfb5a610e5af43e408a00730584f93e3dfddfc44737d5f08fb2d6f8916", 292 | "355a38cd6d9bef15990e2d3308dbd600", 293 | "8115f4988b8c392a7bacb43c8f1ac5650000001241041994483c541e9bc39a6af03ff713aa7745c284e138a42a2435b797b20c4b698cf5118b4f8555317c190eabebfab749c164d3f6bdebe0d441719131a357d8890a13c4dbd4b16ff3dd5a83f7c91ad6e040ac42730a7f0b3cd3245e9f8d6ff31c751d410cfd" 294 | ).unwrap_err(); 295 | match err { 296 | Error::OpenSSLError(_) => {} 297 | _ => panic!("Unexpected error {:?}", err), 298 | }; 299 | } 300 | 301 | #[test] 302 | fn test_decrypt_early_final_record() { 303 | let err = try_decrypt( 304 | "5dda1d918bc407ba3cda12cb8014d49aa7e0269002820304466bc80034ca9240", 305 | "04c95c6520dad11e8f6a1bf8031a40c2a4ee1045c1903be06a1dfa7f829cceb2de02481ae6bd0476121b12c5532d0b231788077efa0683a5bfe0d62339b251cb35", 306 | "40c241fde4269ee1e6d725592d982718", 307 | "dbe215507d1ad3d2eaeabeae6e874d8f0000001241047bc4343f34a8348cdc4e462ffc7c40aa6a8c61a739c4c41d45125505f70e9fc5f9efa86852dd488dcf8e8ea2cafb75e07abd5ee7c9d5c038bafef079571b0bda294411ce98c76dd031c0e580577a4980a375e45ed30429be0e2ee9da7e6df8696d01b8ec" 308 | ).unwrap_err(); 309 | match err { 310 | Error::DecryptPadding => {} 311 | _ => panic!("Unexpected error {:?}", err), 312 | }; 313 | } 314 | } 315 | --------------------------------------------------------------------------------