Skip to content

Commit

Permalink
feat: use device id so that we do not need to provide a totp
Browse files Browse the repository at this point in the history
  • Loading branch information
ccrutchf committed Dec 7, 2024
1 parent 3f525b5 commit 4f890b1
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 49 deletions.
7 changes: 7 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
"request": "launch",
"name": "Launch",
"program": "${workspaceFolder}/target/debug/git-lfs-synology",
"args": [
"login",
"--url",
"https://e4e-nas.ucsd.edu:6021",
"--user",
"ccrutchf"
]
}
]
}
35 changes: 33 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ educe = "0.6.0"
futures-macro = "0.3.31"
futures-util = "0.3.31"
gix-config = "0.42.0"
hostname = "0.4.0"
keyring = { version = "3.6.1", features = ["apple-native", "windows-native", "sync-secret-service"] }
named-lock = "0.4.1"
num-derive = "0.4.2"
Expand Down
10 changes: 1 addition & 9 deletions src/credential_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,15 @@ pub struct Credential {
pub user: String,
#[educe(Debug(ignore))] // Do not include password in logs.
pub password: String,
pub totp: Option<String>,
pub device_id: Option<String>
}

impl Credential {
#[tracing::instrument]
pub fn new(user: String, password: String) -> Credential {
Credential::new_totp(user, password, None)
}

#[tracing::instrument]
pub fn new_totp(user: String, password: String, totp: Option<String>) -> Credential {
Credential {
user,
password,
totp,
device_id: None
}
}
Expand Down Expand Up @@ -158,12 +151,11 @@ impl CredentialManager {
Ok(version) => {
match version {
0 => {
conn.execute(
conn.execute_batch(
"ALTER TABLE Credentials DROP COLUMN totp_command_encrypted;
ALTER TABLE Credentials DROP COLUMN totp_nonce;
ALTER TABLE Credentials ADD COLUMN device_id_encrypted BLOB;
ALTER TABLE Credentials ADD COLUMN device_id_nonce BLOB;",
(), // empty list of parameters.
)?;

self.create_tables(&conn)
Expand Down
35 changes: 29 additions & 6 deletions src/subcommands/login_subcommand.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
use anyhow::{Context, Result};
use clap::ArgMatches;
use std::io::{self, Write};

use crate::credential_manager::{Credential, CredentialManager};
use crate::synology_api::SynologyFileStation;
use crate::synology_api::{SynologyErrorStatus, SynologyFileStation};

use super::Subcommand;

fn get_input(prompt: &str) -> Result<String>{
print!("{}",prompt);
io::stdout().flush()?;
let mut input = String::new();
match io::stdin().read_line(&mut input) {
Ok(_goes_into_input_above) => {},
Err(_no_updates_is_fine) => {},
}
Ok(input.trim().to_string())
}

#[derive(Debug)]
pub struct LoginSubcommand {
}
Expand All @@ -19,23 +31,34 @@ impl Subcommand for LoginSubcommand {
let mut credential_manager = CredentialManager::new()?;

let password: String;
let credential_ref: Option<Credential>;
let device_id: Option<String>;
if credential_manager.has_credential(url)? {
let credential = credential_manager.get_credential(url)?.context("Credential should not be null")?;
password = credential.password.clone();
credential_ref = Some(credential);
device_id = credential.device_id;
}
else {
password = rpassword::prompt_password("Synology NAS Password: ")?;
credential_ref = None;
device_id = None;
}

let credential = Credential::new(
let mut credential = Credential::new(
user.clone(),
password.clone());
credential.device_id = device_id;

let mut file_station = SynologyFileStation::new(url);
file_station.login(&credential).await?;
let credential = match file_station.login(&credential, false, None).await {
Ok(credential) => Ok(credential),
Err(error) => match error {
SynologyErrorStatus::NoTotp => {
let totp = get_input("TOTP: ")?;

file_station.login(&credential, true, Some(totp)).await
},
_ => Err(error)
}
}?;

credential_manager.set_credential(url, &credential)?;

Expand Down
2 changes: 1 addition & 1 deletion src/subcommands/main_subcommand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ impl CustomTransferAgent for MainSubcommand {
let mut file_station = SynologyFileStation::new(nas_url);

let credential = credential_manager.get_credential(nas_url)?.context("Credential should not be null")?;
match file_station.login(&credential).await {
match file_station.login(&credential, false, None).await {
Ok(_) => Ok(()),
Err(error) => {
error_init(1, error.to_string().as_str())?;
Expand Down
69 changes: 40 additions & 29 deletions src/synology_api/file_station.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,18 +207,46 @@ impl SynologyFileStation {
}

#[tracing::instrument]
pub async fn login(&mut self, credential: &Credential) -> Result<(), SynologyErrorStatus> {
let login_url = format!(
"{}/webapi/entry.cgi?api=SYNO.API.Auth&version={}&method=login&account={}&passwd={}&session=FileStation&fromat=sid",
pub async fn login(&mut self, credential: &Credential, enable_device_token: bool, totp: Option<String>) -> Result<Credential, SynologyErrorStatus> {
let device_name = format!(
"{}::{}",
hostname::get()?.to_string_lossy(),
"rust_synology_api"
);

let mut login_url = format!(
"{}/webapi/entry.cgi?api=SYNO.API.Auth&version={}&method=login&account={}&passwd={}&enable_device_token={}&device_name={}&session=FileStation&fromat=sid",
self.url,
6,
credential.user,
encode(credential.password.as_str()) // Encode the password in case it has characters not allowed in URLs in it.
encode(credential.user.as_str()),
encode(credential.password.as_str()), // Encode the password in case it has characters not allowed in URLs in it.
enable_device_token,
device_name
);

if let Some(did) = credential.device_id.clone() {
info!("Credential has device ID");

login_url = format!(
"{}&device_id={}",
login_url,
did
)
}

if let Some(totp) = totp {
info!("TOTP has been provided.");

login_url = format!(
"{}&otp_code={}",
login_url,
totp
)
}

// Make initial request to the server. This will fail if the user needs a TOTP.
let response = reqwest::get(login_url).await;
let (mut login_result, login_error) = self.parse_data_and_error::<LoginResponse, LoginError>(response).await?;
let (login_result, login_error) = self.parse_data_and_error::<LoginResponse, LoginError>(response).await?;

match login_error {
Some(login_error) =>
Expand All @@ -232,28 +260,8 @@ impl SynologyFileStation {
Some(errors) =>
if errors.types.iter().any(|f| f.contains_key("type") && f["type"] == "otp") {
info!("Server requested TOTP");
let totp: Option<String> = None;

match totp {
Some(totp) => {
info!("Requested TOTP from TOTP command");

let login_url = format!(
"{}/webapi/entry.cgi?api=SYNO.API.Auth&version={}&method=login&account={}&passwd={}&session=FileStation&fromat=sid&otp_code={}",
self.url,
6,
credential.user,
errors.token,
totp
);

let response = reqwest::get(login_url).await;
login_result= Some(self.parse::<LoginResponse>(response).await?);

Ok(())
},
None => Err(SynologyErrorStatus::NoTotp)
}

Err(SynologyErrorStatus::NoTotp)
}
else {
Err(SynologyErrorStatus::ServerError(SynologyStatusCode::InvalidUserDoesThisFileOperation))
Expand All @@ -272,7 +280,10 @@ impl SynologyFileStation {
Some(login_result) => {
self.sid = Some(login_result.sid);

Ok(())
let mut cred = Credential::new(credential.user.to_string(), credential.password.to_string());
cred.device_id = login_result.did;

Ok(cred)
},
None => Err(SynologyErrorStatus::UnknownError)
}
Expand Down
3 changes: 2 additions & 1 deletion src/synology_api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ mod progress_reporter;
mod responses;

pub use file_station::SynologyFileStation;
pub use progress_reporter::ProgressReporter;
pub use progress_reporter::ProgressReporter;
pub use responses::SynologyErrorStatus;
3 changes: 2 additions & 1 deletion src/synology_api/responses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ pub struct SynologyError<TErrors> {
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde[rename_all = "snake_case"]]
pub struct LoginResponse {
pub sid: String
pub sid: String,
pub did: Option<String>
}

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand Down

0 comments on commit 4f890b1

Please sign in to comment.