Skip to content

Commit 720d85b

Browse files
committed
feat(nip-06): mnemonic seed phrase
* feat(cli): read secret key and mnemonic from an environment variable * feat(cli): print secret key
1 parent 6248e23 commit 720d85b

File tree

10 files changed

+562
-107
lines changed

10 files changed

+562
-107
lines changed

Cargo.lock

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

Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ edition = "2021"
77

88
[dependencies]
99
aes = "0.8.2"
10+
anyhow = "1.0.69"
1011
base64 = "0.21.0"
1112
bech32 = "0.9.1"
13+
bip32 = { version = "0.4.0", featues = ["secp256k1-ffi"]}
1214
cbc = { version = "0.1.2", features = ["block-padding", "alloc"]}
1315
clap = { version = "4.1.4", features = ["derive"] }
1416
hex = "0.4.3"

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ Library:
1515
- [x] Generate and sign an event
1616
- [x] Construct messages requests
1717
- [x] Parse message responses
18-
- [ ] Direct message support
18+
- [x] Direct message support
19+
- [x] Seed phrases
1920

2021
CLI:
2122

2223
- [x] Read an event as json from stdin and verify
2324
- [x] Generate an event from cli arguments and write to stdout as json.
2425
- [x] Generate message requests
25-
- [ ] Generate a new key and print to stdout
26+
- [x] Generate a new key and print to stdout
2627
- [ ] Read the private key from an environment variable
28+
- [ ] Derive the private key from a mnemonic, read from an environment variable

src/cli.rs

+115-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,119 @@
1-
use std::io::{stdout, Error, ErrorKind, Read, Result, Write};
1+
use std::io::{stdin, stdout, Error, ErrorKind, Read, Write};
22

33
use crate::event::{Event, Kind};
44
use crate::key::Pair;
55
use crate::message::MessageRequest;
66
use crate::request::Request;
77
use crate::Hex;
8+
use anyhow::Result;
9+
use clap::{Parser, Subcommand};
10+
11+
#[derive(Parser)]
12+
#[command(author, version, about, long_about)]
13+
pub struct Args {
14+
#[command(subcommand)]
15+
command: Command,
16+
}
17+
18+
#[derive(Subcommand)]
19+
enum Command {
20+
/// Verify and generate events
21+
Event {
22+
#[command(subcommand)]
23+
subcommand: EventCommand,
24+
},
25+
/// Generate requests
26+
Request {
27+
#[arg(short, long)]
28+
ids: Vec<Hex>,
29+
#[arg(short, long)]
30+
authors: Vec<Hex>,
31+
#[arg(short, long)]
32+
kinds: Vec<u32>,
33+
#[arg(short, long)]
34+
e: Vec<Hex>,
35+
#[arg(short, long)]
36+
p: Vec<Hex>,
37+
#[arg(short, long)]
38+
since: Option<u32>,
39+
#[arg(short, long)]
40+
until: Option<u32>,
41+
#[arg(short, long)]
42+
limit: Option<u16>,
43+
},
44+
/// Generate message requests
45+
MessageRequest {
46+
#[command(subcommand)]
47+
subcommand: MessageRequestCommand,
48+
},
49+
/// Print key
50+
Key,
51+
}
52+
53+
#[derive(Subcommand)]
54+
pub enum EventCommand {
55+
/// Verifies an event on stdin
56+
Verify,
57+
/// Output a new event to stdout
58+
Generate {
59+
#[arg(short, long)]
60+
kind: Kind,
61+
content: String,
62+
},
63+
/// Output a new set metadata event to stdout
64+
SetMetadata {
65+
name: String,
66+
about: String,
67+
picture: String,
68+
},
69+
/// Output a new text note to stdout
70+
TextNote { content: String },
71+
/// Output a new recommend relay to stdout
72+
RecommendRelay { relay: String },
73+
}
74+
75+
// #[derive(Subcommand)]
76+
// pub enum RequestCommand {}
77+
78+
#[derive(Subcommand)]
79+
pub enum MessageRequestCommand {
80+
Event,
81+
Request { id: String },
82+
}
83+
84+
pub fn handle_args(args: Args, pair: &Pair) -> Result<()> {
85+
match args.command {
86+
Command::Event { subcommand } => match subcommand {
87+
EventCommand::Verify => verify_event(stdin())?,
88+
EventCommand::Generate { kind, content } => generate_event(kind, &content)?,
89+
EventCommand::SetMetadata {
90+
name,
91+
about,
92+
picture,
93+
} => set_metadata_event(&name, &about, &picture)?,
94+
EventCommand::TextNote { content } => text_note_event(&content)?,
95+
EventCommand::RecommendRelay { relay } => recommend_relay_event(&relay)?,
96+
},
97+
Command::Request {
98+
ids,
99+
authors,
100+
kinds,
101+
e,
102+
p,
103+
since,
104+
until,
105+
limit,
106+
} => write_request(stdout(), ids, authors, kinds, e, p, since, until, limit)?,
107+
Command::MessageRequest { subcommand } => match subcommand {
108+
MessageRequestCommand::Event => event_message_request(stdin(), stdout())?,
109+
MessageRequestCommand::Request { id } => {
110+
request_message_request(stdin(), stdout(), id)?
111+
}
112+
},
113+
Command::Key => print_key(&mut stdout(), pair)?,
114+
};
115+
Ok(())
116+
}
8117

