Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
4ab0f7d940 |
7 changed files with 77 additions and 92 deletions
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "spotifyio-downloader"
|
name = "spotifyio-downloader"
|
||||||
description = "CLI for downloading music from Spotify"
|
description = "CLI for downloading music from Spotify"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
|
@ -1114,57 +1114,20 @@ impl SpotifyDownloader {
|
||||||
if let Some(item) = item {
|
if let Some(item) = item {
|
||||||
return Ok(Some(item.subdir_path));
|
return Ok(Some(item.subdir_path));
|
||||||
} else {
|
} else {
|
||||||
let row = sqlx::query!(
|
let row = sqlx::query!("select path from tracks where id=$1", id_str)
|
||||||
"select album, album_id, path from tracks where id=$1",
|
|
||||||
id_str
|
|
||||||
)
|
|
||||||
.fetch_optional(&self.i.pool)
|
.fetch_optional(&self.i.pool)
|
||||||
.await?;
|
.await?;
|
||||||
if let Some(row) = row {
|
if let Some(subdir_path) = row.and_then(|r| r.path) {
|
||||||
if let Some(subdir_path) = row.path {
|
|
||||||
let audio_path = path!(self.i.base_dir / subdir_path);
|
let audio_path = path!(self.i.base_dir / subdir_path);
|
||||||
if audio_path.is_file() {
|
if audio_path.is_file() {
|
||||||
return Ok(Some(path!(subdir_path)));
|
return Ok(Some(path!(subdir_path)));
|
||||||
} else {
|
} 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!(
|
tracing::error!(
|
||||||
"[{id}] audio file not found: {}",
|
"[{id}] audio file not found: {}",
|
||||||
audio_path.to_string_lossy()
|
audio_path.to_string_lossy()
|
||||||
);
|
);
|
||||||
success.store(false, Ordering::SeqCst);
|
success.store(false, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
tracing::error!("[{id}] audio file path not found in db");
|
|
||||||
success.store(false, Ordering::SeqCst);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
tracing::error!("[{id}] audio file not found in db");
|
tracing::error!("[{id}] audio file not found in db");
|
||||||
success.store(false, Ordering::SeqCst);
|
success.store(false, Ordering::SeqCst);
|
||||||
|
|
|
@ -52,17 +52,17 @@ byteorder = "1.0"
|
||||||
futures-util = { version = "0.3", features = ["sink"] }
|
futures-util = { version = "0.3", features = ["sink"] }
|
||||||
num-derive = "0.4"
|
num-derive = "0.4"
|
||||||
num-traits = "0.2"
|
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"
|
num-integer = "0.1"
|
||||||
url = "2"
|
url = "2"
|
||||||
shannon = "0.2"
|
shannon = "0.2"
|
||||||
tokio-util = { version = "0.7", features = ["codec"] }
|
tokio-util = { version = "0.7", features = ["codec"] }
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
rand = "0.8"
|
rand = "0.9"
|
||||||
rsa = "0.9.2"
|
rsa = "0.9.2"
|
||||||
httparse = "1.7"
|
httparse = "1.7"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
oauth2 = { version = "5.0.0", default-features = false, features = [
|
oauth2 = { version = "5.0.0-rc.1", default-features = false, features = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
|
|
|
@ -1,47 +1,44 @@
|
||||||
use num_bigint::{BigUint, RandBigInt};
|
use crypto_bigint::{Integer, NonZero, Random, Zero, U768};
|
||||||
use num_integer::Integer;
|
|
||||||
use num_traits::{One, Zero};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use rand::{CryptoRng, Rng};
|
use rand::{CryptoRng, Rng};
|
||||||
|
|
||||||
static DH_GENERATOR: Lazy<BigUint> = Lazy::new(|| BigUint::from_bytes_be(&[0x02]));
|
const DH_GENERATOR: U768 = U768::from_u8(0x02);
|
||||||
static DH_PRIME: Lazy<BigUint> = Lazy::new(|| {
|
const DH_PRIME: U768 = U768::from_be_slice(&[
|
||||||
BigUint::from_bytes_be(&[
|
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34,
|
||||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2,
|
0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, 0xcc, 0x74,
|
||||||
0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
|
0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, 0x34, 0x04, 0xdd,
|
||||||
0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e,
|
0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, 0x37,
|
||||||
0x34, 0x04, 0xdd, 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d,
|
0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6,
|
||||||
0xf2, 0x5f, 0x14, 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, 0xe4, 0x85, 0xb5,
|
0xf4, 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||||
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 {
|
fn powm(mut base: U768, mut exp: U768, modulus: U768) -> U768 {
|
||||||
let mut base = base.clone();
|
let modulus = NonZero::new(modulus).unwrap();
|
||||||
let mut exp = exp.clone();
|
let mut result: U768 = U768::one();
|
||||||
let mut result: BigUint = One::one();
|
|
||||||
|
|
||||||
while !exp.is_zero() {
|
while !bool::from(exp.is_zero()) {
|
||||||
if exp.is_odd() {
|
if bool::from(exp.is_odd()) {
|
||||||
result = (result * &base) % modulus;
|
result = result.mul_mod(&base, &modulus);
|
||||||
}
|
}
|
||||||
exp >>= 1;
|
exp >>= 1;
|
||||||
base = (&base * &base) % modulus;
|
base = base.mul_mod(&base, &modulus);
|
||||||
}
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DhLocalKeys {
|
pub struct DhLocalKeys {
|
||||||
private_key: BigUint,
|
private_key: U768,
|
||||||
public_key: BigUint,
|
public_key: U768,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DhLocalKeys {
|
impl DhLocalKeys {
|
||||||
pub fn random<R: Rng + CryptoRng>(rng: &mut R) -> DhLocalKeys {
|
pub fn random<R: Rng + CryptoRng>(rng: &mut R) -> DhLocalKeys {
|
||||||
let private_key = rng.gen_biguint(95 * 8);
|
let mut private_key = U768::random(rng);
|
||||||
let public_key = powm(&DH_GENERATOR, &private_key, &DH_PRIME);
|
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 {
|
DhLocalKeys {
|
||||||
private_key,
|
private_key,
|
||||||
|
@ -50,15 +47,32 @@ impl DhLocalKeys {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn public_key(&self) -> Vec<u8> {
|
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> {
|
pub fn shared_secret(&self, remote_key: &[u8]) -> Vec<u8> {
|
||||||
let shared_key = powm(
|
let remote_key = U768::from_be_slice(remote_key);
|
||||||
&BigUint::from_bytes_be(remote_key),
|
let shared_key = powm(remote_key, self.private_key, DH_PRIME);
|
||||||
&self.private_key,
|
shared_key.to_be_bytes().to_vec()
|
||||||
&DH_PRIME,
|
}
|
||||||
);
|
}
|
||||||
shared_key.to_bytes_be()
|
|
||||||
|
#[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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use std::{env::consts::ARCH, io};
|
||||||
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use protobuf::Message;
|
use protobuf::Message;
|
||||||
use rand::{thread_rng, RngCore};
|
use rand::RngCore;
|
||||||
use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey};
|
use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey};
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
@ -46,10 +46,11 @@ pub enum HandshakeError {
|
||||||
pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
|
pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
|
||||||
mut connection: T,
|
mut connection: T,
|
||||||
) -> io::Result<Framed<T, ApCodec>> {
|
) -> 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 gc = local_keys.public_key();
|
||||||
let mut accumulator = client_hello(&mut connection, gc).await?;
|
let mut accumulator = client_hello(&mut connection, gc).await?;
|
||||||
let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?;
|
let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?;
|
||||||
|
// remote key: 96B
|
||||||
let remote_key = message
|
let remote_key = message
|
||||||
.challenge
|
.challenge
|
||||||
.get_or_default()
|
.get_or_default()
|
||||||
|
@ -59,6 +60,7 @@ pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
|
||||||
.get_or_default()
|
.get_or_default()
|
||||||
.gs()
|
.gs()
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
// remote signature: 256B
|
||||||
let remote_signature = message
|
let remote_signature = message
|
||||||
.challenge
|
.challenge
|
||||||
.get_or_default()
|
.get_or_default()
|
||||||
|
@ -91,6 +93,12 @@ pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// OK to proceed
|
// 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 shared_secret = local_keys.shared_secret(&remote_key);
|
||||||
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator)?;
|
let (challenge, send_key, recv_key) = compute_keys(&shared_secret, &accumulator)?;
|
||||||
let codec = ApCodec::new(&send_key, &recv_key);
|
let codec = ApCodec::new(&send_key, &recv_key);
|
||||||
|
@ -105,7 +113,7 @@ where
|
||||||
T: AsyncWrite + Unpin,
|
T: AsyncWrite + Unpin,
|
||||||
{
|
{
|
||||||
let mut client_nonce = vec![0; 0x10];
|
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 {
|
let platform = match std::env::consts::OS {
|
||||||
"android" => Platform::PLATFORM_ANDROID_ARM,
|
"android" => Platform::PLATFORM_ANDROID_ARM,
|
||||||
|
|
|
@ -134,7 +134,7 @@ impl<T> ClientPool<T> {
|
||||||
///
|
///
|
||||||
/// If no valid clients were found, [`PoolError::Empty`] is returned
|
/// If no valid clients were found, [`PoolError::Empty`] is returned
|
||||||
pub fn random(&self) -> Result<&T, PoolError> {
|
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)) {
|
for _ in 0..((self.len() - 1).max(1)) {
|
||||||
let entry = &self.entries[pos].entry;
|
let entry = &self.entries[pos].entry;
|
||||||
|
|
|
@ -235,7 +235,7 @@ impl Session {
|
||||||
self.inc_auth_errors();
|
self.inc_auth_errors();
|
||||||
return Err(e);
|
return Err(e);
|
||||||
} else {
|
} else {
|
||||||
warn!("Try another access point...");
|
warn!("Try another access point ({e})...");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue