Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
840 changes: 388 additions & 452 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "cotp"
version = "1.9.4"
authors = ["replydev <[email protected]>"]
edition = "2021"
edition = "2024"
description = "Trustworthy, encrypted, command-line TOTP/HOTP authenticator app with import functionality."
repository = "https://github.com/replydev/cotp"
homepage = "https://github.com/replydev/cotp"
Expand Down
2 changes: 1 addition & 1 deletion src/arguments/add.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::{value_parser, Args};
use clap::{Args, value_parser};
use color_eyre::eyre::{ErrReport, Result};

use zeroize::Zeroize;
Expand Down
2 changes: 1 addition & 1 deletion src/arguments/edit.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use clap::{value_parser, Args};
use clap::{Args, value_parser};
use color_eyre::eyre::eyre;

use crate::otp::{otp_algorithm::OTPAlgorithm, otp_element::OTPDatabase};
Expand Down
2 changes: 1 addition & 1 deletion src/arguments/list.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use clap::Args;
use color_eyre::eyre::{eyre, Result};
use color_eyre::eyre::{Result, eyre};
use serde::Serialize;

use crate::otp::otp_element::{OTPDatabase, OTPElement};
Expand Down
2 changes: 1 addition & 1 deletion src/clipboard.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use base64::{engine::general_purpose, Engine as _};
use base64::{Engine as _, engine::general_purpose};
use color_eyre::eyre::eyre;
use copypasta_ext::prelude::*;
#[cfg(target_os = "linux")]
Expand Down
202 changes: 101 additions & 101 deletions src/crypto/cryptography.rs
Original file line number Diff line number Diff line change
@@ -1,101 +1,101 @@
use argon2::{Config, Variant, Version};
use chacha20poly1305::aead::Aead;
use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce};
use color_eyre::eyre::{eyre, ErrReport};
use data_encoding::BASE64;
use super::encrypted_database::EncryptedDatabase;
const ARGON2ID_SALT_LENGTH: usize = 16;
const XCHACHA20_POLY1305_NONCE_LENGTH: usize = 24;
const XCHACHA20_POLY1305_KEY_LENGTH: usize = 32;
const KEY_DERIVATION_CONFIG: Config = Config {
variant: Variant::Argon2id,
version: Version::Version13,
mem_cost: 32768,
time_cost: 4,
lanes: 4,
secret: &[],
ad: &[],
hash_length: XCHACHA20_POLY1305_KEY_LENGTH as u32,
};
pub fn argon_derive_key(password_bytes: &[u8], salt: &[u8]) -> color_eyre::Result<Vec<u8>> {
argon2::hash_raw(password_bytes, salt, &KEY_DERIVATION_CONFIG).map_err(ErrReport::from)
}
pub fn gen_salt() -> color_eyre::Result<[u8; ARGON2ID_SALT_LENGTH]> {
let mut salt: [u8; ARGON2ID_SALT_LENGTH] = [0; ARGON2ID_SALT_LENGTH];
getrandom::fill(&mut salt).map_err(|e| eyre!(e))?;
Ok(salt)
}
pub fn encrypt_string_with_key(
plain_text: &str,
key: &Vec<u8>,
salt: &[u8],
) -> color_eyre::Result<EncryptedDatabase> {
let wrapped_key = Key::from_slice(key.as_slice());
let aead = XChaCha20Poly1305::new(wrapped_key);
let mut nonce_bytes: [u8; XCHACHA20_POLY1305_NONCE_LENGTH] =
[0; XCHACHA20_POLY1305_NONCE_LENGTH];
getrandom::fill(&mut nonce_bytes).map_err(|e| eyre!(e))?;
let nonce = XNonce::from_slice(&nonce_bytes);
let cipher_text = aead
.encrypt(nonce, plain_text.as_bytes())
.map_err(|e| eyre!("Error during encryption: {e}"))?;
Ok(EncryptedDatabase::new(
1,
BASE64.encode(&nonce_bytes),
BASE64.encode(salt),
BASE64.encode(&cipher_text),
))
}
pub fn decrypt_string(
encrypted_text: &str,
password: &str,
) -> color_eyre::Result<(String, Vec<u8>, Vec<u8>)> {
//encrypted text is an encrypted database json serialized object
let encrypted_database: EncryptedDatabase = serde_json::from_str(encrypted_text)
.map_err(|e| eyre!("Error during encrypted database deserialization: {e}"))?;
let nonce = BASE64
.decode(encrypted_database.nonce().as_bytes())
.expect("Cannot decode Base64 nonce");
let cipher_text = BASE64
.decode(encrypted_database.cipher().as_bytes())
.expect("Cannot decode Base64 cipher");
let salt = BASE64.decode(encrypted_database.salt().as_bytes()).unwrap();
let key: Vec<u8> = argon_derive_key(password.as_bytes(), salt.as_slice())?;
let wrapped_key = Key::from_slice(&key);
let aead = XChaCha20Poly1305::new(wrapped_key);
let nonce = XNonce::from_slice(nonce.as_slice());
let decrypted = aead
.decrypt(nonce, cipher_text.as_slice())
.map_err(|_| eyre!("Wrong password"))?;
let from_utf8 = String::from_utf8(decrypted).map_err(ErrReport::from)?;
Ok((from_utf8, key, salt))
}
#[cfg(test)]
mod tests {
use crate::crypto::cryptography::{argon_derive_key, gen_salt};
use super::{decrypt_string, encrypt_string_with_key};
#[test]
fn test_encryption() {
let salt = gen_salt().unwrap();
let key = argon_derive_key(b"pa$$w0rd", salt.as_ref()).unwrap();
let encrypted = encrypt_string_with_key("Secret data@#[]ò", &key, salt.as_ref()).unwrap();
let (decrypted, _key, _salt) =
decrypt_string(&serde_json::to_string(&encrypted).unwrap(), "pa$$w0rd").unwrap();
assert_eq!(String::from("Secret data@#[]ò"), decrypted);
}
}
use argon2::{Config, Variant, Version};
use chacha20poly1305::aead::Aead;
use chacha20poly1305::{Key, KeyInit, XChaCha20Poly1305, XNonce};
use color_eyre::eyre::{ErrReport, eyre};
use data_encoding::BASE64;

