Skip to content

Commit 65e6a05

Browse files
committed
add: ability to create Vector Accounts with BIP39
The ability to create Vector Accounts (which is essentially a BIP39-derived Nostr Keypair) has finally arrived.
1 parent 218dccf commit 65e6a05

File tree

6 files changed

+116
-3
lines changed

6 files changed

+116
-3
lines changed

src-tauri/Cargo.lock

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ tauri-build = { version = "2.2.0", features = [] }
1919

2020
[dependencies]
2121
nostr-sdk = { version = "0.41.0", features = ["nip04", "nip06", "nip44", "nip59", "nip96"] }
22+
bip39 = { version = "2.1.0", features = ["rand"] }
2223
tokio = { version = "1.44.1", features = ["sync"] }
2324
futures-util = "0.3.31"
2425
tauri = { version = "2.5.0", features = ["protocol-asset", "image-png"] }

src-tauri/src/db.rs

+32
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub struct VectorDB {
1313
pub db_version: Option<u64>,
1414
pub theme: Option<String>,
1515
pub pkey: Option<String>,
16+
pub seed: Option<String>,
1617
}
1718

1819
const DB_PATH: &str = "vector.json";
@@ -236,10 +237,16 @@ pub fn get_db<R: Runtime>(handle: AppHandle<R>) -> Result<VectorDB, String> {
236237
_ => None,
237238
};
238239

240+
let seed = match store.get("seed") {
241+
Some(value) if value.is_string() => Some(value.as_str().unwrap().to_string()),
242+
_ => None,
243+
};
244+
239245
Ok(VectorDB {
240246
db_version,
241247
theme,
242248
pkey,
249+
seed,
243250
})
244251
}
245252

@@ -291,6 +298,31 @@ pub fn get_pkey<R: Runtime>(handle: AppHandle<R>) -> Result<Option<String>, Stri
291298
}
292299
}
293300

