Skip to content

Commit

Permalink
Merge v1.0.0-beta branch to main
Browse files Browse the repository at this point in the history
1. Added a multiple wallet system which allows a user to import, create, backup and delete wallets. They are encrypted with a password and saved in the application data directory in DATA_DIR/dagchat-beta/accounts.dagchat (OS Specific).
2. Added an account system to allow a user to show and hide procedurally generated accounts (including from a specified index) for each wallet. 
3. Messages are encrypted using the same password and are saved per-account under DATA_DIR/messages/UAID.dagchat. UAID is a Unique Account ID (A random 64 character hex string that is associated with each account used to send or receive messages in dagchat via a HashMap, which itself is stored along with accounts in accounts.dagchat). This way a compromised machine does not give information on the addresses used by a user on a machine, but messages can still be saved in account specific files.
4. Added a message viewing tab with options to search and filter.
  • Loading branch information
derfarctor authored Apr 25, 2022
2 parents 5a168aa + 6036fb1 commit d81ea8d
Show file tree
Hide file tree
Showing 11 changed files with 4,508 additions and 720 deletions.
2,258 changes: 2,258 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 8 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "dagchat"
version = "1.1.0"
version = "1.0.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand All @@ -10,6 +10,12 @@ ecies-ed25519 = "0.5.1"
ed25519-dalek = { version = "1.0.1" , package = "ed25519-dalek-blake2-feeless"}
cursive = { version = "0.17.0", default-features = false, features = ["crossterm-backend"] }
cursive_buffered_backend = "0.6.0"
dirs = "4.0.0"
rust-argon2 = "1.0.0"
bincode = "1.3.3"
chrono = "0.4.19"
aes-gcm = "0.9.4"
bitreader = "0.3.6"
sha2 = "0.10.1"
blake2 = "0.10.2"
rand = "0.7.0"
Expand All @@ -21,5 +27,5 @@ serde_json = "1.0.74"
serde = { version = "1.0.136", features = ["derive"] }
reqwest = {version = "0.11.10", features = ["blocking"]}
crossterm = "0.23.2"
arboard = "2.1.0"
clipboard = "0.5.0"
bigdecimal = "0.3.0"
19 changes: 14 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,17 @@ An open source wallet with end-to-end encrypted, on chain messaging for nano and
This application is not yet released. Any versions are test versions and come with no guarantees. Whilst messages are in theory encrypted end-to-end, the encrypted data is being put onto a public blockchain. The cryptography used in the dagchat protocol (and implementation of) has not been audited so it is advised not to include sensitive data within messages.

