#![allow(clippy::missing_safety_doc)] mod configuration; use std::{ ffi::{c_char, CStr, CString}, path::PathBuf, rc::Rc, sync::{ atomic::{AtomicBool, Ordering}, Mutex, }, }; use configuration::Configuration; use log::error; use shared_memory::Shmem; use winapi::{ shared::{ minwindef::{BOOL, DWORD, HINSTANCE, LPVOID, MAX_PATH, TRUE}, winerror::{E_FAIL, S_FALSE, S_OK}, }, um::{ winbase::{GetPrivateProfileIntA, GetPrivateProfileStringA}, winnt::{DLL_PROCESS_ATTACH, HRESULT}, winuser::{GetAsyncKeyState, VK_RETURN}, }, }; use yubideck_common::{create_shared_memory, init_logger, INPUT_SHMEM_SIZE, OUTPUT_SHMEM_SIZE}; static mut INPUT_SHMEM: Option> = None; static mut OUTPUT_SHMEM: Option>> = None; static AIME_ID_PRESENT: AtomicBool = AtomicBool::new(false); static FELICA_ID_PRESENT: AtomicBool = AtomicBool::new(false); static mut REMOTE_CARD_ID: [u8; 10] = [0u8; 10]; lazy_static::lazy_static! { static ref CONFIGURATION: Configuration = { let aime = CString::new("aime").unwrap(); let aime_path = CString::new("aimePath").unwrap(); let aime_path_default = CString::new("DEVICE\\aime.txt").unwrap(); let felica_path = CString::new("felicaPath").unwrap(); let felica_path_default = CString::new("DEVICE\\felica.txt").unwrap(); let felica_gen = CString::new("felicaGen").unwrap(); let aime_gen = CString::new("aimeGen").unwrap(); let scan = CString::new("scan").unwrap(); let cfg_file = CString::new(".\\segatools.ini").unwrap(); let mut aime_path_buf: [c_char; MAX_PATH] = [0; MAX_PATH]; let mut felica_path_buf: [c_char; MAX_PATH] = [0; MAX_PATH]; unsafe { GetPrivateProfileStringA( aime.as_ptr(), aime_path.as_ptr(), aime_path_default.as_ptr(), aime_path_buf.as_mut_ptr(), MAX_PATH as u32, cfg_file.as_ptr() ); GetPrivateProfileStringA( aime.as_ptr(), felica_path.as_ptr(), felica_path_default.as_ptr(), felica_path_buf.as_mut_ptr(), MAX_PATH as u32, cfg_file.as_ptr() ); Configuration { aime_path: CStr::from_ptr(aime_path_buf.as_ptr()).to_str().unwrap().into(), felica_path: CStr::from_ptr(felica_path_buf.as_ptr()).to_str().unwrap().into(), aime_gen: GetPrivateProfileIntA( aime.as_ptr(), aime_gen.as_ptr(), 1, cfg_file.as_ptr() ) == 1, felica_gen: GetPrivateProfileIntA( aime.as_ptr(), felica_gen.as_ptr(), 1, cfg_file.as_ptr() ) == 1, vk_scan: GetPrivateProfileIntA( aime.as_ptr(), scan.as_ptr(), VK_RETURN, cfg_file.as_ptr() ), } } }; } #[no_mangle] extern "system" fn DllMain(_dll_module: HINSTANCE, call_reason: DWORD, _reserved: LPVOID) -> BOOL { if call_reason == DLL_PROCESS_ATTACH { init_logger() } TRUE } /// Get the version of the Aime IO API that this DLL supports. This function /// should return a positive 16-bit integer, where the high byte is the major /// version and the low byte is the minor version (as defined by the Semantic /// Versioning standard). /// The latest API version as of this writing is 0x0100. #[no_mangle] pub extern "C" fn aime_io_get_api_version() -> u16 { 0x0100 } /// Initialize Aime IO provider DLL. Only called once, before any other /// functions exported from this DLL are called (except for /// aime_io_get_api_version). /// /// Minimum API version: 0x0100 #[no_mangle] pub extern "C" fn aime_io_init() -> HRESULT { match create_shared_memory("YubideckInput", INPUT_SHMEM_SIZE, false) { Ok(s) => unsafe { INPUT_SHMEM = Some(Rc::new(s)) }, Err(e) => { error!("Could not acquire shared input memory: {e:#?}"); return E_FAIL; } } match create_shared_memory("YubideckOutput", OUTPUT_SHMEM_SIZE, false) { Ok(s) => { unsafe { OUTPUT_SHMEM = Some(Rc::new(Mutex::new(s))) }; S_OK } Err(e) => { error!("Could not obtain shared output memory: {e:#?}"); E_FAIL } } } fn aime_io_read_id_file(path: &PathBuf) -> Option<[u8; N]> { let min_length = N * 2; let id_string = match std::fs::read_to_string(path) { Ok(s) => s, Err(_) => return None, }; if id_string.len() < min_length { return None; } let mut out = [0u8; N]; if faster_hex::hex_decode(id_string[0..min_length].as_bytes(), &mut out).is_err() { return None; } Some(out) } /// # IMPORTANT /// This is not how Aime access codes are created! This is literally /// just 10 random bytes! Do not use this for anything serious! fn aime_io_generate_aime(path: &PathBuf) -> Option<[u8; 10]> { let mut access_code = [0u8; 10]; loop { for nib in access_code.iter_mut() { *nib = ((rand::random::() % 10) << 4) | (rand::random::() % 10); } if access_code[0] >> 4 != 3 { break; } } let mut encoded_ac = [0u8; 20]; if let Err(e) = faster_hex::hex_encode(&access_code, &mut encoded_ac) { error!("Encoding access code failed: {e:#?}"); return None; } if let Err(e) = std::fs::write(path, encoded_ac) { error!("Could not write generated access code to file: {e:#?}"); return None; } Some(access_code) } /// # IMPORTANT /// This is not how FeliCa IDms are created! This is literally /// just 8 random bytes! Do not use this for anything serious! fn aime_io_generate_felica(path: &PathBuf) -> Option<[u8; 8]> { let mut idm = [0u8; 8]; for nib in idm.iter_mut() { *nib = rand::random::(); } // FeliCa IDm values should have a 0 in their high nibble. I think. idm[0] &= 0x0F; let mut encoded_idm = [0u8; 16]; if let Err(e) = faster_hex::hex_encode(&idm, &mut encoded_idm) { error!("Encoding Felica IDm failed: {e:#?}"); return None; } if let Err(e) = std::fs::write(path, encoded_idm) { error!("Could not write generated FeliCa IDm to file: {e:#?}"); return None; } Some(idm) } /// Poll for IC cards in the vicinity. /// /// - unit_no: Always 0 as of the current API version /// /// Minimum API version: 0x0100 #[no_mangle] pub extern "C" fn aime_io_nfc_poll(unit_no: u8) -> HRESULT { if unit_no != 0 { return S_OK; } let Some(input_shmem) = (unsafe { &INPUT_SHMEM }) else { return S_OK; }; let input = unsafe { input_shmem.as_slice() }; AIME_ID_PRESENT.store(false, Ordering::Relaxed); FELICA_ID_PRESENT.store(false, Ordering::Relaxed); if input[34] != 0 { match input[34] { 1 => AIME_ID_PRESENT.store(true, Ordering::Relaxed), 2 => FELICA_ID_PRESENT.store(true, Ordering::Relaxed), _ => {} } unsafe { REMOTE_CARD_ID.copy_from_slice(&input[35..45]); } return S_OK; } if unsafe { GetAsyncKeyState(CONFIGURATION.vk_scan as i32) } == 0 { return S_OK; } if let Some(aime_id) = aime_io_read_id_file::<10>(&CONFIGURATION.aime_path) { AIME_ID_PRESENT.store(true, Ordering::Relaxed); unsafe { REMOTE_CARD_ID.copy_from_slice(&aime_id) } return S_OK; } if CONFIGURATION.aime_gen { if let Some(aime_id) = aime_io_generate_aime(&CONFIGURATION.aime_path) { AIME_ID_PRESENT.store(true, Ordering::Relaxed); unsafe { REMOTE_CARD_ID.copy_from_slice(&aime_id) } return S_OK; } return E_FAIL; } if let Some(felica_id) = aime_io_read_id_file::<8>(&CONFIGURATION.felica_path) { FELICA_ID_PRESENT.store(true, Ordering::Relaxed); unsafe { REMOTE_CARD_ID[..8].copy_from_slice(&felica_id); } return S_OK; } if CONFIGURATION.felica_gen { if let Some(felica_id) = aime_io_generate_felica(&CONFIGURATION.felica_path) { FELICA_ID_PRESENT.store(true, Ordering::Relaxed); unsafe { REMOTE_CARD_ID[..8].copy_from_slice(&felica_id) } return S_OK; } return E_FAIL; } S_OK } /// Attempt to read out a classic Aime card ID /// /// - unit_no: Always 0 as of the current API version /// - luid: Pointer to a ten-byte buffer that will receive the ID /// - luid_size: Size of the buffer at *luid. Always 10. /// /// Returns: /// /// - S_OK if a classic Aime is present and was read successfully /// - S_FALSE if no classic Aime card is present (*luid will be ignored) /// - Any HRESULT error if an error occured. /// /// Minimum API version: 0x0100 #[no_mangle] pub unsafe extern "C" fn aime_io_nfc_get_aime_id( unit_no: u8, access_code: *mut u8, access_code_size: usize, ) -> HRESULT { if access_code.is_null() || access_code_size != 10 { return S_FALSE; } if unit_no != 0 || !AIME_ID_PRESENT.load(Ordering::Relaxed) { return S_FALSE; } let ac_slice = std::slice::from_raw_parts_mut(access_code, access_code_size); ac_slice.copy_from_slice(&REMOTE_CARD_ID); S_OK } /// Attempt to read out a FeliCa card ID ("IDm"). The following are examples /// of FeliCa cards: /// /// - Amuse IC (which includes new-style Aime-branded cards, among others) /// - Smartphones with FeliCa NFC capability (uncommon outside Japan) /// - Various Japanese e-cash cards and train passes /// /// Parameters: /// /// - unit_no: Always 0 as of the current API version /// - IDm: Output parameter that will receive the card ID /// /// Returns: /// /// - S_OK if a FeliCa device is present and was read successfully /// - S_FALSE if no FeliCa device is present (*IDm will be ignored) /// - Any HRESULT error if an error occured. /// /// Minimum API version: 0x0100 #[no_mangle] pub unsafe extern "C" fn aime_io_nfc_get_felica_id(unit_no: u8, idm: *mut u64) -> HRESULT { if idm.is_null() { return S_FALSE; } if unit_no != 0 || !FELICA_ID_PRESENT.load(Ordering::Relaxed) { return S_FALSE; } *idm = 0; for nib in REMOTE_CARD_ID.iter().take(8) { *idm = (*idm << 8) | (*nib as u64); } S_OK } /// Change the color and brightness of the card reader's RGB lighting /// /// - unit_no: Always 0 as of the current API version /// - r, g, b: Primary color intensity, from 0 to 255 inclusive. /// /// Minimum API version: 0x0100 #[no_mangle] pub extern "C" fn aime_io_led_set_color(unit_no: u8, r: u8, g: u8, b: u8) { if unit_no != 0 { return; } let Some(output_shmem) = (unsafe { &OUTPUT_SHMEM }) else { return; }; let Ok(mut output_shmem) = output_shmem.lock() else { error!("Could not acquire mutex of output shared memory"); return; }; let buf = unsafe { output_shmem.as_slice_mut() }; buf[101] = r; buf[102] = g; buf[103] = b; buf[122] = 1; }