diff --git a/migrations/2024-02-20-210617_user_info/up.sql b/migrations/2024-02-20-210617_user_info/up.sql index 4ad8425..620a0fb 100644 --- a/migrations/2024-02-20-210617_user_info/up.sql +++ b/migrations/2024-02-20-210617_user_info/up.sql @@ -2,6 +2,7 @@ CREATE TABLE app_user ( id SERIAL PRIMARY KEY, pubkey VARCHAR(64) NOT NULL, name VARCHAR(255) NOT NULL UNIQUE, + unblinded_msg VARCHAR(255) NOT NULL UNIQUE, federation_id VARCHAR(64) NOT NULL, federation_invite_code VARCHAR(255) NOT NULL ); diff --git a/src/db.rs b/src/db.rs index 6a20ffe..fe2448d 100644 --- a/src/db.rs +++ b/src/db.rs @@ -13,6 +13,7 @@ use crate::models::{ #[cfg_attr(test, automock)] pub(crate) trait DBConnection { fn check_name_available(&self, name: String) -> anyhow::Result; + fn check_token_not_spent(&self, msg: String) -> anyhow::Result; fn insert_new_user(&self, name: NewAppUser) -> anyhow::Result; fn get_pending_invoices(&self) -> anyhow::Result>; fn insert_new_invoice(&self, invoice: NewInvoice) -> anyhow::Result; @@ -35,6 +36,11 @@ impl DBConnection for PostgresConnection { AppUser::check_available_name(conn, name) } + fn check_token_not_spent(&self, msg: String) -> anyhow::Result { + let conn = &mut self.db.get()?; + AppUser::check_token_not_spent(conn, msg) + } + fn insert_new_user(&self, new_user: NewAppUser) -> anyhow::Result { let conn = &mut self.db.get()?; new_user.insert(conn) diff --git a/src/lnurlp.rs b/src/lnurlp.rs index e9d5032..62b6402 100644 --- a/src/lnurlp.rs +++ b/src/lnurlp.rs @@ -205,6 +205,7 @@ mod tests_integration { pubkey: "e6642fd69bd211f93f7f1f36ca51a26a5290eb2dd1b0d8279a87bb0d480c8443".to_string(), name: username.clone(), federation_id: "".to_string(), + unblinded_msg: "".to_string(), federation_invite_code: "".to_string(), }; diff --git a/src/models/app_user.rs b/src/models/app_user.rs index 94fa449..8e2fd51 100644 --- a/src/models/app_user.rs +++ b/src/models/app_user.rs @@ -11,6 +11,7 @@ pub struct AppUser { pub id: i32, pub pubkey: String, pub name: String, + pub unblinded_msg: String, pub federation_id: String, pub federation_invite_code: String, } @@ -42,6 +43,14 @@ impl AppUser { == 0) } + pub fn check_token_not_spent(conn: &mut PgConnection, msg: String) -> anyhow::Result { + Ok(app_user::table + .filter(app_user::unblinded_msg.eq(msg)) + .count() + .get_result::(conn)? + == 0) + } + pub fn get_by_pubkey( conn: &mut PgConnection, pubkey: String, @@ -59,6 +68,7 @@ pub struct NewAppUser { pub pubkey: String, pub name: String, pub federation_id: String, + pub unblinded_msg: String, pub federation_invite_code: String, } diff --git a/src/models/schema.rs b/src/models/schema.rs index 4163efc..763144d 100644 --- a/src/models/schema.rs +++ b/src/models/schema.rs @@ -7,6 +7,8 @@ diesel::table! { pubkey -> Varchar, #[max_length = 255] name -> Varchar, + #[max_length = 255] + unblinded_msg -> Varchar, #[max_length = 64] federation_id -> Varchar, #[max_length = 255] diff --git a/src/nostr.rs b/src/nostr.rs index 6b16149..1fed4de 100644 --- a/src/nostr.rs +++ b/src/nostr.rs @@ -65,6 +65,7 @@ mod tests_integration { pubkey: "e6642fd69bd211f93f7f1f36ca51a26a5290eb2dd1b0d8279a87bb0d480c8443".to_string(), name: username.clone(), federation_id: "".to_string(), + unblinded_msg: "".to_string(), federation_invite_code: "".to_string(), }; diff --git a/src/register.rs b/src/register.rs index f2d1b4a..ee3ee1c 100644 --- a/src/register.rs +++ b/src/register.rs @@ -36,9 +36,20 @@ pub async fn register( return Err((StatusCode::BAD_REQUEST, "Unavailable".to_string())); } + // verify token and double check that it has not been spent before if !req.verify(state.auth_pk) { return Err((StatusCode::UNAUTHORIZED, "Invalid blind sig".to_string())); } + match state.db.check_token_not_spent(req.msg.0.to_string()) { + Ok(true) => (), + Ok(false) => { + return Err((StatusCode::BAD_REQUEST, "Already Registered".to_string())); + } + Err(e) => { + error!("Error in register: {e:?}"); + return Err((StatusCode::INTERNAL_SERVER_ERROR, "ServerError".to_string())); + } + } match state.db.check_name_available(req.name.clone()) { Ok(true) => (), @@ -70,9 +81,6 @@ pub async fn register( } } - // TODO insert blinding info and new user as an atomic transaction - // TODO save nonce to db and check for replay attacks - match state.db.insert_new_user(req.into()) { Ok(_) => Ok(RegisterResponse {}), Err(e) => { @@ -195,6 +203,7 @@ mod tests_integration { pubkey: "".to_string(), name: commonname.clone(), federation_id: "".to_string(), + unblinded_msg: "test_username_checker".to_string(), federation_invite_code: "".to_string(), }; @@ -237,7 +246,7 @@ mod tests_integration { }; // generate valid blinded message - let msg = tbs::Message::from_bytes(b"Hello World!"); + let msg = tbs::Message::from_bytes(b"register_username_tests"); let blinding_key = BlindingKey::random(); let blinded_msg = blind_message(msg, blinding_key); let blind_sig = signer.blind_sign(blinded_msg); @@ -303,7 +312,7 @@ mod tests_integration { // generate valid blinded message let signer = BlindSigner::derive(&[0u8; 32], 0, 0); - let msg = tbs::Message::from_bytes(b"Hello World!"); + let msg = tbs::Message::from_bytes(b"register_username_add_unknown_federation_tests"); let blinding_key = BlindingKey::random(); let blinded_msg = blind_message(msg, blinding_key); let blind_sig = signer.blind_sign(blinded_msg); @@ -330,4 +339,79 @@ mod tests_integration { } } } + + #[tokio::test] + pub async fn register_username_already_spent_token_tests() { + dotenv::dotenv().ok(); + let pg_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + let db = setup_db(pg_url); + + // swap out fm with a mock here since that's not what is being tested + let mut mock_mm = MockMultiMintWrapperTrait::new(); + mock_mm + .expect_check_has_federation() + .times(1) + .returning(|_| true); + + // nostr + let nostr_nsec_str = std::env::var("NSEC").expect("FM_DB_PATH must be set"); + let nostr_sk = Keys::from_sk_str(&nostr_nsec_str).expect("Invalid NOSTR_SK"); + let nostr = nostr_sdk::Client::new(&nostr_sk); + + // create blind signer + let signer = BlindSigner::derive(&[0u8; 32], 0, 0); + + let mock_mm = Arc::new(mock_mm); + let state = State { + db: db.clone(), + mm: mock_mm, + secp: Secp256k1::new(), + nostr, + auth_pk: signer.pk, + domain: "http://127.0.0.1:8080".to_string(), + }; + + // generate valid blinded message + let msg = tbs::Message::from_bytes(b"register_username_already_spent_token_tests"); + let blinding_key = BlindingKey::random(); + let blinded_msg = blind_message(msg, blinding_key); + let blind_sig = signer.blind_sign(blinded_msg); + let sig = unblind_signature(blinding_key, blind_sig); + + let connect = InviteCode::new( + "ws://test1".parse().unwrap(), + PeerId::from_str("1").unwrap(), + FederationId::dummy(), + ); + let req = RegisterRequest { + name: "registername1".to_string(), + pubkey: "".to_string(), + federation_id: connect.federation_id(), + federation_invite_code: connect.to_string(), + msg, + sig, + }; + + // let the first user register sucessfully + match register(&state, req).await { + Ok(_) => (), + Err(_) => { + panic!("shouldn't error") + } + } + + // second username attempting to register with the same msg + let req2 = RegisterRequest { + name: "registername2".to_string(), + pubkey: "".to_string(), + federation_id: connect.federation_id(), + federation_invite_code: connect.to_string(), + msg, + sig, + }; + + if register(&state, req2).await.is_ok() { + panic!("should not succeed") + } + } } diff --git a/src/routes.rs b/src/routes.rs index 5d56793..31adb27 100644 --- a/src/routes.rs +++ b/src/routes.rs @@ -55,6 +55,7 @@ impl From for NewAppUser { pubkey: request.pubkey, name: request.name, federation_id: request.federation_id.to_string(), + unblinded_msg: request.msg.0.to_string(), federation_invite_code: request.federation_invite_code, } }