Skip to main content

samloader_fus/
fusclient.rs

1// Copyright 2026 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::{auth, xml};
16use aes::cipher::KeyInit;
17use reqwest::blocking::{Client, Response};
18use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue, RANGE, USER_AGENT};
19use xml::BinaryInform;
20
21pub type Aes128EcbDec = ecb::Decryptor<aes::Aes128>;
22
23pub struct FusClient {
24    client: Client,
25    auth: String,
26    nonce: String,
27    encnonce: String,
28    pub info: BinaryInform,
29}
30
31impl FusClient {
32    pub fn new() -> reqwest::Result<Self> {
33        let client = Client::builder().cookie_store(true).build()?;
34        let mut fus = FusClient {
35            client,
36            auth: Default::default(),
37            nonce: Default::default(),
38            encnonce: Default::default(),
39            info: Default::default(),
40        };
41
42        // Initialize nonce
43        fus.make_req("NF_SmartDownloadGenerateNonce.do", "")?;
44
45        Ok(fus)
46    }
47
48    pub fn fetch_binary_info(&mut self, model: &str, region: &str) {
49        // 1. Fetch latest version from version.xml
50        let version_url = format!(
51            "https://fota-cloud-dn.ospserver.net:443/firmware/{}/{}/version.xml",
52            region, model
53        );
54        let version_xml = self
55            .client
56            .get(&version_url)
57            .header(USER_AGENT, "Kies2.0_FUS")
58            .send()
59            .expect("Failed to fetch version.xml")
60            .text()
61            .expect("Failed to read version.xml text");
62
63        let latest_fw = xml::parse_version_xml(&version_xml).expect("Failed to parse version.xml");
64
65        // 2. Compute Binary Inform req using actual latest_fw
66        let req_xml = xml::binary_inform_req_xml(model, region, &latest_fw, &self.nonce);
67
68        let xml = self
69            .make_req("NF_SmartDownloadBinaryInform.do", &req_xml)
70            .and_then(Response::text)
71            .expect("Info request failed");
72
73        self.info = BinaryInform::parse(&xml).expect("Info request invalid");
74    }
75
76    fn make_headers(&self) -> HeaderMap {
77        let auth_val = format!(
78            "FUS nonce=\"{}\", signature=\"{}\", nc=\"\", type=\"\", realm=\"\", newauth=\"1\"",
79            self.encnonce, self.auth
80        );
81
82        let mut headers = HeaderMap::new();
83        headers.insert(AUTHORIZATION, HeaderValue::from_str(&auth_val).unwrap());
84        headers.insert(USER_AGENT, HeaderValue::from_static("SMART 2.0"));
85        headers
86    }
87
88    fn make_req(&mut self, path: &str, data: &str) -> reqwest::Result<Response> {
89        let url = format!("https://neofussvr.sslcs.cdngc.net/{}", path);
90        let resp = self
91            .client
92            .post(&url)
93            .headers(self.make_headers())
94            .body(data.to_string())
95            .send()?
96            .error_for_status()?;
97
98        if let Some(nonce) = resp
99            .headers()
100            .get("NONCE")
101            .or_else(|| resp.headers().get("nonce"))
102            .and_then(|n| n.to_str().ok())
103        {
104            let nonce_str = nonce.to_string();
105            if !nonce_str.is_empty() && nonce_str != self.encnonce {
106                self.encnonce = nonce_str;
107                self.nonce = self.encnonce.clone();
108                self.auth = auth::decrypt_nonce(&self.encnonce);
109            }
110        }
111
112        Ok(resp)
113    }
114
115    pub fn init_download(&mut self) {
116        let init_xml = xml::binary_init_req_xml(
117            &self.info.filename,
118            &self.nonce,
119            &self.info.version,
120            &self.info.model_type,
121            &self.info.region,
122        );
123        self.make_req("NF_SmartDownloadBinaryInitForMass.do", &init_xml)
124            .expect("Download init failed");
125    }
126
127    pub fn download_file(&self, start: Option<u64>, end: Option<u64>) -> reqwest::Result<Response> {
128        let mut headers = self.make_headers();
129        match (start, end) {
130            (Some(s), Some(e)) => headers.insert(
131                RANGE,
132                HeaderValue::from_str(&format!("bytes={}-{}", s, e)).unwrap(),
133            ),
134            (None, Some(e)) => headers.insert(
135                RANGE,
136                HeaderValue::from_str(&format!("bytes=0-{}", e)).unwrap(),
137            ),
138            (Some(s), None) => headers.insert(
139                RANGE,
140                HeaderValue::from_str(&format!("bytes={}-", s)).unwrap(),
141            ),
142            _ => None,
143        };
144
145        let url = format!(
146            "http://cloud-neofussvr.samsungmobile.com/NF_SmartDownloadBinaryForMass.do?file={}{}",
147            self.info.path, self.info.filename
148        );
149        self.client
150            .get(url)
151            .headers(headers)
152            .send()?
153            .error_for_status()
154    }
155
156    pub fn get_decryptor(&self) -> Aes128EcbDec {
157        Aes128EcbDec::new_from_slice(self.info.key.as_slice()).unwrap()
158    }
159}