# Features
- Import accounts using a 24 word mnemonic phrase or a 64 character seed. Or generate a new seed within the app. ![image](https://user-images.githubusercontent.com/97409490/162834155-f9680392-8e4f-41b9-8721-4b3b36fb5da1.png)
- Send on chain, end to end encrypted memos/messages using the dagchat protocol. ![image](https://user-images.githubusercontent.com/97409490/162834972-494bc8e5-7c5a-40f2-b14d-eb47313746c2.png)
- Receive your nano and banano, and read incoming messages all in the same place. ![image](https://user-images.githubusercontent.com/97409490/162835371-1f18feac-75c1-4eb6-8885-b957a34f1132.png) ![image](https://user-images.githubusercontent.com/97409490/162835416-22b6df85-ed37-4cb1-9ab1-db34c3b97725.png)
## Other images
![image](https://user-images.githubusercontent.com/97409490/162835489-e49ad8ba-a0b7-4ccb-9145-6935aaf8f73b.png) ![image](https://user-images.githubusercontent.com/97409490/162835520-b0763c63-6eae-4954-aeee-554488530e34.png)
- Import multiple wallets using a mnemonic phrase, hex seed, or even private key. Each wallet supports many accounts which can be shown procedurally or by specifying an index. <br>![image](https://user-images.githubusercontent.com/97409490/165167183-11114b67-71e3-4fcd-85a6-a2a4ff6a0f1e.png) ![image](https://user-images.githubusercontent.com/97409490/165167265-30516a86-5c99-448f-930a-b3ccd1d4bd08.png)
- Send on chain, end to end encrypted memos/messages using the dagchat protocol. <br>![image](https://user-images.githubusercontent.com/97409490/165167726-ec9a9fa9-ffa0-4c2f-8a63-eddc612abdbf.png)
- Receive your nano and banano, and read incoming messages all in the same place. <br>![image](https://user-images.githubusercontent.com/97409490/165168179-358d2fac-57b5-4ef9-b1ec-35db00f3fe2b.png)
- Messages are identified automatically by the wallet.
<br>![image](https://user-images.githubusercontent.com/97409490/165168312-18bc63d4-8912-4278-9f83-b2390400ba49.png)
- Messages when sent and received are automatically encrypted and saved to your computer. They can be read again in the message history tab.
<br>![image](https://user-images.githubusercontent.com/97409490/165168937-b41d7884-4dd5-4c60-a934-e57a07b82742.png)

# Building from source
To build dagchat from source, you will need to have rust and cargo installed on your machine: https://www.rust-lang.org/tools/install
1. Clone the repository or download the zip and extract it.
2. If you are building for Linux (Windows and MacOS skip this step) you will need to install some other dependencies that are used for the rust-clipboard crate that manages copying and pasting in dagchat: `sudo apt-get install libxcb1-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev`.
3. Enter the repository's directory (either the clone, or the extracted zip) and run `cargo build --release` to build an executable in release mode. This will appear in `/target/release/`.
4. The application should be built and ready to run.
118 changes: 101 additions & 17 deletions src/dcutil.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
use aes_gcm::aead::{generic_array::GenericArray, Aead, NewAead};
use aes_gcm::{Aes256Gcm, Nonce};
use argon2::{self, Config};
use bigdecimal::BigDecimal;
use bitreader::BitReader;
use blake2::digest::{Update, VariableOutput};
use blake2::Blake2bVar;
use data_encoding::Encoding;
use data_encoding_macro::new_encoding;
use ecies_ed25519;
use ed25519_dalek;
use rand::RngCore;
use serde;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::{Digest, Sha256};
use std::collections::HashMap;
use std::str;
use std::str::FromStr;

// Will do something dynamic with reps in future
use crate::defaults;
use crate::defaults::*;

// Used to update progress bar in cursive app
// Each counter has 1000 ticks
Expand Down Expand Up @@ -133,7 +139,6 @@ pub struct BlocksResponse {
data: HashMap<String, BlockResponse>,
}


#[derive(Serialize, Deserialize, Debug)]
pub struct BlocksInfoResponse {
blocks: BlocksResponse,
Expand Down Expand Up @@ -165,7 +170,7 @@ pub fn send_message(
node_url: &str,
addr_prefix: &str,
counter: &Counter,
) {
) -> String {
let public_key_bytes = to_public_key(&target_address);
let mut message = message.clone();
let pad = (message.len() + 28) % 32;
Expand All @@ -174,13 +179,12 @@ pub fn send_message(
}
let public_key = ecies_ed25519::PublicKey::from_bytes(&public_key_bytes).unwrap();
counter.tick(50);
//println!("Encrypting message for send: {}", message);

let mut csprng = rand::thread_rng();
let encrypted_bytes =
ecies_ed25519::encrypt(&public_key, message.as_bytes(), &mut csprng).unwrap();
let blocks_needed = ((60 + message.len()) / 32) + 1;
counter.tick(50);
//println!("Blocks needed: {}", blocks_needed);

let mut block_data = [0u8; 32];
let mut first_block_hash = [0u8; 32];
Expand All @@ -189,7 +193,7 @@ pub fn send_message(
let sender_pub = ed25519_dalek::PublicKey::from(
&ed25519_dalek::SecretKey::from_bytes(private_key_bytes).unwrap(),
);
let sender_address = get_address(sender_pub.as_bytes(), addr_prefix);
let sender_address = get_address(sender_pub.as_bytes(), Some(addr_prefix));

// Set up the previous block hash and balance to start publishing blocks
// Also note the representative from before sending, in order to change back afterwards
Expand Down Expand Up @@ -217,7 +221,6 @@ pub fn send_message(
} else {
block_data.copy_from_slice(&encrypted_bytes[start..end]);
}
//println!("Block data as addr: {:?}", get_address(&block_data, addr_prefix));
let block_hash = get_block_hash(
private_key_bytes,
&block_data,
Expand Down Expand Up @@ -260,6 +263,7 @@ pub fn send_message(
&addr_prefix,
);
publish_block(block, sub.clone(), node_url);
hex::encode(last_block_hash)
}

pub fn change_rep(
Expand Down Expand Up @@ -309,9 +313,9 @@ pub fn receive_block(
if account_info_opt.is_none() {
// OPEN BLOCK
if addr_prefix == "nano_" {
representative = to_public_key(defaults::DEFAULT_REP_NANO);
representative = to_public_key(DEFAULT_REP_NANO);
} else if addr_prefix == "ban_" {
representative = to_public_key(defaults::DEFAULT_REP_BANANO);
representative = to_public_key(DEFAULT_REP_BANANO);
} else {
panic!("Unknown network... no default rep to open account.");
}
Expand Down Expand Up @@ -356,7 +360,7 @@ pub fn send(
let sender_pub = ed25519_dalek::PublicKey::from(
&ed25519_dalek::SecretKey::from_bytes(private_key_bytes).unwrap(),
);
let sender_address = get_address(sender_pub.as_bytes(), addr_prefix);
let sender_address = get_address(sender_pub.as_bytes(), Some(addr_prefix));

// Safe because account must be opened to have got this far
let account_info = get_account_info(&sender_address, node_url).unwrap();
Expand Down Expand Up @@ -488,7 +492,18 @@ pub fn get_blocks_info(hashes: Vec<String>, node_url: &str) -> BlocksInfoRespons
include_not_found: true,
};
let body = serde_json::to_string(&request).unwrap();
//eprintln!("Body: {}", body);
let response = post_node(body, node_url);

// Somewhat hacky way to catch this error. Would be better off implementing
// some proper serde deserialisation with untagged flag.
if response.contains("\"blocks\": \"\"") {
return BlocksInfoResponse {
blocks: BlocksResponse {
data: HashMap::new(),
},
};
}
let blocks_info_response: BlocksInfoResponse = serde_json::from_str(&response).unwrap();
blocks_info_response
}
Expand Down Expand Up @@ -632,9 +647,9 @@ pub fn get_signed_block(
//let work = generate_work(&previous, "banano");
let block = Block {
type_name: String::from("state"),
account: get_address(public.as_bytes(), addr_prefix),
account: get_address(public.as_bytes(), Some(addr_prefix)),
previous: hex::encode(previous),
representative: get_address(rep, addr_prefix),
representative: get_address(rep, Some(addr_prefix)),
balance: balance.to_string(),
link: hex::encode(link),
signature: hex::encode(&signed_bytes),
Expand Down Expand Up @@ -676,23 +691,74 @@ pub fn validate_mnemonic(mnemonic: &str) -> Option<[u8; 32]> {
Some(entropy)
}

pub fn get_private_key(seed_bytes: &[u8; 32]) -> [u8; 32] {
pub fn derive_key(password: &str, salt: &[u8]) -> Vec<u8> {
let config = Config::default();
let hash = argon2::hash_raw(password.as_bytes(), salt, &config).unwrap();
hash
}

pub fn decrypt_bytes(encrypted_bytes: &Vec<u8>, password: &str) -> Result<Vec<u8>, String> {
let salt = &encrypted_bytes[..SALT_LENGTH];

let key_bytes = derive_key(password, salt);
let key = GenericArray::from_slice(&key_bytes);

let aead = Aes256Gcm::new(&key);
let nonce = Nonce::from_slice(&encrypted_bytes[SALT_LENGTH..SALT_LENGTH + IV_LENGTH]);
let encrypted = &encrypted_bytes[SALT_LENGTH + IV_LENGTH..];
let decrypted = aead.decrypt(nonce, encrypted);
match decrypted {
Ok(decrypted) => {
return Ok(decrypted);
}
Err(e) => {
return Err(format!("Failed to decrypt bytes. Error: {}", e));
}
}
}

pub fn encrypt_bytes(bytes: &Vec<u8>, password: &str) -> Vec<u8> {
let mut csprng = rand::thread_rng();
let mut salt = [0u8; SALT_LENGTH];
csprng.fill_bytes(&mut salt);
let key_bytes = derive_key(password, &salt);
let key = GenericArray::from_slice(&key_bytes);

let aead = Aes256Gcm::new(key);

let mut nonce_bytes = [0u8; IV_LENGTH];
csprng.fill_bytes(&mut nonce_bytes);
let nonce = Nonce::from_slice(&nonce_bytes);

let ciphertext = aead.encrypt(nonce, &bytes[..]).unwrap();
let mut encrypted_bytes = Vec::with_capacity(SALT_LENGTH + IV_LENGTH + ciphertext.len());
encrypted_bytes.extend(salt);
encrypted_bytes.extend(nonce);
encrypted_bytes.extend(ciphertext);
encrypted_bytes
}

pub fn get_private_key(seed_bytes: &[u8; 32], idx: u32) -> [u8; 32] {
let mut hasher = Blake2bVar::new(32).unwrap();
let mut buf = [0u8; 32];
hasher.update(seed_bytes);
hasher.update(&[0u8; 4]);
hasher.update(&idx.to_be_bytes());
hasher.finalize_variable(&mut buf).unwrap();
buf
}

pub fn get_address(pub_key_bytes: &[u8], prefix: &str) -> String {
pub fn get_address(pub_key_bytes: &[u8], prefix: Option<&str>) -> String {
let mut pub_key_vec = pub_key_bytes.to_vec();
let mut h = [0u8; 3].to_vec();
h.append(&mut pub_key_vec);
let checksum = ADDR_ENCODING.encode(&compute_address_checksum(pub_key_bytes));
let address = {
let encoded_addr = ADDR_ENCODING.encode(&h);
let mut addr = String::from(prefix);

let mut addr = String::from("");
if prefix.is_some() {
addr = String::from(prefix.unwrap());
}
addr.push_str(encoded_addr.get(4..).unwrap());
addr.push_str(&checksum);
addr
Expand Down Expand Up @@ -731,7 +797,7 @@ pub fn validate_address(addr: &str) -> bool {

let pub_key_vec = ADDR_ENCODING.decode(encoded_addr.as_bytes());

// Lazily catch decoding error - return false
// Catch decoding error - return false
let mut pub_key_vec = match pub_key_vec {
Ok(pub_key_vec) => pub_key_vec,
Err(_) => return false,
Expand All @@ -749,6 +815,24 @@ pub fn validate_address(addr: &str) -> bool {
}
}

pub fn seed_to_mnemonic(seed_bytes: &[u8]) -> String {
let mut hasher = Sha256::new();
Digest::update(&mut hasher, seed_bytes);
let check = hasher.finalize();

let mut combined = Vec::from(seed_bytes);
combined.extend(&check);

let mut reader = BitReader::new(&combined);

let mut words: Vec<&str> = Vec::new();
for _ in 0..24 {
let n = reader.read_u16(11);
words.push(WORD_LIST[n.unwrap() as usize].as_ref());
}
words.join(" ")
}

fn compute_address_checksum(pub_key_bytes: &[u8]) -> [u8; 5] {
let mut hasher = Blake2bVar::new(5).unwrap();
let mut buf = [0u8; 5];
Expand Down
14 changes: 11 additions & 3 deletions src/defaults.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
pub const DEFAULT_REP_BANANO: &str = "ban_3catgir1p6b1edo5trp7fdb8gsxx4y5ffshbphj73zzy5hu678rsry7srh8b";
pub const DEFAULT_REP_NANO: &str = "nano_3zx7rus19yr5qi5zmkawnzo5ehxr7i73xqghhondhfrzftgstgk4gxbubwfq";
pub const SHOW_TO_DP: usize = 6;
pub const DEFAULT_REP_BANANO: &str =
"ban_3catgir1p6b1edo5trp7fdb8gsxx4y5ffshbphj73zzy5hu678rsry7srh8b";
pub const DEFAULT_REP_NANO: &str =
"nano_3zx7rus19yr5qi5zmkawnzo5ehxr7i73xqghhondhfrzftgstgk4gxbubwfq";
pub const SHOW_TO_DP: usize = 6;
pub const DATA_DIR_PATH: &str = "dagchat-beta";
pub const MESSAGES_DIR_PATH: &str = "messages";
pub const WALLETS_PATH: &str = "accounts.dagchat";
pub const ADDRESS_BOOK_PATH: &str = "addressbook.dagchat";
pub const SALT_LENGTH: usize = 16;
pub const IV_LENGTH: usize = 12;
Loading

0 comments on commit d81ea8d

Please sign in to comment.