jolokia 0.8.0

Simple, strong encryption.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
mod cmd;

use std::io::{self, Read, Write};
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::SystemTime;
use std::{env, fs, process};

use lessify::Pager;
use secrecy::{ExposeSecret, SecretSlice};

use jolokia::traits::{Cipher, GeneratedKey};

use cmd::{cli, ui};

// TODO: This deserves refactoring. Error handling is inconsistent, and
// `if is_in_place` logic is brittle because correctness is not enforced
// by the compiler. But it's fine for now as long as we don't add new
// features.

fn main() {
    let args = match cli::Args::build_from_args(env::args().skip(1)) {
        Ok(args) => args,
        Err(err) => {
            eprintln!(
                "\
{fatal}: {err}.
Try '{bin} -h' for help.",
                fatal = ui::Color::error("fatal"),
                bin = env!("CARGO_BIN_NAME")
            );
            process::exit(2);
        }
    };

    if args.long_help {
        long_help();
    } else if args.short_help {
        short_help();
    } else if args.version {
        version();
    } else if let Some(command) = args.command {
        if let Err(reason) = execute_command(command, &args) {
            eprintln!(
                "{error}: {reason}{}",
                // Errors from dependencies may or may not end with `.`.
                if reason.ends_with('.') { "" } else { "." },
                error = ui::Color::error("error"),
            );
            process::exit(1);
        }
    } else {
        // No arguments.
        short_help();
    }
}

fn execute_command(command: cli::Command, args: &cli::Args) -> Result<(), String> {
    let algorithm = args.algorithm.unwrap_or_default();
    let cipher: Box<dyn Cipher> = algorithm.into();
    let add_newline = args.output == cli::Output::Stdout;

    match command {
        cli::Command::KeyGen => cmd::keygen(cipher.as_ref(), add_newline),
        cli::Command::Encrypt | cli::Command::Decrypt => {
            let is_in_place = is_input_file_used_for_output(args);

            let cipher = cipher.as_ref();
            let key = get_key_or_default(args, algorithm);
            let message = get_message_or_exit(args);
            let output = if is_in_place {
                get_temporary_file_or_exit(args)
            } else {
                get_output_or_exit(args)
            };

            let key = key.expose_secret();
            if command == cli::Command::Encrypt {
                cmd::encrypt(cipher, key, message, output, args.raw, add_newline)?;
            } else if command == cli::Command::Decrypt {
                cmd::decrypt(cipher, key, message, output, args.raw)?;
            }

            if is_in_place {
                override_output_file_with_temporary_file_or_exit(args);
            }

            Ok(())
        }
    }
}

fn is_input_file_used_for_output(args: &cli::Args) -> bool {
    let (Some(cli::Message::File(input_file)), cli::Output::File(output_file)) =
        (&args.message, &args.output)
    else {
        return false;
    };
    let (Ok(input_file), Ok(output_file)) = (input_file.canonicalize(), output_file.canonicalize())
    else {
        return false;
    };
    input_file == output_file
}

fn get_key_or_default(args: &cli::Args, algorithm: cli::Algorithm) -> SecretSlice<u8> {
    if algorithm == cli::Algorithm::RotN || algorithm == cli::Algorithm::Brainfuck {
        // Special do-not-warn cases.
        algorithm.default_key().get_symmetric().clone()
    } else if let Some(ref key) = args.key {
        SecretSlice::from(key.expose_secret().as_bytes().to_vec())
    } else {
        eprintln!(
            "\
{warning}: Using {package}'s default cipher key.

                       {b}THIS IS NOT SECURE!{rt}

Anyone using {package} will be able to decrypt your messages. To generate
a unique cipher key, run `{bin} keygen`, and use it on the command line
with `--key`, or set the `{key_env_var}` environment variable.",
            warning = ui::Color::warning("warning"),
            package = env!("CARGO_PKG_NAME"),
            bin = env!("CARGO_BIN_NAME"),
            key_env_var = cli::KEY_ENV_VAR,
            b = ui::Color::maybe_color(ui::color::BOLD),
            rt = ui::Color::maybe_color(ui::color::RESET),
        );

        let key = algorithm.default_key();
        match key {
            GeneratedKey::Symmetric(_) => key.get_symmetric(),
            GeneratedKey::Asymmetric { .. } => match args.command {
                Some(cli::Command::Encrypt) => key.get_asymmetric_public(),
                Some(cli::Command::Decrypt) => key.get_asymmetric_private(),
                _ => unreachable!(),
            },
            GeneratedKey::None => unreachable!(),
        }
        .clone()
    }
}

