diff --git a/chirpstack/src/api/error.rs b/chirpstack/src/api/error.rs index a606f963..ffff9715 100644 --- a/chirpstack/src/api/error.rs +++ b/chirpstack/src/api/error.rs @@ -28,6 +28,9 @@ impl ToStatus for storage::error::Error { storage::error::Error::InvalidMIC => { Status::new(Code::InvalidArgument, format!("{}", self)) } + storage::error::Error::InvalidDevNonce => { + Status::new(Code::InvalidArgument, format!("{}", self)) + } storage::error::Error::Validation(_) => { Status::new(Code::InvalidArgument, format!("{}", self)) } diff --git a/chirpstack/src/storage/device_keys.rs b/chirpstack/src/storage/device_keys.rs index c39429ab..1dea81c2 100644 --- a/chirpstack/src/storage/device_keys.rs +++ b/chirpstack/src/storage/device_keys.rs @@ -157,6 +157,43 @@ pub async fn reset_nonces(dev_eui: &EUI64) -> Result { Ok(dk) } +pub async fn validate_and_store_dev_nonce( + dev_eui: &EUI64, + dev_nonce: i32, +) -> Result { + let dk = task::spawn_blocking({ + let dev_eui = *dev_eui; + move || -> Result { + let c = get_db_conn()?; + c.transaction::(|| { + let mut dk: DeviceKeys = device_keys::dsl::device_keys + .find(&dev_eui) + .for_update() + .first(&c) + .map_err(|e| Error::from_diesel(e, dev_eui.to_string()))?; + + if dk.dev_nonces.contains(&(dev_nonce)) { + return Err(Error::InvalidDevNonce); + } + + dk.dev_nonces.push(dev_nonce); + + diesel::update(device_keys::dsl::device_keys.find(&dev_eui)) + .set(( + device_keys::updated_at.eq(Utc::now()), + device_keys::dev_nonces.eq(&dk.dev_nonces), + )) + .get_result(&c) + .map_err(|e| Error::from_diesel(e, dev_eui.to_string())) + }) + } + }) + .await??; + + info!(dev_eui = %dev_eui, dev_nonce = dev_nonce, "Device-nonce validated and stored"); + Ok(dk) +} + #[cfg(test)] pub mod test { use super::*; diff --git a/chirpstack/src/storage/error.rs b/chirpstack/src/storage/error.rs index 01b8cc6f..4d70a6cf 100644 --- a/chirpstack/src/storage/error.rs +++ b/chirpstack/src/storage/error.rs @@ -24,6 +24,9 @@ pub enum Error { #[error("Invalid MIC")] InvalidMIC, + #[error("Invalid DevNonce")] + InvalidDevNonce, + #[error("Validation error: {0}")] Validation(String), diff --git a/chirpstack/src/uplink/join.rs b/chirpstack/src/uplink/join.rs index e1d3ff0b..e9b0940c 100644 --- a/chirpstack/src/uplink/join.rs +++ b/chirpstack/src/uplink/join.rs @@ -17,7 +17,9 @@ use super::{filter_rx_info_by_tenant_id, helpers, UplinkFrameSet}; use crate::api::helpers::ToProto; use crate::backend::{joinserver, keywrap}; use crate::storage::device_session; -use crate::storage::{application, device, device_keys, device_profile, metrics, tenant}; +use crate::storage::{ + application, device, device_keys, device_profile, error::Error as StorageError, metrics, tenant, +}; use crate::{config, downlink, framelog, integration, metalog, region}; use chirpstack_api::{api, common, integration as integration_pb, internal, meta}; @@ -87,9 +89,8 @@ impl JoinRequest { ctx.get_join_accept_from_js().await?; } else { // Using internal keys - ctx.get_device_keys().await?; + ctx.validate_dev_nonce_and_get_device_keys().await?; ctx.validate_mic().await?; - ctx.validate_dev_nonce().await?; ctx.construct_join_accept_and_set_keys()?; ctx.save_device_keys().await?; } @@ -208,10 +209,55 @@ impl JoinRequest { Ok(()) } - async fn get_device_keys(&mut self) -> Result<()> { - trace!("Getting device-keys"); + async fn validate_dev_nonce_and_get_device_keys(&mut self) -> Result<()> { + trace!("Validate dev-nonce and get device-keys"); + let dev = self.device.as_ref().unwrap(); + let app = self.application.as_ref().unwrap(); + let join_request = self.join_request.as_ref().unwrap(); + + self.device_keys = Some( + match device_keys::validate_and_store_dev_nonce( + &dev.dev_eui, + join_request.dev_nonce as i32, + ) + .await + { + Ok(v) => v, + Err(v) => match v { + StorageError::InvalidDevNonce => { + integration::log_event( + &app.id, + &dev.variables, + &integration_pb::LogEvent { + deduplication_id: self.uplink_frame_set.uplink_set_id.to_string(), + time: Some(Utc::now().into()), + device_info: self.device_info.clone(), + level: integration_pb::LogLevel::Error.into(), + code: integration_pb::LogCode::Otaa.into(), + description: "DevNonce has already been used".into(), + ..Default::default() + }, + ) + .await?; + + metrics::save( + &format!("device:{}", dev.dev_eui), + &metrics::Record { + time: Local::now(), + metrics: [("error_OTAA".into(), 1f64)].iter().cloned().collect(), + }, + ) + .await?; + + return Err(v.into()); + } + _ => { + return Err(v.into()); + } + }, + }, + ); - self.device_keys = Some(device_keys::get(&self.device.as_ref().unwrap().dev_eui).await?); Ok(()) } @@ -258,48 +304,6 @@ impl JoinRequest { Err(anyhow!("Invalid MIC")) } - async fn validate_dev_nonce(&mut self) -> Result<()> { - let device_keys = self.device_keys.as_mut().unwrap(); - let join_request = self.join_request.as_ref().unwrap(); - - if !device_keys - .dev_nonces - .contains(&(join_request.dev_nonce as i32)) - { - device_keys.dev_nonces.push(join_request.dev_nonce as i32); - return Ok(()); - } - - let app = self.application.as_ref().unwrap(); - let dev = self.device.as_ref().unwrap(); - - integration::log_event( - &app.id, - &dev.variables, - &integration_pb::LogEvent { - deduplication_id: self.uplink_frame_set.uplink_set_id.to_string(), - time: Some(Utc::now().into()), - device_info: self.device_info.clone(), - level: integration_pb::LogLevel::Error.into(), - code: integration_pb::LogCode::Otaa.into(), - description: "DevNonce has already been used".into(), - ..Default::default() - }, - ) - .await?; - - metrics::save( - &format!("device:{}", dev.dev_eui), - &metrics::Record { - time: Local::now(), - metrics: [("error_OTAA".into(), 1f64)].iter().cloned().collect(), - }, - ) - .await?; - - Err(anyhow!("DevNone has already been used")) - } - fn get_random_dev_addr(&mut self) -> Result<()> { let conf = config::get();