Merge pull request 'nex account support' (#42) from friends/3ds into v0
All checks were successful
Build and Test / minecraft-wiiu (push) Successful in 4m54s
Build and Test / puyopuyo (push) Successful in 4m54s
Build and Test / splatoon (push) Successful in 5m4s
Build and Test / splatoon-testfire (push) Successful in 5m5s
Build and Test / wii-u-chat (push) Successful in 5m6s
Build and Test / wii-sports-club (push) Successful in 5m10s
Build and Test / mario-tennis (push) Successful in 5m13s
Build and Test / fast-racing-neo (push) Successful in 5m15s
Build and Test / friends (push) Successful in 5m17s
Build and Test / sonic-transformed (push) Successful in 5m21s
Build and Test / super-mario-maker (push) Successful in 5m35s

Reviewed-on: #42
This commit is contained in:
red binder 2026-07-06 14:18:59 +02:00
commit bc856feaf2
14 changed files with 1325 additions and 246 deletions

710
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,18 @@ sonic-transformed:
RNEX_VIRTUAL_PORT_SECURE: "1:10"
RNEX_DEFAULT_PORT: 10000
RNEX_ACCESS_KEY: "b26a3421"
ac-new-leaf:
include-in-checkall: true
features:
- prudpv1
- datastore
- v3-10-22
settings:
AUTH_REPORT_VERSION: "branch:origin/release/ngs/3.10.x.200x build:3_10_22_2006_0"
RNEX_VIRTUAL_PORT_INSECURE: "1:10"
RNEX_VIRTUAL_PORT_SECURE: "1:10"
RNEX_DEFAULT_PORT: 10000
RNEX_ACCESS_KEY: "d6f08b40"
wii-sports-club:
include-in-checkall: true
features:

View file

@ -37,6 +37,8 @@ urlencoding = "2.1.3"
futures = "0.3.32"
async-trait = "0.1.89"
ctor = "1.0.7"
nex-account = { version = "0.2.1", registry = "spbr" }
tonic = "0.14.6"
[dev-dependencies]
# criterion = "0.7.0"

View file

@ -1,12 +1,17 @@
use crate::grpc::account::Error::SomethingHappened;
use json::{JsonValue, object};
use nex_account::grpc::Pid;
use nex_account::grpc::nex_account_service_client::NexAccountServiceClient;
use once_cell::sync::Lazy;
use rnex_core::PID;
use std::array::TryFromSliceError;
use std::ops::Deref;
use std::sync::LazyLock;
use std::{env, result};
use thiserror::Error;
use tokio::task::{JoinError, spawn_blocking};
use tonic::transport::Channel;
static API_KEY: Lazy<String> = Lazy::new(|| {
let key = env::var("ACCOUNT_GQL_API_KEY").expect("no graphql ip specified");
@ -26,8 +31,10 @@ pub enum Error {
RequestError(#[from] ureq::Error),
#[error(transparent)]
Json(#[from] json::Error),
//#[error(transparent)]
//Status(#[from] tonic::Status),
#[error(transparent)]
Status(#[from] tonic::Status),
#[error(transparent)]
Transport(#[from] tonic::transport::Error),
#[error("invalid password size: {0}")]
PasswordConversion(#[from] TryFromSliceError),
#[error("something happened")]
@ -38,141 +45,101 @@ pub enum Error {
pub type Result<T> = result::Result<T, Error>;
pub struct Client; //(reqwest::Client);
static NEX_ACCOUNT_URL: LazyLock<String> =
LazyLock::new(|| env::var("NEX_ACCOUNT_ENDPOINT").expect("NEX_ACCOUNT_ENDPOINT not set"));
pub struct Client(NexAccountServiceClient<Channel>); //(reqwest::Client);
impl Client {
pub async fn new() -> Result<Self> {
//Ok(Self(reqwest::ClientBuilder::new().build()?))
Ok(Self)
let client = NexAccountServiceClient::connect(NEX_ACCOUNT_URL.as_str()).await?;
Ok(Self(client))
}
async fn do_request(&self, request_data: JsonValue) -> Result<JsonValue> {
let request = ureq::post(CLIENT_URI.as_str())
.header("X-API-Key", API_KEY.deref())
.content_type("application/json");
let mut response = spawn_blocking(move || request.send(request_data.to_string())).await??;
pub async fn get_nex_key(&mut self, pid: PID) -> Result<[u8; 16]> {
let prekey = self.0.get_nex_key_by_pid(Pid { pid }).await?.into_inner();
let str_body = response.body_mut().read_to_string()?;
Ok(json::parse(&str_body)?)
/*
let mut request = reqwest::Request::new(Method::POST, Url::from_str(CLIENT_URI.as_str()).unwrap());
log::warn!("prekey is {:?}", prekey);
*(request.body_mut()) = Some(Body::from(request_data.to_string()));
request.headers_mut().insert("X-API-Key", HeaderValue::from_str(&API_KEY).unwrap());
request.headers_mut().insert("Content-Type", HeaderValue::from_str("application/json").unwrap());
let nexkey: [u8; 16] = prekey
.key
.try_into()
.map_err(|_| Error::SomethingHappened)?;
let response = self.0.execute(request).await?;
Ok(json::parse(&response.text().await?)?)
*/
}
pub async fn get_nex_password(&mut self, pid: PID) -> Result<[u8; 16]> {
let req = self
.do_request(object! {
"query": r"query($pid: Int!){
userByPid(pid: $pid){
nexPassword
}
}",
"variables": {
"pid": pid
}
})
.await?;
let Some(val) = req
.entries()
.find(|v| v.0 == "data")
.ok_or(SomethingHappened)?
.1
.entries()
.find(|v| v.0 == "userByPid")
.ok_or(SomethingHappened)?
.1
.entries()
.find(|v| v.0 == "nexPassword")
.ok_or(SomethingHappened)?
.1
.as_str()
else {
return Err(SomethingHappened);
};
Ok(val.as_bytes().try_into().map_err(|_| SomethingHappened)?)
Ok(nexkey)
}
pub async fn get_user_level(&mut self, pid: PID) -> Result<i32> {
let req = self
.do_request(object! {
"query": r"query($pid: Int!){
userByPid(pid: $pid){
accountLevel
}
}",
"variables": {
"pid": pid
}
})
.await?;
// let req = self
// .do_request(object! {
// "query": r"query($pid: Int!){
// userByPid(pid: $pid){
// accountLevel
// }
// }",
// "variables": {
// "pid": pid
// }
// })
// .await?;
//
// let Some(val) = req
// .entries()
// .find(|v| v.0 == "data")
// .ok_or(SomethingHappened)?
// .1
// .entries()
// .find(|v| v.0 == "userByPid")
// .ok_or(SomethingHappened)?
// .1
// .entries()
// .find(|v| v.0 == "accountLevel")
// .ok_or(SomethingHappened)?
// .1
// .as_i32()
// else {
// return Err(SomethingHappened);
// };
let Some(val) = req
.entries()
.find(|v| v.0 == "data")
.ok_or(SomethingHappened)?
.1
.entries()
.find(|v| v.0 == "userByPid")
.ok_or(SomethingHappened)?
.1
.entries()
.find(|v| v.0 == "accountLevel")
.ok_or(SomethingHappened)?
.1
.as_i32()
else {
return Err(SomethingHappened);
};
Ok(val)
// everyone is tester until this is implemented
Ok(0)
}
pub async fn get_pid_from_token(&mut self, token: String) -> Result<PID> {
let req = self
.do_request(object! {
"query":
r"query($token: String!){
token(tokenData: $token){
pid
}
}",
"variables": {
"token": token
}
})
.await?;
// this breaks switch nex servers and should be fixed eventually
let Some(val) = req
.entries()
.find(|v| v.0 == "data")
.ok_or(SomethingHappened)?
.1
.entries()
.find(|v| v.0 == "token")
.ok_or(SomethingHappened)?
.1
.entries()
.find(|v| v.0 == "pid")
.ok_or(SomethingHappened)?
.1
.as_i32()
else {
return Err(SomethingHappened);
};
Ok(val)
}
// pub async fn get_pid_from_token(&mut self, token: String) -> Result<PID> {
// let req = self
// .do_request(object! {
// "query":
// r"query($token: String!){
// token(tokenData: $token){
// pid
// }
// }",
// "variables": {
// "token": token
// }
// })
// .await?;
// // this breaks switch nex servers and should be fixed eventually
// let Some(val) = req
// .entries()
// .find(|v| v.0 == "data")
// .ok_or(SomethingHappened)?
// .1
// .entries()
// .find(|v| v.0 == "token")
// .ok_or(SomethingHappened)?
// .1
// .entries()
// .find(|v| v.0 == "pid")
// .ok_or(SomethingHappened)?
// .1
// .as_i32()
// else {
// return Err(SomethingHappened);
// };
//
// Ok(val)
// }
/*pub async fn get_user_data(&mut self , pid: u32) -> Result<GetUserDataResponse>{
let req = Request::new(GetUserDataRequest{

View file

@ -3,6 +3,7 @@ use cfg_if::cfg_if;
use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike, Utc};
use hmac::Hmac;
use hmac::Mac;
use md5::digest::generic_array::GenericArray;
use md5::{Digest, Md5};
use rc4::KeyInit;
use rc4::cipher::StreamCipherCoreWrapper;
@ -28,24 +29,6 @@ pub const SESSION_KEY_LENGTH: usize = SessionLengthTy::USIZE;
type Md5Hmac = Hmac<md5::Md5>;
pub fn derive_key(pid: PID, password: &[u8]) -> [u8; 16] {
let iteration_count = 65000 + pid % 1024;
// we do one iteration out here to ensure the key is always 16 bytes
let mut key: [u8; 16] = {
let mut md5 = Md5::new();
md5.update(password);
md5.finalize().try_into().unwrap()
};
for _ in 1..iteration_count {
let mut md5 = Md5::new();
md5.update(key);
key = md5.finalize().try_into().unwrap();
}
key
}
#[derive(Pod, Zeroable, Copy, Clone, Debug, Eq, PartialEq)]
#[repr(transparent)]
pub struct KerberosDateTime(pub u64);

View file

@ -1,4 +1,5 @@
use macros::RmcSerialize;
use md5::{Digest, Md5};
use rnex_core::PID;
@ -6,27 +7,42 @@ use rnex_core::PID;
pub struct Account {
pub pid: PID,
pub username: String,
pub kerbros_password: Box<[u8]>,
pub nex_key: [u8; 16],
}
impl Account {
pub fn new(pid: PID, username: &str, passwd: &str) -> Self {
let iteration_count = 65000 + pid % 1024;
// we do one iteration out here to ensure the key is always 16 bytes
let mut key: [u8; 16] = {
let mut md5 = Md5::new();
md5.update(passwd);
md5.finalize().try_into().unwrap()
};
for _ in 1..iteration_count {
let mut md5 = Md5::new();
md5.update(key);
key = md5.finalize().try_into().unwrap();
}
Self {
kerbros_password: passwd.as_bytes().into(),
nex_key: key.into(),
username: username.into(),
pid,
}
}
pub fn new_raw_password(pid: PID, username: &str, passwd: &[u8]) -> Self {
pub fn new_raw_key(pid: PID, username: &str, nex_key: [u8; 16]) -> Self {
Self {
kerbros_password: passwd.into(),
username: username.into(),
pid,
nex_key,
}
}
pub fn get_login_data(&self) -> (PID, &[u8]) {
(self.pid, &self.kerbros_password)
pub fn get_login_data(&self) -> (PID, [u8; 16]) {
(self.pid, self.nex_key)
}
}

View file

@ -5,7 +5,7 @@ use cfg_if::cfg_if;
use log::{info, warn};
use macros::rmc_struct;
use rnex_core::PID;
use rnex_core::kerberos::{KerberosDateTime, Ticket, derive_key};
use rnex_core::kerberos::{KerberosDateTime, Ticket};
use rnex_core::nex::account::Account;
use rnex_core::rmc::protocols::OnlyRemote;
use rnex_core::rmc::protocols::auth::{Auth, RawAuth, RawAuthInfo, RemoteAuth};
@ -34,11 +34,11 @@ pub struct AuthHandler {
}
pub fn generate_ticket(
source_act_login_data: (PID, &[u8]),
dest_act_login_data: (PID, &[u8]),
source_act_login_data: (PID, [u8; 16]),
dest_act_login_data: (PID, [u8; 16]),
) -> Box<[u8]> {
let source_key = derive_key(source_act_login_data.0, source_act_login_data.1);
let dest_key = derive_key(dest_act_login_data.0, dest_act_login_data.1);
let source_key = source_act_login_data.1;
let dest_key = dest_act_login_data.1;
let internal_data = kerberos::TicketInternalData::new(source_act_login_data.0);
@ -53,12 +53,12 @@ pub fn generate_ticket(
}
pub fn generate_ticket_with_string_user_key(
source_act: PID,
dest_act_login_data: (PID, &[u8]),
dest_act_login_data: (PID, [u8; 16]),
) -> (String, Box<[u8]>) {
let source_key: [u8; 8] = rand::random();
let key_string = hex::encode(source_key);
let key_data: [u8; 16] = key_string.as_bytes().try_into().unwrap();
let dest_key = derive_key(dest_act_login_data.0, dest_act_login_data.1);
let dest_key = dest_act_login_data.1;
let internal_data = kerberos::TicketInternalData::new(source_act);
@ -72,22 +72,22 @@ pub fn generate_ticket_with_string_user_key(
(key_string, encrypted_session_ticket)
}
async fn get_login_data_by_pid(pid: PID) -> Option<(PID, Box<[u8]>)> {
async fn get_login_data_by_pid(pid: PID) -> Option<(PID, [u8; 16])> {
if pid == GUEST_ACCOUNT.pid {
let source_login_data = GUEST_ACCOUNT.get_login_data();
return Some((source_login_data.0, source_login_data.1.into()));
return Some((source_login_data.0, source_login_data.1));
}
let Ok(mut client) = account::Client::new().await else {
return None;
};
let Ok(passwd) = client.get_nex_password(pid).await else {
let Ok(passwd) = client.get_nex_key(pid).await else {
return None;
};
Some((pid, passwd.into()))
Some((pid, passwd))
}
fn station_url_from_sock_addr(sock_addr: SocketAddrV4) -> String {
@ -109,6 +109,7 @@ impl AuthHandler {
#[cfg(feature = "guest_login")]
{
if name == GUEST_ACCOUNT.username {
log::info!("guest account login");
let source_login_data = GUEST_ACCOUNT.get_login_data();
let destination_login_data = self.destination_server_acct.get_login_data();
@ -118,25 +119,31 @@ impl AuthHandler {
));
}
}
log::info!("parsing pid");
let Ok(pid) = name.parse() else {
warn!("unable to connect to parse pid: {}", name);
return Err(ErrorCode::Core_InvalidArgument);
};
log::info!("creating account grpc client");
let Ok(mut client) = account::Client::new().await else {
warn!("unable to connect to grpc");
return Err(ErrorCode::Core_Exception);
};
let Ok(passwd) = client.get_nex_password(pid).await else {
log::info!("grabbing nex key");
let Ok(passwd) = client.get_nex_key(pid).await else {
warn!("unable to get nex password for pid: {}:", pid);
return Err(ErrorCode::Core_Exception);
};
let source_login_data = (pid, &passwd[..]);
log::info!("source login data");
let source_login_data = (pid, passwd);
println!("{}, {:?}", pid, passwd);
let destination_login_data = self.destination_server_acct.get_login_data();
log::info!("we are a-ok here");
Ok((
pid,
generate_ticket(source_login_data, destination_login_data),
@ -322,7 +329,7 @@ impl Auth for AuthHandler {
let result = QResult::success(Core_Unknown);
let ticket = generate_ticket((pid, &passwd[..]), desgination_login_data);
let ticket = generate_ticket((pid, passwd), desgination_login_data);
Ok((result, ticket.into()))
}

View file

@ -1810,4 +1810,112 @@ impl DataStore for User {
Ok((ranking_results, q_results))
}
async fn ctr_pickup_course_search_object(&self, course_search_param: DataStoreSearchParam, extra_data: Vec<String>) -> Result<Vec<DataStoreCustomRankingResult>, ErrorCode> {
let mut courses = Vec::new();
let mut stream = sqlx::query!(
r#"
SELECT
object.data_id,
object.owner,
object.size,
object.name,
object.data_type,
object.meta_binary,
object.permission,
object.permission_recipients,
object.delete_permission,
object.delete_permission_recipients,
object.period,
object.refer_data_id,
object.flag,
object.tags,
object.creation_date,
object.update_date,
ranking.value
FROM (
SELECT * FROM datastore.objects object
WHERE
object.upload_completed = TRUE AND
object.deleted = FALSE AND
object.under_review = FALSE
) object
JOIN (
SELECT data_id, value
FROM datastore.object_custom_rankings ranking
WHERE ranking.application_id = 0
) ranking
ON
object.data_id = ranking.data_id
ORDER BY RANDOM()
LIMIT 100
"#
)
.fetch(get_db());
while let Some(row) = stream.try_next().await.map_err(|e| {
eprintln!("stream error: {:?}", e);
ErrorCode::DataStore_NotFound
})? {
let permission = Permission {
permission: row.permission.unwrap_or(0) as u8,
recipient_ids: row.permission_recipients.unwrap_or_default(),
};
let del_permission = Permission {
permission: row.delete_permission.unwrap_or(0) as u8,
recipient_ids: row.delete_permission_recipients.unwrap_or_default(),
};
let meta_binary = row.meta_binary.map(QBuffer).unwrap_or_default();
let created_time = row
.creation_date
.map(KerberosDateTime::from_naive)
.unwrap_or_default();
let updated_time = row
.update_date
.map(KerberosDateTime::from_naive)
.unwrap_or_default();
let referred_time = row
.creation_date
.map(KerberosDateTime::from_naive)
.unwrap_or_default();
let mut meta_info = GetMetaInfo {
dataid: row.data_id,
owner: row.owner.unwrap_or(0),
size: row.size.unwrap_or(0) as u32,
name: row.name,
data_type: row.data_type.unwrap_or(0) as u16,
meta_binary,
permission,
del_permission,
period: row.period.unwrap_or(0) as u16,
status: 0,
referred_count: 0,
refer_dat_id: row.refer_data_id.unwrap_or(0) as u32,
flag: row.flag.unwrap_or(0) as u32,
tags: row.tags.unwrap_or_default(),
expire_time: KerberosDateTime::PRACTICALLY_NEVER,
created_time,
updated_time,
referred_time,
ratings: get_rating_with_slot_data_id(row.data_id).await?,
};
let course = DataStoreCustomRankingResult {
order: 0,
score: row.value.unwrap_or(0) as u32,
meta_info,
};
courses.push(course);
}
Ok(courses)
}
}

View file

@ -1,14 +1,21 @@
use std::env;
use std::io::{Cursor, Write};
use std::ops::Deref;
use std::sync::Weak;
use std::sync::{Arc, atomic::AtomicU32};
use std::sync::{LazyLock, Weak};
use bytemuck::bytes_of;
use base64::{Engine as _, engine::general_purpose};
use bytemuck::{Pod, Zeroable, bytes_of};
use hex::decode;
use hmac::Mac;
use log::info;
use macros::rmc_struct;
use rnex_core::rmc::protocols::account_management::{
AccountManagement, RawAccountManagement, RawAccountManagementInfo, RemoteAccountManagement,
AccountExtraInfo, AccountManagement, RawAccountManagement, RawAccountManagementInfo,
RemoteAccountManagement,
};
use rnex_core::rmc::protocols::friends_3ds::{
Friends3DS, RawFriends3DS, RawFriends3DSInfo, RemoteFriends3DS,
};
use rnex_core::rmc::protocols::friends_wiiu::{
FriendsWiiU, RawFriendsWiiU, RawFriendsWiiUInfo, RemoteFriendsWiiU,
@ -49,10 +56,19 @@ use rnex_core::rmc::structures::data::Data;
use crate::executables::common::get_db;
use crate::rmc::protocols::friends_3ds::{
FriendComment, FriendMii, FriendMiiList, FriendPersistentInfo, FriendPicture, FriendPresence,
FriendRelationship, Mii, MiiList, MyProfile, NintendoPresence, PlayedGame,
};
use nex_account::grpc::ActCreateInfo;
use nex_account::grpc::nex_account_service_client::NexAccountServiceClient;
use nex_account::{derive_pid_hmac, grpc_client};
define_rmc_proto!(
proto FriendsUser{
Secure,
FriendsWiiU
FriendsWiiU,
Friends3DS
}
);
define_rmc_proto!(
@ -67,11 +83,22 @@ define_rmc_proto!(
}
);
static NEX_ACCOUNT_URL: LazyLock<String> =
LazyLock::new(|| env::var("NEX_ACCOUNT_ENDPOINT").expect("NEX_ACCOUNT_ENDPOINT not set"));
pub struct UserData {
info: NNAInfo,
presence: NintendoPresenceV2,
}
#[repr(C, packed)]
#[derive(Pod, Zeroable, Copy, Clone, Debug)]
pub struct NascToken {
pub pid: i32,
pub time: [u8; 14],
pub pwd_hash: [u8; 4],
}
#[rmc_struct(FriendsUser)]
pub struct FriendsUser {
pub fm: Arc<FriendsManager>,
@ -100,6 +127,250 @@ impl FriendsManager {
}
}
// ALL of this is stubbed
impl Friends3DS for FriendsUser {
async fn update_profile(&self, profile: MyProfile) -> Result<(), ErrorCode> {
Ok(())
}
async fn update_mii(&self, profile: Mii) -> Result<(), ErrorCode> {
Ok(())
}
async fn update_mii_list(&self, profile: MiiList) -> Result<(), ErrorCode> {
Ok(())
}
async fn update_played_games(&self, profile: Vec<PlayedGame>) -> Result<(), ErrorCode> {
Ok(())
}
async fn update_preference(
&self,
show_online_status: bool,
show_current_title: bool,
block_friend_requests: bool,
) -> Result<(), ErrorCode> {
// stubbed
Ok(())
}
async fn get_friend_mii(
&self,
friends: Vec<crate::rmc::protocols::friends_3ds::FriendInfo>,
) -> Result<Vec<FriendMii>, ErrorCode> {
// sorry for the copying pretendo but i don't have a mii on hand rn
let data: Vec<u8> = vec![
0x03, 0x00, 0x00, 0x40, 0xE9, 0x55, 0xA2, 0x09, 0xE7, 0xC7, 0x41, 0x82, 0xD9, 0x7D,
0x0B, 0x2D, 0x03, 0xB3, 0xB8, 0x8D, 0x27, 0xD9, 0x00, 0x00, 0x01, 0x40, 0x62, 0x00,
0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x61, 0x00, 0x00, 0x00, 0x45, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x40, 0x12, 0x00, 0x81, 0x01, 0x04, 0x68, 0x43, 0x18,
0x20, 0x34, 0x46, 0x14, 0x81, 0x12, 0x17, 0x68, 0x0D, 0x00, 0x00, 0x29, 0x03, 0x52,
0x48, 0x50, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFE, 0x86,
];
let dummymii = FriendMii {
data: Data {},
pid: 69,
mii: Mii {
data: Data {},
name: "test".to_string(),
profanity: false,
char_set: 0,
mii_data: data,
},
modified_at: Default::default(),
};
Ok(vec![dummymii])
}
async fn get_friend_mii_list(
&self,
friends: Vec<crate::rmc::protocols::friends_3ds::FriendInfo>,
) -> Result<Vec<FriendMiiList>, ErrorCode> {
Err(ErrorCode::Core_NotImplemented)
}
async fn is_active_game(
&self,
unk: Vec<u32>,
game_key: crate::rmc::protocols::friends_3ds::GameKey,
) -> Result<Vec<u32>, ErrorCode> {
Err(ErrorCode::Core_NotImplemented)
}
async fn get_principal_id_by_local_friend_code(
&self,
unk1: u64,
unk2: Vec<u64>,
) -> Result<Vec<FriendRelationship>, ErrorCode> {
Err(ErrorCode::Core_NotImplemented)
}
async fn get_friend_relationships(
&self,
unk2: Vec<u32>,
) -> Result<Vec<FriendRelationship>, ErrorCode> {
let dummy = FriendRelationship {
data: Data {},
pid: 69,
local_friend_code: 3268487429723707977,
relationship_type: 1,
};
Ok(vec![dummy])
}
async fn add_friend_by_pid(&self, unk: u64, pid: PID) -> Result<FriendRelationship, ErrorCode> {
Err(ErrorCode::Core_NotImplemented)
}
async fn add_friend_by_lst_pid(
&self,
unk: u64,
pid: Vec<PID>,
) -> Result<Vec<FriendRelationship>, ErrorCode> {
Err(ErrorCode::Core_NotImplemented)
}
async fn remove_friend_by_local_code(&self, local_code: u64) -> Result<(), ErrorCode> {
Err(ErrorCode::Core_NotImplemented)
}
async fn remove_friend_by_pid(&self, pid: PID) -> Result<(), ErrorCode> {
Err(ErrorCode::Core_NotImplemented)
}
async fn get_all_friends(&self) -> Result<Vec<FriendRelationship>, ErrorCode> {
let dummy = FriendRelationship {
data: Data {},
pid: 69,
local_friend_code: 3268487429723707977,
relationship_type: 1,
};
Ok(vec![dummy])
}
async fn update_blacklist(&self) -> Result<(), ErrorCode> {
Ok(())
}
async fn sync_friend(
&self,
unk1: u64,
unk2: Vec<u32>,
unk3: Vec<u64>,
) -> Result<Vec<FriendRelationship>, ErrorCode> {
log::info!("params: {:?}, {:?}, {:?}", unk1, unk2, unk3);
let dummy = FriendRelationship {
data: Data {},
pid: 69,
local_friend_code: 3268487429723707977,
relationship_type: 1,
};
Ok(vec![dummy])
}
async fn update_presence(
&self,
nintendo_presence: NintendoPresence,
unk: bool,
) -> Result<(), ErrorCode> {
Ok(())
}
async fn update_favorite_game_key(
&self,
game_key: rnex_core::rmc::protocols::friends_3ds::GameKey,
) -> Result<(), ErrorCode> {
log::info!("favorite game key: {:?}", game_key);
Ok(())
}
async fn update_comment(&self, comment: String) -> Result<(), ErrorCode> {
Ok(())
}
async fn update_picture(&self, unk: u32, picture: Vec<u8>) -> Result<(), ErrorCode> {
Err(ErrorCode::Core_NotImplemented)
}
async fn get_friend_presence(&self, unk: Vec<u32>) -> Result<Vec<FriendPresence>, ErrorCode> {
log::info!("pids: {:?}", unk);
let presence = FriendPresence {
data: Data {},
pid: 69,
presence: NintendoPresence {
data: Data {},
changed_bit_flag: 0xFFFFFFFF,
game_key: rnex_core::rmc::protocols::friends_3ds::GameKey {
data: Data {},
title_id: 1125899907457280,
version: 2064,
},
game_mode_desctiption: "".to_string(),
join_availibility_flag: 0,
mm_system_type: 0,
join_game_id: 0,
join_game_mode: 0,
owner_pid: 0,
join_group_id: 0,
application_arg: vec![],
},
};
Ok(vec![presence])
}
async fn get_friend_comment(
&self,
unk: Vec<crate::rmc::protocols::friends_3ds::FriendInfo>,
) -> Result<Vec<FriendComment>, ErrorCode> {
Err(ErrorCode::Core_NotImplemented)
}
async fn get_friend_picture(&self, unk: Vec<u32>) -> Result<Vec<FriendPicture>, ErrorCode> {
Err(ErrorCode::Core_NotImplemented)
}
async fn get_friend_persistent_info(
&self,
unk: Vec<u32>,
) -> Result<Vec<FriendPersistentInfo>, ErrorCode> {
let dummypersistentinfo = FriendPersistentInfo {
data: Data {},
pid: 69,
region: 0,
country: 0,
area: 0,
language: 0,
platform: 0,
game_key: rnex_core::rmc::protocols::friends_3ds::GameKey {
data: Data {},
title_id: 1125899907457280,
version: 2064,
},
message: "yo whats up".to_string(),
msg_updated_at: KerberosDateTime::now(),
friended_at: KerberosDateTime::now(),
last_online: KerberosDateTime::now(),
};
Ok(vec![dummypersistentinfo])
}
async fn send_invitation(&self, unk: Vec<u32>) -> Result<(), ErrorCode> {
Err(ErrorCode::Core_NotImplemented)
}
}
impl FriendsWiiU for FriendsUser {
async fn update_and_get_all_information(
&self,
@ -121,7 +392,27 @@ impl FriendsWiiU for FriendsUser {
ErrorCode,
> {
// let query = query!("select ", self.pid).fetch_all(get_db()).await;
Err(ErrorCode::Core_NotImplemented)
Ok((
PrincipalPreference {
data: Data {},
block_friend_request: false,
show_online: true,
show_playing_title: false,
},
Comment {
data: Data {},
last_changed: KerberosDateTime::now(),
message: "stub(will be impl'd later)".into(),
unk: 0,
},
vec![],
vec![],
vec![],
vec![],
false,
vec![],
false,
))
}
async fn add_friend(&self, friend: PID) -> Result<(FriendRequest, FriendInfo), ErrorCode> {
@ -288,28 +579,51 @@ impl AccountManagement for FriendsGuest {
auth_data: Any,
) -> Result<(PID, String), ErrorCode> {
println!("{}, {}, {}, {}", principal_name, key, groups, email);
if auth_data.name == "NintendoCreateAccountData" {
let Ok(data) =
NintendoCreateAccountData::deserialize(&mut Cursor::new(&auth_data.data))
else {
return Err(ErrorCode::Authentication_InvalidParam);
};
let pid = data.nna_info.principal_basic_info.pid;
info!("create account: {}", pid);
let nex_token = if let Ok(extra_info) = auth_data.try_get_as::<AccountExtraInfo>() {
extra_info.nex_token
} else if let Ok(data) = auth_data.try_get_as::<NintendoCreateAccountData>() {
data.nex_token
} else {
return Err(ErrorCode::Authentication_InvalidParam);
};
let (pid, nex_key) = nex_account::decode_nexact_token(&nex_token).map_err(|e| {
log::error!("failed to decode token: {}", e);
log::info!("{:?}", nex_token);
ErrorCode::Authentication_InvalidParam
})?;
let Ok(mut mac) = HMacMd5::new_from_slice(key.as_bytes()) else {
return Err(ErrorCode::Authentication_InvalidParam);
};
//let mac = derive_pid_hmac(data.nna_info.principal_basic_info.pid, &nexkey);
mac.write_all(bytes_of(&pid))
.expect("failed to write to hmac???");
let mac = mac.finalize().into_bytes();
let mut client = grpc_client().await.map_err(|e| {
eprintln!("error occurred: {:?}", e);
ErrorCode::Core_Unknown
})?;
let hex_str = hex::encode(mac);
let new_account: ActCreateInfo = ActCreateInfo {
principal_name,
key: nex_key.into(),
email,
pid,
};
return Ok((pid, hex_str));
let nexkey = client
.create_new_sequential_or_update_and_get_account(new_account)
.await
.map_err(|e| ErrorCode::Core_Unknown)?
.into_inner();
if nexkey.key.len() != 16 {
log::error!("nex key was not 16 bytes long");
return Err(ErrorCode::Authentication_InvalidParam);
}
Err(ErrorCode::Core_NotImplemented)
let nexkeyarray: [u8; 16] = nexkey.key.try_into().expect("how...?");
let mac = derive_pid_hmac(pid, &nexkeyarray);
let hex_str = hex::encode(mac);
return Ok((pid, hex_str));
}
}

View file

@ -45,27 +45,33 @@ use tokio::sync::mpsc::Sender;
use cfg_if::cfg_if;
use log::{error, info};
use macros::rmc_struct;
use rnex_core::prudp::socket_addr::PRUDPSockAddr;
use rnex_core::rmc::protocols::message_delivery::{
MessageDelivery, RawMessageDelivery, RawMessageDeliveryInfo, RemoteMessageDelivery,
use rnex_core::{
prudp::socket_addr::PRUDPSockAddr,
rmc::{
protocols::{
message_delivery::{
MessageDelivery, RawMessageDelivery, RawMessageDeliveryInfo, RemoteMessageDelivery,
RemoteMessageDeliveryNoResponse,
},
messaging::UserMessage,
notifications::{NotificationEvent, RemoteNotification},
ranking::{
CompetitionRankingGetParam, CompetitionRankingScoreData,
CompetitionRankingScoreInfo,
},
},
response::ErrorCode::{Core_InvalidArgument, RendezVous_AccountExpired},
structures::{
matchmake::{Gathering, MatchmakeSessionSearchCriteria},
qbuffer::QBuffer,
qresult::QResult,
ranking::UploadCompetitionData,
},
},
};
use rnex_core::rmc::protocols::notifications::{NotificationEvent, RemoteNotification};
use rnex_core::rmc::protocols::ranking::{
CompetitionRankingGetParam, CompetitionRankingScoreData, CompetitionRankingScoreInfo,
};
use rnex_core::rmc::response::ErrorCode::{Core_InvalidArgument, RendezVous_AccountExpired};
use rnex_core::rmc::structures::qbuffer::QBuffer;
use rnex_core::rmc::structures::qresult::QResult;
use rnex_core::rmc::structures::ranking::UploadCompetitionData;
use std::sync::{Arc, Weak};
use tokio::sync::{Mutex, RwLock};
use crate::kerberos::Ticket;
use crate::rmc::protocols::message_delivery::RemoteMessageDeliveryNoResponse;
use crate::rmc::protocols::messaging::UserMessage;
use crate::rmc::structures::matchmake::Gathering;
use crate::rmc::structures::matchmake::MatchmakeSessionSearchCriteria;
cfg_if! {
if #[cfg(feature = "datastore")] {
use rnex_core::rmc::protocols::datastore::{DataStore, RawDataStore, RawDataStoreInfo, RemoteDataStore};
@ -91,7 +97,8 @@ cfg_if! {
Matchmake,
NatTraversal,
Utility,
Ranking
Ranking,
MessageDelivery
}
);
}
@ -864,6 +871,15 @@ impl Utility for User {
async fn acquire_nex_unique_id(&self) -> Result<u64, ErrorCode> {
return Ok(rand::random());
}
async fn get_integer_settings(&self, index: u32) -> Result<Vec<(u16, i32)>, ErrorCode> {
Ok(vec![
(0, 1),
(1, 2),
(2, 0),
(3, 4)
])
}
}
impl Ranking for User {

View file

@ -6,7 +6,7 @@ use typenum::U16;
use v_byte_helpers::{IS_BIG_ENDIAN, ReadExtensions};
use crate::{
kerberos::{SESSION_KEY_LENGTH, SessionLengthTy, TicketInternalData, derive_key},
kerberos::{SESSION_KEY_LENGTH, SessionLengthTy, TicketInternalData},
nex::account::Account,
rmc::structures::RmcSerialize,
};
@ -26,7 +26,7 @@ pub fn read_secure_connection_data(
let ticket_data = &mut ticket_data[0..ticket_data_size - 0x10];
let server_key = derive_key(act.pid, &act.kerbros_password[..]);
let server_key = act.nex_key;
let mut rc4: StreamCipherCoreWrapper<Rc4Core<U16>> =
Rc4::new_from_slice(&server_key).expect("unable to init rc4 keystream");

View file

@ -453,6 +453,12 @@ pub trait DataStore {
&self,
application_id: u32,
) -> Result<bool, ErrorCode>;
#[method_id(82)]
async fn ctr_pickup_course_search_object(
&self,
course_search_param: DataStoreSearchParam,
extra_data: Vec<String>,
) -> Result<Vec<DataStoreCustomRankingResult>, ErrorCode>;
#[method_id(87)]
async fn report_course(
&self,

View file

@ -24,9 +24,9 @@ pub struct MyProfile {
pub struct Mii {
#[extends]
pub data: Data,
pub unk1: String,
pub unk2: bool,
pub unk3: u8,
pub name: String,
pub profanity: bool,
pub char_set: u8, // 0 is JPN/USA/EUR, 1 is CHN, 2 is KOR and 3 is TWN
pub mii_data: Vec<u8>,
}
@ -41,7 +41,7 @@ pub struct MiiList {
pub mii_data: Vec<Vec<u8>>,
}
#[derive(RmcSerialize)]
#[derive(RmcSerialize, Debug)]
#[rmc_struct(0)]
pub struct GameKey {
#[extends]
@ -62,7 +62,7 @@ pub struct PlayedGame {
#[derive(RmcSerialize)]
#[rmc_struct(0)]
pub struct FriendInfo {
pub unk1: u32,
pub pid: u32,
pub unk2: KerberosDateTime,
}
@ -91,9 +91,9 @@ pub struct FriendMiiList {
pub struct FriendRelationship {
#[extends]
pub data: Data,
pub unk1: u32,
pub unk2: u64,
pub unk3: u8,
pub pid: u32,
pub local_friend_code: u64,
pub relationship_type: u8,
}
#[derive(RmcSerialize)]
@ -117,7 +117,7 @@ pub struct NintendoPresence {
pub struct FriendPresence {
#[extends]
pub data: Data,
pub unk: u32,
pub pid: u32,
pub presence: NintendoPresence,
}
#[derive(RmcSerialize)]

View file

@ -6,4 +6,6 @@ use rnex_core::rmc::response::ErrorCode;
pub trait Utility {
#[method_id(1)]
async fn acquire_nex_unique_id(&self) -> Result<u64, ErrorCode>;
#[method_id(7)]
async fn get_integer_settings(&self, index: u32) -> Result<Vec<(u16, i32)>, ErrorCode>;
}