fn get_message_or_exit(args: &cli::Args) -> Box<dyn Read> {
    if let Some(ref message) = args.message {
        match message {
            cli::Message::String(message) => Box::new(io::Cursor::new(message.to_owned())),
            cli::Message::File(file) => {
                let f = match fs::File::open(file) {
                    Ok(f) => f,
                    Err(reason) => {
                        eprintln!(
                            "{error}: Could not read '{}': {reason}.",
                            file.display(),
                            error = ui::Color::error("error")
                        );
                        process::exit(1);
                    }
                };
                let reader = io::BufReader::new(f);
                Box::new(reader)
            }
            cli::Message::Stdin => Box::new(io::stdin()),
        }
    } else {
        eprintln!(
            "{fatal}: You must provide a message.",
            fatal = ui::Color::error("fatal")
        );
        process::exit(2);
    }
}

fn get_output_or_exit(args: &cli::Args) -> Box<dyn Write> {
    match args.output {
        cli::Output::File(ref file) => {
            let f = match fs::File::create(file) {
                Ok(f) => f,
                Err(reason) => {
                    eprintln!(
                        "{error}: Could not open file for writing '{}': {reason}.",
                        file.display(),
                        error = ui::Color::error("error")
                    );
                    process::exit(1);
                }
            };
            let writer = io::BufWriter::new(f);
            Box::new(writer)
        }
        cli::Output::Stdout | cli::Output::Redirected => Box::new(io::stdout()),
    }
}

fn get_temporary_file_or_exit(args: &cli::Args) -> Box<dyn Write> {
    debug_assert!(
        is_input_file_used_for_output(args),
        "Should only get called if in-place ciphering."
    );
    let tmp_file = build_temporary_file_path(args);
    let f = match fs::File::create(&tmp_file) {
        Ok(f) => f,
        Err(reason) => {
            eprintln!(
                "{error}: Could not open file for writing '{}': {reason}.",
                tmp_file.display(),
                error = ui::Color::error("error")
            );
            process::exit(1);
        }
    };
    let writer = io::BufWriter::new(f);
    Box::new(writer)
}

fn override_output_file_with_temporary_file_or_exit(args: &cli::Args) {
    debug_assert!(
        is_input_file_used_for_output(args),
        "Should only get called if in-place ciphering."
    );
    let cli::Output::File(ref file) = args.output else {
        unreachable!("if in-place, it's necessarily a file");
    };
    let tmp_file = build_temporary_file_path(args);
    if let Err(reason) = std::fs::rename(tmp_file, file) {
        eprintln!(
            "{error}: Could not override '{}': {reason}.",
            file.display(),
            error = ui::Color::error("error")
        );
        process::exit(1);
    }
}

// TODO: Not good that we need to call this twice, refactoring would do
// some good. This whole in-place thing doesn't "fit in" with the
// current design, it's too crafty.
fn build_temporary_file_path(args: &cli::Args) -> PathBuf {
    // File name can't change from create to rename.
    static EXTENSION: OnceLock<String> = OnceLock::new();

    let cli::Output::File(ref file) = args.output else {
        unreachable!("if in-place, it's necessarily a file");
    };

    file.with_extension(EXTENSION.get_or_init(|| {
        let mut extension = env!("CARGO_CRATE_NAME").to_string();
        if let Ok(timestamp) = SystemTime::now()
            .duration_since(SystemTime::UNIX_EPOCH)
            .map(|t| t.as_micros())
        {
            extension = format!("{timestamp}.{extension}");
        }
        extension
    }))
}

fn short_help() {
    println!("{}", short_help_message());
    println!(
        "For full help, see `{bin} --help`.",
        bin = env!("CARGO_BIN_NAME")
    );
}

fn short_help_message() -> String {
    format!(
        "\
{description}

Usage: {bin} [<options>] <command> [<args>]

Commands:
  keygen                 Generate cipher key
  encrypt                Encrypt plaintext
  decrypt                Decrypt ciphertext

Args:
  <MESSAGE>
  -k, --key <KEY>        Cipher key (base64)
  -r, --raw              Handle message as raw binary
  -f, --file <FILE>      Read message from file
    -i, --in-place       Write output to input file
  -o, --output <FILE>    Write output to file

Options:
  -h, --help             Show help message and exit
  -V, --version          Show the version and exit
",
        description = env!("CARGO_PKG_DESCRIPTION"),
        bin = env!("CARGO_BIN_NAME"),
    )
}