use super::encrypted_database::EncryptedDatabase;

const ARGON2ID_SALT_LENGTH: usize = 16;
const XCHACHA20_POLY1305_NONCE_LENGTH: usize = 24;
const XCHACHA20_POLY1305_KEY_LENGTH: usize = 32;
const KEY_DERIVATION_CONFIG: Config = Config {
variant: Variant::Argon2id,
version: Version::Version13,
mem_cost: 32768,
time_cost: 4,
lanes: 4,
secret: &[],
ad: &[],
hash_length: XCHACHA20_POLY1305_KEY_LENGTH as u32,
};

pub fn argon_derive_key(password_bytes: &[u8], salt: &[u8]) -> color_eyre::Result<Vec<u8>> {
argon2::hash_raw(password_bytes, salt, &KEY_DERIVATION_CONFIG).map_err(ErrReport::from)
}

pub fn gen_salt() -> color_eyre::Result<[u8; ARGON2ID_SALT_LENGTH]> {
let mut salt: [u8; ARGON2ID_SALT_LENGTH] = [0; ARGON2ID_SALT_LENGTH];
getrandom::fill(&mut salt).map_err(|e| eyre!(e))?;
Ok(salt)
}

pub fn encrypt_string_with_key(
plain_text: &str,
key: &Vec<u8>,
salt: &[u8],
) -> color_eyre::Result<EncryptedDatabase> {
let wrapped_key = Key::from_slice(key.as_slice());

let aead = XChaCha20Poly1305::new(wrapped_key);
let mut nonce_bytes: [u8; XCHACHA20_POLY1305_NONCE_LENGTH] =
[0; XCHACHA20_POLY1305_NONCE_LENGTH];

getrandom::fill(&mut nonce_bytes).map_err(|e| eyre!(e))?;

let nonce = XNonce::from_slice(&nonce_bytes);
let cipher_text = aead
.encrypt(nonce, plain_text.as_bytes())
.map_err(|e| eyre!("Error during encryption: {e}"))?;
Ok(EncryptedDatabase::new(
1,
BASE64.encode(&nonce_bytes),
BASE64.encode(salt),
BASE64.encode(&cipher_text),
))
}

