Compare commits

..

1 commit

7 changed files with 77 additions and 92 deletions

View file

@ -1,7 +1,7 @@
[package]
name = "spotifyio-downloader"
description = "CLI for downloading music from Spotify"
version = "0.5.1"
version = "0.5.0"
edition.workspace = true
authors.workspace = true
license.workspace = true

View file

@ -1114,55 +1114,18 @@ impl SpotifyDownloader {
if let Some(item) = item {
return Ok(Some(item.subdir_path));
} else {
let row = sqlx::query!(
"select album, album_id, path from tracks where id=$1",
id_str
)
.fetch_optional(&self.i.pool)
.await?;
if let Some(row) = row {
if let Some(subdir_path) = row.path {
let audio_path = path!(self.i.base_dir / subdir_path);
if audio_path.is_file() {
return Ok(Some(path!(subdir_path)));
} else {
if let (Some(album), Some(album_id)) =
(row.album, row.album_id)
{
// Try new path (with album ID)
let pb = PathBuf::from(subdir_path);
let mut cmp = pb.components().collect::<Vec<_>>();
if cmp.len() > 1 {
let album_dirname = better_filenamify(
&album,
Some(&format!(" [{}]", &album_id[..8])),
);
cmp[1] = std::path::Component::Normal(
std::ffi::OsStr::new(&album_dirname),
);
let subdir_path =
cmp.into_iter().collect::<PathBuf>();
let audio_path =
path!(self.i.base_dir / subdir_path);
if audio_path.is_file() {
tracing::info!(
"Audio file moved, new path: {}",
subdir_path.to_string_lossy()
);
return Ok(Some(subdir_path));
}
}
}
tracing::error!(
"[{id}] audio file not found: {}",
audio_path.to_string_lossy()
);
success.store(false, Ordering::SeqCst);
}
let row = sqlx::query!("select path from tracks where id=$1", id_str)
.fetch_optional(&self.i.pool)
.await?;
if let Some(subdir_path) = row.and_then(|r| r.path) {
let audio_path = path!(self.i.base_dir / subdir_path);
if audio_path.is_file() {
return Ok(Some(path!(subdir_path)));
} else {
tracing::error!("[{id}] audio file path not found in db");
tracing::error!(
"[{id}] audio file not found: {}",
audio_path.to_string_lossy()
);
success.store(false, Ordering::SeqCst);
}
} else {

View file

@ -52,17 +52,17 @@ byteorder = "1.0"
futures-util = { version = "0.3", features = ["sink"] }
num-derive = "0.4"
num-traits = "0.2"
num-bigint = { version = "0.4", features = ["rand"] }
crypto-bigint = { version = "0.7.0-pre.3", features = ["rand"] }
num-integer = "0.1"
url = "2"
shannon = "0.2"
tokio-util = { version = "0.7", features = ["codec"] }
hmac = "0.12"
rand = "0.8"
rand = "0.9"
rsa = "0.9.2"
httparse = "1.7"
base64 = "0.22"
oauth2 = { version = "5.0.0", default-features = false, features = [
oauth2 = { version = "5.0.0-rc.1", default-features = false, features = [
"reqwest",
], optional = true }
pin-project-lite = "0.2"

View file

@ -1,47 +1,44 @@
use num_bigint::{BigUint, RandBigInt};
use num_integer::Integer;
use num_traits::{One, Zero};
use once_cell::sync::Lazy;
use crypto_bigint::{Integer, NonZero, Random, Zero, U768};
use rand::{CryptoRng, Rng};
static DH_GENERATOR: Lazy<BigUint> = Lazy::new(|| BigUint::from_bytes_be(&[0x02]));
static DH_PRIME: Lazy<BigUint> = Lazy::new(|| {
BigUint::from_bytes_be(&[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2,
0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e,
0x34, 0x04, 0xdd, 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d,
0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5,
0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
])
});
const DH_GENERATOR: U768 = U768::from_u8(0x02);
const DH_PRIME: U768 = U768::from_be_slice(&[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34,
0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74,
0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd,
0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37,
0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6,
0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
]);
fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint {
let mut base = base.clone();
let mut exp = exp.clone();
let mut result: BigUint = One::one();
fn powm(mut base: U768, mut exp: U768, modulus: U768) -> U768 {
let modulus = NonZero::new(modulus).unwrap();
let mut result: U768 = U768::one();
while !exp.is_zero() {
if exp.is_odd() {
result = (result * &base) % modulus;
while !bool::from(exp.is_zero()) {
if bool::from(exp.is_odd()) {
result = result.mul_mod(&base, &modulus);
}
exp >>= 1;
base = (&base * &base) % modulus;
base = base.mul_mod(&base, &modulus);
}
result
}
pub struct DhLocalKeys {
private_key: BigUint,
public_key: BigUint,
private_key: U768,
public_key: U768,
}
impl DhLocalKeys {
pub fn random<R: Rng + CryptoRng>(rng: &mut R) -> DhLocalKeys {
let private_key = rng.gen_biguint(95 * 8);
let public_key = powm(&DH_GENERATOR, &private_key, &DH_PRIME);
let mut private_key = U768::random(rng);
let b0: U768 = U768::from_u8(0xff) << 95;
let mask = b0.not();
private_key &= mask;
let public_key = powm(DH_GENERATOR, private_key, DH_PRIME);
DhLocalKeys {
private_key,
@ -50,15 +47,32 @@ impl DhLocalKeys {
}
pub fn public_key(&self) -> Vec<u8> {
self.public_key.to_bytes_be()
self.public_key.to_be_bytes().to_vec()
}
pub fn shared_secret(&self, remote_key: &[u8]) -> Vec<u8> {
let shared_key = powm(
&BigUint::from_bytes_be(remote_key),
&self.private_key,
&DH_PRIME,
);
shared_key.to_bytes_be()
let remote_key = U768::from_be_slice(remote_key);
let shared_key = powm(remote_key, self.private_key, DH_PRIME);
shared_key.to_be_bytes().to_vec()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crypto_bigint::Encoding;
use hex_lit::hex;
#[test]
fn shared_secret() {
let private_key = U768::from_be_bytes(hex!("7D390DB1F04214A6EE49176618BFC3293C8441C0129601C5BA1D6D47CEF3F7E39D5F4686238FFD96D6B31CE1E37A759939251CFDCC231FC604E4BA656999FED8FC2AC43284DAC6896BB2107208A0E3BFA8F1600028FAFF92E030B7298E9CE0C2"));
let public_key = powm(DH_GENERATOR, private_key, DH_PRIME);
let keys = DhLocalKeys {
private_key,
public_key,
};
let ssc = keys.shared_secret(&hex!("6D6344F664177F93EC376C538A54FD8D156265A5E697F66579C9C268B660C6E47C36F3BD154F2DC09167E7CAB9B1BC7202770C1677AD1DA83926BC11DFEC49DAC66C38656A5223D3F3B936C0D6915743FEB799BD25047C1DA0FE9C93732296A1"));
assert_eq!(ssc, hex!("a320b52ab9b49fd99e539868d7c491d4567fd08107fe857f5a05a8c6d1c3fe1f60f61843b843b65f215cdfc46d3eaddeffea6f43cbb4e08b405a358de050483cc8ef231512ca2b58cdfa9c9d33b7791c2a55bacd476f1ebe82373f8c1332d051"));
}
}

View file

@ -3,7 +3,7 @@ use std::{env::consts::ARCH, io};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use hmac::{Hmac, Mac};
use protobuf::Message;
use rand::{thread_rng, RngCore};
use rand::RngCore;
use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey};
use sha1::{Digest, Sha1};
use thiserror::Error;
@ -46,10 +46,11 @@ pub enum HandshakeError {
pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
mut connection: T,
) -> io::Result<Framed<T, ApCodec>> {
let local_keys = DhLocalKeys::random(&mut thread_rng());
let local_keys = DhLocalKeys::random(&mut rand::rng());
let gc = local_keys.public_key();
let mut accumulator = client_hello(&mut connection, gc).await?;
let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?;
// remote key: 96B
let remote_key = message
.challenge
.get_or_default()
@ -59,6 +60,7 @@ pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
.get_or_default()
.gs()
.to_owned();
// remote signature: 256B
let remote_signature = message
.challenge
.get_or_default()
@ -91,6 +93,12 @@ pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
})?;
// OK to proceed
if remote_key.len() != 96 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
HandshakeError::InvalidLength,
));
}
let shared_secret = local_keys.shared_secret(&remote_key);
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator)?;
let codec = ApCodec::new(&send_key, &recv_key);
@ -105,7 +113,7 @@ where
T: AsyncWrite + Unpin,
{
let mut client_nonce = vec![0; 0x10];
thread_rng().fill_bytes(&mut client_nonce);
rand::rng().fill_bytes(&mut client_nonce);
let platform = match std::env::consts::OS {
"android" => Platform::PLATFORM_ANDROID_ARM,

View file

@ -134,7 +134,7 @@ impl<T> ClientPool<T> {
///
/// If no valid clients were found, [`PoolError::Empty`] is returned
pub fn random(&self) -> Result<&T, PoolError> {
let mut pos = rand::thread_rng().gen_range(0..self.len());
let mut pos = rand::rng().random_range(0..self.len());
for _ in 0..((self.len() - 1).max(1)) {
let entry = &self.entries[pos].entry;

View file

@ -235,7 +235,7 @@ impl Session {
self.inc_auth_errors();
return Err(e);
} else {
warn!("Try another access point...");
warn!("Try another access point ({e})...");
continue;
}
}