#[allow(clippy::too_many_lines)]
fn long_help() {
    Pager::page_or_print(&format!(
        "\
{help}
What does {package} do?
  {package} provides strong, modern, hard-to-misuse encryption for the
  general public.

  {warning}: {package} has not been audited for security. It is based on
  audited dependencies for the underlying algorithm implementations, but
  the final package (what you're using) was not.

  {caution}: Do not encrypt data you can't afford to lose. Be especially
  cautious of in-place encryption; always make a backup first. If you
  lose your key, or if there's a bug, {b}YOUR DATA WILL NOT BE RECOVERABLE{rt}.

Algorithms:
  {u}Name{rt}                 {u}Key Size{rt}               {u}Type{rt}
  ChaCha20-Poly1305    32-bytes (256-bits)    Symmetric
  HPKE                 32-bytes (256-bits)    Asymmetric
  ROT-n                0..255 (insecure)      Symmetric

Key:
  In {package}, a key is always a base64-encoded string of bytes. The
  size of the key varies depending on the selected algorithm.

  To generate a new key run:

      {h}${rt} {bin} keygen
      hNbaua5cGlUNsEp4HSUTSJG7gl5IURQiTvnABzhFW4w

  To use the key, pass it as `--key` or `-k`:

      {h}${rt} {bin} encrypt \"foo\" --key hNbaua5cGlUNsEp4HSUTSJG7gl5IURQiTvnABzhFW4w
      Q0gyMAGSwlWJdALzAAAAE448viN3l+rwa7W4RdkRI0V/VckAAAAA

  Or as an environment variable (but `--key` has precedence):

      {h}${rt} export {key_env_var}=hNbaua5cGlUNsEp4HSUTSJG7gl5IURQiTvnABzhFW4w
      {h}${rt} {bin} encrypt \"foo\"
      Q0gyMAGSwlWJdALzAAAAE448viN3l+rwa7W4RdkRI0V/VckAAAAA

  The key can also be the name of a file that contains a key:

      {h}${rt} echo hNbaua5cGlUNsEp4HSUTSJG7gl5IURQiTvnABzhFW4w > /secrets/{bin}.key
      {h}${rt} {bin} decrypt --key /secrets/{bin}.key Q0gyMAGSwlWJdALzAAAAE448viN3l+rwa7W4RdkRI0V/VckAAAAA
      foo

  To set a key permanently, the recommended solution is to point the
  environment variable to a file:

      {h}${rt} echo hNbaua5cGlUNsEp4HSUTSJG7gl5IURQiTvnABzhFW4w > ~/.{bin}.key
      {h}${rt} echo 'export {key_env_var}=\"$HOME/.{bin}.key\"' >> ~/.bashrc

Message:
  The message can be passed on the command line:

      {h}${rt} {bin} encrypt \"bar\"
      Q0gyMAHPNRsLieAOAAAAE/ssTCh2zCm73t+aQf9aKNepgPkAAAAA

  Or from a file:

      {h}${rt} {bin} encrypt --file bar.txt
      Q0gyMAHPNRsLieAOAAAAE/ssTCh2zCm73t+aQf9aKNepgPkAAAAA

  Or via `stdin` (but the command line has precedence):

      {h}${rt} cat bar.txt | {bin} encrypt
      Q0gyMAHPNRsLieAOAAAAE/ssTCh2zCm73t+aQf9aKNepgPkAAAAA

  By definition, you can round-trip it:

      {h}${rt} {bin} encrypt \"hello, world\" -o encrypted.txt
      {h}${rt} {bin} decrypt -f encrypted.txt
      hello, world

  You can also encrypt or decrypt a file in-place:

      {h}${rt} {bin} encrypt -f cat.gif --in-place
      {h}${rt} {bin} decrypt -f cat.gif -i

Raw I/O:
  If you do not want base64 encoding, you can pass the `--raw` or `-r`
  flag. This makes sense for larger files for which you don't want the
  ~33% size overhead of base64.

      {h}${rt} {bin} encrypt --raw \"hello, world\" > hello.enc
      {h}${rt} cat hello.enc | {bin} decrypt --raw
      hello, world

  Base64 is the simplest and safest option for most users. It makes it
  easy to copy-paste and share ciphertext. Use `--raw` only if you know
  what you're doing.

Compression:
  BYOC. {package} does not provide built-in compression, but you can
  bring your own:

      {h}${rt} gzip -c cat.gif | {bin} encrypt -r > out.enc
      {h}${rt} {bin} decrypt -r -f out.enc | gunzip > cat.gif

  If you need to compress and encrypt multiple files or directories,
  consider `tar`ing them:

      {h}${rt} tar -czf - cat.gif more-gifs/ | {bin} encrypt -r > out.enc
      {h}${rt} {bin} decrypt -r -f out.enc | tar -xzf -

  It makes sense to combine compression with `--raw` to get the smallest
  file size possible.
",
        help = short_help_message(),
        bin = env!("CARGO_BIN_NAME"),
        package = env!("CARGO_PKG_NAME"),
        key_env_var = cli::KEY_ENV_VAR,
        warning = ui::Color::warning("warning"),
        caution = ui::Color::error("caution"),
        h = ui::Color::maybe_color(ui::color::HIGHLIGHT),
        b = ui::Color::maybe_color(ui::color::BOLD),
        u = ui::Color::maybe_color(ui::color::UNDERLINE),
        rt = ui::Color::maybe_color(ui::color::RESET),
    ));
}

fn version() {
    println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
}