pub fn decrypt_string(
encrypted_text: &str,
password: &str,
) -> color_eyre::Result<(String, Vec<u8>, Vec<u8>)> {
//encrypted text is an encrypted database json serialized object
let encrypted_database: EncryptedDatabase = serde_json::from_str(encrypted_text)
.map_err(|e| eyre!("Error during encrypted database deserialization: {e}"))?;
let nonce = BASE64
.decode(encrypted_database.nonce().as_bytes())
.expect("Cannot decode Base64 nonce");
let cipher_text = BASE64
.decode(encrypted_database.cipher().as_bytes())
.expect("Cannot decode Base64 cipher");
let salt = BASE64.decode(encrypted_database.salt().as_bytes()).unwrap();

let key: Vec<u8> = argon_derive_key(password.as_bytes(), salt.as_slice())?;

let wrapped_key = Key::from_slice(&key);

let aead = XChaCha20Poly1305::new(wrapped_key);
let nonce = XNonce::from_slice(nonce.as_slice());
let decrypted = aead
.decrypt(nonce, cipher_text.as_slice())
.map_err(|_| eyre!("Wrong password"))?;
let from_utf8 = String::from_utf8(decrypted).map_err(ErrReport::from)?;
Ok((from_utf8, key, salt))
}

#[cfg(test)]
mod tests {
use crate::crypto::cryptography::{argon_derive_key, gen_salt};

use super::{decrypt_string, encrypt_string_with_key};

#[test]
fn test_encryption() {
let salt = gen_salt().unwrap();
let key = argon_derive_key(b"pa$$w0rd", salt.as_ref()).unwrap();
let encrypted = encrypt_string_with_key("Secret data@#[]ò", &key, salt.as_ref()).unwrap();
let (decrypted, _key, _salt) =
decrypt_string(&serde_json::to_string(&encrypted).unwrap(), "pa$$w0rd").unwrap();
assert_eq!(String::from("Secret data@#[]ò"), decrypted);
}
}
4 changes: 2 additions & 2 deletions src/importers/aegis_encrypted.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use aes_gcm::aead::generic_array::GenericArray;
use aes_gcm::aead::Aead;
use aes_gcm::aead::generic_array::GenericArray;
use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; // Or `Aes128Gcm`
use data_encoding::BASE64;
use hex::FromHex;
Expand All @@ -8,7 +8,7 @@ use zeroize::Zeroize;

use crate::otp::otp_element::OTPElement;
use crate::utils;
use scrypt::{scrypt, Params};
use scrypt::{Params, scrypt};

use super::aegis::AegisDb;

Expand Down
2 changes: 1 addition & 1 deletion src/importers/freeotp_plus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ mod tests {
use crate::otp::otp_element::OTPDatabase;
use color_eyre::Result;

use super::{encode_secret, FreeOTPPlusJson};
use super::{FreeOTPPlusJson, encode_secret};

#[test]
fn test_secret_conversion() {
Expand Down
2 changes: 1 addition & 1 deletion src/importers/importer.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{fmt::Debug, fs::read_to_string, path::PathBuf};

use color_eyre::eyre::{eyre, Result};
use color_eyre::eyre::{Result, eyre};
use serde::Deserialize;

use crate::otp::otp_element::OTPElement;
Expand Down
Loading
Loading