9118
pub fn io_error(message: &str) -> Error {
10119
Error::new(ErrorKind::Other, message)
@@ -99,3 +208,8 @@ pub fn request_message_request<R: Read, W: Write>(reader: R, writer: W, id: Stri
99208
serde_json::to_writer(writer, &message)?;
100209
Ok(())
101210
}
211+
212+
pub fn print_key<W: Write>(writer: &mut W, pair: &Pair) -> Result<()> {
213+
writer.write_all(pair.secret_key().unwrap().display_secret_as_nsec().as_ref())?;
214+
Ok(())
215+
}

src/env.rs

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use std::env::{self, VarError};
2+
3+
use anyhow::{Error, Result};
4+
5+
pub fn var(key: &str) -> Var<String> {
6+
Var(env::var(key).map_err(Error::from))
7+
}
8+
9+
pub struct Var<T>(Result<T>);
10+
11+
impl<T> Var<T> {
12+
pub fn new(var: T) -> Self {
13+
Var(Ok(var))
14+
}
15+
pub fn or_missing(self, value: Self) -> Self {
16+
let result = self.0.or_else(|err: Error| match err.downcast_ref() {
17+
Some(VarError::NotPresent) => value.0,
18+
_ => Err(err),
19+
});
20+
Self(result)
21+
}
22+
23+
pub fn and_then<F, U>(self, f: F) -> Var<U>
24+
where
25+
F: FnOnce(T) -> Result<U>,
26+
{
27+
let result = self.0.and_then(f);
28+
Var(result)
29+
}
30+
31+
pub fn to_result(self) -> Result<T> {
32+
self.0
33+
}
34+
}

src/event.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ pub struct Contact {
156156

157157
type Result<T> = std::result::Result<T, Error>;
158158

159-
#[derive(Debug)]
159+
#[derive(Debug, thiserror::Error)]
160+
#[error("event error")]
160161
pub enum Error {
161162
HashMismatch,
162163
Signature(signature::Error),

src/key.rs

+45
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::str::FromStr;
44
use crate::bech32;
55
use crate::bech32::nsec::SECRET_PREFIX;
66
use crate::encryption;
7+
use crate::mnemonic;
8+
use crate::mnemonic::Mnemonic;
79
use crate::signature::Signature;
810
use secp256k1 as ec;
911
use secp256k1::schnorr;
@@ -23,6 +25,15 @@ pub struct Pair {
2325
}
2426

2527
impl Pair {
28+
pub fn new<S>(secret_key: S) -> Result<Self>
29+
where
30+
S: AsRef<str>,
31+
{
32+
let sk = SecretKey::from_str(secret_key.as_ref())?;
33+
let pair = Self::from(&sk);
34+
Ok(pair)
35+
}
36+
2637
/// Generates a new SECP256k1 key pair.
2738
pub fn generate() -> Self {
2839
let (sk, pk) = ec::generate_keypair(&mut ec::rand::thread_rng());
@@ -35,6 +46,16 @@ impl Pair {
3546
}
3647
}
3748

49+
/// Creates a new pair from a mnemonic.
50+
/// Defined in [NIP-01](https://github.com/nostr-protocol/nips/blob/master/06.md).
51+
pub fn from_mnemonic<S>(s: S) -> Result<Self>
52+
where
53+
S: AsRef<str>,
54+
{
55+
let mnemonic = Mnemonic::new(s.as_ref())?;
56+
Pair::try_from(&mnemonic)
57+
}
58+
3859
pub fn new_shared_secret(ours: &SecretKey, theirs: &PublicKey) -> Self {
3960
let pk = theirs.0.public_key(ec::Parity::Even); // parity is not important
4061
let sk = ours.0;
@@ -103,6 +124,17 @@ impl From<&PublicKey> for Pair {
103124
}
104125
}
105126

127+
impl TryFrom<&Mnemonic> for Pair {
128+
type Error = Error;
129+
130+
fn try_from(mnemonic: &Mnemonic) -> result::Result<Self, Self::Error> {
131+
let bytes = mnemonic.to_bytes();
132+
let sk = SecretKey::try_from(&bytes[..])?;
133+
let pair = Pair::from(&sk);
134+
Ok(pair)
135+
}
136+
}
137+
106138
/// Secret key
107139
#[derive(Clone, Copy)]
108140
pub struct SecretKey(ec::SecretKey);
@@ -205,11 +237,14 @@ pub enum Error {
205237
Signature(String),
206238
#[error("encryption")]
207239
Encryption(#[from] encryption::Error),
240+
#[error("mnemonic")]
241+
Mnemonic(#[from] mnemonic::Error),
208242
}
209243

210244
#[cfg(test)]
211245
pub mod tests {
212246
use super::*;
247+
use crate::bech32::ToBech32;
213248

214249
fn get_secret_key() -> SecretKey {
215250
SecretKey::from_str("0f1429676edf1ff8e5ca8202c8741cb695fc3ce24ec3adc0fcf234116f08f849")
@@ -275,4 +310,14 @@ pub mod tests {
275310
assert_eq!(got, want);
276311
Ok(())
277312
}
313+
314+
#[test]
315+
fn from_mnemonic_works() -> Result<()> {
316+
let s = crate::mnemonic::tests::get_mnemonic_str();
317+
let pair = Pair::from_mnemonic(s)?;
318+
let got = pair.public_key().to_bech32();
319+
let want = "npub1gw5zyqa9yj2rrq5u683y9sfdpv49hmgfkw37hupgvf5vrtdmr60sspjdzz";
320+
assert_eq!(got, want);
321+
Ok(())
322+
}
278323
}

src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
pub mod bech32;
22
pub mod cli;
33
pub mod encryption;
4+
pub mod env;
45
pub mod event;
56
pub mod key;
67
pub mod message;
8+
pub mod mnemonic;
79
pub mod request;
810
pub mod signature;
911
pub mod time;

0 commit comments

Comments
 (0)