diff --git a/api/proto/api/device.proto b/api/proto/api/device.proto index 11d5f04d..49fad456 100644 --- a/api/proto/api/device.proto +++ b/api/proto/api/device.proto @@ -278,6 +278,11 @@ message DeviceKeys { // Application root key (128 bit). // Note: This field only needs to be set for LoRaWAN 1.1.x devices! string app_key = 3; + + // Gen App Key (128 bit). + // Note: This field only needs to be set for LoRaWAN 1.0.x devices that + // implement TS005 (remote multicast setup). + string gen_app_key = 4; } message CreateDeviceRequest { diff --git a/api/rust/proto/chirpstack/api/device.proto b/api/rust/proto/chirpstack/api/device.proto index 11d5f04d..49fad456 100644 --- a/api/rust/proto/chirpstack/api/device.proto +++ b/api/rust/proto/chirpstack/api/device.proto @@ -278,6 +278,11 @@ message DeviceKeys { // Application root key (128 bit). // Note: This field only needs to be set for LoRaWAN 1.1.x devices! string app_key = 3; + + // Gen App Key (128 bit). + // Note: This field only needs to be set for LoRaWAN 1.0.x devices that + // implement TS005 (remote multicast setup). + string gen_app_key = 4; } message CreateDeviceRequest { diff --git a/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/down.sql b/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/down.sql index 21bf56cb..4fa3c925 100644 --- a/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/down.sql +++ b/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/down.sql @@ -1,3 +1,6 @@ +alter table device_keys + drop column gen_app_key; + drop table fuota_deployment_job; drop table fuota_deployment_gateway; drop table fuota_deployment_device; diff --git a/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/up.sql b/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/up.sql index 1e093d1f..36decf57 100644 --- a/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/up.sql +++ b/chirpstack/migrations_postgres/2025-01-21-093745_add_fuota_support/up.sql @@ -7,6 +7,8 @@ create table fuota_deployment ( name varchar(100) not null, application_id uuid not null references application on delete cascade, device_profile_id uuid not null references device_profile on delete cascade, + multicast_addr bytea not null, + multicast_key bytea not null, multicast_group_type char(1) not null, multicast_class_c_scheduling_type varchar(20) not null, multicast_dr smallint not null, @@ -54,9 +56,16 @@ create table fuota_deployment_job ( max_retry_count smallint not null, attempt_count smallint not null, scheduler_run_after timestamp with time zone not null, + return_msg text not null, primary key (fuota_deployment_id, job) ); create index idx_fuota_deployment_job_completed_at on fuota_deployment_job(completed_at); create index idx_fuota_deployment_job_scheduler_run_after on fuota_deployment_job(scheduler_run_after); + +alter table device_keys + add column gen_app_key bytea not null default decode('00000000000000000000000000000000', 'hex'); + +alter table device_keys + alter column gen_app_key drop default; diff --git a/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/down.sql b/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/down.sql index 21bf56cb..4fa3c925 100644 --- a/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/down.sql +++ b/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/down.sql @@ -1,3 +1,6 @@ +alter table device_keys + drop column gen_app_key; + drop table fuota_deployment_job; drop table fuota_deployment_gateway; drop table fuota_deployment_device; diff --git a/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/up.sql b/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/up.sql index 0f8218b1..f6e6c589 100644 --- a/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/up.sql +++ b/chirpstack/migrations_sqlite/2025-01-27-100007_add_fuota_support/up.sql @@ -7,6 +7,8 @@ create table fuota_deployment ( name varchar(100) not null, application_id text not null references application on delete cascade, device_profile_id text not null references device_profile on delete cascade, + multicast_addr blob not null, + multicast_key blob not null, multicast_group_type char(1) not null, multicast_class_c_scheduling_type varchar(20) not null, multicast_dr smallint not null, @@ -54,9 +56,13 @@ create table fuota_deployment_job ( max_retry_count smallint not null, attempt_count smallint not null, scheduler_run_after datetime not null, + return_msg text not null, primary key (fuota_deployment_id, job) ); create index idx_fuota_deployment_job_completed_at on fuota_deployment_job(completed_at); create index idx_fuota_deployment_job_scheduler_run_after on fuota_deployment_job(scheduler_run_after); + +alter table device_keys + add column gen_app_key blob not null default x'00000000000000000000000000000000'; diff --git a/chirpstack/src/api/device.rs b/chirpstack/src/api/device.rs index 1664c0fa..c1acf62a 100644 --- a/chirpstack/src/api/device.rs +++ b/chirpstack/src/api/device.rs @@ -354,11 +354,8 @@ impl DeviceService for Device { let dk = device_keys::DeviceKeys { dev_eui, nwk_key: AES128Key::from_str(&req_dk.nwk_key).map_err(|e| e.status())?, - app_key: if !req_dk.app_key.is_empty() { - AES128Key::from_str(&req_dk.app_key).map_err(|e| e.status())? - } else { - AES128Key::null() - }, + app_key: AES128Key::from_str(&req_dk.app_key).map_err(|e| e.status())?, + gen_app_key: AES128Key::from_str(&req_dk.gen_app_key).map_err(|e| e.status())?, ..Default::default() }; @@ -392,6 +389,7 @@ impl DeviceService for Device { dev_eui: dk.dev_eui.to_string(), nwk_key: dk.nwk_key.to_string(), app_key: dk.app_key.to_string(), + gen_app_key: dk.gen_app_key.to_string(), }), created_at: Some(helpers::datetime_to_prost_timestamp(&dk.created_at)), updated_at: Some(helpers::datetime_to_prost_timestamp(&dk.updated_at)), @@ -428,11 +426,8 @@ impl DeviceService for Device { dev_nonces: dk.dev_nonces, join_nonce: dk.join_nonce, nwk_key: AES128Key::from_str(&req_dk.nwk_key).map_err(|e| e.status())?, - app_key: if !req_dk.app_key.is_empty() { - AES128Key::from_str(&req_dk.app_key).map_err(|e| e.status())? - } else { - AES128Key::null() - }, + app_key: AES128Key::from_str(&req_dk.app_key).map_err(|e| e.status())?, + gen_app_key: AES128Key::from_str(&req_dk.gen_app_key).map_err(|e| e.status())?, ..Default::default() }; let _ = device_keys::update(dk).await.map_err(|e| e.status())?; @@ -1393,6 +1388,7 @@ pub mod test { dev_eui: "0102030405060708".into(), nwk_key: "01020304050607080102030405060708".into(), app_key: "02020304050607080202030405060708".into(), + gen_app_key: "03020304050607080202030405060708".into(), }), }, ); @@ -1411,6 +1407,7 @@ pub mod test { dev_eui: "0102030405060708".into(), nwk_key: "01020304050607080102030405060708".into(), app_key: "02020304050607080202030405060708".into(), + gen_app_key: "03020304050607080202030405060708".into(), }), get_keys_resp.get_ref().device_keys ); @@ -1423,6 +1420,7 @@ pub mod test { dev_eui: "0102030405060708".into(), nwk_key: "01020304050607080102030405060708".into(), app_key: "03020304050607080302030405060708".into(), + gen_app_key: "03020304050607080202030405060708".into(), }), }, ); @@ -1441,6 +1439,7 @@ pub mod test { dev_eui: "0102030405060708".into(), nwk_key: "01020304050607080102030405060708".into(), app_key: "03020304050607080302030405060708".into(), + gen_app_key: "03020304050607080202030405060708".into(), }), get_keys_resp.get_ref().device_keys ); diff --git a/chirpstack/src/api/fuota.rs b/chirpstack/src/api/fuota.rs index 9787ccf0..461f1879 100644 --- a/chirpstack/src/api/fuota.rs +++ b/chirpstack/src/api/fuota.rs @@ -8,9 +8,11 @@ use chirpstack_api::api; use chirpstack_api::api::fuota_service_server::FuotaService; use lrwn::EUI64; +use crate::aeskey::get_random_aes_key; use crate::api::auth::validator; use crate::api::error::ToStatus; use crate::api::helpers::{self, FromProto, ToProto}; +use crate::devaddr::get_random_dev_addr; use crate::storage::{fields, fuota}; pub struct Fuota { @@ -50,6 +52,8 @@ impl FuotaService for Fuota { name: req_dp.name.clone(), application_id: app_id.into(), device_profile_id: dp_id.into(), + multicast_addr: get_random_dev_addr(), + multicast_key: get_random_aes_key(), multicast_group_type: match req_dp.multicast_group_type() { api::MulticastGroupType::ClassB => "B", api::MulticastGroupType::ClassC => "C", @@ -284,8 +288,7 @@ impl FuotaService for Fuota { fuota::create_job(fuota::FuotaDeploymentJob { fuota_deployment_id: d.id, - job: fields::FuotaJob::McGroupSetup, - max_retry_count: d.unicast_max_retry_count, + job: fields::FuotaJob::CreateMcGroup, ..Default::default() }) .await @@ -733,7 +736,7 @@ mod test { .unwrap(); assert_eq!(1, jobs.len()); assert_eq!(create_resp.id, jobs[0].fuota_deployment_id.to_string()); - assert_eq!(fields::FuotaJob::McGroupSetup, jobs[0].job); + assert_eq!(fields::FuotaJob::CreateMcGroup, jobs[0].job); // add device let add_dev_req = get_request( diff --git a/chirpstack/src/main.rs b/chirpstack/src/main.rs index 95e79a2e..9fbd517d 100644 --- a/chirpstack/src/main.rs +++ b/chirpstack/src/main.rs @@ -19,7 +19,9 @@ use tracing_subscriber::{filter, prelude::*}; use lrwn::EUI64; mod adr; +mod aeskey; mod api; +mod applayer; mod backend; mod certificate; mod cmd; diff --git a/chirpstack/src/storage/device.rs b/chirpstack/src/storage/device.rs index a8261e0c..da357689 100644 --- a/chirpstack/src/storage/device.rs +++ b/chirpstack/src/storage/device.rs @@ -1140,6 +1140,8 @@ pub mod test { count: 1, limit: 10, offset: 0, + order: OrderBy::Name, + order_by_desc: false, }, FilterTest { name: "filter by tags - 1.2.0".into(), @@ -1154,6 +1156,8 @@ pub mod test { count: 1, limit: 10, offset: 0, + order: OrderBy::Name, + order_by_desc: false, }, ]; diff --git a/chirpstack/src/storage/device_keys.rs b/chirpstack/src/storage/device_keys.rs index 03619cbb..c8cbdcf4 100644 --- a/chirpstack/src/storage/device_keys.rs +++ b/chirpstack/src/storage/device_keys.rs @@ -20,6 +20,7 @@ pub struct DeviceKeys { pub app_key: AES128Key, pub dev_nonces: fields::DevNonces, pub join_nonce: i32, + pub gen_app_key: AES128Key, } impl Default for DeviceKeys { @@ -27,19 +28,14 @@ impl Default for DeviceKeys { let now = Utc::now(); DeviceKeys { - dev_eui: EUI64::from_be_bytes([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + dev_eui: Default::default(), created_at: now, updated_at: now, - nwk_key: AES128Key::from_bytes([ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - ]), - app_key: AES128Key::from_bytes([ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, - ]), - dev_nonces: fields::DevNonces::default(), + nwk_key: Default::default(), + app_key: Default::default(), + dev_nonces: Default::default(), join_nonce: 0, + gen_app_key: Default::default(), } } } diff --git a/chirpstack/src/storage/fields/fuota.rs b/chirpstack/src/storage/fields/fuota.rs index 1cab5f82..56c62691 100644 --- a/chirpstack/src/storage/fields/fuota.rs +++ b/chirpstack/src/storage/fields/fuota.rs @@ -83,6 +83,9 @@ impl serialize::ToSql for RequestFragmentationSessionStatus { #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, AsExpression, FromSqlRow)] #[diesel(sql_type = Text)] pub enum FuotaJob { + CreateMcGroup, + AddDevsToMcGroup, + AddGwsToMcGroup, McGroupSetup, McSession, FragSessionSetup, @@ -99,6 +102,9 @@ impl fmt::Display for FuotaJob { impl From<&FuotaJob> for String { fn from(value: &FuotaJob) -> Self { match value { + FuotaJob::CreateMcGroup => "CREATE_MC_GROUP", + FuotaJob::AddDevsToMcGroup => "ADD_DEVS_TO_MC_GROUP", + FuotaJob::AddGwsToMcGroup => "ADD_GWS_TO_MC_GROUP", FuotaJob::McGroupSetup => "MC_GROUP_SETUP", FuotaJob::McSession => "MC_SESSION", FuotaJob::FragSessionSetup => "FRAG_SESSION_SETUP", @@ -114,6 +120,9 @@ impl TryFrom<&str> for FuotaJob { fn try_from(value: &str) -> Result { Ok(match value { + "CREATE_MC_GROUP" => Self::CreateMcGroup, + "ADD_DEVS_TO_MC_GROUP" => Self::AddDevsToMcGroup, + "ADD_GWS_TO_MC_GROUP" => Self::AddGwsToMcGroup, "MC_GROUP_SETUP" => Self::McGroupSetup, "MC_SESSION" => Self::McSession, "FRAG_SESSION_SETUP" => Self::FragSessionSetup, diff --git a/chirpstack/src/storage/fuota.rs b/chirpstack/src/storage/fuota.rs index 49d367fc..969dacf3 100644 --- a/chirpstack/src/storage/fuota.rs +++ b/chirpstack/src/storage/fuota.rs @@ -13,7 +13,7 @@ use crate::storage::schema::{ fuota_deployment_job, gateway, tenant, }; use crate::storage::{self, db_transaction, device_profile, fields, get_async_db_conn}; -use lrwn::EUI64; +use lrwn::{AES128Key, DevAddr, EUI64}; #[derive(Clone, Queryable, Insertable, Debug, PartialEq, Eq, Validate)] #[diesel(table_name = fuota_deployment)] @@ -26,6 +26,8 @@ pub struct FuotaDeployment { pub name: String, pub application_id: fields::Uuid, pub device_profile_id: fields::Uuid, + pub multicast_addr: DevAddr, + pub multicast_key: AES128Key, pub multicast_group_type: String, pub multicast_class_c_scheduling_type: fields::MulticastGroupSchedulingType, pub multicast_dr: i16, @@ -56,6 +58,8 @@ impl Default for FuotaDeployment { name: "".into(), application_id: Uuid::nil().into(), device_profile_id: Uuid::nil().into(), + multicast_addr: Default::default(), + multicast_key: Default::default(), multicast_group_type: "".into(), multicast_class_c_scheduling_type: fields::MulticastGroupSchedulingType::DELAY, multicast_dr: 0, @@ -144,6 +148,7 @@ pub struct FuotaDeploymentJob { pub max_retry_count: i16, pub attempt_count: i16, pub scheduler_run_after: DateTime, + pub return_msg: String, } impl Default for FuotaDeploymentJob { @@ -158,6 +163,7 @@ impl Default for FuotaDeploymentJob { max_retry_count: 0, attempt_count: 0, scheduler_run_after: now, + return_msg: "".into(), } } } @@ -324,15 +330,21 @@ pub async fn get_devices( limit: i64, offset: i64, ) -> Result, Error> { - fuota_deployment_device::dsl::fuota_deployment_device + let mut q = fuota_deployment_device::dsl::fuota_deployment_device .filter( fuota_deployment_device::dsl::fuota_deployment_id .eq(fields::Uuid::from(fuota_deployment_id)), ) - .order_by(fuota_deployment_device::dsl::dev_eui) - .limit(limit) - .offset(offset) - .load(&mut get_async_db_conn().await?) + .into_boxed(); + + if limit != -1 { + q = q + .order_by(fuota_deployment_device::dsl::dev_eui) + .limit(limit) + .offset(offset); + } + + q.load(&mut get_async_db_conn().await?) .await .map_err(|e| Error::from_diesel(e, "".into())) } @@ -449,15 +461,21 @@ pub async fn get_gateways( limit: i64, offset: i64, ) -> Result, Error> { - fuota_deployment_gateway::dsl::fuota_deployment_gateway + let mut q = fuota_deployment_gateway::dsl::fuota_deployment_gateway .filter( fuota_deployment_gateway::dsl::fuota_deployment_id .eq(fields::Uuid::from(fuota_deployment_id)), ) - .order_by(fuota_deployment_gateway::dsl::gateway_id) - .limit(limit) - .offset(offset) - .load(&mut get_async_db_conn().await?) + .into_boxed(); + + if limit != -1 { + q = q + .order_by(fuota_deployment_gateway::dsl::gateway_id) + .limit(limit) + .offset(offset); + } + + q.load(&mut get_async_db_conn().await?) .await .map_err(|e| Error::from_diesel(e, "".into())) } @@ -501,6 +519,7 @@ pub async fn update_job(j: FuotaDeploymentJob) -> Result Result { // Multiply by the number of fragments (+1 for additional margin). let mc_class_c_duration_secs = mc_class_c_margin_secs * (total_fragments + 1 as usize); - // Calculate the timeout value. In case of Class-B, timeout is defined as seconds, + // Calculate the timeout value. In case of Class-C, timeout is defined as seconds, // where the number of seconds is 2^timeout. for i in 0..16 { // i = 0-15 @@ -1115,7 +1134,7 @@ mod test { payload: vec![0; 10], ..Default::default() }, - expected_timeout: 3, + expected_timeout: 4, expected_error: None, }, ]; diff --git a/chirpstack/src/storage/schema_postgres.rs b/chirpstack/src/storage/schema_postgres.rs index 78941669..449ddafa 100644 --- a/chirpstack/src/storage/schema_postgres.rs +++ b/chirpstack/src/storage/schema_postgres.rs @@ -77,6 +77,7 @@ diesel::table! { app_key -> Bytea, dev_nonces -> Jsonb, join_nonce -> Int4, + gen_app_key -> Bytea, } } @@ -192,6 +193,8 @@ diesel::table! { name -> Varchar, application_id -> Uuid, device_profile_id -> Uuid, + multicast_addr -> Bytea, + multicast_key -> Bytea, #[max_length = 1] multicast_group_type -> Bpchar, #[max_length = 20] @@ -244,6 +247,7 @@ diesel::table! { max_retry_count -> Int2, attempt_count -> Int2, scheduler_run_after -> Timestamptz, + return_msg -> Text, } } diff --git a/chirpstack/src/storage/schema_sqlite.rs b/chirpstack/src/storage/schema_sqlite.rs index b2ca2150..2d4af070 100644 --- a/chirpstack/src/storage/schema_sqlite.rs +++ b/chirpstack/src/storage/schema_sqlite.rs @@ -72,6 +72,7 @@ diesel::table! { app_key -> Binary, dev_nonces -> Text, join_nonce -> Integer, + gen_app_key -> Binary, } } @@ -171,6 +172,8 @@ diesel::table! { name -> Text, application_id -> Text, device_profile_id -> Text, + multicast_addr -> Binary, + multicast_key -> Binary, multicast_group_type -> Text, multicast_class_c_scheduling_type -> Text, multicast_dr -> SmallInt, @@ -219,6 +222,7 @@ diesel::table! { max_retry_count -> SmallInt, attempt_count -> SmallInt, scheduler_run_after -> TimestamptzSqlite, + return_msg -> Text, } } diff --git a/ui/src/views/devices/SetDeviceKeys.tsx b/ui/src/views/devices/SetDeviceKeys.tsx index 8bd63097..5b4a210b 100644 --- a/ui/src/views/devices/SetDeviceKeys.tsx +++ b/ui/src/views/devices/SetDeviceKeys.tsx @@ -37,6 +37,7 @@ function LW10DeviceKeysForm(props: FormProps) { // NOTE: this is not an error! In the LoRaWAN 1.1 specs, the what was previously // the AppKey has been renamed to the NwkKey and a new value AppKey was added. dk.setNwkKey(v.nwkKey); + dk.setGenAppKey(v.genAppKey); props.onFinish(dk); }; @@ -56,6 +57,12 @@ function LW10DeviceKeysForm(props: FormProps) { value={props.initialValues.getNwkKey()} required /> +