301+
#[command]
302+
pub async fn set_seed<R: Runtime>(handle: AppHandle<R>, seed: String) -> Result<(), String> {
303+
let store = get_store(&handle);
304+
// Encrypt the seed phrase before storing it
305+
let encrypted_seed = crate::internal_encrypt(seed, None).await;
306+
store.set("seed".to_string(), serde_json::json!(encrypted_seed));
307+
Ok(())
308+
}
309+
310+
#[command]
311+
pub async fn get_seed<R: Runtime>(handle: AppHandle<R>) -> Result<Option<String>, String> {
312+
let store = get_store(&handle);
313+
match store.get("seed") {
314+
Some(value) if value.is_string() => {
315+
let encrypted_seed = value.as_str().unwrap().to_string();
316+
// Decrypt the seed phrase
317+
match crate::internal_decrypt(encrypted_seed, None).await {
318+
Ok(decrypted) => Ok(Some(decrypted)),
319+
Err(_) => Err("Failed to decrypt seed phrase".to_string()),
320+
}
321+
},
322+
_ => Ok(None),
323+
}
324+
}
325+
294326
#[command]
295327
pub fn remove_setting<R: Runtime>(handle: AppHandle<R>, key: String) -> Result<bool, String> {
296328
let store = get_store(&handle);

src-tauri/src/lib.rs

+53-2
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ static PUBLIC_NIP96_CONFIG: OnceCell<ServerConfig> = OnceCell::new();
5757
static TRUSTED_PRIVATE_NIP96: &str = "https://medea-small.jskitty.cat";
5858
static PRIVATE_NIP96_CONFIG: OnceCell<ServerConfig> = OnceCell::new();
5959

60+
61+
static MNEMONIC_SEED: OnceCell<String> = OnceCell::new();
6062
static ENCRYPTION_KEY: OnceCell<[u8; 32]> = OnceCell::new();
6163
static NOSTR_CLIENT: OnceCell<Client> = OnceCell::new();
6264
static TAURI_APP: OnceCell<AppHandle> = OnceCell::new();
@@ -2362,7 +2364,19 @@ pub async fn internal_decrypt(ciphertext: String, password: Option<String>) -> R
23622364
// Tauri command that uses the internal function
23632365
#[tauri::command]
23642366
async fn encrypt(input: String, password: Option<String>) -> String {
2365-
internal_encrypt(input, password).await
2367+
let res = internal_encrypt(input, password).await;
2368+
2369+
// If we have one; save the in-memory seedphrase in an encrypted at-rest format
2370+
match MNEMONIC_SEED.get() {
2371+
Some(seed) => {
2372+
// Save the seed phrase to the database
2373+
let handle = TAURI_APP.get().unwrap();
2374+
let _ = db::set_seed(handle.clone(), seed.to_string()).await;
2375+
}
2376+
_ => ()
2377+
}
2378+
2379+
res
23662380
}
23672381

23682382
// Tauri command that uses the internal function
@@ -2628,6 +2642,40 @@ async fn logout<R: Runtime>(handle: AppHandle<R>) {
26282642
handle.restart();
26292643
}
26302644

2645+
/// Creates a new Nostr keypair derived from a BIP39 Seed Phrase
2646+
#[tauri::command]
2647+
async fn create_account() -> Result<LoginKeyPair, String> {
2648+
// Generate a BIP39 Mnemonic Seed Phrase
2649+
let mnemonic = bip39::Mnemonic::generate(12).map_err(|e| e.to_string())?;
2650+
let mnemonic_string = mnemonic.to_string();
2651+
2652+
// Derive our nsec from our Mnemonic
2653+
let keys = Keys::from_mnemonic(mnemonic_string.clone(), None).map_err(|e| e.to_string())?;
2654+
2655+
// Initialise the Nostr client
2656+
let client = Client::builder()
2657+
.signer(keys.clone())
2658+
.opts(Options::new().gossip(false))
2659+
.build();
2660+
NOSTR_CLIENT.set(client).unwrap();
2661+
2662+
// Add our profile (at least, the npub of it) to our state
2663+
let npub = keys.public_key.to_bech32().map_err(|e| e.to_string())?;
2664+
let mut profile = Profile::new();
2665+
profile.id = npub.clone();
2666+
profile.mine = true;
2667+
STATE.lock().await.profiles.push(profile);
2668+
2669+
// Save the seed in memory, ready for post-pin-setup encryption
2670+
let _ = MNEMONIC_SEED.set(mnemonic_string);
2671+
2672+
// Return the keypair in the same format as the login function
2673+
Ok(LoginKeyPair {
2674+
public: npub,
2675+
private: keys.secret_key().to_bech32().map_err(|e| e.to_string())?,
2676+
})
2677+
}
2678+
26312679
/// Marks a specific message as read
26322680
#[tauri::command]
26332681
async fn mark_as_read(npub: String) -> bool {
@@ -2760,6 +2808,8 @@ pub fn run() {
27602808
db::set_theme,
27612809
db::get_pkey,
27622810
db::set_pkey,
2811+
db::get_seed,
2812+
db::set_seed,
27632813
db::remove_setting,
27642814
fetch_messages,
27652815
message,
@@ -2784,7 +2834,8 @@ pub fn run() {
27842834
fetch_msg_metadata,
27852835
mark_as_read,
27862836
update_unread_counter,
2787-
logout
2837+
logout,
2838+
create_account
27882839
])
27892840
.run(tauri::generate_context!())
27902841
.expect("error while running tauri application");

src/index.html

+8-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,14 @@ <h2 style="color: #ff2ea9;">Dangerzone</h2>
125125
<div id="login-form" class="fadein-anim">
126126
<img src="./icons/vector-logo.svg" style="width: 260px; margin-top: 50px;">
127127
<h4 class="startup-subtext-gradient" style="margin-top: 5px;">Private & Encrypted Messenger</h4>
128-
<div id="login-import" style="position: absolute; bottom: 0px; width: 100%;">
128+
<div id="login-start">
129+
<button id="start-account-creation-btn" style="background-color: #59fcb3; color: black; width: 225px;">Create Account</button>
130+
<br>
131+
<button id="start-login-btn" style="background-color: transparent; border-color: #59fcb3; margin-top: 10px; width: 225px;">Login</button>
132+
<br>
133+
<img src="./icons/by-formlesslabs.svg" style="width: 275px;">
134+
</div>
135+
<div id="login-import" style="position: absolute; bottom: 0px; width: 100%; display: none;">
129136
<img src="./icons/by-formlesslabs.svg" style="width: 275px;">
130137
<div class="row input-box" style="height: 52px;">
131138
<input type="password" style="width: 100%; margin-left: 10px; margin-top: 5px; margin-bottom: 5px; background-color: black;" id="login-input" placeholder="Enter nsec or Seed Phrase..." />

src/main.js

+19
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ getVersion().then(v => {
1212

1313
const domTheme = document.getElementById('theme');
1414

15+
const domLoginStart = document.getElementById('login-start');
16+
const domLoginAccountCreationBtn = document.getElementById('start-account-creation-btn');
17+
const domLoginAccountBtn = document.getElementById('start-login-btn');
1518
const domLogin = document.getElementById('login-form');
1619
const domLoginImport = document.getElementById('login-import');
1720
const domLoginInput = document.getElementById('login-input');
@@ -1112,6 +1115,7 @@ function renderCurrentProfile(cProfile) {
11121115
* @param {boolean} fUnlock - Whether we're unlocking an existing key, or encrypting the given one
11131116
*/
11141117
function openEncryptionFlow(pkey, fUnlock = false) {
1118+
domLoginStart.style.display = 'none';
11151119
domLoginImport.style.display = 'none';
11161120
domLoginEncrypt.style.display = '';
11171121

@@ -2025,6 +2029,21 @@ window.addEventListener("DOMContentLoaded", async () => {
20252029
// Hook up our static buttons
20262030
domSettingsBtn.onclick = openSettings;
20272031
domChatlistBtn.onclick = openChatlist;
2032+
domLoginAccountCreationBtn.onclick = async () => {
2033+
try {
2034+
const { public, private } = await invoke("create_account");
2035+
strPubkey = public;
2036+
// Open the Encryption Flow
2037+
openEncryptionFlow(private);
2038+
} catch (e) {
2039+
// Display the backend error
2040+
popupConfirm(e, '', true);
2041+
}
2042+
};
2043+
domLoginAccountBtn.onclick = () => {
2044+
domLoginImport.style.display = '';
2045+
domLoginStart.style.display = 'none';
2046+
};
20282047
domLoginBtn.onclick = async () => {
20292048
// Import and derive our keys
20302049
try {

0 commit comments

Comments
 (0)