use std::collections::HashMap;
use std::fs::File;
use std::path::{Path, PathBuf};
use anyhow::{Context, bail, ensure};
use nitro_shared::util::utc_timestamp;
use rsa::traits::PublicKeyParts;
use rsa::{Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey};
use serde::{Deserialize, Serialize};
use crate::mc::Keypair;
use crate::passkey::{decrypt_chunks, encrypt_chunks};
const REFRESH_TOKEN_EXPIRATION: u64 = 15552000;
pub struct AuthDatabase {
dir: PathBuf,
contents: DatabaseContents,
}
impl AuthDatabase {
pub fn open(path: &Path) -> anyhow::Result<Self> {
std::fs::create_dir_all(path).context("Failed to ensure database directory exists")?;
let database_path = Self::get_db_path(path);
let contents = if database_path.exists() {
let file = File::open(&database_path).context("Failed to open database file")?;
serde_json::from_reader(file).context("Failed to deserialize database contents")?
} else {
DatabaseContents::default()
};
let out = Self {
dir: path.to_owned(),
contents,
};
Ok(out)
}
pub fn write(&self) -> anyhow::Result<()> {
let path = Self::get_db_path(&self.dir);
let file = File::create(path).context("Failed to create database file")?;
serde_json::to_writer_pretty(file, &self.contents)
.context("Failed to write database contents")?;
Ok(())
}
fn get_db_path(dir: &Path) -> PathBuf {
dir.join("db.json")
}
pub fn is_account_valid(&self, account_id: &str) -> bool {
if let Some(account) = &self.contents.accounts.get(account_id) {
let Ok(now) = utc_timestamp() else {
return false;
};
now < account.expires
} else {
false
}
}
pub fn update_account(
&mut self,
account: DatabaseAccount,
account_id: &str,
) -> anyhow::Result<()> {
self.contents
.accounts
.insert(account_id.to_string(), account);
self.write().context("Failed to write to database")?;
Ok(())
}
pub fn remove_account(&mut self, account_id: &str) -> anyhow::Result<()> {
self.contents.accounts.remove(account_id);
self.write().context("Failed to write to database")?;
Ok(())
}
pub fn logout_account(&mut self, account_id: &str) -> anyhow::Result<()> {
if let Some(account) = self.contents.accounts.get_mut(account_id) {
account.sensitive = SensitiveAccountInfoSerialized::None;
}
Ok(())
}
pub fn get_account(&self, account_id: &str) -> Option<&DatabaseAccount> {
self.contents.accounts.get(account_id)
}
pub fn get_account_mut(&mut self, account_id: &str) -> Option<&mut DatabaseAccount> {
self.contents.accounts.get_mut(account_id)
}
pub fn get_valid_account(&self, account_id: &str) -> Option<&DatabaseAccount> {
if self.is_account_valid(account_id) {
self.get_account(account_id)
} else {
None
}
}
pub fn has_logged_in_account(&self) -> bool {
self.contents
.accounts
.values()
.any(|x| x.sensitive != SensitiveAccountInfoSerialized::None)
}
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(default)]
struct DatabaseContents {
#[serde(alias = "users")]
accounts: HashMap<String, DatabaseAccount>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DatabaseAccount {
pub id: String,
pub username: String,
pub uuid: String,
pub expires: u64,
pub sensitive: SensitiveAccountInfoSerialized,
pub passkey: Option<PasskeyInfo>,
}
impl DatabaseAccount {
pub fn new(
id: String,
username: String,
uuid: String,
expires: u64,
sensitive: SensitiveAccountInfo,
) -> anyhow::Result<Self> {
let mut out = DatabaseAccount {
id,
username,
uuid,
expires,
sensitive: SensitiveAccountInfoSerialized::Encrypted(Vec::new()),
passkey: None,
};
out.set_sensitive_info(sensitive)
.context("Failed to set sensitive information for account in database")?;
Ok(out)
}
pub fn has_passkey(&self) -> bool {
self.passkey.is_some()
}
pub fn is_logged_in(&self) -> bool {
!matches!(self.sensitive, SensitiveAccountInfoSerialized::None)
}
pub fn get_private_key(&self, passkey: &str) -> anyhow::Result<Option<RsaPrivateKey>> {
if self.passkey.is_some() {
let input_key = crate::passkey::generate_keys(passkey)
.context("Failed to generate private key from input passkey")?;
let expected_pub_key = self
.get_public_key()
.context("Failed to get stored public key")?
.expect("Passkey info should be Some");
ensure!(
input_key.to_public_key() == expected_pub_key,
"Passkey did not match"
);
Ok(Some(input_key))
} else {
Ok(None)
}
}
pub fn get_public_key(&self) -> anyhow::Result<Option<RsaPublicKey>> {
if let Some(passkey_info) = &self.passkey {
let key =
hex::decode(&passkey_info.public_key).context("Failed to decode public key hex")?;
let key = crate::passkey::recreate_public_key_bytes(&key)
.context("Failed to recreate public key from stored data")?;
Ok(Some(key))
} else {
Ok(None)
}
}
pub fn get_sensitive_info_no_passkey(&self) -> anyhow::Result<SensitiveAccountInfo> {
ensure!(
self.passkey.is_none(),
"Account has a passkey that was not used"
);
let SensitiveAccountInfoSerialized::Raw(raw) = &self.sensitive else {
bail!("Sensitive info is encrypted, not raw");
};
Ok(raw.clone())
}
pub fn get_sensitive_info_with_key(
&self,
private_key: &RsaPrivateKey,
) -> anyhow::Result<SensitiveAccountInfo> {
let SensitiveAccountInfoSerialized::Encrypted(encrypted) = &self.sensitive else {
bail!("Sensitive account info is raw or empty");
};
let mut hex_decoded = Vec::new();
for chunk in encrypted {
let decoded = hex::decode(chunk)
.context("Failed to deserialize hex of sensitive account info")?;
hex_decoded.push(decoded);
}
let decoded = decrypt_chunks(&hex_decoded, private_key, Pkcs1v15Encrypt)
.context("Failed to decrypt sensitive account info")?;
let deserialized = serde_json::from_slice(&decoded)
.context("Failed to deserialize sensitive account info")?;
Ok(deserialized)
}
pub fn set_sensitive_info(&mut self, sensitive: SensitiveAccountInfo) -> anyhow::Result<()> {
if self.has_passkey() {
let public_key = self
.get_public_key()
.context("Failed to get account public key")?
.expect("Account should have passkey");
self.set_sensitive_info_impl(sensitive, &public_key)?;
} else {
self.sensitive = SensitiveAccountInfoSerialized::Raw(sensitive);
}
Ok(())
}
fn set_sensitive_info_impl(
&mut self,
sensitive: SensitiveAccountInfo,
public_key: &RsaPublicKey,
) -> anyhow::Result<()> {
let serialized =
serde_json::to_vec(&sensitive).context("Failed to serialize sensitive account info")?;
let mut rng = rand::thread_rng();
let encoded = encrypt_chunks(&serialized, public_key, &mut rng, Pkcs1v15Encrypt, 128)
.context("Failed to encrypt sensitive account info")?;
let mut hex_encoded = Vec::new();
for chunk in encoded {
let encoded = hex::encode(chunk);
hex_encoded.push(encoded);
}
self.sensitive = SensitiveAccountInfoSerialized::Encrypted(hex_encoded);
Ok(())
}
pub fn update_passkey(
&mut self,
old_passkey: Option<&str>,
passkey: &str,
) -> anyhow::Result<()> {
let old_private_key = if let Some(old_passkey) = old_passkey {
Some(
crate::passkey::generate_keys(old_passkey)
.context("Failed to generate private key from old passkey")?,
)
} else {
None
};
let private_key = crate::passkey::generate_keys(passkey)
.context("Failed to generate private key from new passkey")?;
let pub_key = private_key.to_public_key();
let sensitive = if self.has_passkey() {
let Some(old_private_key) = old_private_key else {
bail!("No old passkey provided to update sensitive account data");
};
self.get_sensitive_info_with_key(&old_private_key)
} else {
self.get_sensitive_info_no_passkey()
}
.context("Failed to get existing sensitive account data")?;
self.set_sensitive_info_impl(sensitive, &pub_key)
.context("Failed to set new sensitive account data")?;
let n = pub_key.n().to_bytes_le();
let n = hex::encode(n);
self.passkey = Some(PasskeyInfo { public_key: n });
Ok(())
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct SensitiveAccountInfo {
pub refresh_token: Option<String>,
pub xbox_uid: Option<String>,
pub keypair: Option<Keypair>,
pub access_token: Option<String>,
pub access_token_expires: Option<u64>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct PasskeyInfo {
pub public_key: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(untagged)]
pub enum SensitiveAccountInfoSerialized {
None,
Raw(SensitiveAccountInfo),
Encrypted(Vec<String>),
}
pub fn calculate_expiration_date() -> u64 {
let now = utc_timestamp().unwrap_or_default();
now + REFRESH_TOKEN_EXPIRATION
}