├── .appveyor.yml ├── .cargo-ok ├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── ux-report.md └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src ├── identity.rs ├── lib.rs ├── recipient.rs ├── shim.rs └── utils.rs ├── tests └── web.rs └── www ├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── public ├── index.html ├── mitm.html └── sw.js ├── src ├── App.vue ├── components │ ├── DecryptPane.vue │ ├── DropZone.vue │ ├── EncryptPane.vue │ └── FileInfo.vue ├── main.js └── polyfills.js └── vue.config.js /.appveyor.yml: -------------------------------------------------------------------------------- 1 | install: 2 | - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe 3 | - if not defined RUSTFLAGS rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly 4 | - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin 5 | - rustc -V 6 | - cargo -V 7 | 8 | build: false 9 | 10 | test_script: 11 | - cargo test --locked 12 | -------------------------------------------------------------------------------- /.cargo-ok: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/str4d/wage/1a647173026fece59d2985f2547befc93f3e5ffc/.cargo-ok -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report about a bug in this implementation. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Environment 11 | 12 | * OS: 13 | * Browser version: 14 | * wage version: 15 | 16 | ## What were you trying to do 17 | 18 | ## What happened 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ux-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: UX report 3 | about: Was wage hard to use? It's not you, it's us. We want to hear about it. 4 | title: 'UX: ' 5 | labels: 'UX report' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | 15 | ## What were you trying to do 16 | 17 | ## What happened 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI checks 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | name: Tests 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Install 13 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 14 | 15 | - run: cargo test 16 | - run: wasm-pack test --headless --chrome 17 | - run: wasm-pack test --headless --firefox 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | bin/ 5 | pkg/ 6 | wasm-pack.log 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | sudo: false 3 | 4 | cache: cargo 5 | 6 | matrix: 7 | include: 8 | 9 | # Builds with wasm-pack. 10 | - rust: beta 11 | env: RUST_BACKTRACE=1 12 | addons: 13 | firefox: latest 14 | chrome: stable 15 | before_script: 16 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 17 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 18 | - cargo install-update -a 19 | - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -s -- -f 20 | script: 21 | - cargo generate --git . --name testing 22 | # Having a broken Cargo.toml (in that it has curlies in fields) anywhere 23 | # in any of our parent dirs is problematic. 24 | - mv Cargo.toml Cargo.toml.tmpl 25 | - cd testing 26 | - wasm-pack build 27 | - wasm-pack test --chrome --firefox --headless 28 | 29 | # Builds on nightly. 30 | - rust: nightly 31 | env: RUST_BACKTRACE=1 32 | before_script: 33 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 34 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 35 | - cargo install-update -a 36 | - rustup target add wasm32-unknown-unknown 37 | script: 38 | - cargo generate --git . --name testing 39 | - mv Cargo.toml Cargo.toml.tmpl 40 | - cd testing 41 | - cargo check 42 | - cargo check --target wasm32-unknown-unknown 43 | - cargo check --no-default-features 44 | - cargo check --target wasm32-unknown-unknown --no-default-features 45 | - cargo check --no-default-features --features console_error_panic_hook 46 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 47 | - cargo check --no-default-features --features "console_error_panic_hook wee_alloc" 48 | - cargo check --target wasm32-unknown-unknown --no-default-features --features "console_error_panic_hook wee_alloc" 49 | 50 | # Builds on beta. 51 | - rust: beta 52 | env: RUST_BACKTRACE=1 53 | before_script: 54 | - (test -x $HOME/.cargo/bin/cargo-install-update || cargo install cargo-update) 55 | - (test -x $HOME/.cargo/bin/cargo-generate || cargo install --vers "^0.2" cargo-generate) 56 | - cargo install-update -a 57 | - rustup target add wasm32-unknown-unknown 58 | script: 59 | - cargo generate --git . --name testing 60 | - mv Cargo.toml Cargo.toml.tmpl 61 | - cd testing 62 | - cargo check 63 | - cargo check --target wasm32-unknown-unknown 64 | - cargo check --no-default-features 65 | - cargo check --target wasm32-unknown-unknown --no-default-features 66 | - cargo check --no-default-features --features console_error_panic_hook 67 | - cargo check --target wasm32-unknown-unknown --no-default-features --features console_error_panic_hook 68 | # Note: no enabling the `wee_alloc` feature here because it requires 69 | # nightly for now. 70 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wage" 3 | version = "0.0.0" 4 | authors = ["Jack Grigg "] 5 | edition = "2018" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "rlib"] 9 | 10 | [features] 11 | default = ["console_error_panic_hook"] 12 | 13 | [dependencies] 14 | chrono = { version = "0.4", features = ["wasmbind"] } 15 | futures = "0.3" 16 | # We need to have a dependency on every version of getrandom in our tree, to set 17 | # their corresponding feature flags to enable WASM support. 18 | getrandom_1 = { package = "getrandom", version = "0.1", features = ["wasm-bindgen"] } 19 | getrandom = { version = "0.2", features = ["js"] } 20 | i18n-embed = { version = "0.13", features = ["fluent-system", "web-sys-requester"] } 21 | js-sys = "0.3" 22 | pin-project = "1" 23 | wasm-bindgen = "0.2.63" 24 | wasm-bindgen-futures = "0.4" 25 | wasm-streams = "0.3" 26 | 27 | # The `console_error_panic_hook` crate provides better debugging of panics by 28 | # logging them with `console.error`. This is great for development, but requires 29 | # all the `std::fmt` and `std::panicking` infrastructure, so isn't great for 30 | # code size when deploying. 31 | console_error_panic_hook = { version = "0.1.1", optional = true } 32 | 33 | # `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size 34 | # compared to the default allocator's ~10K. It is slower than the default 35 | # allocator, however. 36 | # 37 | # Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now. 38 | wee_alloc = { version = "0.4.2", optional = true } 39 | 40 | [dependencies.age] 41 | version = "0.9" 42 | features = ["armor", "async", "web-sys"] 43 | 44 | [dependencies.web-sys] 45 | version = "0.3" 46 | features = [ 47 | "Blob", 48 | "BlobPropertyBag", 49 | "File", 50 | "ReadableStream", 51 | "console", 52 | ] 53 | 54 | [dev-dependencies] 55 | wasm-bindgen-test = "0.3" 56 | 57 | [profile.release] 58 | # Tell `rustc` to optimize for small code size. 59 | opt-level = "s" 60 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Jack Grigg 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wage 2 | 3 | A WASM package and web app for encrypting and decrypting age-encrypted files, 4 | powered by [rage](https://github.com/str4d/rage). 5 | 6 | Currently in beta. The WASM library and webapp are mostly complete, but their 7 | APIs and UX are still expected to change as improvements are made. Known missing 8 | features: 9 | 10 | - [ ] Multi-file archive-and-encrypt. 11 | - [ ] Optional armoring for encryption. 12 | 13 | ## Development 14 | 15 | First, build `wage` itself as a Rust WASM package: 16 | ``` 17 | wasm-pack build 18 | ``` 19 | 20 | Then set up and run the webapp: 21 | ``` 22 | cd www 23 | npm install 24 | npm run serve 25 | ``` 26 | 27 | The webapp server will hot-reload on changes to the webapp itself. After 28 | making changes to the Rust WASM package, rebuild the package and restart 29 | the server: 30 | ``` 31 | [Ctrl+C] 32 | cd .. 33 | wasm-pack build 34 | cd www 35 | npm run serve 36 | ``` 37 | 38 | File downloading for encrypted or decrypted files will likely not work unless 39 | you are either accessing the webapp via localhost, or have configured it with a 40 | TLS certificate. 41 | 42 | ## License 43 | 44 | Licensed under either of 45 | 46 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 47 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 48 | 49 | at your option. 50 | 51 | ### Contribution 52 | 53 | Unless you explicitly state otherwise, any contribution intentionally 54 | submitted for inclusion in the work by you, as defined in the Apache-2.0 55 | license, shall be dual licensed as above, without any additional terms or 56 | conditions. 57 | 58 | -------------------------------------------------------------------------------- /src/identity.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use age::x25519; 4 | use futures::{io::BufReader, AsyncBufReadExt, StreamExt, TryStreamExt}; 5 | use js_sys::Uint8Array; 6 | use wasm_bindgen::{JsCast, JsValue}; 7 | use wasm_streams::ReadableStream; 8 | 9 | fn parse_identity(s: &str) -> Result, JsValue> { 10 | if let Ok(sk) = s.parse::() { 11 | Ok(Box::new(sk)) 12 | } else { 13 | Err(JsValue::from( 14 | "String does not contain a supported identity", 15 | )) 16 | } 17 | } 18 | 19 | /// Reads file contents as a list of identities 20 | pub(crate) async fn read_identities_list( 21 | file: web_sys::File, 22 | ) -> Result>, JsValue> { 23 | // Convert from the opaque web_sys::ReadableStream Rust type to the fully-functional 24 | // wasm_streams::readable::ReadableStream. 25 | // TODO: Switching from ponyfill to polyfill causes `.dyn_into().unwrap_throw()` 26 | // to throw, while `.unchecked_into()` works fine. I do not understand why :( 27 | let stream = ReadableStream::from_raw(file.stream().unchecked_into()); 28 | 29 | let buf = BufReader::new( 30 | stream 31 | .into_stream() 32 | .map_ok(|chunk| Uint8Array::from(chunk).to_vec()) 33 | .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("JS error: {:?}", e))) 34 | .into_async_read(), 35 | ); 36 | 37 | let mut lines = buf.lines().enumerate(); 38 | 39 | let mut identities = vec![]; 40 | while let Some((line_number, line)) = lines.next().await { 41 | let line = line.map_err(|e| JsValue::from(format!("{}", e)))?; 42 | 43 | // Skip empty lines and comments 44 | if line.is_empty() || line.find('#') == Some(0) { 45 | continue; 46 | } 47 | 48 | match parse_identity(&line) { 49 | Ok(kind) => identities.push(kind), 50 | Err(_) => { 51 | // Return a line number in place of the line, so we don't leak the file 52 | // contents in error messages. 53 | return Err(JsValue::from(format!( 54 | "identities file {} contains non-identity data on line {}", 55 | file.name(), 56 | line_number + 1 57 | ))); 58 | } 59 | } 60 | } 61 | 62 | Ok(identities) 63 | } 64 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod identity; 2 | mod recipient; 3 | mod shim; 4 | mod utils; 5 | 6 | use age::secrecy::{ExposeSecret, SecretString}; 7 | use futures::AsyncRead; 8 | use js_sys::Array; 9 | use wasm_bindgen::prelude::*; 10 | use wasm_bindgen::JsCast; 11 | use wasm_streams::{readable::ReadableStream, writable::WritableStream}; 12 | use web_sys::{Blob, BlobPropertyBag}; 13 | 14 | // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global 15 | // allocator. 16 | #[cfg(feature = "wee_alloc")] 17 | #[global_allocator] 18 | static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; 19 | 20 | const CHUNK_SIZE: usize = 65536; 21 | 22 | /// The standard age identity. 23 | #[wasm_bindgen] 24 | pub struct X25519Identity { 25 | identity: age::x25519::Identity, 26 | created: chrono::DateTime, 27 | } 28 | 29 | #[wasm_bindgen] 30 | impl X25519Identity { 31 | /// Generates a new age identity. 32 | pub fn generate() -> Self { 33 | // This is an entrance from JS to our WASM APIs; perform one-time setup steps. 34 | utils::set_panic_hook(); 35 | utils::select_language(); 36 | 37 | X25519Identity { 38 | identity: age::x25519::Identity::generate(), 39 | created: chrono::Local::now(), 40 | } 41 | } 42 | 43 | /// Writes this identity to a blob that can be saved as a file. 44 | pub fn write(&self) -> Result { 45 | let output = format!( 46 | "# created: {}\n# recipient: {}\n{}", 47 | self.created 48 | .to_rfc3339_opts(chrono::SecondsFormat::Secs, true), 49 | self.identity.to_public(), 50 | self.identity.to_string().expose_secret() 51 | ); 52 | 53 | Blob::new_with_u8_array_sequence_and_options( 54 | &Array::of1(&JsValue::from_str(&output)).into(), 55 | &BlobPropertyBag::new().type_("text/plain;charset=utf-8"), 56 | ) 57 | } 58 | 59 | /// Returns the recipient corresponding to this identity. 60 | pub fn recipient(&self) -> String { 61 | self.identity.to_public().to_string() 62 | } 63 | } 64 | 65 | /// A set of identities with which an age file can be decrypted. 66 | #[wasm_bindgen] 67 | pub struct Identities(Vec>); 68 | 69 | #[wasm_bindgen] 70 | impl Identities { 71 | /// Creates a new set containing the given identities file. 72 | /// 73 | /// Returns an error if the file is not an identities file. 74 | pub async fn from_file(file: web_sys::File) -> Result { 75 | // This is an entrance from JS to our WASM APIs; perform one-time setup steps. 76 | utils::set_panic_hook(); 77 | utils::select_language(); 78 | 79 | Identities(vec![]).add_file(file).await 80 | } 81 | 82 | /// Adds the given identities file to this set of identities. 83 | /// 84 | /// Returns an error if the file is not a identities file. 85 | pub async fn add_file(mut self, file: web_sys::File) -> Result { 86 | self.0.extend(identity::read_identities_list(file).await?); 87 | Ok(self) 88 | } 89 | 90 | /// Merge two sets of identities. 91 | pub fn merge(mut self, other: Identities) -> Identities { 92 | self.0.extend(other.0); 93 | self 94 | } 95 | } 96 | 97 | /// A set of recipients to which an age file can be encrypted. 98 | #[wasm_bindgen] 99 | pub struct Recipients(Vec); 100 | 101 | #[wasm_bindgen] 102 | impl Recipients { 103 | /// Creates a new set containing the given recipient file. 104 | /// 105 | /// Returns an error if the file is not a recipients file. 106 | pub async fn from_file(file: web_sys::File) -> Result { 107 | // This is an entrance from JS to our WASM APIs; perform one-time setup steps. 108 | utils::set_panic_hook(); 109 | utils::select_language(); 110 | 111 | Recipients(vec![]).add_file(file).await 112 | } 113 | 114 | /// Creates a new set containing the given recipient. 115 | /// 116 | /// Returns an error if the string is not a valid recipient. 117 | pub fn from_recipient(recipient: &str) -> Result { 118 | // This is an entrance from JS to our WASM APIs; perform one-time setup steps. 119 | utils::set_panic_hook(); 120 | utils::select_language(); 121 | 122 | recipient::from_string(recipient).map(|r| Recipients(vec![r])) 123 | } 124 | 125 | /// Adds the given recipients file to this set of recipients. 126 | /// 127 | /// Returns an error if the file is not a recipients file. 128 | pub async fn add_file(mut self, file: web_sys::File) -> Result { 129 | self.0.push(recipient::read_recipients_list(file).await?); 130 | Ok(self) 131 | } 132 | 133 | /// Adds the given recipient to this set of recipients. 134 | /// 135 | /// Returns an error if the string is not a valid recipient. 136 | pub fn add_recipient(mut self, recipient: &str) -> Result { 137 | self.0.push(recipient::from_string(recipient)?); 138 | Ok(self) 139 | } 140 | 141 | /// Merge two sets of recipients. De-duplication is not performed. 142 | pub fn merge(mut self, other: Recipients) -> Recipients { 143 | self.0.extend(other.0); 144 | self 145 | } 146 | 147 | /// Returns an `Encryptor` that will create an age file encrypted to the list of 148 | /// recipients, or `None` if this set of recipients is empty. 149 | pub fn into_encryptor(self) -> Option { 150 | let mut recipients: Vec<_> = self 151 | .0 152 | .into_iter() 153 | .map(|s| match s { 154 | recipient::Source::File { recipients } => recipients, 155 | recipient::Source::String(k) => vec![k], 156 | }) 157 | .flatten() 158 | .collect(); 159 | recipients.sort_unstable(); 160 | recipients.dedup(); 161 | 162 | age::Encryptor::with_recipients( 163 | recipients 164 | .into_iter() 165 | .map(|k| match k { 166 | recipient::Kind::Native(r) => Box::new(r) as Box, 167 | }) 168 | .collect(), 169 | ) 170 | .map(Encryptor) 171 | } 172 | } 173 | 174 | /// A newtype around an [`age::Encryptor`]. 175 | #[wasm_bindgen] 176 | pub struct Encryptor(age::Encryptor); 177 | 178 | #[wasm_bindgen] 179 | impl Encryptor { 180 | /// Returns an `Encryptor` that will create an age file encrypted with a passphrase. 181 | /// 182 | /// This API should only be used with a passphrase that was provided by (or generated 183 | /// for) a human. For programmatic use cases, instead generate a `SecretKey` and then 184 | /// use `Encryptor::with_recipients`. 185 | pub fn with_user_passphrase(passphrase: String) -> Encryptor { 186 | // This is an entrance from JS to our WASM APIs; perform one-time setup steps. 187 | utils::set_panic_hook(); 188 | utils::select_language(); 189 | 190 | Encryptor(age::Encryptor::with_user_passphrase(SecretString::new( 191 | passphrase, 192 | ))) 193 | } 194 | 195 | /// Creates a wrapper around a writer that will encrypt its input. 196 | /// 197 | /// Returns errors from the underlying writer while writing the header. 198 | pub async fn wrap_output( 199 | self, 200 | output: wasm_streams::writable::sys::WritableStream, 201 | ) -> Result { 202 | // Convert from the opaque web_sys::WritableStream Rust type to the fully-functional 203 | // wasm_streams::writable::WritableStream. 204 | let stream = WritableStream::from_raw(output); 205 | 206 | let writer = self 207 | .0 208 | .wrap_async_output(stream.into_async_write()) 209 | .await 210 | .map_err(|e| JsValue::from(format!("{}", e)))?; 211 | 212 | Ok(WritableStream::from_sink(shim::WriteSinker::new(writer)).into_raw()) 213 | } 214 | } 215 | 216 | /// A newtype around an [`age::Decryptor`]. 217 | #[wasm_bindgen] 218 | pub struct Decryptor(age::Decryptor>); 219 | 220 | #[wasm_bindgen] 221 | pub enum DecryptorType { 222 | Recipients, 223 | Passphrase, 224 | } 225 | 226 | #[wasm_bindgen] 227 | impl Decryptor { 228 | /// Attempts to parse the given file as an age-encrypted file, and returns a decryptor. 229 | pub async fn new(file: web_sys::File) -> Result { 230 | // This is an entrance from JS to our WASM APIs; perform one-time setup steps. 231 | utils::set_panic_hook(); 232 | utils::select_language(); 233 | 234 | // Convert from the opaque web_sys::ReadableStream Rust type to the fully-functional 235 | // wasm_streams::readable::ReadableStream. 236 | // TODO: Switching from ponyfill to polyfill causes `.dyn_into().unwrap_throw()` 237 | // to throw, while `.unchecked_into()` works fine. I do not understand why :( 238 | let stream = ReadableStream::from_raw(file.stream().unchecked_into()); 239 | 240 | let reader: Box = Box::new( 241 | age::armor::ArmoredReader::from_async_reader(stream.into_async_read()), 242 | ); 243 | 244 | let inner = age::Decryptor::new_async(reader) 245 | .await 246 | .map_err(|e| JsValue::from_str(&format!("{}", e)))?; 247 | 248 | Ok(Decryptor(inner)) 249 | } 250 | 251 | /// Returns the type of this decryptor, indicating what is required to decrypt this 252 | /// file. 253 | /// 254 | /// - `DecryptorType::Recipients` if the file was encrypted to a list of recipients, 255 | /// and requires identities for decryption. 256 | /// - `DecryptorType::Passphrase` if the file was encrypted to a passphrase. 257 | pub fn requires(&self) -> DecryptorType { 258 | match self.0 { 259 | age::Decryptor::Recipients(_) => DecryptorType::Recipients, 260 | age::Decryptor::Passphrase(_) => DecryptorType::Passphrase, 261 | } 262 | } 263 | 264 | /// Consumes the decryptor and returns the decrypted stream. 265 | /// 266 | /// Panics if `self.requires() == DecryptorType::Passphrase`. 267 | pub async fn decrypt_with_identities( 268 | self, 269 | identities: Identities, 270 | ) -> Result { 271 | let decryptor = match self.0 { 272 | age::Decryptor::Recipients(d) => d, 273 | age::Decryptor::Passphrase(_) => panic!("Shouldn't be called"), 274 | }; 275 | 276 | let reader = decryptor 277 | .decrypt_async(identities.0.iter().map(|i| &**i)) 278 | .map_err(|e| JsValue::from(format!("{}", e)))?; 279 | 280 | Ok(ReadableStream::from_async_read(reader, CHUNK_SIZE).into_raw()) 281 | } 282 | 283 | /// Consumes the decryptor and returns the decrypted stream. 284 | /// 285 | /// Panics if `self.requires() == DecryptorType::Recipients`. 286 | pub async fn decrypt_with_passphrase( 287 | self, 288 | passphrase: String, 289 | ) -> Result { 290 | let decryptor = match self.0 { 291 | age::Decryptor::Recipients(_) => panic!("Shouldn't be called"), 292 | age::Decryptor::Passphrase(d) => d, 293 | }; 294 | 295 | let reader = decryptor 296 | .decrypt_async(&SecretString::new(passphrase), None) 297 | .map_err(|e| JsValue::from(format!("{}", e)))?; 298 | 299 | Ok(ReadableStream::from_async_read(reader, CHUNK_SIZE).into_raw()) 300 | } 301 | } 302 | -------------------------------------------------------------------------------- /src/recipient.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use age::x25519; 4 | use futures::{io::BufReader, AsyncBufReadExt, StreamExt, TryStreamExt}; 5 | use js_sys::Uint8Array; 6 | use wasm_bindgen::{JsCast, JsValue}; 7 | use wasm_streams::ReadableStream; 8 | 9 | pub(crate) enum Source { 10 | File { recipients: Vec }, 11 | String(Kind), 12 | } 13 | 14 | pub(crate) enum Kind { 15 | Native(x25519::Recipient), 16 | } 17 | 18 | impl PartialEq for Kind { 19 | fn eq(&self, other: &Self) -> bool { 20 | match (self, other) { 21 | (Kind::Native(a), Kind::Native(b)) => a.to_string().eq(&b.to_string()), 22 | } 23 | } 24 | } 25 | 26 | impl Eq for Kind {} 27 | 28 | impl PartialOrd for Kind { 29 | fn partial_cmp(&self, other: &Self) -> Option { 30 | Some(self.cmp(other)) 31 | } 32 | } 33 | 34 | impl Ord for Kind { 35 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 36 | match (self, other) { 37 | (Kind::Native(a), Kind::Native(b)) => a.to_string().cmp(&b.to_string()), 38 | } 39 | } 40 | } 41 | 42 | fn parse_recipient(s: &str) -> Result { 43 | if let Ok(pk) = s.parse::() { 44 | Ok(Kind::Native(pk)) 45 | } else { 46 | Err(JsValue::from( 47 | "String does not contain a supported recipient", 48 | )) 49 | } 50 | } 51 | 52 | /// Parses a recipient from a string. 53 | pub(crate) fn from_string(s: &str) -> Result { 54 | parse_recipient(s).map(Source::String) 55 | } 56 | 57 | /// Reads file contents as a list of recipients 58 | pub(crate) async fn read_recipients_list(file: web_sys::File) -> Result { 59 | // Convert from the opaque web_sys::ReadableStream Rust type to the fully-functional 60 | // wasm_streams::readable::ReadableStream. 61 | // TODO: Switching from ponyfill to polyfill causes `.dyn_into().unwrap_throw()` 62 | // to throw, while `.unchecked_into()` works fine. I do not understand why :( 63 | let stream = ReadableStream::from_raw(file.stream().unchecked_into()); 64 | 65 | let buf = BufReader::new( 66 | stream 67 | .into_stream() 68 | .map_ok(|chunk| Uint8Array::from(chunk).to_vec()) 69 | .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("JS error: {:?}", e))) 70 | .into_async_read(), 71 | ); 72 | 73 | let mut lines = buf.lines().enumerate(); 74 | 75 | let mut recipients = vec![]; 76 | while let Some((line_number, line)) = lines.next().await { 77 | let line = line.map_err(|e| JsValue::from(format!("{}", e)))?; 78 | 79 | // Skip empty lines and comments 80 | if line.is_empty() || line.find('#') == Some(0) { 81 | continue; 82 | } 83 | 84 | match parse_recipient(&line) { 85 | Ok(kind) => recipients.push(kind), 86 | Err(_) => { 87 | // Return a line number in place of the line, so we don't leak the file 88 | // contents in error messages. 89 | return Err(JsValue::from(format!( 90 | "recipients file {} contains non-recipient data on line {}", 91 | file.name(), 92 | line_number + 1 93 | ))); 94 | } 95 | } 96 | } 97 | 98 | Ok(Source::File { recipients }) 99 | } 100 | -------------------------------------------------------------------------------- /src/shim.rs: -------------------------------------------------------------------------------- 1 | use age::secrecy::{ExposeSecret, SecretVec}; 2 | use futures::{ 3 | io::AsyncWrite, 4 | ready, 5 | sink::Sink, 6 | task::{Context, Poll}, 7 | }; 8 | use js_sys::Uint8Array; 9 | use pin_project::pin_project; 10 | use std::pin::Pin; 11 | use wasm_bindgen::prelude::*; 12 | 13 | struct Chunk { 14 | bytes: SecretVec, 15 | offset: usize, 16 | } 17 | 18 | /// Wraps an `age::stream::StreamWriter` in a chunked `Sink` interface. 19 | #[pin_project(project = WriteSinkerProj)] 20 | pub(crate) struct WriteSinker { 21 | #[pin] 22 | writer: W, 23 | chunk: Option, 24 | } 25 | 26 | impl WriteSinker { 27 | pub(crate) fn new(writer: W) -> Self { 28 | WriteSinker { 29 | writer, 30 | chunk: None, 31 | } 32 | } 33 | 34 | fn poll_write_chunk(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 35 | let WriteSinkerProj { mut writer, chunk } = self.project(); 36 | 37 | if let Some(chunk) = chunk.as_mut() { 38 | loop { 39 | chunk.offset += ready!(writer 40 | .as_mut() 41 | .poll_write(cx, &chunk.bytes.expose_secret()[chunk.offset..])) 42 | .map_err(|e| JsValue::from(format!("{}", e)))?; 43 | if chunk.offset == chunk.bytes.expose_secret().len() { 44 | break; 45 | } 46 | } 47 | } 48 | *chunk = None; 49 | 50 | Poll::Ready(Ok(())) 51 | } 52 | } 53 | 54 | impl Sink for WriteSinker { 55 | type Error = JsValue; 56 | 57 | fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 58 | self.as_mut().poll_write_chunk(cx) 59 | } 60 | 61 | fn start_send(mut self: Pin<&mut Self>, chunk: JsValue) -> Result<(), Self::Error> { 62 | if self.chunk.is_none() { 63 | self.chunk = Some(Chunk { 64 | bytes: SecretVec::new(Uint8Array::from(chunk).to_vec()), 65 | offset: 0, 66 | }); 67 | Ok(()) 68 | } else { 69 | Err(JsValue::from_str( 70 | "Called WriteSinker::start_send while not ready", 71 | )) 72 | } 73 | } 74 | 75 | fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 76 | ready!(self.as_mut().poll_write_chunk(cx))?; 77 | self.project() 78 | .writer 79 | .poll_flush(cx) 80 | .map_err(|e| JsValue::from(format!("{}", e))) 81 | } 82 | 83 | fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 84 | ready!(self.as_mut().poll_write_chunk(cx))?; 85 | self.project() 86 | .writer 87 | .poll_close(cx) 88 | .map_err(|e| JsValue::from(format!("{}", e))) 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use i18n_embed::WebLanguageRequester; 2 | use std::sync::Once; 3 | 4 | pub fn set_panic_hook() { 5 | // When the `console_error_panic_hook` feature is enabled, we can call the 6 | // `set_panic_hook` function at least once during initialization, and then 7 | // we will get better error messages if our code ever panics. 8 | // 9 | // For more details see 10 | // https://github.com/rustwasm/console_error_panic_hook#readme 11 | #[cfg(feature = "console_error_panic_hook")] 12 | console_error_panic_hook::set_once(); 13 | } 14 | 15 | pub fn select_language() { 16 | static SET_HOOK: Once = Once::new(); 17 | SET_HOOK.call_once(|| { 18 | let requested_languages = WebLanguageRequester::requested_languages(); 19 | age::localizer().select(&requested_languages).unwrap(); 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /tests/web.rs: -------------------------------------------------------------------------------- 1 | //! Test suite for the Web and headless browsers. 2 | 3 | #![cfg(target_arch = "wasm32")] 4 | 5 | extern crate wasm_bindgen_test; 6 | use wasm_bindgen_test::*; 7 | 8 | wasm_bindgen_test_configure!(run_in_browser); 9 | 10 | #[wasm_bindgen_test] 11 | fn pass() { 12 | assert_eq!(1 + 1, 2); 13 | } 14 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 | # rage-encrypt-app 2 | 3 | An example webapp using the `wage` JavaScript library. 4 | 5 | ## Project setup 6 | ``` 7 | npm install 8 | ``` 9 | 10 | ### Compiles and hot-reloads for development 11 | ``` 12 | npm run serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | ``` 17 | npm run build 18 | ``` 19 | 20 | ### Lints and fixes files 21 | ``` 22 | npm run lint 23 | ``` 24 | 25 | ### Customize configuration 26 | See [Configuration Reference](https://cli.vuejs.org/config/). 27 | -------------------------------------------------------------------------------- /www/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rage-encrypt-app", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@fortawesome/fontawesome-svg-core": "^1.2.32", 12 | "@fortawesome/free-solid-svg-icons": "^5.15.1", 13 | "@fortawesome/vue-fontawesome": "^2.0.0", 14 | "@types/streamsaver": "^2.0.0", 15 | "buefy": "^0.9.4", 16 | "core-js": "^3.6.4", 17 | "file-saver": "^2.0.5", 18 | "font-awesome-filetypes": "^2.1.0", 19 | "streamsaver": "^2.0.5", 20 | "vue": "^2.6.12", 21 | "wage": "file:../pkg", 22 | "web-streams-polyfill": "^3.0.0" 23 | }, 24 | "devDependencies": { 25 | "@vue/cli-plugin-babel": "~4.5.0", 26 | "@vue/cli-plugin-eslint": "~4.5.0", 27 | "@vue/cli-service": "~4.5.0", 28 | "babel-eslint": "^10.1.0", 29 | "eslint": "^6.7.2", 30 | "eslint-plugin-vue": "^6.2.2", 31 | "vue-template-compiler": "^2.6.12" 32 | }, 33 | "eslintConfig": { 34 | "root": true, 35 | "env": { 36 | "node": true 37 | }, 38 | "extends": [ 39 | "plugin:vue/essential", 40 | "eslint:recommended" 41 | ], 42 | "parserOptions": { 43 | "parser": "babel-eslint" 44 | }, 45 | "rules": {} 46 | }, 47 | "browserslist": [ 48 | "> 1%", 49 | "last 2 versions", 50 | "not dead" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /www/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | rage-encry.pt 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /www/public/mitm.html: -------------------------------------------------------------------------------- 1 | 14 | 167 | -------------------------------------------------------------------------------- /www/public/sw.js: -------------------------------------------------------------------------------- 1 | /* global self ReadableStream Response */ 2 | 3 | self.addEventListener('install', () => { 4 | self.skipWaiting() 5 | }) 6 | 7 | self.addEventListener('activate', event => { 8 | event.waitUntil(self.clients.claim()) 9 | }) 10 | 11 | const map = new Map() 12 | 13 | // This should be called once per download 14 | // Each event has a dataChannel that the data will be piped through 15 | self.onmessage = event => { 16 | // We send a heartbeat every x secound to keep the 17 | // service worker alive if a transferable stream is not sent 18 | if (event.data === 'ping') { 19 | return 20 | } 21 | 22 | const data = event.data 23 | const downloadUrl = data.url || self.registration.scope + Math.random() + '/' + (typeof data === 'string' ? data : data.filename) 24 | const port = event.ports[0] 25 | const metadata = new Array(3) // [stream, data, port] 26 | 27 | metadata[1] = data 28 | metadata[2] = port 29 | 30 | // Note to self: 31 | // old streamsaver v1.2.0 might still use `readableStream`... 32 | // but v2.0.0 will always transfer the stream throught MessageChannel #94 33 | if (event.data.readableStream) { 34 | metadata[0] = event.data.readableStream 35 | } else if (event.data.transferringReadable) { 36 | port.onmessage = evt => { 37 | port.onmessage = null 38 | metadata[0] = evt.data.readableStream 39 | } 40 | } else { 41 | metadata[0] = createStream(port) 42 | } 43 | 44 | map.set(downloadUrl, metadata) 45 | port.postMessage({ download: downloadUrl }) 46 | } 47 | 48 | function createStream (port) { 49 | // ReadableStream is only supported by chrome 52 50 | return new ReadableStream({ 51 | start (controller) { 52 | // When we receive data on the messageChannel, we write 53 | port.onmessage = ({ data }) => { 54 | if (data === 'end') { 55 | return controller.close() 56 | } 57 | 58 | if (data === 'abort') { 59 | controller.error('Aborted the download') 60 | return 61 | } 62 | 63 | controller.enqueue(data) 64 | } 65 | }, 66 | cancel () { 67 | console.log('user aborted') 68 | } 69 | }) 70 | } 71 | 72 | self.onfetch = event => { 73 | const url = event.request.url 74 | 75 | // this only works for Firefox 76 | if (url.endsWith('/ping')) { 77 | return event.respondWith(new Response('pong')) 78 | } 79 | 80 | const hijacke = map.get(url) 81 | 82 | if (!hijacke) return null 83 | 84 | const [ stream, data, port ] = hijacke 85 | 86 | map.delete(url) 87 | 88 | // Not comfortable letting any user control all headers 89 | // so we only copy over the length & disposition 90 | const responseHeaders = new Headers({ 91 | 'Content-Type': 'application/octet-stream; charset=utf-8', 92 | 93 | // To be on the safe side, The link can be opened in a iframe. 94 | // but octet-stream should stop it. 95 | 'Content-Security-Policy': "default-src 'none'", 96 | 'X-Content-Security-Policy': "default-src 'none'", 97 | 'X-WebKit-CSP': "default-src 'none'", 98 | 'X-XSS-Protection': '1; mode=block' 99 | }) 100 | 101 | let headers = new Headers(data.headers || {}) 102 | 103 | if (headers.has('Content-Length')) { 104 | responseHeaders.set('Content-Length', headers.get('Content-Length')) 105 | } 106 | 107 | if (headers.has('Content-Disposition')) { 108 | responseHeaders.set('Content-Disposition', headers.get('Content-Disposition')) 109 | } 110 | 111 | // data, data.filename and size should not be used anymore 112 | if (data.size) { 113 | console.warn('Depricated') 114 | responseHeaders.set('Content-Length', data.size) 115 | } 116 | 117 | let fileName = typeof data === 'string' ? data : data.filename 118 | if (fileName) { 119 | console.warn('Depricated') 120 | // Make filename RFC5987 compatible 121 | fileName = encodeURIComponent(fileName).replace(/['()]/g, escape).replace(/\*/g, '%2A') 122 | responseHeaders.set('Content-Disposition', "attachment; filename*=UTF-8''" + fileName) 123 | } 124 | 125 | event.respondWith(new Response(stream, { headers: responseHeaders })) 126 | 127 | port.postMessage({ debug: 'Download started' }) 128 | } 129 | -------------------------------------------------------------------------------- /www/src/App.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 354 | 355 | 382 | -------------------------------------------------------------------------------- /www/src/components/DecryptPane.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 143 | -------------------------------------------------------------------------------- /www/src/components/DropZone.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 52 | -------------------------------------------------------------------------------- /www/src/components/EncryptPane.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 238 | -------------------------------------------------------------------------------- /www/src/components/FileInfo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | 25 | 33 | -------------------------------------------------------------------------------- /www/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | 4 | import streamSaver from "streamsaver"; 5 | streamSaver.mitm = new URL(window.location); 6 | streamSaver.mitm.pathname = 'mitm.html' 7 | streamSaver.mitm.search = `version=${streamSaver.version.full}` 8 | window.streamSaver = streamSaver; 9 | 10 | import { library } from "@fortawesome/fontawesome-svg-core"; 11 | import { 12 | faArrowUp, 13 | faExclamationCircle, 14 | faEye, 15 | faEyeSlash, 16 | faFile, 17 | faFileAlt, 18 | faFileArchive, 19 | faFileAudio, 20 | faFileCode, 21 | faFileCsv, 22 | faFileExcel, 23 | faFileImage, 24 | faFilePdf, 25 | faFilePowerpoint, 26 | faFileVideo, 27 | faFileWord, 28 | faKey, 29 | faTrash, 30 | faUpload, 31 | } from "@fortawesome/free-solid-svg-icons"; 32 | library.add(faArrowUp); 33 | library.add(faExclamationCircle); 34 | library.add(faEye); 35 | library.add(faEyeSlash); 36 | library.add(faFile); 37 | library.add(faFileAlt); 38 | library.add(faFileArchive); 39 | library.add(faFileAudio); 40 | library.add(faFileCode); 41 | library.add(faFileCsv); 42 | library.add(faFileExcel); 43 | library.add(faFileImage); 44 | library.add(faFilePdf); 45 | library.add(faFilePowerpoint); 46 | library.add(faFileVideo); 47 | library.add(faFileWord); 48 | library.add(faKey); 49 | library.add(faTrash); 50 | library.add(faUpload); 51 | 52 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; 53 | Vue.component('font-awesome-icon', FontAwesomeIcon); 54 | 55 | import Buefy from 'buefy' 56 | import 'buefy/dist/buefy.css' 57 | Vue.use(Buefy, { 58 | defaultIconComponent: 'font-awesome-icon', 59 | defaultIconPack: 'fas', 60 | }); 61 | 62 | Vue.config.productionTip = false 63 | 64 | new Vue({ 65 | render: h => h(App), 66 | }).$mount('#app') 67 | -------------------------------------------------------------------------------- /www/src/polyfills.js: -------------------------------------------------------------------------------- 1 | import 'web-streams-polyfill/es6'; 2 | -------------------------------------------------------------------------------- /www/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | chainWebpack: config => { 3 | config.entry('polyfills').add('./src/polyfills.js').end(); 4 | config.plugin('html').tap(args => { 5 | // Load polyfills before the main app. 6 | args[0].chunksSortMode = function (a, b) { 7 | var order = ["polyfills", "app"]; 8 | return order.indexOf(a.names[0]) - order.indexOf(b.names[0]); 9 | }; 10 | return args; 11 | }); 12 | config.resolve.symlinks(false); 13 | } 14 | } 15 | --------------------------------------------------------------------------------