Skip to content

Commit ccd8fd7

Browse files
committed
initial public commit
0 parents  commit ccd8fd7

24 files changed

+2563
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.idea
2+
test_age.txt
3+
arcus-thunderbird-*.zip

Containerfile

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
FROM fedora:36
2+
3+
RUN dnf install -y git perl \
4+
rust cargo pkg-config openssl openssl-devel \
5+
rust-std-static-wasm32-unknown-unknown.noarch
6+
7+
RUN cargo install wasm-pack
8+
9+
ENV USER bitkeks

LICENSE_MIT

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
Copyright (c) 2023 Dominik Pataky
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.

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Arcus
2+
3+
This project contains the source code of the Thunderbird add-on Arcus.
4+
5+
It's a Rust-based encryption and decryption tool integrated into the email client, powered by Rust, https://github.com/str4d/rage and WebAssembly.
6+
7+
See: [addons.thunderbird.net/en-GB/thunderbird/addon/arcus/](https://addons.thunderbird.net/en-GB/thunderbird/addon/arcus/)
8+
9+
## License
10+
11+
Copyright 2023 Dominik Pataky
12+
13+
Licensed under the MIT license.

arcus-wasm/.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/target
2+
**/*.rs.bk
3+
Cargo.lock
4+
bin/
5+
pkg/
6+
wasm-pack.log
7+
8+
index.html
9+
index.js

arcus-wasm/Cargo.toml

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[package]
2+
name = "arcus"
3+
version = "0.9.0"
4+
authors = ["bitkeks"]
5+
edition = "2021"
6+
7+
[lib]
8+
crate-type = ["cdylib", "rlib"]
9+
10+
[features]
11+
default = ["console_error_panic_hook", "wee_alloc"]
12+
13+
[dependencies]
14+
wasm-bindgen = "0.2.63"
15+
wasm-streams = "0.3"
16+
js-sys = "0.3"
17+
chrono = { version = "0.4", features = ["wasmbind"] }
18+
getrandom = { version = "0.2", features = ["js"] }
19+
getrandom_1 = { package = "getrandom", version = "0.1", features = ["wasm-bindgen"] }
20+
21+
# The `console_error_panic_hook` crate provides better debugging of panics by
22+
# logging them with `console.error`. This is great for development, but requires
23+
# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
24+
# code size when deploying.
25+
console_error_panic_hook = { version = "0.1.6", optional = true }
26+
27+
# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
28+
# compared to the default allocator's ~10K. It is slower than the default
29+
# allocator, however.
30+
wee_alloc = { version = "0.4.5", optional = true }
31+
32+
[dependencies.age]
33+
version = "0.9"
34+
features = ["armor", "async", "web-sys"]
35+
36+
[dependencies.web-sys]
37+
version = "0.3"
38+
features = [
39+
"Blob",
40+
"BlobPropertyBag",
41+
"File",
42+
"ReadableStream",
43+
"console",
44+
]
45+
46+
[dev-dependencies]
47+
wasm-bindgen-test = "0.3.13"
48+
49+
[profile.release]
50+
# Tell `rustc` to optimize for small code size.
51+
opt-level = "s"
52+
# Tell LLVM to optimize as well
53+
lto = true

arcus-wasm/src/encrypt.rs

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
use age;
2+
use age::secrecy::{ExposeSecret, SecretString};
3+
use chrono;
4+
use js_sys::Array;
5+
use wasm_bindgen::prelude::*;
6+
use wasm_streams::{readable::ReadableStream, writable::WritableStream};
7+
use web_sys::{Blob, BlobPropertyBag};
8+
9+
use crate::utils;
10+
11+
/// A newtype around an [`age::Encryptor`].
12+
#[wasm_bindgen]
13+
pub struct Encryptor(age::Encryptor);
14+
15+
#[wasm_bindgen]
16+
impl Encryptor {
17+
/// Returns an `Encryptor` that will create an age file encrypted with a passphrase.
18+
///
19+
/// This API should only be used with a passphrase that was provided by (or generated
20+
/// for) a human. For programmatic use cases, instead generate a `SecretKey` and then
21+
/// use `Encryptor::with_recipients`.
22+
pub fn with_user_passphrase(passphrase: String) -> Encryptor {
23+
// This is an entrance from JS to our WASM APIs; perform one-time setup steps.
24+
utils::set_panic_hook();
25+
26+
Encryptor(age::Encryptor::with_user_passphrase(SecretString::new(
27+
passphrase,
28+
)))
29+
}
30+
31+
// /// Creates a wrapper around a writer that will encrypt its input.
32+
// ///
33+
// /// Returns errors from the underlying writer while writing the header.
34+
// pub async fn wrap_output(
35+
// self,
36+
// output: wasm_streams::writable::sys::WritableStream,
37+
// ) -> Result<wasm_streams::writable::sys::WritableStream, JsValue> {
38+
// // Convert from the opaque web_sys::WritableStream Rust type to the fully-functional
39+
// // wasm_streams::writable::WritableStream.
40+
// let stream = WritableStream::from_raw(output);
41+
//
42+
// let writer = self
43+
// .0
44+
// .wrap_async_output(stream.into_async_write())
45+
// .await
46+
// .map_err(|e| JsValue::from(format!("{}", e)))?;
47+
//
48+
// Ok(WritableStream::from_sink(shim::WriteSinker::new(writer)).into_raw())
49+
// }
50+
}

arcus-wasm/src/lib.rs

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
use std::error::Error;
2+
use std::io::{Read, Write};
3+
use std::iter;
4+
use std::str::FromStr;
5+
use std::string::FromUtf8Error;
6+
use age;
7+
use age::armor::{ArmoredReader, ArmoredWriter};
8+
use age::armor::Format::AsciiArmor;
9+
use age::{DecryptError, Decryptor};
10+
use age::secrecy::{ExposeSecret, Secret, SecretString};
11+
use age::x25519::{Identity, Recipient};
12+
use chrono;
13+
use js_sys::{Array, JsString};
14+
use wasm_bindgen::prelude::*;
15+
use wasm_streams::{readable::ReadableStream, writable::WritableStream};
16+
use web_sys::{Blob, BlobPropertyBag};
17+
18+
mod utils;
19+
mod encrypt;
20+
21+
#[cfg(feature = "wee_alloc")]
22+
#[global_allocator]
23+
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
24+
25+
#[wasm_bindgen]
26+
extern {
27+
#[wasm_bindgen(js_namespace = console)]
28+
fn log(s: &str);
29+
}
30+
31+
macro_rules! console_log {
32+
// Note that this is using the `log` function imported above during
33+
// `bare_bones`
34+
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
35+
}
36+
37+
#[wasm_bindgen]
38+
pub struct ArcusKeypair {
39+
pubkey: Recipient,
40+
secret: SecretString
41+
}
42+
43+
#[wasm_bindgen]
44+
impl ArcusKeypair {
45+
#[wasm_bindgen(constructor)]
46+
pub fn new() -> Self {
47+
let kp = Identity::generate();
48+
Self {
49+
pubkey: kp.to_public(),
50+
secret: kp.to_string()
51+
}
52+
}
53+
54+
pub fn get_pubkey(&self) -> String {
55+
self.pubkey.to_string().clone()
56+
}
57+
58+
pub fn get_secret(&self) -> String {
59+
self.secret.expose_secret().to_string().clone()
60+
}
61+
}
62+
63+
#[wasm_bindgen]
64+
pub struct Arcus {}
65+
66+
#[wasm_bindgen]
67+
impl Arcus {
68+
#[wasm_bindgen(constructor)]
69+
pub fn new() -> Self {
70+
Self {}
71+
}
72+
73+
pub fn encrypt_text(&self, cleartext: String, recipient_pubkey: String) -> String {
74+
utils::set_panic_hook();
75+
76+
let new_array = Array::new();
77+
new_array.push(&JsString::from(recipient_pubkey));
78+
79+
self.encrypt_text_multi(cleartext, new_array)
80+
}
81+
82+
pub fn encrypt_text_multi(&self, cleartext: String, recipient_pubkeys: Array) -> String {
83+
utils::set_panic_hook();
84+
85+
let mut recs: Vec<Box<dyn age::Recipient + Send + 'static>> = Vec::new();
86+
87+
for recipient_pubkey in recipient_pubkeys.iter() {
88+
if !recipient_pubkey.is_string() {
89+
console_log!("{:?} is not a string", recipient_pubkey);
90+
continue;
91+
}
92+
93+
let pubkey = recipient_pubkey.as_string().unwrap();
94+
let rpk_str = pubkey.as_str();
95+
96+
let re = match age::x25519::Recipient::from_str(rpk_str) {
97+
Ok(recipient) => recipient,
98+
Err(e) => return "invalid recipient string".to_string()
99+
};
100+
101+
recs.push(Box::new(re));
102+
}
103+
104+
let encrypted = {
105+
let encryptor = age::Encryptor::with_recipients(recs)
106+
.expect("we provided a recipient");
107+
108+
let mut enc_vec = vec![];
109+
let mut writer = encryptor.wrap_output(
110+
ArmoredWriter::wrap_output(&mut enc_vec, AsciiArmor)
111+
.expect("armored wrap_output failed"))
112+
.expect("wrap_output failed");
113+
114+
writer.write_all(cleartext.as_bytes())
115+
.expect("write_all failed");
116+
117+
match writer.finish() {
118+
Ok(aw) => {
119+
aw.finish().expect("armored writer could not finish");
120+
},
121+
Err(e) => {
122+
console_log!("{:?}", e);
123+
}
124+
};
125+
126+
enc_vec
127+
};
128+
129+
let res = match String::from_utf8(encrypted) {
130+
Ok(ct) => ct,
131+
Err(e) => e.to_string()
132+
};
133+
134+
res
135+
}
136+
137+
pub fn decrypt_text(&self, ciphertext: String, own_secretkey: String) -> String {
138+
utils::set_panic_hook();
139+
140+
let secret_id = match age::x25519::Identity::from_str(own_secretkey.as_str()) {
141+
Ok(identity) => identity,
142+
Err(e) => {
143+
console_log!("{:?}", e);
144+
return "error parsing secret key".to_string()
145+
}
146+
};
147+
148+
let decrypted = {
149+
let decryptor = match age::Decryptor::new(ArmoredReader::new(ciphertext.as_bytes())).expect("Error") {
150+
age::Decryptor::Recipients(d) => d,
151+
_ => unreachable!(),
152+
};
153+
154+
let key = age::x25519::Identity::from_str(own_secretkey.as_str()).unwrap();
155+
156+
let mut dec_vec = vec![];
157+
let mut reader = decryptor
158+
.decrypt(iter::once(&key as &dyn age::Identity))
159+
.expect("decryptor.decrypt failed");
160+
reader.read_to_end(&mut dec_vec).expect("read_to_end failed");
161+
dec_vec
162+
};
163+
164+
let res = match String::from_utf8(decrypted) {
165+
Ok(ct) => ct,
166+
Err(e) => e.to_string()
167+
};
168+
169+
res
170+
}
171+
172+
pub fn generate_key(&self) -> ArcusKeypair {
173+
ArcusKeypair::new()
174+
}
175+
176+
pub fn pub_from_secret(&self, secret_key: String) -> String {
177+
let id = match age::x25519::Identity::from_str(secret_key.as_str()) {
178+
Ok(parsed_id) => parsed_id,
179+
Err(e) => return e.to_string()
180+
};
181+
182+
id.to_public().to_string()
183+
}
184+
}
185+
186+
187+
#[wasm_bindgen]
188+
pub struct X25519Identity {
189+
identity: age::x25519::Identity,
190+
created: chrono::DateTime<chrono::Local>,
191+
}
192+
193+
#[wasm_bindgen]
194+
impl X25519Identity {
195+
/// Generates a new age identity.
196+
pub fn generate() -> Self {
197+
// This is an entrance from JS to our WASM APIs; perform one-time setup steps.
198+
utils::set_panic_hook();
199+
200+
X25519Identity {
201+
identity: age::x25519::Identity::generate(),
202+
created: chrono::Local::now(),
203+
}
204+
}
205+
206+
/// Writes this identity to a blob that can be saved as a file.
207+
pub fn write(&self) -> Result<Blob, JsValue> {
208+
let output = format!(
209+
"# created: {}\n# recipient: {}\n{}",
210+
self.created
211+
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
212+
self.identity.to_public(),
213+
self.identity.to_string().expose_secret()
214+
);
215+
216+
Blob::new_with_u8_array_sequence_and_options(
217+
&Array::of1(&JsValue::from_str(&output)).into(),
218+
&BlobPropertyBag::new().type_("text/plain;charset=utf-8"),
219+
)
220+
}
221+
222+
/// Returns the recipient corresponding to this identity.
223+
pub fn recipient(&self) -> String {
224+
self.identity.to_public().to_string()
225+
}
226+
}
227+

0 commit comments

Comments
 (0)