add certificate verification
This commit is contained in:
parent
196bfbf203
commit
4864fb83a8
4 changed files with 387 additions and 67 deletions
|
|
@ -1,38 +1,39 @@
|
|||
use std::io::Write;
|
||||
use std::io::{Cursor, Write};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
// Don't import until required.
|
||||
// use argon2::{Algorithm, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
// use argon2::password_hash::rand_core::OsRng;
|
||||
// use argon2::password_hash::SaltString;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use bytemuck::bytes_of;
|
||||
use chrono::{NaiveDate, NaiveDateTime, Utc};
|
||||
use rocket::http::Status;
|
||||
use rocket::{async_trait, Request};
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use sha2::{Digest, Sha256};
|
||||
use crate::Pool;
|
||||
use crate::error::{Error, Errors};
|
||||
use crate::nnid::oauth::TokenData;
|
||||
use crate::Pool;
|
||||
use base64::Engine;
|
||||
use base64::prelude::BASE64_STANDARD;
|
||||
use binrw::{BinRead, binread};
|
||||
use bytemuck::bytes_of;
|
||||
use chrono::{NaiveDate, NaiveDateTime, Utc};
|
||||
use openssl::bn::BigNum;
|
||||
use openssl::ecdsa::EcdsaSig;
|
||||
use rand::Rng;
|
||||
use rocket::http::Status;
|
||||
use rocket::request::{FromRequest, Outcome};
|
||||
use rocket::{Request, async_trait};
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
macro_rules! request_try {
|
||||
($expression:expr) => {
|
||||
match $expression{
|
||||
match $expression {
|
||||
Ok(v) => v,
|
||||
Err(e) => return Outcome::Error((Status::BadRequest, e))
|
||||
Err(e) => return Outcome::Error((Status::BadRequest, e)),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const INVALID_TOKEN_ERRORS: Errors<'static> = Errors{
|
||||
error: &[
|
||||
Error{
|
||||
message: "Invalid access token",
|
||||
code: "0005"
|
||||
}
|
||||
]
|
||||
const INVALID_TOKEN_ERRORS: Errors<'static> = Errors {
|
||||
error: &[Error {
|
||||
message: "Invalid access token",
|
||||
code: "0005",
|
||||
}],
|
||||
};
|
||||
|
||||
// optimization note: add token caching
|
||||
|
|
@ -56,52 +57,48 @@ pub struct User {
|
|||
pub creation_date: NaiveDateTime,
|
||||
pub updated: NaiveDateTime,
|
||||
pub nex_password: String,
|
||||
pub verification_code: Option<i32>
|
||||
pub verification_code: Option<i32>,
|
||||
}
|
||||
|
||||
fn generate_nintendo_hash(pid: i32, text_password: &str) -> String{
|
||||
fn generate_nintendo_hash(pid: i32, text_password: &str) -> String {
|
||||
let mut sha = Sha256::new();
|
||||
|
||||
sha.write_all(&bytes_of(&pid)).unwrap();
|
||||
sha.write_all(&[0x02, 0x65, 0x43 ,0x46]).unwrap();
|
||||
sha.write_all(&[0x02, 0x65, 0x43, 0x46]).unwrap();
|
||||
sha.write_all(text_password.as_bytes()).unwrap();
|
||||
|
||||
hex::encode(&sha.finalize()[..])
|
||||
}
|
||||
|
||||
impl User{
|
||||
pub async fn get_by_username(name: &str, pool: &Pool) -> Option<Self>{
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT * FROM users WHERE username = $1",
|
||||
name
|
||||
).fetch_one(pool)
|
||||
impl User {
|
||||
pub async fn get_by_username(name: &str, pool: &Pool) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT * FROM users WHERE username = $1", name)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn generate_nintendo_hash(&self, text_password: &str) -> String{
|
||||
fn generate_nintendo_hash(&self, text_password: &str) -> String {
|
||||
generate_nintendo_hash(self.pid, text_password)
|
||||
}
|
||||
|
||||
pub fn verify_cleartext_password(&self, cleartext_password: &str) -> Option<bool>{
|
||||
pub fn verify_cleartext_password(&self, cleartext_password: &str) -> Option<bool> {
|
||||
let nintendo_hash = self.generate_nintendo_hash(cleartext_password);
|
||||
|
||||
self.verify_hashed_password(&nintendo_hash)
|
||||
}
|
||||
|
||||
pub fn verify_hashed_password(&self, hashed_password: &str) -> Option<bool>{
|
||||
pub fn verify_hashed_password(&self, hashed_password: &str) -> Option<bool> {
|
||||
bcrypt::verify(hashed_password, &self.password).ok()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_password(pid: i32, cleartext_password: &str) -> Option<String>{
|
||||
pub fn generate_password(pid: i32, cleartext_password: &str) -> Option<String> {
|
||||
let password = generate_nintendo_hash(pid, cleartext_password);
|
||||
|
||||
bcrypt::hash(password, 10).ok()
|
||||
}
|
||||
|
||||
|
||||
pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option<User> {
|
||||
let data = match BASE64_STANDARD.decode(&token) {
|
||||
Ok(d) => d,
|
||||
|
|
@ -131,7 +128,9 @@ pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option<Use
|
|||
User,
|
||||
"SELECT * FROM users WHERE username = $1",
|
||||
login_username
|
||||
).fetch_one(connection).await;
|
||||
)
|
||||
.fetch_one(connection)
|
||||
.await;
|
||||
|
||||
let user = match user_result {
|
||||
Ok(u) => u,
|
||||
|
|
@ -150,26 +149,27 @@ pub async fn read_basic_auth_token(connection: &Pool, token: &str) -> Option<Use
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
pub async fn read_bearer_auth_token(connection: &Pool, token: &str) -> Option<User> {
|
||||
let data = TokenData::decode(token)?;
|
||||
|
||||
let token_info =
|
||||
sqlx::query!(
|
||||
"select * from tokens where pid = $1 and token_id = $2 and random =$3",
|
||||
data.pid, data.token_id, data.random
|
||||
).
|
||||
fetch_one(connection).await.ok()?;
|
||||
let token_info = sqlx::query!(
|
||||
"select * from tokens where pid = $1 and token_id = $2 and random =$3",
|
||||
data.pid,
|
||||
data.token_id,
|
||||
data.random
|
||||
)
|
||||
.fetch_one(connection)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
if token_info.expires.and_utc() < Utc::now(){
|
||||
return None
|
||||
if token_info.expires.and_utc() < Utc::now() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let user = sqlx::query_as!(
|
||||
User,
|
||||
"SELECT * FROM users WHERE pid = $1",
|
||||
token_info.pid
|
||||
).fetch_one(connection).await.ok()?;
|
||||
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE pid = $1", token_info.pid)
|
||||
.fetch_one(connection)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
Some(user)
|
||||
}
|
||||
|
|
@ -180,7 +180,7 @@ pub fn generate_nex_password() -> String {
|
|||
|
||||
while output.len() < 16 {
|
||||
let offset: u8 = rng.gen_range(0..62);
|
||||
|
||||
|
||||
let character = if offset < 10 {
|
||||
(offset + b'0') as char
|
||||
} else if offset < 36 {
|
||||
|
|
@ -188,59 +188,77 @@ pub fn generate_nex_password() -> String {
|
|||
} else {
|
||||
(offset + 61) as char
|
||||
};
|
||||
|
||||
|
||||
output.push(character);
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
pub struct Auth<const FORCE_BEARER_AUTH: bool>(pub User);
|
||||
pub struct Auth<const FORCE_BEARER_AUTH: bool = true, const USE_CERT: bool = FORCE_BEARER_AUTH>(
|
||||
pub User,
|
||||
);
|
||||
|
||||
impl<const FORCE_BEARER_AUTH: bool> AsRef<User> for Auth<FORCE_BEARER_AUTH>{
|
||||
impl<const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> AsRef<User>
|
||||
for Auth<FORCE_BEARER_AUTH, USE_CERT>
|
||||
{
|
||||
fn as_ref(&self) -> &User {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<const FORCE_BEARER_AUTH: bool> AsMut<User> for Auth<FORCE_BEARER_AUTH>{
|
||||
impl<const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> AsMut<User>
|
||||
for Auth<FORCE_BEARER_AUTH, USE_CERT>
|
||||
{
|
||||
fn as_mut(&mut self) -> &mut User {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<const FORCE_BEARER_AUTH: bool> Deref for Auth<FORCE_BEARER_AUTH>{
|
||||
impl<const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> Deref
|
||||
for Auth<FORCE_BEARER_AUTH, USE_CERT>
|
||||
{
|
||||
type Target = User;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<const FORCE_BEARER_AUTH: bool> DerefMut for Auth<FORCE_BEARER_AUTH>{
|
||||
impl<const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> DerefMut
|
||||
for Auth<FORCE_BEARER_AUTH, USE_CERT>
|
||||
{
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<const FORCE_BEARER_AUTH: bool> Into<User> for Auth<FORCE_BEARER_AUTH>{
|
||||
impl<const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> Into<User>
|
||||
for Auth<FORCE_BEARER_AUTH, USE_CERT>
|
||||
{
|
||||
fn into(self) -> User {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#[async_trait]
|
||||
impl<'r, const FORCE_BEARER_AUTH: bool> FromRequest<'r> for Auth<FORCE_BEARER_AUTH>{
|
||||
impl<'r, const FORCE_BEARER_AUTH: bool, const USE_CERT: bool> FromRequest<'r>
|
||||
for Auth<FORCE_BEARER_AUTH, USE_CERT>
|
||||
{
|
||||
type Error = Errors<'static>;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
let pool: &Pool = request.rocket().state().unwrap();
|
||||
|
||||
let auth = request_try!(request.headers().get("Authorization").next().ok_or(INVALID_TOKEN_ERRORS));
|
||||
let auth = request_try!(
|
||||
request
|
||||
.headers()
|
||||
.get("Authorization")
|
||||
.next()
|
||||
.ok_or(INVALID_TOKEN_ERRORS)
|
||||
);
|
||||
|
||||
let (auth_type, token) = request_try!(auth.split_once(' ').ok_or(INVALID_TOKEN_ERRORS));
|
||||
|
||||
let user = match auth_type{
|
||||
let user = match auth_type {
|
||||
"Basic" if !FORCE_BEARER_AUTH => read_basic_auth_token(pool, token).await,
|
||||
"Bearer" => read_bearer_auth_token(pool, token).await,
|
||||
_ => return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS)),
|
||||
|
|
@ -250,10 +268,24 @@ impl<'r, const FORCE_BEARER_AUTH: bool> FromRequest<'r> for Auth<FORCE_BEARER_AU
|
|||
return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS));
|
||||
};
|
||||
|
||||
if user.account_level < 0{
|
||||
if user.account_level < 0 {
|
||||
return Outcome::Error((Status::BadRequest, INVALID_TOKEN_ERRORS));
|
||||
}
|
||||
|
||||
|
||||
if USE_CERT {
|
||||
let cert = request_try!(
|
||||
request
|
||||
.headers()
|
||||
.get("X-Nintendo-Device-Cert")
|
||||
.next()
|
||||
.ok_or(INVALID_TOKEN_ERRORS)
|
||||
);
|
||||
|
||||
let Some(cert) = Certificate::new(&cert) else {
|
||||
return Outcome::Error((Status::BadGateway, INVALID_TOKEN_ERRORS));
|
||||
};
|
||||
}
|
||||
|
||||
// let user = User{
|
||||
// nex_password: format!("{:a>16}", user.nex_password),
|
||||
// ..user
|
||||
|
|
@ -261,4 +293,80 @@ impl<'r, const FORCE_BEARER_AUTH: bool> FromRequest<'r> for Auth<FORCE_BEARER_AU
|
|||
|
||||
Outcome::Success(Self(user))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[binread]
|
||||
#[br(big)]
|
||||
#[derive(Debug)]
|
||||
pub struct Certificate {
|
||||
issuer: [u8; 0x40],
|
||||
key_type: u32,
|
||||
cert_name: [u8; 0x40],
|
||||
key_id: u32,
|
||||
pubkey_data: [u8; 0x40],
|
||||
}
|
||||
|
||||
#[binread]
|
||||
#[br(big)]
|
||||
#[br(magic(0x10005u32))]
|
||||
struct OuterCertificate {
|
||||
signature: [u8; 0x3C],
|
||||
padding: [u8; 0x40],
|
||||
data: [u8; 0x100],
|
||||
}
|
||||
|
||||
const PUB_PEM: &[u8] = br#"-----BEGIN PUBLIC KEY-----
|
||||
MFIwEAYHKoZIzj0CAQYFK4EEABsDPgAEAP1WBBgs8XUJIQDDCK5IOZEbb5+h1TqV
|
||||
rwgzSUcrAAFxMWm1kf/TDL9z2nZkuo0N+VtNEQREZDXA7aQv
|
||||
-----END PUBLIC KEY-----"#;
|
||||
|
||||
impl Certificate {
|
||||
fn new(data: &str) -> Option<Self> {
|
||||
let data = BASE64_STANDARD.decode(data).unwrap();
|
||||
|
||||
let cert = OuterCertificate::read(&mut Cursor::new(&data)).ok()?;
|
||||
|
||||
println!("key");
|
||||
|
||||
let key = openssl::ec::EcKey::public_key_from_pem(PUB_PEM).ok()?;
|
||||
|
||||
let sig_components = read_p1363(&cert.signature)?;
|
||||
|
||||
let sig = EcdsaSig::from_private_components(sig_components.0, sig_components.1).unwrap();
|
||||
|
||||
let mut hasher = openssl::sha::Sha256::new();
|
||||
|
||||
hasher.update(&cert.data);
|
||||
|
||||
let hash = hasher.finish();
|
||||
|
||||
if !sig.verify(&hash[..], &key).ok()? {
|
||||
return None;
|
||||
}
|
||||
|
||||
Certificate::read(&mut Cursor::new(cert.data)).ok()
|
||||
}
|
||||
|
||||
pub fn hash(&self) -> [u8; 32] {
|
||||
let mut hasher = openssl::sha::Sha256::new();
|
||||
|
||||
hasher.update(&self.issuer[..]);
|
||||
hasher.update(bytes_of(&self.key_id));
|
||||
hasher.update(&self.cert_name[..]);
|
||||
hasher.update(bytes_of(&self.key_type));
|
||||
hasher.update(&self.pubkey_data[..]);
|
||||
|
||||
hasher.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fn read_p1363(data: &[u8]) -> Option<(BigNum, BigNum)> {
|
||||
if data.len() % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
let half_len = data.len() / 2;
|
||||
Some((
|
||||
BigNum::from_slice(&data[..half_len]).ok()?,
|
||||
BigNum::from_slice(&data[half_len..]).ok()?,
|
||||
))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue