Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5aca92a2cc | ||
|
|
7471885119 | ||
|
|
5038fb0c28 | ||
|
|
ff9f493460 | ||
|
|
89e82927ee | ||
|
|
651d9fa495 | ||
|
|
3141100457 | ||
|
|
6b4e81c935 | ||
|
|
e243d5b64e | ||
|
|
252cd0cee6 | ||
|
|
14abcc50b5 | ||
|
|
134aadad5a | ||
|
|
a56bc9e4fa | ||
|
|
5bd466a9b6 | ||
|
|
16c58512ec | ||
|
|
b1b0b13f2a | ||
|
|
03e77faf06 | ||
|
|
7e381924ab | ||
|
|
5763610e54 | ||
|
|
49956a3f84 | ||
|
|
b2915a8142 | ||
|
|
e002aa9ec2 | ||
|
|
0aaf460dbf |
2
.gitignore
vendored
@@ -22,3 +22,5 @@ libs/tweeny
|
||||
|
||||
compile_commands.json
|
||||
out
|
||||
|
||||
usb_test/
|
||||
|
||||
42
README.md
@@ -1,12 +1,12 @@
|
||||
# sphaira
|
||||
# Sphaira
|
||||
|
||||
A homebrew menu for the switch.
|
||||
A homebrew menu for the Nintendo Switch.
|
||||
|
||||
[See the gbatemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
|
||||
[See the GBATemp thread for more details / discussion](https://gbatemp.net/threads/sphaira-hbmenu-replacement.664523/).
|
||||
|
||||
[We have now have a Discord server!](https://discord.gg/8vZBsrprEc). Please use the issues tab to report bugs, as it is much easier for me to track.
|
||||
|
||||
## showcase
|
||||
## Showcase
|
||||
|
||||
| | |
|
||||
:-------------------------:|:-------------------------:
|
||||
@@ -15,29 +15,29 @@ A homebrew menu for the switch.
|
||||
 | 
|
||||
 | 
|
||||
|
||||
## bug reports
|
||||
## Bug reports
|
||||
|
||||
for any bug reports, please use the issues tab and explain in as much detail as possible!
|
||||
For any bug reports, please use the issues tab and explain in as much detail as possible!
|
||||
|
||||
please include:
|
||||
Please include:
|
||||
|
||||
- CFW type (i assume Atmosphere, but someone out there is still using Rajnx)
|
||||
- CFW version
|
||||
- FW version
|
||||
- The bug itself and how to reproduce it
|
||||
- CFW type (i assume Atmosphere, but someone out there is still using Rajnx);
|
||||
- CFW version;
|
||||
- FW version;
|
||||
- The bug itself and how to reproduce it.
|
||||
|
||||
## ftp
|
||||
## FTP
|
||||
|
||||
ftp can be enabled via the network menu. It uses the same config as ftpsrv `/config/ftpsrv/config.ini`. [See here for the full list
|
||||
FTP can be enabled via the network menu. It uses the same config as ftpsrv `/config/ftpsrv/config.ini`. [See here for the full list
|
||||
of all configs available](https://github.com/ITotalJustice/ftpsrv/blob/master/assets/config.ini.template).
|
||||
|
||||
## mtp
|
||||
## MTP
|
||||
|
||||
mtp can be enabled via the network menu.
|
||||
MTP can be enabled via the Network menu.
|
||||
|
||||
## file assoc
|
||||
## File association
|
||||
|
||||
sphaira has file assoc support. lets say your app supports loading .png files, then you could write an assoc file, then when using the file browser, clicking on a .png file will launch your app along with the .png file as argv[1]. This was primarly added for rom loading support for emulators / frontends such as retroarch, melonds, mgba etc.
|
||||
Sphaira has file association support. Let's say your app supports loading .png files, then you could write an association file, then when using the file browser, clicking on a .png file will launch your app along with the .png file as argv[1]. This was primarly added for rom loading support for emulators / frontends such as RetroArch, MelonDS, mGBA etc.
|
||||
|
||||
```ini
|
||||
[config]
|
||||
@@ -45,9 +45,9 @@ path=/switch/your_app.nro
|
||||
supported_extensions=jpg|png|mp4|mp3
|
||||
```
|
||||
|
||||
the `path` field is optional. if left out, it will use the name of the ini to find the nro. For example, if the ini is called mgba.ini, it will try to find the nro in /switch/mgba.nro and /switch/folder/mgba.nro.
|
||||
The `path` field is optional. If left out, it will use the name of the ini to find the nro. For example, if the ini is called mgba.ini, it will try to find the nro in /switch/mgba.nro and /switch/folder/mgba.nro.
|
||||
|
||||
see `assets/romfs/assoc/` for more examples of file assoc entries
|
||||
See `assets/romfs/assoc/` for more examples of file assoc entries.
|
||||
|
||||
## Credits
|
||||
|
||||
@@ -59,7 +59,7 @@ see `assets/romfs/assoc/` for more examples of file assoc entries
|
||||
- deko3d-nanovg
|
||||
- libpulsar
|
||||
- minIni
|
||||
- gbatemp
|
||||
- GBATemp
|
||||
- hb-appstore
|
||||
- haze
|
||||
- everyone who has contributed to this project!
|
||||
- Everyone who has contributed to this project!
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "Theme auswählen",
|
||||
"Shuffle": "Zufällig",
|
||||
"Music": "Musik",
|
||||
"12 Hour Time": "",
|
||||
"Network": "Netzwerk",
|
||||
"Network Options": "Netzwerk-Optionen",
|
||||
"Ftp": "FTP",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "Vollständig entfernen ",
|
||||
"Are you sure you want to delete ": "Wirklich löschen ",
|
||||
"Are you sure you wish to cancel?": "Wirklich abbrechen?",
|
||||
"Audio disabled due to suspended game": "",
|
||||
"If this message appears repeatedly, please open an issue.": "Bei wiederholtem Auftreten bitte Issue erstellen."
|
||||
}
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "Select Theme",
|
||||
"Shuffle": "Shuffle",
|
||||
"Music": "Music",
|
||||
"12 Hour Time": "12 Hour Time",
|
||||
"Network": "Network",
|
||||
"Network Options": "Network Options",
|
||||
"Ftp": "FTP",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "Completely remove ",
|
||||
"Are you sure you want to delete ": "Are you sure you want to delete ",
|
||||
"Are you sure you wish to cancel?": "Are you sure you wish to cancel?",
|
||||
"Audio disabled due to suspended game": "Audio disabled due to suspended game",
|
||||
"If this message appears repeatedly, please open an issue.": "If this message appears repeatedly, please open an issue."
|
||||
}
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "Seleccionar tema",
|
||||
"Shuffle": "Barajar",
|
||||
"Music": "Música",
|
||||
"12 Hour Time": "",
|
||||
"Network": "Red",
|
||||
"Network Options": "Opciones de red",
|
||||
"Ftp": "FTP",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "Eliminar completamente",
|
||||
"Are you sure you want to delete ": "¿Estás seguro que quieres eliminar? ",
|
||||
"Are you sure you wish to cancel?": "¿Estás seguro que deseas cancelar?",
|
||||
"Audio disabled due to suspended game": "",
|
||||
"If this message appears repeatedly, please open an issue.": "Si este mensaje aparece repetidamente, por favor abrir un 'issue'."
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "Choisir un Thème",
|
||||
"Shuffle": "Aléatoire",
|
||||
"Music": "Musique",
|
||||
"12 Hour Time": "Temps sur 12 heures",
|
||||
"Network": "Réseau",
|
||||
"Network Options": "Options Réseau",
|
||||
"Ftp": "FTP",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "Supprimer totalement ",
|
||||
"Are you sure you want to delete ": "Êtes-vous sûr de vouloir supprimer ",
|
||||
"Are you sure you wish to cancel?": "Souhaitez-vous vraiment annuler?",
|
||||
"Audio disabled due to suspended game": "Audio désactivé à cause d'un jeu suspendu",
|
||||
"If this message appears repeatedly, please open an issue.": "Si ce message apparait en boucle veuillez ouvrir une issue."
|
||||
}
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "Seleziona tema",
|
||||
"Shuffle": "Mescola",
|
||||
"Music": "Musica",
|
||||
"12 Hour Time": "",
|
||||
"Network": "Rete",
|
||||
"Network Options": "Opzioni di rete",
|
||||
"Ftp": "FTP",
|
||||
@@ -203,53 +204,54 @@
|
||||
"Load Default": "Carica predefinito",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "",
|
||||
"Nsfw": "",
|
||||
"Page": "",
|
||||
"Page %zu / %zu": "Page %zu / %zu",
|
||||
"Enter Page Number": "",
|
||||
"Bad Page": "",
|
||||
"Download theme?": "",
|
||||
"Themezer Options": "Impostazioni Themezer",
|
||||
"Nsfw": "NSFW",
|
||||
"Page": "Pagina",
|
||||
"Page %zu / %zu": "Pagina %zu / %zu",
|
||||
"Enter Page Number": "Inserisci il numero della pagina",
|
||||
"Bad Page": "Pagina invalida",
|
||||
"Download theme?": "Vuoi scaricare il tema?",
|
||||
|
||||
"GitHub": "",
|
||||
"Downloading json": "",
|
||||
"Select asset to download for ": "",
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Scaricamento json",
|
||||
"Select asset to download for": "Scegli l'asset da scaricare per",
|
||||
|
||||
"Installing ": "",
|
||||
"Uninstalling ": "",
|
||||
"Deleting ": "",
|
||||
"Deleting": "",
|
||||
"Pasting ": "",
|
||||
"Pasting": "",
|
||||
"Removing ": "",
|
||||
"Scanning ": "",
|
||||
"Creating ": "",
|
||||
"Copying ": "",
|
||||
"Trying to load ": "",
|
||||
"Downloading ": "",
|
||||
"Downloaded ": "",
|
||||
"Removed ": "",
|
||||
"Checking MD5": "",
|
||||
"Loading...": "",
|
||||
"Loading": "",
|
||||
"Empty!": "",
|
||||
"Not Ready...": "",
|
||||
"Error loading page!": "",
|
||||
"Update avaliable: ": "",
|
||||
"Download update: ": "",
|
||||
"Updated to ": "",
|
||||
"Press OK to restart Sphaira": "",
|
||||
"Restart Sphaira?": "",
|
||||
"Failed to download update": "",
|
||||
"Restore hbmenu?": "",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "",
|
||||
"Failed to restore hbmenu, using sphaira instead": "",
|
||||
"Restored hbmenu, closing sphaira": "",
|
||||
"Restored hbmenu": "",
|
||||
"Delete Selected files?": "",
|
||||
"Completely remove ": "",
|
||||
"Installing ": "Installazione",
|
||||
"Uninstalling ": "Disinstallazione",
|
||||
"Deleting ": "Eliminazione",
|
||||
"Deleting": "Eliminazione",
|
||||
"Pasting ": "Incollo",
|
||||
"Pasting": "Incollo",
|
||||
"Removing ": "Rimozione",
|
||||
"Scanning ": "Scan",
|
||||
"Creating ": "Creazione",
|
||||
"Copying ": "Copio",
|
||||
"Trying to load ": "Cercando di caricare",
|
||||
"Downloading ": "Scaricando",
|
||||
"Downloaded ": "Scaricato",
|
||||
"Removed ": ""Rimosso,
|
||||
"Checking MD5": "Controllo MD5",
|
||||
"Loading...": "Caricamento...",
|
||||
"Loading": "Caricamento",
|
||||
"Empty!": "Vuoto!",
|
||||
"Not Ready...": "Non pronto...",
|
||||
"Error loading page!": "Errore nel caricare la pagina!",
|
||||
"Update avaliable: ": "Aggiornamento disponibile",
|
||||
"Download update: ": "Scarica aggiornamento",
|
||||
"Updated to ": "Aggiornato a",
|
||||
"Press OK to restart Sphaira": "Premi OK per riavviare Sphaira",
|
||||
"Restart Sphaira?": "Vuoi riavviare Sphaira?",
|
||||
"Failed to download update": "Download aggiornamento fallito",
|
||||
"Restore hbmenu?": "Vuoi ripristinare hbmenu?",
|
||||
"Failed to find /switch/hbmenu.nro\nUse the Appstore to re-install hbmenu": "Impossibile trovare /switch/hbmenu.nro\nUsa l'Appstore per reinstallare hbmenu",
|
||||
"Failed to restore hbmenu, please re-download hbmenu": "Impossibile ripristinare hbmenu, per favore riscaricalo",
|
||||
"Failed to restore hbmenu, using sphaira instead": "Impossibile ripristinare hbmenu, uso Sphaira invece",
|
||||
"Restored hbmenu, closing sphaira": "hbmenu ripristinato, chiudo Sphaira",
|
||||
"Restored hbmenu": "hbmenu ripristinato",
|
||||
"Delete Selected files?": "Vuoi rimuovere i file selezionati?",
|
||||
"Completely remove ": "Elimina definitivamente",
|
||||
"Are you sure you want to delete ": "Sei sicuro di voler eliminare? ",
|
||||
"Are you sure you wish to cancel?": "",
|
||||
"If this message appears repeatedly, please open an issue.": ""
|
||||
}
|
||||
"Are you sure you wish to cancel?": "Sei sicuro di voler annullare?",
|
||||
"Audio disabled due to suspended game": "Audio disabilitato poichè un app è in pausa",
|
||||
"If this message appears repeatedly, please open an issue.": "Se questo messaggio appare frequentemente, segnala il bug."
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "テーマを選ぶ",
|
||||
"Shuffle": "シャッフル",
|
||||
"Music": "BGM",
|
||||
"12 Hour Time": "",
|
||||
"Network": "ネットワーク",
|
||||
"Network Options": "ネットワーク設定",
|
||||
"Ftp": "FTP",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "除去しますか ",
|
||||
"Are you sure you want to delete ": "消去してもよろしいですか ",
|
||||
"Are you sure you wish to cancel?": "本当に取り消しますか?",
|
||||
"Audio disabled due to suspended game": "ゲームが一時停止状態の場合、オーディオは無効になります",
|
||||
"If this message appears repeatedly, please open an issue.": "このメッセージが繰り返し表示される場合は、問題を開いてください。"
|
||||
}
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "테마 선택",
|
||||
"Shuffle": "셔플",
|
||||
"Music": "BGM",
|
||||
"12 Hour Time": "",
|
||||
"Network": "네트워크",
|
||||
"Network Options": "네트워크 옵션",
|
||||
"Ftp": "FTP (무선)",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "정말 삭제할까요 ",
|
||||
"Are you sure you want to delete ": "정말 삭제할까요 ",
|
||||
"Are you sure you wish to cancel?": "정말 취소할까요?",
|
||||
"Audio disabled due to suspended game": "게임 실행 중에는 BGM이 비활성화 됩니다",
|
||||
"If this message appears repeatedly, please open an issue.": "해당 메시지가 반복해서 나타나는 경우, 이슈를 등록하세요."
|
||||
}
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "Selecteer Thema",
|
||||
"Shuffle": "Schudden",
|
||||
"Music": "Muziek",
|
||||
"12 Hour Time": "",
|
||||
"Network": "Netwerk",
|
||||
"Network Options": "Netwerkopties",
|
||||
"Ftp": "FTP",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "",
|
||||
"Are you sure you want to delete ": "Weet u zeker dat u wilt verwijderen ",
|
||||
"Are you sure you wish to cancel?": "",
|
||||
"Audio disabled due to suspended game": "",
|
||||
"If this message appears repeatedly, please open an issue.": ""
|
||||
}
|
||||
@@ -66,10 +66,11 @@
|
||||
"Select Theme": "Tema atual",
|
||||
"Shuffle": "Embaralhar temas",
|
||||
"Music": "Música",
|
||||
"12 Hour Time": "",
|
||||
"Network": "Rede",
|
||||
"Network Options": "Opções de rede",
|
||||
"Ftp": "FTP",
|
||||
"Mtp": "MTP",
|
||||
"Ftp": "Servidor FTP",
|
||||
"Mtp": "Escuta MTP",
|
||||
"Nxlink": "Nxlink",
|
||||
"Nxlink Connected": "Nxlink conectado",
|
||||
"Nxlink Upload": "Envio Nxlink",
|
||||
@@ -96,7 +97,7 @@
|
||||
"Misc": "Diversos",
|
||||
"Misc Options": "Opções diversas",
|
||||
"Web": "Navegador de internet",
|
||||
"Install forwarders": "Instalar forwarders",
|
||||
"Install forwarders": "Instalar atalhos (forwarders)",
|
||||
"Install location": "Local de instalação",
|
||||
"Show install warning": "Mostrar aviso de instalação",
|
||||
"Text scroll speed": "Rolagem do texto",
|
||||
@@ -133,8 +134,8 @@
|
||||
"Homebrew": "Aplicativos",
|
||||
"Homebrew Options": "Opções do aplicativo",
|
||||
"Hide Sphaira": "Esconder sphaira",
|
||||
"Install Forwarder": "Instalar forwarder",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar forwarders pode\nresultar em um banimento!",
|
||||
"Install Forwarder": "Instalar atalho (forwarder)",
|
||||
"WARNING: Installing forwarders will lead to a ban!": "AVISO: Instalar atalhos pode\nresultar em um banimento!",
|
||||
"Installing Forwarder": "Instalando forwarder",
|
||||
"Creating Program": "Criando Program",
|
||||
"Creating Control": "Criando Control",
|
||||
@@ -164,7 +165,7 @@
|
||||
"More by Author": "Mais deste autor",
|
||||
"Leave Feedback": "Deixar um feedback",
|
||||
|
||||
"Irs": "IRS",
|
||||
"Irs": "Sensor infravermelho",
|
||||
"Ambient Noise Level: ": "Nível de ruído ambiente: ",
|
||||
"Controller": "Controle",
|
||||
"Pad ": "Pad ",
|
||||
@@ -179,7 +180,7 @@
|
||||
"270 (Upside down)": "270 (de cabeça para baixo)",
|
||||
"Colour": "Cor",
|
||||
"Grey": "Cinza",
|
||||
"Ironbow": "Arco de ferro",
|
||||
"Ironbow": "Ferro",
|
||||
"Green": "Verde",
|
||||
"Red": "Vermelho",
|
||||
"Blue": "Azul",
|
||||
@@ -198,9 +199,9 @@
|
||||
"80x60": "80×60",
|
||||
"40x30": "40×30",
|
||||
"20x15": "20×15",
|
||||
"Trimming Format": "Formato de corte",
|
||||
"Trimming Format": "Formato do recorte",
|
||||
"External Light Filter": "Filtro de luz externa",
|
||||
"Load Default": "Carregar padrão",
|
||||
"Load Default": "Restaurar padrão",
|
||||
|
||||
"Themezer": "Themezer",
|
||||
"Themezer Options": "Opções do Themezer",
|
||||
@@ -213,7 +214,7 @@
|
||||
|
||||
"GitHub": "GitHub",
|
||||
"Downloading json": "Baixando JSON",
|
||||
"Select asset to download for ": "Selecione o recurso para baixar em ",
|
||||
"Select asset to download for ": "Selecione o recurso para baixar de ",
|
||||
|
||||
"Installing ": "Instalando ",
|
||||
"Uninstalling ": "Desinstalando ",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "Remover completamente ",
|
||||
"Are you sure you want to delete ": "Você tem certeza que quer excluir ",
|
||||
"Are you sure you wish to cancel?": "Você tem certeza que quer cancelar?",
|
||||
"Audio disabled due to suspended game": "Áudio desativado devido ao software suspenso",
|
||||
"If this message appears repeatedly, please open an issue.": "Se esta mensagem aparecer repetidamente, abra um issue."
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "Выберите тему",
|
||||
"Shuffle": "Перетасовать",
|
||||
"Music": "Музыка",
|
||||
"12 Hour Time": "",
|
||||
"Network": "Сеть",
|
||||
"Network Options": "Параметры сети",
|
||||
"Ftp": "FTP",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "",
|
||||
"Are you sure you want to delete ": "Вы уверены, что хотите удалить ",
|
||||
"Are you sure you wish to cancel?": "",
|
||||
"Audio disabled due to suspended game": "",
|
||||
"If this message appears repeatedly, please open an issue.": ""
|
||||
}
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "Välj tema",
|
||||
"Shuffle": "Blanda",
|
||||
"Music": "Musik",
|
||||
"12 Hour Time": "",
|
||||
"Network": "Nätverk",
|
||||
"Network Options": "Nätverksalternativ",
|
||||
"Ftp": "FTP",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "Ta bort helt ",
|
||||
"Are you sure you want to delete ": "Är du säker på att du vill radera ",
|
||||
"Are you sure you wish to cancel?": "Är du säker på att du vill avbryta?",
|
||||
"Audio disabled due to suspended game": "Ljud är avstängt på grund av bakgrundsprogram",
|
||||
"If this message appears repeatedly, please open an issue.": "Om detta meddelande visas upprepade gånger, vänligen öppna en felanmälan.",
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "Chọn Theme",
|
||||
"Shuffle": "Trộn",
|
||||
"Music": "Âm nhạc",
|
||||
"12 Hour Time": "",
|
||||
"Network": "Mạng",
|
||||
"Network Options": "Tuỳ chọn mạng",
|
||||
"Ftp": "FTP",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "Đã gỡ thành công ",
|
||||
"Are you sure you want to delete ": "Bạn có muốn xoá ",
|
||||
"Are you sure you wish to cancel?": "Bạn có chắn muốn huỷ không?",
|
||||
"Audio disabled due to suspended game": "",
|
||||
"If this message appears repeatedly, please open an issue.": "Nếu thấy tin nhắn này, hãy báo lỗi."
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,9 @@
|
||||
"Sd": "SD卡",
|
||||
"Image System memory": "主机内存图像",
|
||||
"Image microSD card": "SD卡图像",
|
||||
"Slow": "",
|
||||
"Normal": "",
|
||||
"Fast": "",
|
||||
"Slow": "慢",
|
||||
"Normal": "正常",
|
||||
"Fast": "快",
|
||||
"Yes": "是",
|
||||
"No": "否",
|
||||
"Enabled": "启用",
|
||||
@@ -66,6 +66,7 @@
|
||||
"Select Theme": "选择主题",
|
||||
"Shuffle": "随机播放",
|
||||
"Music": "音乐",
|
||||
"12 Hour Time": "",
|
||||
"Network": "网络",
|
||||
"Network Options": "网络选项",
|
||||
"Ftp": "FTP",
|
||||
@@ -99,7 +100,7 @@
|
||||
"Install forwarders": "允许安装前端应用",
|
||||
"Install location": "安装位置",
|
||||
"Show install warning": "显示安装警告",
|
||||
"Text scroll speed": "",
|
||||
"Text scroll speed": "文本滚动速度",
|
||||
|
||||
"FileBrowser": "文件浏览",
|
||||
"%zd files": "%zd 个文件",
|
||||
@@ -251,5 +252,6 @@
|
||||
"Completely remove ": "彻底删除 ",
|
||||
"Are you sure you want to delete ": "您确定要删除吗 ",
|
||||
"Are you sure you wish to cancel?": "您确定要取消吗?",
|
||||
"If this message appears repeatedly, please open an issue.": "如果此消息反复出现,请提交一个 issue。"
|
||||
"Audio disabled due to suspended game": "由于游戏暂停,音频已禁用",
|
||||
"If this message appears repeatedly, please open an issue.": "若此消息反复出现,请提交问题报告。"
|
||||
}
|
||||
|
||||
BIN
assets/romfs/theme/icons-sp/icon_SP_audio.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_file.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_folder.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_image.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_nro.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_video.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
assets/romfs/theme/icons-sp/icon_SP_zip.png
Normal file
|
After Width: | Height: | Size: 5.4 KiB |
15
assets/romfs/themes/black_theme_sp_icons.ini
Normal file
@@ -0,0 +1,15 @@
|
||||
[meta]
|
||||
name=Black alt-icons-SP
|
||||
author=spkatsi
|
||||
version=1.0.0
|
||||
inherit=romfs:/themes/base_black_theme.ini
|
||||
|
||||
[theme]
|
||||
icon_audio = romfs:/theme/icons-sp/icon_SP_audio.png
|
||||
icon_video = romfs:/theme/icons-sp/icon_SP_video.png
|
||||
icon_image = romfs:/theme/icons-sp/icon_SP_image.png
|
||||
icon_file = romfs:/theme/icons-sp/icon_SP_file.png
|
||||
icon_folder = romfs:/theme/icons-sp/icon_SP_folder.png
|
||||
icon_zip = romfs:/theme/icons-sp/icon_SP_zip.png
|
||||
icon_nro = romfs:/theme/icons-sp/icon_SP_nro.png
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
set(sphaira_VERSION 0.6.0)
|
||||
set(sphaira_VERSION 0.7.0)
|
||||
|
||||
project(sphaira
|
||||
VERSION ${sphaira_VERSION}
|
||||
@@ -46,6 +46,7 @@ add_executable(sphaira
|
||||
source/ui/menus/menu_base.cpp
|
||||
source/ui/menus/themezer.cpp
|
||||
source/ui/menus/ghdl.cpp
|
||||
source/ui/menus/usb_menu.cpp
|
||||
|
||||
source/ui/error_box.cpp
|
||||
source/ui/notification.cpp
|
||||
@@ -74,6 +75,20 @@ add_executable(sphaira
|
||||
source/web.cpp
|
||||
source/i18n.cpp
|
||||
source/ftpsrv_helper.cpp
|
||||
|
||||
source/yati/yati.cpp
|
||||
source/yati/container/nsp.cpp
|
||||
source/yati/container/xci.cpp
|
||||
source/yati/source/file.cpp
|
||||
source/yati/source/stdio.cpp
|
||||
source/yati/source/usb.cpp
|
||||
|
||||
source/yati/nx/es.cpp
|
||||
source/yati/nx/keys.cpp
|
||||
source/yati/nx/nca.cpp
|
||||
source/yati/nx/ncm.cpp
|
||||
source/yati/nx/ns.cpp
|
||||
source/yati/nx/nxdumptool_rsa.c
|
||||
)
|
||||
|
||||
target_compile_definitions(sphaira PRIVATE
|
||||
@@ -161,6 +176,24 @@ FetchContent_Declare(minIni
|
||||
GIT_TAG 11cac8b
|
||||
)
|
||||
|
||||
FetchContent_Declare(zstd
|
||||
GIT_REPOSITORY https://github.com/facebook/zstd.git
|
||||
GIT_TAG v1.5.7
|
||||
SOURCE_SUBDIR build/cmake
|
||||
)
|
||||
|
||||
set(USE_NEW_ZSTD ON)
|
||||
|
||||
set(ZSTD_BUILD_STATIC ON)
|
||||
set(ZSTD_BUILD_SHARED OFF)
|
||||
set(ZSTD_BUILD_COMPRESSION OFF)
|
||||
set(ZSTD_BUILD_DECOMPRESSION ON)
|
||||
set(ZSTD_BUILD_DICTBUILDER OFF)
|
||||
set(ZSTD_LEGACY_SUPPORT OFF)
|
||||
set(ZSTD_MULTITHREAD_SUPPORT OFF)
|
||||
set(ZSTD_BUILD_PROGRAMS OFF)
|
||||
set(ZSTD_BUILD_TESTS OFF)
|
||||
|
||||
set(MININI_LIB_NAME minIni)
|
||||
set(MININI_USE_STDIO ON)
|
||||
set(MININI_USE_NX ON)
|
||||
@@ -195,6 +228,7 @@ FetchContent_MakeAvailable(
|
||||
stb
|
||||
minIni
|
||||
yyjson
|
||||
zstd
|
||||
)
|
||||
|
||||
set(FTPSRV_LIB_BUILD TRUE)
|
||||
@@ -274,6 +308,11 @@ find_package(CURL REQUIRED)
|
||||
find_path(mbedtls_inc mbedtls REQUIRED)
|
||||
find_library(mbedcrypto_lib mbedcrypto REQUIRED)
|
||||
|
||||
if (NOT USE_NEW_ZSTD)
|
||||
find_path(zstd_inc zstd.h REQUIRED)
|
||||
find_library(zstd_lib zstd REQUIRED)
|
||||
endif()
|
||||
|
||||
set_target_properties(sphaira PROPERTIES
|
||||
C_STANDARD 11
|
||||
C_EXTENSIONS ON
|
||||
@@ -296,6 +335,15 @@ target_link_libraries(sphaira PRIVATE
|
||||
${mbedcrypto_lib}
|
||||
)
|
||||
|
||||
if (USE_NEW_ZSTD)
|
||||
message(STATUS "USING UPSTREAM ZSTD")
|
||||
target_link_libraries(sphaira PRIVATE libzstd_static)
|
||||
else()
|
||||
message(STATUS "USING LOCAL ZSTD")
|
||||
target_link_libraries(sphaira PRIVATE ${zstd_lib})
|
||||
target_include_directories(sphaira PRIVATE ${zstd_inc})
|
||||
endif()
|
||||
|
||||
target_include_directories(sphaira PRIVATE
|
||||
include
|
||||
${minizip_inc}
|
||||
@@ -330,7 +378,7 @@ nx_generate_nacp(
|
||||
OUTPUT sphaira.nacp
|
||||
NAME ${CMAKE_PROJECT_NAME}
|
||||
AUTHOR TotalJustice
|
||||
VERSION ${CMAKE_PROJECT_VERSION}
|
||||
VERSION ${sphaira_VERSION}
|
||||
)
|
||||
|
||||
# create nro
|
||||
|
||||
@@ -44,6 +44,8 @@ public:
|
||||
~App();
|
||||
void Loop();
|
||||
|
||||
static App* GetApp();
|
||||
|
||||
static void Exit();
|
||||
static void ExitRestart();
|
||||
static auto GetVg() -> NVGcontext*;
|
||||
@@ -77,8 +79,8 @@ public:
|
||||
static auto GetInstallEnable() -> bool;
|
||||
static auto GetInstallSdEnable() -> bool;
|
||||
static auto GetInstallPrompt() -> bool;
|
||||
static auto GetThemeShuffleEnable() -> bool;
|
||||
static auto GetThemeMusicEnable() -> bool;
|
||||
static auto Get12HourTimeEnable() -> bool;
|
||||
static auto GetLanguage() -> long;
|
||||
static auto GetTextScrollSpeed() -> long;
|
||||
|
||||
@@ -90,8 +92,8 @@ public:
|
||||
static void SetInstallEnable(bool enable);
|
||||
static void SetInstallSdEnable(bool enable);
|
||||
static void SetInstallPrompt(bool enable);
|
||||
static void SetThemeShuffleEnable(bool enable);
|
||||
static void SetThemeMusicEnable(bool enable);
|
||||
static void Set12HourTimeEnable(bool enable);
|
||||
static void SetLanguage(long index);
|
||||
static void SetTextScrollSpeed(long index);
|
||||
|
||||
@@ -119,6 +121,21 @@ public:
|
||||
return type == AppletType_Application || type == AppletType_SystemApplication;
|
||||
}
|
||||
|
||||
static auto IsApplet() -> bool {
|
||||
return !IsApplication();
|
||||
}
|
||||
|
||||
// returns true if launched in applet mode with a title suspended in the background.
|
||||
static auto IsAppletWithSuspendedApp() -> bool {
|
||||
R_UNLESS(IsApplet(), false);
|
||||
R_TRY_RESULT(pmdmntInitialize(), false);
|
||||
ON_SCOPE_EXIT(pmdmntExit());
|
||||
|
||||
u64 pid;
|
||||
return R_SUCCEEDED(pmdmntGetApplicationProcessId(&pid));
|
||||
}
|
||||
|
||||
|
||||
// private:
|
||||
static constexpr inline auto CONFIG_PATH = "/config/sphaira/config.ini";
|
||||
static constexpr inline auto PLAYLOG_PATH = "/config/sphaira/playlog.ini";
|
||||
@@ -157,12 +174,31 @@ public:
|
||||
option::OptionBool m_ftp_enabled{INI_SECTION, "ftp_enabled", false};
|
||||
option::OptionBool m_log_enabled{INI_SECTION, "log_enabled", false};
|
||||
option::OptionBool m_replace_hbmenu{INI_SECTION, "replace_hbmenu", false};
|
||||
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
|
||||
option::OptionBool m_12hour_time{INI_SECTION, "12hour_time", false};
|
||||
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
|
||||
|
||||
// install options
|
||||
option::OptionBool m_install{INI_SECTION, "install", false};
|
||||
option::OptionBool m_install_sd{INI_SECTION, "install_sd", true};
|
||||
option::OptionLong m_install_prompt{INI_SECTION, "install_prompt", true};
|
||||
option::OptionBool m_theme_shuffle{INI_SECTION, "theme_shuffle", false};
|
||||
option::OptionBool m_theme_music{INI_SECTION, "theme_music", true};
|
||||
option::OptionLong m_language{INI_SECTION, "language", 0}; // auto
|
||||
option::OptionBool m_allow_downgrade{INI_SECTION, "allow_downgrade", false};
|
||||
option::OptionBool m_skip_if_already_installed{INI_SECTION, "skip_if_already_installed", true};
|
||||
option::OptionBool m_ticket_only{INI_SECTION, "ticket_only", false};
|
||||
option::OptionBool m_patch_ticket{INI_SECTION, "patch_ticket", true};
|
||||
option::OptionBool m_skip_base{INI_SECTION, "skip_base", false};
|
||||
option::OptionBool m_skip_patch{INI_SECTION, "skip_patch", false};
|
||||
option::OptionBool m_skip_addon{INI_SECTION, "skip_addon", false};
|
||||
option::OptionBool m_skip_data_patch{INI_SECTION, "skip_data_patch", false};
|
||||
option::OptionBool m_skip_ticket{INI_SECTION, "skip_ticket", false};
|
||||
option::OptionBool m_skip_nca_hash_verify{INI_SECTION, "skip_nca_hash_verify", false};
|
||||
option::OptionBool m_skip_rsa_header_fixed_key_verify{INI_SECTION, "skip_rsa_header_fixed_key_verify", false};
|
||||
option::OptionBool m_skip_rsa_npdm_fixed_key_verify{INI_SECTION, "skip_rsa_npdm_fixed_key_verify", false};
|
||||
option::OptionBool m_ignore_distribution_bit{INI_SECTION, "ignore_distribution_bit", false};
|
||||
option::OptionBool m_convert_to_standard_crypto{INI_SECTION, "convert_to_standard_crypto", false};
|
||||
option::OptionBool m_lower_master_key{INI_SECTION, "lower_master_key", false};
|
||||
option::OptionBool m_lower_system_version{INI_SECTION, "lower_system_version", false};
|
||||
|
||||
// todo: move this into it's own menu
|
||||
option::OptionLong m_text_scroll_speed{"accessibility", "text_scroll_speed", 1}; // normal
|
||||
|
||||
|
||||
@@ -87,6 +87,9 @@ auto ToFile(const Api& e) -> ApiResult;
|
||||
auto ToMemoryAsync(const Api& e) -> bool;
|
||||
auto ToFileAsync(const Api& e) -> bool;
|
||||
|
||||
// uses curl to convert string to their %XX
|
||||
auto EscapeString(const std::string& str) -> std::string;
|
||||
|
||||
struct Api {
|
||||
Api() = default;
|
||||
|
||||
|
||||
@@ -1,24 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define sphaira_USE_LOG 1
|
||||
|
||||
#include <cstdarg>
|
||||
#include <stdarg.h>
|
||||
|
||||
#if sphaira_USE_LOG
|
||||
auto log_file_init() -> bool;
|
||||
auto log_nxlink_init() -> bool;
|
||||
bool log_file_init();
|
||||
bool log_nxlink_init();
|
||||
void log_file_exit();
|
||||
void log_nxlink_exit();
|
||||
void log_write(const char* s, ...) __attribute__ ((format (printf, 1, 2)));
|
||||
void log_write_arg(const char* s, std::va_list& v);
|
||||
void log_write_arg(const char* s, va_list* v);
|
||||
#else
|
||||
inline auto log_file_init() -> bool {
|
||||
inline bool log_file_init() {
|
||||
return true;
|
||||
}
|
||||
inline auto log_nxlink_init() -> bool {
|
||||
inline bool log_nxlink_init() {
|
||||
return true;
|
||||
}
|
||||
#define log_file_exit()
|
||||
#define log_nxlink_exit()
|
||||
#define log_write(...)
|
||||
#define log_write_arg(...)
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -122,6 +122,8 @@ struct Menu final : MenuBase {
|
||||
private:
|
||||
void SetIndex(s64 index);
|
||||
void InstallForwarder();
|
||||
void InstallFile(const FileEntry& target);
|
||||
void InstallFiles(const std::vector<FileEntry>& targets);
|
||||
auto Scan(const fs::FsPath& new_path, bool is_walk_up = false) -> Result;
|
||||
|
||||
void LoadAssocEntriesPath(const fs::FsPath& path);
|
||||
|
||||
43
sphaira/include/ui/menus/usb_menu.hpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "ui/menus/menu_base.hpp"
|
||||
#include "yati/source/usb.hpp"
|
||||
|
||||
namespace sphaira::ui::menu::usb {
|
||||
|
||||
enum class State {
|
||||
// not connected.
|
||||
None,
|
||||
// just connected, starts the transfer.
|
||||
Connected,
|
||||
// set whilst transfer is in progress.
|
||||
Progress,
|
||||
// set when the transfer is finished.
|
||||
Done,
|
||||
// failed to connect.
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct Menu final : MenuBase {
|
||||
Menu();
|
||||
~Menu();
|
||||
|
||||
void Update(Controller* controller, TouchInfo* touch) override;
|
||||
void Draw(NVGcontext* vg, Theme* theme) override;
|
||||
void OnFocusGained() override;
|
||||
|
||||
// this should be private
|
||||
// private:
|
||||
std::shared_ptr<yati::source::Usb> m_usb_source{};
|
||||
bool m_was_mtp_enabled{};
|
||||
|
||||
Thread m_thread{};
|
||||
Mutex m_mutex{};
|
||||
// the below are shared across threads, lock with the above mutex!
|
||||
State m_state{State::None};
|
||||
bool m_usb_has_connection{};
|
||||
u32 m_usb_speed{};
|
||||
u32 m_usb_count{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::ui::menu::usb
|
||||
@@ -21,6 +21,7 @@ struct ProgressBox final : Widget {
|
||||
auto Update(Controller* controller, TouchInfo* touch) -> void override;
|
||||
auto Draw(NVGcontext* vg, Theme* theme) -> void override;
|
||||
|
||||
auto SetTitle(const std::string& title) -> ProgressBox&;
|
||||
auto NewTransfer(const std::string& transfer) -> ProgressBox&;
|
||||
auto UpdateTransfer(s64 offset, s64 size) -> ProgressBox&;
|
||||
void RequestExit();
|
||||
|
||||
39
sphaira/include/yati/container/base.hpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include "yati/source/base.hpp"
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
|
||||
enum class CollectionType {
|
||||
CollectionType_NCA,
|
||||
CollectionType_NCZ,
|
||||
CollectionType_TIK,
|
||||
CollectionType_CERT,
|
||||
};
|
||||
|
||||
struct CollectionEntry {
|
||||
// collection name within file.
|
||||
std::string name{};
|
||||
// collection offset within file.
|
||||
s64 offset{};
|
||||
// collection size within file, may be compressed size.
|
||||
s64 size{};
|
||||
};
|
||||
|
||||
using Collections = std::vector<CollectionEntry>;
|
||||
|
||||
struct Base {
|
||||
using Source = source::Base;
|
||||
|
||||
Base(Source* source) : m_source{source} { }
|
||||
virtual ~Base() = default;
|
||||
virtual Result GetCollections(Collections& out) = 0;
|
||||
|
||||
protected:
|
||||
Source* m_source;
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
14
sphaira/include/yati/container/nsp.hpp
Normal file
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
|
||||
struct Nsp final : Base {
|
||||
using Base::Base;
|
||||
Result GetCollections(Collections& out) override;
|
||||
static Result Validate(source::Base* source);
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
16
sphaira/include/yati/container/xci.hpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
|
||||
struct Xci final : Base {
|
||||
using Base::Base;
|
||||
Result GetCollections(Collections& out) override;
|
||||
static Result Validate(source::Base* source);
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
57
sphaira/include/yati/nx/crypto.hpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::crypto {
|
||||
|
||||
struct Aes128 {
|
||||
Aes128(const void *key, bool is_encryptor) {
|
||||
m_is_encryptor = is_encryptor;
|
||||
aes128ContextCreate(&m_ctx, key, is_encryptor);
|
||||
}
|
||||
|
||||
void Run(void *dst, const void *src) {
|
||||
if (m_is_encryptor) {
|
||||
aes128EncryptBlock(&m_ctx, dst, src);
|
||||
} else {
|
||||
aes128DecryptBlock(&m_ctx, dst, src);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Aes128Context m_ctx;
|
||||
bool m_is_encryptor;
|
||||
};
|
||||
|
||||
struct Aes128Xts {
|
||||
Aes128Xts(const u8 *key, bool is_encryptor) : Aes128Xts{key, key + 0x10, is_encryptor} { }
|
||||
Aes128Xts(const void *key0, const void *key1, bool is_encryptor) {
|
||||
m_is_encryptor = is_encryptor;
|
||||
aes128XtsContextCreate(&m_ctx, key0, key1, is_encryptor);
|
||||
}
|
||||
|
||||
void Run(void *dst, const void *src, u64 sector, u64 sector_size, u64 data_size) {
|
||||
for (u64 pos = 0; pos < data_size; pos += sector_size) {
|
||||
aes128XtsContextResetSector(&m_ctx, sector++, true);
|
||||
if (m_is_encryptor) {
|
||||
aes128XtsEncrypt(&m_ctx, static_cast<u8*>(dst) + pos, static_cast<const u8*>(src) + pos, sector_size);
|
||||
} else {
|
||||
aes128XtsDecrypt(&m_ctx, static_cast<u8*>(dst) + pos, static_cast<const u8*>(src) + pos, sector_size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Aes128XtsContext m_ctx;
|
||||
bool m_is_encryptor;
|
||||
};
|
||||
|
||||
static inline void cryptoAes128(const void *in, void *out, const void* key, bool is_encryptor) {
|
||||
Aes128(key, is_encryptor).Run(out, in);
|
||||
}
|
||||
|
||||
static inline void cryptoAes128Xts(const void* in, void* out, const u8* key, u64 sector, u64 sector_size, u64 data_size, bool is_encryptor) {
|
||||
Aes128Xts(key, is_encryptor).Run(out, in, sector, sector_size, data_size);
|
||||
}
|
||||
|
||||
} // namespace sphaira::crypto
|
||||
83
sphaira/include/yati/nx/es.hpp
Normal file
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include <span>
|
||||
#include "ncm.hpp"
|
||||
#include "keys.hpp"
|
||||
|
||||
namespace sphaira::es {
|
||||
|
||||
enum { TicketModule = 522 };
|
||||
|
||||
enum : Result {
|
||||
// found ticket has missmatching rights_id from it's name.
|
||||
Result_InvalidTicketBadRightsId = MAKERESULT(TicketModule, 71),
|
||||
Result_InvalidTicketVersion = MAKERESULT(TicketModule, 72),
|
||||
Result_InvalidTicketKeyType = MAKERESULT(TicketModule, 73),
|
||||
Result_InvalidTicketKeyRevision = MAKERESULT(TicketModule, 74),
|
||||
};
|
||||
|
||||
enum TicketSigantureType {
|
||||
TicketSigantureType_RSA_4096_SHA1 = 0x010000,
|
||||
TicketSigantureType_RSA_2048_SHA1 = 0x010001,
|
||||
TicketSigantureType_ECDSA_SHA1 = 0x010002,
|
||||
TicketSigantureType_RSA_4096_SHA256 = 0x010003,
|
||||
TicketSigantureType_RSA_2048_SHA256 = 0x010004,
|
||||
TicketSigantureType_ECDSA_SHA256 = 0x010005,
|
||||
TicketSigantureType_HMAC_SHA1_160 = 0x010006,
|
||||
};
|
||||
|
||||
enum TicketTitleKeyType {
|
||||
TicketTitleKeyType_Common = 0,
|
||||
TicketTitleKeyType_Personalized = 1,
|
||||
};
|
||||
|
||||
enum TicketPropertiesBitfield {
|
||||
TicketPropertiesBitfield_None = 0,
|
||||
// temporary ticket, removed on restart
|
||||
TicketPropertiesBitfield_Temporary = 1 << 4,
|
||||
};
|
||||
|
||||
struct TicketData {
|
||||
u8 issuer[0x40];
|
||||
u8 title_key_block[0x100];
|
||||
u8 ticket_version1;
|
||||
u8 title_key_type;
|
||||
u16 ticket_version2;
|
||||
u8 license_type;
|
||||
u8 master_key_revision;
|
||||
u16 properties_bitfield;
|
||||
u8 _0x148[0x8];
|
||||
u64 ticket_id;
|
||||
u64 device_id;
|
||||
FsRightsId rights_id;
|
||||
u32 account_id;
|
||||
u8 _0x174[0xC];
|
||||
u8 _0x180[0x140];
|
||||
};
|
||||
static_assert(sizeof(TicketData) == 0x2C0);
|
||||
|
||||
struct EticketRsaDeviceKey {
|
||||
u8 ctr[AES_128_KEY_SIZE];
|
||||
u8 private_exponent[0x100];
|
||||
u8 modulus[0x100];
|
||||
u32 public_exponent; ///< Stored using big endian byte order. Must match ETICKET_RSA_DEVICE_KEY_PUBLIC_EXPONENT.
|
||||
u8 padding[0x14];
|
||||
u64 device_id;
|
||||
u8 ghash[0x10];
|
||||
};
|
||||
static_assert(sizeof(EticketRsaDeviceKey) == 0x240);
|
||||
|
||||
// es functions.
|
||||
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size);
|
||||
|
||||
// ticket functions.
|
||||
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out);
|
||||
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out);
|
||||
Result SetTicketData(std::span<u8> ticket, const es::TicketData* in);
|
||||
|
||||
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys);
|
||||
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys);
|
||||
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys, bool convert_personalised);
|
||||
|
||||
} // namespace sphaira::es
|
||||
70
sphaira/include/yati/nx/keys.hpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include "defines.hpp"
|
||||
|
||||
namespace sphaira::keys {
|
||||
|
||||
struct KeyEntry {
|
||||
u8 key[AES_128_KEY_SIZE]{};
|
||||
|
||||
auto IsValid() const -> bool {
|
||||
const KeyEntry empty{};
|
||||
return std::memcmp(key, &empty, sizeof(key));
|
||||
}
|
||||
};
|
||||
|
||||
using KeySection = std::array<KeyEntry, 0x20>;
|
||||
struct Keys {
|
||||
u8 header_key[0x20]{};
|
||||
// the below are only found if read_from_file=true
|
||||
KeySection key_area_key[0x3]{}; // index
|
||||
KeySection titlekek{};
|
||||
KeySection master_key{};
|
||||
KeyEntry eticket_rsa_kek{};
|
||||
SetCalRsa2048DeviceKey eticket_device_key{};
|
||||
|
||||
static auto FixKey(u8 key) -> u8 {
|
||||
if (key) {
|
||||
return key - 1;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
auto HasNcaKeyArea(u8 key, u8 index) const -> bool {
|
||||
return key_area_key[index][FixKey(key)].IsValid();
|
||||
}
|
||||
|
||||
auto HasTitleKek(u8 key) const -> bool {
|
||||
return titlekek[FixKey(key)].IsValid();
|
||||
}
|
||||
|
||||
auto HasMasterKey(u8 key) const -> bool {
|
||||
return master_key[FixKey(key)].IsValid();
|
||||
}
|
||||
|
||||
auto GetNcaKeyArea(KeyEntry* out, u8 key, u8 index) const -> Result {
|
||||
R_UNLESS(HasNcaKeyArea(key, index), 0x1);
|
||||
*out = key_area_key[index][FixKey(key)];
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto GetTitleKek(KeyEntry* out, u8 key) const -> Result {
|
||||
R_UNLESS(HasTitleKek(key), 0x1);
|
||||
*out = titlekek[FixKey(key)];
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
auto GetMasterKey(KeyEntry* out, u8 key) const -> Result {
|
||||
R_UNLESS(HasMasterKey(key), 0x1);
|
||||
*out = master_key[FixKey(key)];
|
||||
R_SUCCEED();
|
||||
}
|
||||
};
|
||||
|
||||
void parse_hex_key(void* key, const char* hex);
|
||||
Result parse_keys(Keys& out, bool read_from_file);
|
||||
|
||||
} // namespace sphaira::keys
|
||||
218
sphaira/include/yati/nx/nca.hpp
Normal file
@@ -0,0 +1,218 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include "keys.hpp"
|
||||
|
||||
namespace sphaira::nca {
|
||||
|
||||
#define NCA0_MAGIC 0x3041434E
|
||||
#define NCA2_MAGIC 0x3241434E
|
||||
#define NCA3_MAGIC 0x3341434E
|
||||
|
||||
#define NCA_SECTOR_SIZE 0x200
|
||||
#define NCA_XTS_SECTION_SIZE 0xC00
|
||||
#define NCA_SECTION_TOTAL 0x4
|
||||
#define NCA_MEDIA_REAL(x)((x * 0x200))
|
||||
|
||||
#define NCA_PROGRAM_LOGO_OFFSET 0x8000
|
||||
#define NCA_META_CNMT_OFFSET 0xC20
|
||||
|
||||
enum KeyGenerationOld {
|
||||
KeyGenerationOld_100 = 0x0,
|
||||
KeyGenerationOld_Unused = 0x1,
|
||||
KeyGenerationOld_300 = 0x2,
|
||||
};
|
||||
|
||||
enum KeyGeneration {
|
||||
KeyGeneration_301 = 0x3,
|
||||
KeyGeneration_400 = 0x4,
|
||||
KeyGeneration_500 = 0x5,
|
||||
KeyGeneration_600 = 0x6,
|
||||
KeyGeneration_620 = 0x7,
|
||||
KeyGeneration_700 = 0x8,
|
||||
KeyGeneration_810 = 0x9,
|
||||
KeyGeneration_900 = 0x0A,
|
||||
KeyGeneration_910 = 0x0B,
|
||||
KeyGeneration_1210 = 0x0C,
|
||||
KeyGeneration_1300 = 0x0D,
|
||||
KeyGeneration_1400 = 0x0E,
|
||||
KeyGeneration_1500 = 0x0F,
|
||||
KeyGeneration_1600 = 0x10,
|
||||
KeyGeneration_1700 = 0x11,
|
||||
KeyGeneration_1800 = 0x12,
|
||||
KeyGeneration_1900 = 0x13,
|
||||
KeyGeneration_Invalid = 0xFF,
|
||||
};
|
||||
|
||||
enum KeyAreaEncryptionKeyIndex {
|
||||
KeyAreaEncryptionKeyIndex_Application = 0x0,
|
||||
KeyAreaEncryptionKeyIndex_Ocean = 0x1,
|
||||
KeyAreaEncryptionKeyIndex_System = 0x2
|
||||
};
|
||||
|
||||
enum DistributionType {
|
||||
DistributionType_System = 0x0,
|
||||
DistributionType_GameCard = 0x1
|
||||
};
|
||||
|
||||
enum ContentType {
|
||||
ContentType_Program = 0x0,
|
||||
ContentType_Meta = 0x1,
|
||||
ContentType_Control = 0x2,
|
||||
ContentType_Manual = 0x3,
|
||||
ContentType_Data = 0x4,
|
||||
ContentType_PublicData = 0x5,
|
||||
};
|
||||
|
||||
enum FileSystemType {
|
||||
FileSystemType_RomFS = 0x0,
|
||||
FileSystemType_PFS0 = 0x1
|
||||
};
|
||||
|
||||
enum HashType {
|
||||
HashType_Auto = 0x0,
|
||||
HashType_HierarchicalSha256 = 0x2,
|
||||
HashType_HierarchicalIntegrity = 0x3
|
||||
};
|
||||
|
||||
enum EncryptionType {
|
||||
EncryptionType_Auto = 0x0,
|
||||
EncryptionType_None = 0x1,
|
||||
EncryptionType_AesXts = 0x2,
|
||||
EncryptionType_AesCtr = 0x3,
|
||||
EncryptionType_AesCtrEx = 0x4,
|
||||
EncryptionType_AesCtrSkipLayerHash = 0x5, // [14.0.0+]
|
||||
EncryptionType_AesCtrExSkipLayerHash = 0x6, // [14.0.0+]
|
||||
};
|
||||
|
||||
struct SectionTableEntry {
|
||||
u32 media_start_offset; // divided by 0x200.
|
||||
u32 media_end_offset; // divided by 0x200.
|
||||
u8 _0x8[0x4]; // unknown.
|
||||
u8 _0xC[0x4]; // unknown.
|
||||
};
|
||||
|
||||
struct LayerRegion {
|
||||
u64 offset;
|
||||
u64 size;
|
||||
};
|
||||
|
||||
struct HierarchicalSha256Data {
|
||||
u8 master_hash[0x20];
|
||||
u32 block_size;
|
||||
u32 layer_count;
|
||||
LayerRegion hash_layer;
|
||||
LayerRegion pfs0_layer;
|
||||
LayerRegion unused_layers[3];
|
||||
u8 _0x78[0x80];
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct HierarchicalIntegrityVerificationLevelInformation {
|
||||
u64 logical_offset;
|
||||
u64 hash_data_size;
|
||||
u32 block_size; // log2
|
||||
u32 _0x14; // reserved
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct InfoLevelHash {
|
||||
u32 max_layers;
|
||||
HierarchicalIntegrityVerificationLevelInformation levels[6];
|
||||
u8 signature_salt[0x20];
|
||||
};
|
||||
|
||||
struct IntegrityMetaInfo {
|
||||
u32 magic; // IVFC
|
||||
u32 version;
|
||||
u32 master_hash_size;
|
||||
InfoLevelHash info_level_hash;
|
||||
u8 master_hash[0x20];
|
||||
u8 _0xE0[0x18];
|
||||
};
|
||||
|
||||
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
|
||||
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
|
||||
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
|
||||
|
||||
struct FsHeader {
|
||||
u16 version; // always 2.
|
||||
u8 fs_type; // see FileSystemType.
|
||||
u8 hash_type; // see HashType.
|
||||
u8 encryption_type; // see EncryptionType.
|
||||
u8 metadata_hash_type;
|
||||
u8 _0x6[0x2]; // empty.
|
||||
|
||||
union {
|
||||
HierarchicalSha256Data hierarchical_sha256_data;
|
||||
IntegrityMetaInfo integrity_meta_info; // used for romfs
|
||||
} hash_data;
|
||||
|
||||
u8 patch_info[0x40];
|
||||
u64 section_ctr;
|
||||
u8 spares_info[0x30];
|
||||
u8 compression_info[0x28];
|
||||
u8 meta_data_hash_data_info[0x30];
|
||||
u8 reserved[0x30];
|
||||
};
|
||||
static_assert(sizeof(FsHeader) == 0x200);
|
||||
static_assert(sizeof(FsHeader::hash_data) == 0xF8);
|
||||
|
||||
struct SectionHeaderHash {
|
||||
u8 sha256[0x20];
|
||||
};
|
||||
|
||||
struct KeyArea {
|
||||
u8 area[0x10];
|
||||
};
|
||||
|
||||
struct Header {
|
||||
u8 rsa_fixed_key[0x100];
|
||||
u8 rsa_npdm[0x100]; // key from npdm.
|
||||
u32 magic;
|
||||
u8 distribution_type; // see DistributionType.
|
||||
u8 content_type; // see ContentType.
|
||||
u8 old_key_gen; // see KeyGenerationOld.
|
||||
u8 kaek_index; // see KeyAreaEncryptionKeyIndex.
|
||||
u64 size;
|
||||
u64 title_id;
|
||||
u32 context_id;
|
||||
u32 sdk_version;
|
||||
u8 key_gen; // see KeyGeneration.
|
||||
u8 sig_key_gen;
|
||||
u8 _0x222[0xE]; // empty.
|
||||
FsRightsId rights_id;
|
||||
|
||||
SectionTableEntry fs_table[NCA_SECTION_TOTAL];
|
||||
SectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
|
||||
KeyArea key_area[NCA_SECTION_TOTAL];
|
||||
|
||||
u8 _0x340[0xC0]; // empty.
|
||||
|
||||
FsHeader fs_header[NCA_SECTION_TOTAL];
|
||||
|
||||
auto GetKeyGeneration() const -> u8 {
|
||||
if (old_key_gen < key_gen) {
|
||||
return key_gen;
|
||||
} else {
|
||||
return old_key_gen;
|
||||
}
|
||||
}
|
||||
|
||||
void SetKeyGeneration(u8 key_generation) {
|
||||
if (key_generation <= 0x2) {
|
||||
old_key_gen = key_generation;
|
||||
key_gen = 0;
|
||||
} else {
|
||||
old_key_gen = 0x2;
|
||||
key_gen = key_generation;
|
||||
}
|
||||
}
|
||||
};
|
||||
static_assert(sizeof(Header) == 0xC00);
|
||||
|
||||
Result DecryptKeak(const keys::Keys& keys, Header& header);
|
||||
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation);
|
||||
Result VerifyFixedKey(const Header& header);
|
||||
|
||||
} // namespace sphaira::nca
|
||||
39
sphaira/include/yati/nx/ncm.hpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::ncm {
|
||||
|
||||
struct PackagedContentMeta {
|
||||
u64 title_id;
|
||||
u32 title_version;
|
||||
u8 meta_type; // NcmContentMetaType
|
||||
u8 content_meta_platform; // [17.0.0+]
|
||||
NcmContentMetaHeader meta_header;
|
||||
u8 install_type; // NcmContentInstallType
|
||||
u8 _0x17;
|
||||
u32 required_sys_version;
|
||||
u8 _0x1C[0x4];
|
||||
};
|
||||
static_assert(sizeof(PackagedContentMeta) == 0x20);
|
||||
|
||||
struct ContentStorageRecord {
|
||||
NcmContentMetaKey key;
|
||||
u8 storage_id;
|
||||
u8 padding[0x7];
|
||||
};
|
||||
|
||||
union ExtendedHeader {
|
||||
NcmApplicationMetaExtendedHeader application;
|
||||
NcmPatchMetaExtendedHeader patch;
|
||||
NcmAddOnContentMetaExtendedHeader addon;
|
||||
NcmLegacyAddOnContentMetaExtendedHeader addon_legacy;
|
||||
NcmDataPatchMetaExtendedHeader data_patch;
|
||||
};
|
||||
|
||||
auto GetAppId(const NcmContentMetaKey& key) -> u64;
|
||||
|
||||
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id);
|
||||
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id);
|
||||
|
||||
} // namespace sphaira::ncm
|
||||
54
sphaira/include/yati/nx/ncz.hpp
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::ncz {
|
||||
|
||||
#define NCZ_SECTION_MAGIC 0x4E544345535A434EUL
|
||||
// todo: byteswap this
|
||||
#define NCZ_BLOCK_MAGIC std::byteswap(0x4E435A424C4F434BUL)
|
||||
|
||||
#define NCZ_SECTION_OFFSET (0x4000 + sizeof(ncz::Header))
|
||||
|
||||
struct Header {
|
||||
u64 magic; // NCZ_SECTION_MAGIC
|
||||
u64 total_sections;
|
||||
};
|
||||
|
||||
struct BlockHeader {
|
||||
u64 magic; // NCZ_BLOCK_MAGIC
|
||||
u8 version;
|
||||
u8 type;
|
||||
u8 padding;
|
||||
u8 block_size_exponent;
|
||||
u32 total_blocks;
|
||||
u64 decompressed_size;
|
||||
};
|
||||
|
||||
struct Block {
|
||||
u32 size;
|
||||
};
|
||||
|
||||
struct BlockInfo {
|
||||
u64 offset; // compressed offset.
|
||||
u64 size; // compressed size.
|
||||
|
||||
auto InRange(u64 off) const -> bool {
|
||||
return off < offset + size && off >= offset;
|
||||
}
|
||||
};
|
||||
|
||||
struct Section {
|
||||
u64 offset;
|
||||
u64 size;
|
||||
u64 crypto_type;
|
||||
u64 padding;
|
||||
u8 key[0x10];
|
||||
u8 counter[0x10];
|
||||
|
||||
auto InRange(u64 off) const -> bool {
|
||||
return off < offset + size && off >= offset;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace sphaira::ncz
|
||||
62
sphaira/include/yati/nx/npdm.hpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::npdm {
|
||||
|
||||
struct Meta {
|
||||
u32 magic; // "META"
|
||||
u32 signature_key_generation; // +9.0.0
|
||||
u32 _0x8;
|
||||
u8 flags;
|
||||
u8 _0xD;
|
||||
u8 main_thread_priority;
|
||||
u8 main_thread_core_num;
|
||||
u32 _0x10;
|
||||
u32 sys_resource_size; // +3.0.0
|
||||
u32 version;
|
||||
u32 main_thread_stack_size;
|
||||
char title_name[0x10];
|
||||
char product_code[0x10];
|
||||
u8 _0x40[0x30];
|
||||
u32 aci0_offset;
|
||||
u32 aci0_size;
|
||||
u32 acid_offset;
|
||||
u32 acid_size;
|
||||
};
|
||||
|
||||
struct Acid {
|
||||
u8 rsa_sig[0x100];
|
||||
u8 rsa_pub[0x100];
|
||||
u32 magic; // "ACID"
|
||||
u32 size;
|
||||
u8 version;
|
||||
u8 _0x209[0x1];
|
||||
u8 _0x20A[0x2];
|
||||
u32 flags;
|
||||
u64 program_id_min;
|
||||
u64 program_id_max;
|
||||
u32 fac_offset;
|
||||
u32 fac_size;
|
||||
u32 sac_offset;
|
||||
u32 sac_size;
|
||||
u32 kac_offset;
|
||||
u32 kac_size;
|
||||
u8 _0x238[0x8];
|
||||
};
|
||||
|
||||
struct Aci0 {
|
||||
u32 magic; // "ACI0"
|
||||
u8 _0x4[0xC];
|
||||
u64 program_id;
|
||||
u8 _0x18[0x8];
|
||||
u32 fac_offset;
|
||||
u32 fac_size;
|
||||
u32 sac_offset;
|
||||
u32 sac_size;
|
||||
u32 kac_offset;
|
||||
u32 kac_size;
|
||||
u8 _0x38[0x8];
|
||||
};
|
||||
|
||||
} // namespace sphaira::npdm
|
||||
22
sphaira/include/yati/nx/ns.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <switch.h>
|
||||
#include "ncm.hpp"
|
||||
|
||||
namespace sphaira::ns {
|
||||
|
||||
enum ApplicationRecordType {
|
||||
// installed
|
||||
ApplicationRecordType_Installed = 0x3,
|
||||
// application is gamecard, but gamecard isn't insterted
|
||||
ApplicationRecordType_GamecardMissing = 0x5,
|
||||
// archived
|
||||
ApplicationRecordType_Archived = 0xB,
|
||||
};
|
||||
|
||||
Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count);
|
||||
Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read);
|
||||
Result DeleteApplicationRecord(Service* srv, u64 tid);
|
||||
Result InvalidateApplicationControlCache(Service* srv, u64 tid);
|
||||
|
||||
} // namespace sphaira::ns
|
||||
62
sphaira/include/yati/nx/nxdumptool_rsa.h
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* rsa.c
|
||||
*
|
||||
* Copyright (c) 2018-2019, SciresM.
|
||||
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
|
||||
*
|
||||
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
|
||||
*
|
||||
* nxdumptool is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* nxdumptool is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef __RSA_H__
|
||||
#define __RSA_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#define RSA2048_BYTES 0x100
|
||||
#define RSA2048_BITS (RSA2048_BYTES * 8)
|
||||
|
||||
#define RSA2048_SIG_SIZE RSA2048_BYTES
|
||||
#define RSA2048_PUBKEY_SIZE RSA2048_BYTES
|
||||
|
||||
/// Verifies a RSA-2048-PSS with SHA-256 signature.
|
||||
/// Suitable for NCA and NPDM signatures.
|
||||
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||
bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size);
|
||||
|
||||
/// Verifies a RSA-2048-PKCS#1 v1.5 with SHA-256 signature.
|
||||
/// Suitable for ticket and certificate chain signatures.
|
||||
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||
bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size);
|
||||
|
||||
/// Performs RSA-2048-OAEP decryption.
|
||||
/// Suitable to decrypt the titlekey block from personalized tickets.
|
||||
/// The provided signature and modulus must have sizes of at least RSA2048_SIG_SIZE and RSA2048_PUBKEY_SIZE, respectively.
|
||||
/// 'label' and 'label_size' arguments are optional -- if not needed, these may be set to NULL and 0, respectively.
|
||||
bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \
|
||||
size_t private_exponent_size, const void *label, size_t label_size, size_t *out_size);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __RSA_H__ */
|
||||
265
sphaira/include/yati/nx/tik.h
Normal file
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
* tik.h
|
||||
*
|
||||
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
|
||||
*
|
||||
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
|
||||
*
|
||||
* nxdumptool is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* nxdumptool is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef __TIK_H__
|
||||
#define __TIK_H__
|
||||
|
||||
#include "signature.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define SIGNED_TIK_MIN_SIZE sizeof(TikSigHmac160) /* Assuming no ESV1/ESV2 records are available. */
|
||||
#define SIGNED_TIK_MAX_SIZE 0x400 /* Max ticket entry size in the ES ticket system savedata file. */
|
||||
|
||||
#define TIK_FORMAT_VERSION 2
|
||||
|
||||
#define GENERATE_TIK_STRUCT(sigtype, tiksize) \
|
||||
typedef struct { \
|
||||
SignatureBlock##sigtype sig_block; \
|
||||
TikCommonBlock tik_common_block; \
|
||||
u8 es_section_record_data[]; \
|
||||
} TikSig##sigtype; \
|
||||
NXDT_ASSERT(TikSig##sigtype, tiksize);
|
||||
|
||||
typedef enum {
|
||||
TikTitleKeyType_Common = 0,
|
||||
TikTitleKeyType_Personalized = 1,
|
||||
TikTitleKeyType_Count = 2 ///< Total values supported by this enum.
|
||||
} TikTitleKeyType;
|
||||
|
||||
typedef enum {
|
||||
TikLicenseType_Permanent = 0,
|
||||
TikLicenseType_Demo = 1,
|
||||
TikLicenseType_Trial = 2,
|
||||
TikLicenseType_Rental = 3,
|
||||
TikLicenseType_Subscription = 4,
|
||||
TikLicenseType_Service = 5,
|
||||
TikLicenseType_Count = 6 ///< Total values supported by this enum.
|
||||
} TikLicenseType;
|
||||
|
||||
typedef enum {
|
||||
TikPropertyMask_None = 0,
|
||||
TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats.
|
||||
TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats.
|
||||
TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats.
|
||||
TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console.
|
||||
TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted.
|
||||
TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification.
|
||||
TikPropertyMask_Count = 6 ///< Total values supported by this enum.
|
||||
} TikPropertyMask;
|
||||
|
||||
/// Placed after the ticket signature block.
|
||||
typedef struct {
|
||||
char issuer[0x40];
|
||||
u8 titlekey_block[0x100];
|
||||
u8 format_version; ///< Always matches TIK_FORMAT_VERSION.
|
||||
u8 titlekey_type; ///< TikTitleKeyType.
|
||||
u16 ticket_version;
|
||||
u8 license_type; ///< TikLicenseType.
|
||||
u8 key_generation; ///< NcaKeyGeneration.
|
||||
u16 property_mask; ///< TikPropertyMask.
|
||||
u8 reserved[0x8];
|
||||
u64 ticket_id;
|
||||
u64 device_id;
|
||||
FsRightsId rights_id;
|
||||
u32 account_id;
|
||||
u32 sect_total_size;
|
||||
u32 sect_hdr_offset;
|
||||
u16 sect_hdr_count;
|
||||
u16 sect_hdr_entry_size;
|
||||
} TikCommonBlock;
|
||||
|
||||
NXDT_ASSERT(TikCommonBlock, 0x180);
|
||||
|
||||
/// ESV1/ESV2 section records are placed right after the ticket data. These aren't available in TikTitleKeyType_Common tickets.
|
||||
/// These are only used if the sect_* fields from the common block are non-zero (other than 'sect_hdr_offset').
|
||||
/// Each ESV2 section record is followed by a 'record_count' number of ESV1 records, each one of 'record_size' size.
|
||||
|
||||
typedef enum {
|
||||
TikSectionType_None = 0,
|
||||
TikSectionType_Permanent = 1,
|
||||
TikSectionType_Subscription = 2,
|
||||
TikSectionType_Content = 3,
|
||||
TikSectionType_ContentConsumption = 4,
|
||||
TikSectionType_AccessTitle = 5,
|
||||
TikSectionType_LimitedResource = 6,
|
||||
TikSectionType_Count = 7 ///< Total values supported by this enum.
|
||||
} TikSectionType;
|
||||
|
||||
typedef struct {
|
||||
u32 sect_offset;
|
||||
u32 record_size;
|
||||
u32 section_size;
|
||||
u16 record_count;
|
||||
u16 section_type; ///< TikSectionType.
|
||||
} TikESV2SectionRecord;
|
||||
|
||||
/// Used with TikSectionType_Permanent.
|
||||
typedef struct {
|
||||
u8 ref_id[0x10];
|
||||
u32 ref_id_attr;
|
||||
} TikESV1PermanentRecord;
|
||||
|
||||
/// Used with TikSectionType_Subscription.
|
||||
typedef struct {
|
||||
u32 limit;
|
||||
u8 ref_id[0x10];
|
||||
u32 ref_id_attr;
|
||||
} TikESV1SubscriptionRecord;
|
||||
|
||||
/// Used with TikSectionType_Content.
|
||||
typedef struct {
|
||||
u32 offset;
|
||||
u8 access_mask[0x80];
|
||||
} TikESV1ContentRecord;
|
||||
|
||||
/// Used with TikSectionType_ContentConsumption.
|
||||
typedef struct {
|
||||
u16 index;
|
||||
u16 code;
|
||||
u32 limit;
|
||||
} TikESV1ContentConsumptionRecord;
|
||||
|
||||
/// Used with TikSectionType_AccessTitle.
|
||||
typedef struct {
|
||||
u64 access_title_id;
|
||||
u64 access_title_mask;
|
||||
} TikESV1AccessTitleRecord;
|
||||
|
||||
/// Used with TikSectionType_LimitedResource.
|
||||
typedef struct {
|
||||
u32 limit;
|
||||
u8 ref_id[0x10];
|
||||
u32 ref_id_attr;
|
||||
} TikESV1LimitedResourceRecord;
|
||||
|
||||
/// All tickets generated below use a little endian sig_type field.
|
||||
GENERATE_TIK_STRUCT(Rsa4096, 0x3C0); /// RSA-4096 signature.
|
||||
GENERATE_TIK_STRUCT(Rsa2048, 0x2C0); /// RSA-2048 signature.
|
||||
GENERATE_TIK_STRUCT(Ecc480, 0x200); /// ECC signature.
|
||||
GENERATE_TIK_STRUCT(Hmac160, 0x1C0); /// HMAC signature.
|
||||
|
||||
/// Ticket type.
|
||||
typedef enum {
|
||||
TikType_None = 0,
|
||||
TikType_SigRsa4096 = 1,
|
||||
TikType_SigRsa2048 = 2,
|
||||
TikType_SigEcc480 = 3,
|
||||
TikType_SigHmac160 = 4,
|
||||
TikType_Count = 5 ///< Total values supported by this enum.
|
||||
} TikType;
|
||||
|
||||
/// Used to store ticket type, size and raw data, as well as titlekey data.
|
||||
typedef struct {
|
||||
u8 type; ///< TikType.
|
||||
u64 size; ///< Raw ticket size.
|
||||
u8 data[SIGNED_TIK_MAX_SIZE]; ///< Raw ticket data.
|
||||
u8 key_generation; ///< NcaKeyGeneration.
|
||||
u8 enc_titlekey[0x10]; ///< Titlekey with titlekek crypto (RSA-OAEP unwrapped if dealing with a TikTitleKeyType_Personalized ticket).
|
||||
char enc_titlekey_str[0x21]; ///< Character string representation of enc_titlekey.
|
||||
u8 dec_titlekey[0x10]; ///< Titlekey without titlekek crypto. Ready to use for NCA FS section decryption.
|
||||
char dec_titlekey_str[0x21]; ///< Character string representation of dec_titlekey.
|
||||
char rights_id_str[0x21]; ///< Character string representation of the rights ID from the ticket.
|
||||
} Ticket;
|
||||
|
||||
/// Retrieves a ticket from either the ES ticket system savedata file (eMMC BIS System partition) or the secure Hash FS partition from an inserted gamecard.
|
||||
/// Both the input rights ID and key generation values must have been retrieved from a NCA that depends on the desired ticket.
|
||||
/// Titlekey is also RSA-OAEP unwrapped (if needed) and titlekek-decrypted right away.
|
||||
bool tikRetrieveTicketByRightsId(Ticket *dst, const FsRightsId *id, u8 key_generation, bool use_gamecard);
|
||||
|
||||
/// Converts a TikTitleKeyType_Personalized ticket into a TikTitleKeyType_Common ticket and optionally generates a raw certificate chain for the new signature issuer.
|
||||
/// Bear in mind the 'size' member from the Ticket parameter will be updated by this function to remove any possible references to ESV1/ESV2 records.
|
||||
/// If both 'out_raw_cert_chain' and 'out_raw_cert_chain_size' pointers are provided, raw certificate chain data will be saved to them.
|
||||
/// certGenerateRawCertificateChainBySignatureIssuer() is used internally, so the output buffer must be freed by the user.
|
||||
bool tikConvertPersonalizedTicketToCommonTicket(Ticket *tik, u8 **out_raw_cert_chain, u64 *out_raw_cert_chain_size);
|
||||
|
||||
/// Helper inline functions for signed ticket blobs.
|
||||
|
||||
NX_INLINE TikCommonBlock *tikGetCommonBlockFromSignedTicketBlob(void *buf)
|
||||
{
|
||||
return (TikCommonBlock*)signatureGetPayloadFromSignedBlob(buf, false);
|
||||
}
|
||||
|
||||
NX_INLINE u64 tikGetSectionRecordsSizeFromSignedTicketBlob(void *buf)
|
||||
{
|
||||
TikCommonBlock *tik_common_block = tikGetCommonBlockFromSignedTicketBlob(buf);
|
||||
if (!tik_common_block) return 0;
|
||||
|
||||
u64 offset = sizeof(TikCommonBlock), out_size = 0;
|
||||
|
||||
for(u32 i = 0; i < tik_common_block->sect_hdr_count; i++)
|
||||
{
|
||||
TikESV2SectionRecord *rec = (TikESV2SectionRecord*)((u8*)tik_common_block + offset);
|
||||
offset += (sizeof(TikESV2SectionRecord) + ((u64)rec->record_count * (u64)rec->record_size));
|
||||
out_size += offset;
|
||||
}
|
||||
|
||||
return out_size;
|
||||
}
|
||||
|
||||
NX_INLINE bool tikIsValidSignedTicketBlob(void *buf)
|
||||
{
|
||||
u64 ticket_size = (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock));
|
||||
return (ticket_size > sizeof(TikCommonBlock) && (ticket_size + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) <= SIGNED_TIK_MAX_SIZE);
|
||||
}
|
||||
|
||||
NX_INLINE u64 tikGetSignedTicketBlobSize(void *buf)
|
||||
{
|
||||
return (tikIsValidSignedTicketBlob(buf) ? (signatureGetBlockSizeFromSignedBlob(buf, false) + sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0);
|
||||
}
|
||||
|
||||
NX_INLINE u64 tikGetSignedTicketBlobHashAreaSize(void *buf)
|
||||
{
|
||||
return (tikIsValidSignedTicketBlob(buf) ? (sizeof(TikCommonBlock) + tikGetSectionRecordsSizeFromSignedTicketBlob(buf)) : 0);
|
||||
}
|
||||
|
||||
/// Helper inline functions for Ticket elements.
|
||||
|
||||
NX_INLINE bool tikIsValidTicket(Ticket *tik)
|
||||
{
|
||||
return (tik && tik->type > TikType_None && tik->type < TikType_Count && tik->size >= SIGNED_TIK_MIN_SIZE && tik->size <= SIGNED_TIK_MAX_SIZE && tikIsValidSignedTicketBlob(tik->data));
|
||||
}
|
||||
|
||||
NX_INLINE TikCommonBlock *tikGetCommonBlockFromTicket(Ticket *tik)
|
||||
{
|
||||
return (tikIsValidTicket(tik) ? tikGetCommonBlockFromSignedTicketBlob(tik->data) : NULL);
|
||||
}
|
||||
|
||||
NX_INLINE bool tikIsPersonalizedTicket(Ticket *tik)
|
||||
{
|
||||
TikCommonBlock *tik_common_block = tikGetCommonBlockFromTicket(tik);
|
||||
return (tik_common_block ? (tik_common_block->titlekey_type == TikTitleKeyType_Personalized) : false);
|
||||
}
|
||||
|
||||
NX_INLINE u64 tikGetHashAreaSizeFromTicket(Ticket *tik)
|
||||
{
|
||||
return (tikIsValidTicket(tik) ? tikGetSignedTicketBlobHashAreaSize(tik->data) : 0);
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* __TIK_H__ */
|
||||
21
sphaira/include/yati/source/base.hpp
Normal file
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct Base {
|
||||
virtual ~Base() = default;
|
||||
// virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
virtual Result Read(void* buf, s64 off, s64 size, u64* bytes_read) = 0;
|
||||
|
||||
Result GetOpenResult() const {
|
||||
return m_open_result;
|
||||
}
|
||||
|
||||
protected:
|
||||
Result m_open_result{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
19
sphaira/include/yati/source/file.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct File final : Base {
|
||||
File(FsFileSystem* fs, const fs::FsPath& path);
|
||||
~File();
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
|
||||
private:
|
||||
FsFile m_file{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
20
sphaira/include/yati/source/stdio.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <cstdio>
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct Stdio final : Base {
|
||||
Stdio(const char* path);
|
||||
~Stdio();
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
|
||||
private:
|
||||
std::FILE* m_file{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
51
sphaira/include/yati/source/usb.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#pragma once
|
||||
|
||||
#include "base.hpp"
|
||||
#include "fs.hpp"
|
||||
#include <switch.h>
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
struct Usb final : Base {
|
||||
enum { USBModule = 523 };
|
||||
|
||||
enum : Result {
|
||||
Result_BadMagic = MAKERESULT(USBModule, 0),
|
||||
Result_BadVersion = MAKERESULT(USBModule, 1),
|
||||
Result_BadCount = MAKERESULT(USBModule, 2),
|
||||
Result_BadTransferSize = MAKERESULT(USBModule, 3),
|
||||
Result_BadTotalSize = MAKERESULT(USBModule, 4),
|
||||
};
|
||||
|
||||
Usb(u64 transfer_timeout);
|
||||
~Usb();
|
||||
|
||||
Result Read(void* buf, s64 off, s64 size, u64* bytes_read) override;
|
||||
Result Finished() const;
|
||||
|
||||
Result Init();
|
||||
Result WaitForConnection(u64 timeout, u32& speed, u32& count);
|
||||
|
||||
private:
|
||||
enum UsbSessionEndpoint {
|
||||
UsbSessionEndpoint_In = 0,
|
||||
UsbSessionEndpoint_Out = 1,
|
||||
};
|
||||
|
||||
Result SendCommand(s64 off, s64 size) const;
|
||||
Result InternalRead(void* buf, s64 off, s64 size) const;
|
||||
|
||||
bool GetConfigured() const;
|
||||
Event *GetCompletionEvent(UsbSessionEndpoint ep) const;
|
||||
Result WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const;
|
||||
Result TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const;
|
||||
Result GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const;
|
||||
Result TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const;
|
||||
|
||||
private:
|
||||
UsbDsInterface* m_interface{};
|
||||
UsbDsEndpoint* m_endpoints[2]{};
|
||||
u64 m_transfer_timeout{};
|
||||
};
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
127
sphaira/include/yati/yati.hpp
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Notes:
|
||||
* - nca's that use title key encryption are decrypted using Tegra SE, whereas
|
||||
* standard crypto uses software decryption.
|
||||
* The latter is almost always (slightly) faster, and removed the need for es patch.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "fs.hpp"
|
||||
#include "source/base.hpp"
|
||||
#include "ui/progress_box.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::yati {
|
||||
|
||||
enum { YatiModule = 521 };
|
||||
|
||||
enum : Result {
|
||||
// unkown container for the source provided.
|
||||
Result_ContainerNotFound = MAKERESULT(YatiModule, 10),
|
||||
Result_Cancelled = MAKERESULT(YatiModule, 11),
|
||||
|
||||
// nca required by the cnmt but not found in collection.
|
||||
Result_NcaNotFound = MAKERESULT(YatiModule, 30),
|
||||
Result_InvalidNcaReadSize = MAKERESULT(YatiModule, 31),
|
||||
Result_InvalidNcaSigKeyGen = MAKERESULT(YatiModule, 32),
|
||||
Result_InvalidNcaMagic = MAKERESULT(YatiModule, 33),
|
||||
Result_InvalidNcaSignature0 = MAKERESULT(YatiModule, 34),
|
||||
Result_InvalidNcaSignature1 = MAKERESULT(YatiModule, 35),
|
||||
// invalid sha256 over the entire nca.
|
||||
Result_InvalidNcaSha256 = MAKERESULT(YatiModule, 36),
|
||||
|
||||
// section could not be found.
|
||||
Result_NczSectionNotFound = MAKERESULT(YatiModule, 50),
|
||||
// section count == 0.
|
||||
Result_InvalidNczSectionCount = MAKERESULT(YatiModule, 51),
|
||||
// block could not be found.
|
||||
Result_NczBlockNotFound = MAKERESULT(YatiModule, 52),
|
||||
// block version != 2.
|
||||
Result_InvalidNczBlockVersion = MAKERESULT(YatiModule, 53),
|
||||
// block type != 1.
|
||||
Result_InvalidNczBlockType = MAKERESULT(YatiModule, 54),
|
||||
// block count == 0.
|
||||
Result_InvalidNczBlockTotal = MAKERESULT(YatiModule, 55),
|
||||
// block size exponent < 14 || > 32.
|
||||
Result_InvalidNczBlockSizeExponent = MAKERESULT(YatiModule, 56),
|
||||
// zstd error while decompressing ncz.
|
||||
Result_InvalidNczZstdError = MAKERESULT(YatiModule, 57),
|
||||
|
||||
// nca has rights_id but matching ticket wasn't found.
|
||||
Result_TicketNotFound = MAKERESULT(YatiModule, 70),
|
||||
// found ticket has missmatching rights_id from it's name.
|
||||
Result_InvalidTicketBadRightsId = MAKERESULT(YatiModule, 71),
|
||||
Result_InvalidTicketVersion = MAKERESULT(YatiModule, 72),
|
||||
Result_InvalidTicketKeyType = MAKERESULT(YatiModule, 73),
|
||||
Result_InvalidTicketKeyRevision = MAKERESULT(YatiModule, 74),
|
||||
|
||||
// cert not found for the ticket.
|
||||
Result_CertNotFound = MAKERESULT(YatiModule, 90),
|
||||
|
||||
// unable to fetch header from ncm database.
|
||||
Result_NcmDbCorruptHeader = MAKERESULT(YatiModule, 110),
|
||||
// unable to total infos from ncm database.
|
||||
Result_NcmDbCorruptInfos = MAKERESULT(YatiModule, 111),
|
||||
};
|
||||
|
||||
struct Config {
|
||||
bool sd_card_install{};
|
||||
|
||||
// enables downgrading patch / data patch (dlc) version.
|
||||
bool allow_downgrade{};
|
||||
|
||||
// ignores the install if already installed.
|
||||
// checks that every nca is available.
|
||||
bool skip_if_already_installed{};
|
||||
|
||||
// installs tickets only.
|
||||
bool ticket_only{};
|
||||
|
||||
// converts personalised tickets to common tickets, allows for offline play.
|
||||
// this breaks ticket signature so es needs to be patched.
|
||||
// modified common tickets are patched regardless of this setting.
|
||||
bool patch_ticket{};
|
||||
|
||||
// flags to enable / disable install of specific types.
|
||||
bool skip_base{};
|
||||
bool skip_patch{};
|
||||
bool skip_addon{};
|
||||
bool skip_data_patch{};
|
||||
bool skip_ticket{};
|
||||
|
||||
// enables the option to skip sha256 verification.
|
||||
bool skip_nca_hash_verify{};
|
||||
|
||||
// enables the option to skip rsa nca fixed key verification.
|
||||
bool skip_rsa_header_fixed_key_verify{};
|
||||
|
||||
// enables the option to skip rsa npdm fixed key verification.
|
||||
bool skip_rsa_npdm_fixed_key_verify{};
|
||||
|
||||
// if set, it will ignore the distribution bit in the nca header.
|
||||
bool ignore_distribution_bit{};
|
||||
|
||||
// converts titlekey to standard crypto, also known as "ticketless".
|
||||
// this will not work with addon (dlc), so, addon tickets will be installed.
|
||||
bool convert_to_standard_crypto{};
|
||||
|
||||
// encrypts the keak with master key 0, this allows the game to be launched on every fw.
|
||||
// implicitly performs standard crypto.
|
||||
bool lower_master_key{};
|
||||
|
||||
// sets the system_firmware field in the cnmt extended header.
|
||||
// if mkey is higher than fw version, the game still won't launch
|
||||
// as the fw won't have the key to decrypt keak.
|
||||
bool lower_system_version{};
|
||||
};
|
||||
|
||||
Result InstallFromFile(FsFileSystem* fs, const fs::FsPath& path);
|
||||
Result InstallFromStdioFile(const char* path);
|
||||
Result InstallFromSource(std::shared_ptr<source::Base> source);
|
||||
|
||||
Result InstallFromFile(ui::ProgressBox* pbox, FsFileSystem* fs, const fs::FsPath& path);
|
||||
Result InstallFromStdioFile(ui::ProgressBox* pbox, const char* path);
|
||||
Result InstallFromSource(ui::ProgressBox* pbox, std::shared_ptr<source::Base> source);
|
||||
|
||||
} // namespace sphaira::yati
|
||||
@@ -580,10 +580,6 @@ auto App::GetInstallPrompt() -> bool {
|
||||
return g_app->m_install_prompt.Get();
|
||||
}
|
||||
|
||||
auto App::GetThemeShuffleEnable() -> bool {
|
||||
return g_app->m_theme_shuffle.Get();
|
||||
}
|
||||
|
||||
auto App::GetThemeMusicEnable() -> bool {
|
||||
return g_app->m_theme_music.Get();
|
||||
}
|
||||
@@ -604,6 +600,10 @@ auto App::GetTextScrollSpeed() -> long {
|
||||
return g_app->m_text_scroll_speed.Get();
|
||||
}
|
||||
|
||||
auto App::Get12HourTimeEnable() -> bool {
|
||||
return g_app->m_12hour_time.Get();
|
||||
}
|
||||
|
||||
void App::SetNxlinkEnable(bool enable) {
|
||||
if (App::GetNxlinkEnable() != enable) {
|
||||
g_app->m_nxlink_enabled.Set(enable);
|
||||
@@ -737,15 +737,15 @@ void App::SetInstallPrompt(bool enable) {
|
||||
g_app->m_install_prompt.Set(enable);
|
||||
}
|
||||
|
||||
void App::SetThemeShuffleEnable(bool enable) {
|
||||
g_app->m_theme_shuffle.Set(enable);
|
||||
}
|
||||
|
||||
void App::SetThemeMusicEnable(bool enable) {
|
||||
g_app->m_theme_music.Set(enable);
|
||||
PlaySoundEffect(SoundEffect::SoundEffect_Music);
|
||||
}
|
||||
|
||||
void App::Set12HourTimeEnable(bool enable) {
|
||||
g_app->m_12hour_time.Set(enable);
|
||||
}
|
||||
|
||||
void App::SetMtpEnable(bool enable) {
|
||||
if (App::GetMtpEnable() != enable) {
|
||||
g_app->m_mtp_enabled.Set(enable);
|
||||
@@ -1008,6 +1008,10 @@ void App::Draw() {
|
||||
this->queue.presentImage(this->swapchain, slot);
|
||||
}
|
||||
|
||||
auto App::GetApp() -> App* {
|
||||
return g_app;
|
||||
}
|
||||
|
||||
auto App::GetVg() -> NVGcontext* {
|
||||
return g_app->vg;
|
||||
}
|
||||
@@ -1282,9 +1286,11 @@ App::App(const char* argv0) {
|
||||
}
|
||||
}
|
||||
|
||||
// only enable audio in non-applet mode due to audren fatal.
|
||||
// disable audio in applet mode with a suspended application due to audren fatal.
|
||||
// see: https://github.com/ITotalJustice/sphaira/issues/92
|
||||
if (IsApplication()) {
|
||||
if (IsAppletWithSuspendedApp()) {
|
||||
App::Notify("Audio disabled due to suspended game"_i18n);
|
||||
} else {
|
||||
plsrPlayerInit();
|
||||
}
|
||||
|
||||
@@ -1313,11 +1319,7 @@ App::App(const char* argv0) {
|
||||
|
||||
fs::FsPath theme_path{};
|
||||
constexpr fs::FsPath default_theme_path{"romfs:/themes/abyss_theme.ini"};
|
||||
if (App::GetThemeShuffleEnable() && m_theme_meta_entries.size()) {
|
||||
theme_path = m_theme_meta_entries[randomGet64() % m_theme_meta_entries.size()].ini_path;
|
||||
} else {
|
||||
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
|
||||
}
|
||||
ini_gets("config", "theme", default_theme_path, theme_path, sizeof(theme_path), CONFIG_PATH);
|
||||
|
||||
// try and load previous theme, default to previous version otherwise.
|
||||
ThemeMeta theme_meta;
|
||||
|
||||
@@ -757,4 +757,14 @@ auto ToFileAsync(const Api& e) -> bool {
|
||||
return g_thread_queue.Add(e);
|
||||
}
|
||||
|
||||
auto EscapeString(const std::string& str) -> std::string {
|
||||
std::string result;
|
||||
const auto s = curl_escape(str.data(), str.length());
|
||||
if (s) {
|
||||
result = s;
|
||||
curl_free(s);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace sphaira::curl
|
||||
|
||||
@@ -132,9 +132,9 @@ void log_file_write(const char* msg) {
|
||||
}
|
||||
|
||||
void log_file_fwrite(const char* fmt, ...) {
|
||||
std::va_list v{};
|
||||
va_list v{};
|
||||
va_start(v, fmt);
|
||||
log_write_arg(fmt, v);
|
||||
log_write_arg(fmt, &v);
|
||||
va_end(v);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,18 +14,20 @@ std::FILE* file{};
|
||||
int nxlink_socket{};
|
||||
std::mutex mutex{};
|
||||
|
||||
void log_write_arg_internal(const char* s, std::va_list& v) {
|
||||
void log_write_arg_internal(const char* s, std::va_list* v) {
|
||||
if (file) {
|
||||
std::vfprintf(file, s, v);
|
||||
std::vfprintf(file, s, *v);
|
||||
std::fflush(file);
|
||||
}
|
||||
if (nxlink_socket) {
|
||||
std::vprintf(s, v);
|
||||
std::vprintf(s, *v);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
auto log_file_init() -> bool {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (file) {
|
||||
@@ -70,11 +72,11 @@ void log_write(const char* s, ...) {
|
||||
|
||||
std::va_list v{};
|
||||
va_start(v, s);
|
||||
log_write_arg_internal(s, v);
|
||||
log_write_arg_internal(s, &v);
|
||||
va_end(v);
|
||||
}
|
||||
|
||||
void log_write_arg(const char* s, std::va_list& v) {
|
||||
void log_write_arg(const char* s, va_list* v) {
|
||||
std::scoped_lock lock{mutex};
|
||||
if (!file && !nxlink_socket) {
|
||||
return;
|
||||
@@ -83,4 +85,6 @@ void log_write_arg(const char* s, std::va_list& v) {
|
||||
log_write_arg_internal(s, v);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
#endif
|
||||
|
||||
@@ -7,6 +7,14 @@
|
||||
#include <string_view>
|
||||
#include <span>
|
||||
|
||||
#include "yati/nx/nca.hpp"
|
||||
#include "yati/nx/ncm.hpp"
|
||||
#include "yati/nx/npdm.hpp"
|
||||
#include "yati/nx/ns.hpp"
|
||||
#include "yati/nx/es.hpp"
|
||||
#include "yati/nx/keys.hpp"
|
||||
#include "yati/nx/crypto.hpp"
|
||||
|
||||
#include "owo.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "app.hpp"
|
||||
@@ -23,52 +31,9 @@ constexpr u32 PFS0_EXEFS_HASH_BLOCK_SIZE = 0x10000;
|
||||
constexpr u32 PFS0_LOGO_HASH_BLOCK_SIZE = 0x1000;
|
||||
constexpr u32 PFS0_META_HASH_BLOCK_SIZE = 0x1000;
|
||||
constexpr u32 PFS0_PADDING_SIZE = 0x200;
|
||||
constexpr u32 NCA_SECTION_TOTAL = 0x4;
|
||||
constexpr u32 ROMFS_ENTRY_EMPTY = 0xFFFFFFFF;
|
||||
constexpr u32 ROMFS_FILEPARTITION_OFS = 0x200;
|
||||
|
||||
enum NcaDistributionType {
|
||||
NcaDistributionType_System = 0x0,
|
||||
NcaDistributionType_GameCard = 0x1
|
||||
};
|
||||
|
||||
enum NcaContentType {
|
||||
NcaContentType_Program = 0x0,
|
||||
NcaContentType_Meta = 0x1,
|
||||
NcaContentType_Control = 0x2,
|
||||
NcaContentType_Manual = 0x3,
|
||||
NcaContentType_Data = 0x4,
|
||||
NcaContentType_PublicData = 0x5,
|
||||
};
|
||||
|
||||
enum NcaFileSystemType {
|
||||
NcaFileSystemType_RomFS = 0x0,
|
||||
NcaFileSystemType_PFS0 = 0x1
|
||||
};
|
||||
|
||||
enum NcaHashType {
|
||||
NcaHashType_Auto = 0x0,
|
||||
NcaHashType_HierarchicalSha256 = 0x2,
|
||||
NcaHashType_HierarchicalIntegrity = 0x3
|
||||
};
|
||||
|
||||
enum NcaEncryptionType {
|
||||
NcaEncryptionType_Auto = 0x0,
|
||||
NcaEncryptionType_None = 0x1,
|
||||
NcaEncryptionType_AesCtrOld = 0x2,
|
||||
NcaEncryptionType_AesCtr = 0x3,
|
||||
NcaEncryptionType_AesCtrEx = 0x4
|
||||
};
|
||||
|
||||
enum NsApplicationRecordType {
|
||||
// installed
|
||||
NsApplicationRecordType_Installed = 0x3,
|
||||
// application is gamecard, but gamecard isn't insterted
|
||||
NsApplicationRecordType_GamecardMissing = 0x5,
|
||||
// archived
|
||||
NsApplicationRecordType_Archived = 0xB,
|
||||
};
|
||||
|
||||
// stdio-like wrapper for std::vector
|
||||
struct BufHelper {
|
||||
BufHelper() = default;
|
||||
@@ -124,12 +89,6 @@ struct CnmtHeader {
|
||||
};
|
||||
static_assert(sizeof(CnmtHeader) == 0x20);
|
||||
|
||||
struct NcmContentStorageRecord {
|
||||
NcmContentMetaKey key;
|
||||
u8 storage_id; //
|
||||
u8 padding[0x7];
|
||||
};
|
||||
|
||||
struct NcmContentMetaData {
|
||||
NcmContentMetaHeader header;
|
||||
NcmApplicationMetaExtendedHeader extended;
|
||||
@@ -142,7 +101,7 @@ struct NcaMetaEntry {
|
||||
NcaEntry nca_entry;
|
||||
NcmContentMetaHeader content_meta_header{};
|
||||
NcmContentMetaKey content_meta_key{};
|
||||
NcmContentStorageRecord content_storage_record{};
|
||||
ncm::ContentStorageRecord content_storage_record{};
|
||||
NcmContentMetaData content_meta_data{};
|
||||
};
|
||||
|
||||
@@ -171,61 +130,6 @@ struct FileEntry {
|
||||
|
||||
using FileEntries = std::vector<FileEntry>;
|
||||
|
||||
struct NpdmMeta {
|
||||
u32 magic; // "META"
|
||||
u32 signature_key_generation; // +9.0.0
|
||||
u32 _0x8;
|
||||
u8 flags;
|
||||
u8 _0xD;
|
||||
u8 main_thread_priority;
|
||||
u8 main_thread_core_num;
|
||||
u32 _0x10;
|
||||
u32 sys_resource_size; // +3.0.0
|
||||
u32 version;
|
||||
u32 main_thread_stack_size;
|
||||
char title_name[0x10];
|
||||
char product_code[0x10];
|
||||
u8 _0x40[0x30];
|
||||
u32 aci0_offset;
|
||||
u32 aci0_size;
|
||||
u32 acid_offset;
|
||||
u32 acid_size;
|
||||
};
|
||||
|
||||
struct NpdmAcid {
|
||||
u8 rsa_sig[0x100];
|
||||
u8 rsa_pub[0x100];
|
||||
u32 magic; // "ACID"
|
||||
u32 size;
|
||||
u8 version;
|
||||
u8 _0x209[0x1];
|
||||
u8 _0x20A[0x2];
|
||||
u32 flags;
|
||||
u64 program_id_min;
|
||||
u64 program_id_max;
|
||||
u32 fac_offset;
|
||||
u32 fac_size;
|
||||
u32 sac_offset;
|
||||
u32 sac_size;
|
||||
u32 kac_offset;
|
||||
u32 kac_size;
|
||||
u8 _0x238[0x8];
|
||||
};
|
||||
|
||||
struct NpdmAci0 {
|
||||
u32 magic; // "ACI0"
|
||||
u8 _0x4[0xC];
|
||||
u64 program_id;
|
||||
u8 _0x18[0x8];
|
||||
u32 fac_offset;
|
||||
u32 fac_size;
|
||||
u32 sac_offset;
|
||||
u32 sac_size;
|
||||
u32 kac_offset;
|
||||
u32 kac_size;
|
||||
u8 _0x38[0x8];
|
||||
};
|
||||
|
||||
struct NpdmPatch {
|
||||
char title_name[0x10]{"Application"};
|
||||
char product_code[0x10]{};
|
||||
@@ -238,56 +142,6 @@ struct NcapPatch {
|
||||
u64 tid;
|
||||
};
|
||||
|
||||
struct NcaSectionTableEntry {
|
||||
u32 media_start_offset; // divided by 0x200.
|
||||
u32 media_end_offset; // divided by 0x200.
|
||||
u8 _0x8[0x4]; // unknown.
|
||||
u8 _0xC[0x4]; // unknown.
|
||||
};
|
||||
|
||||
struct LayerRegion {
|
||||
u64 offset;
|
||||
u64 size;
|
||||
};
|
||||
|
||||
struct HierarchicalSha256Data {
|
||||
u8 master_hash[0x20];
|
||||
u32 block_size;
|
||||
u32 layer_count;
|
||||
LayerRegion hash_layer;
|
||||
LayerRegion pfs0_layer;
|
||||
LayerRegion unused_layers[3];
|
||||
u8 _0x78[0x80];
|
||||
};
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct HierarchicalIntegrityVerificationLevelInformation {
|
||||
u64 logical_offset;
|
||||
u64 hash_data_size;
|
||||
u32 block_size; // log2
|
||||
u32 _0x14; // reserved
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
struct InfoLevelHash {
|
||||
u32 max_layers;
|
||||
HierarchicalIntegrityVerificationLevelInformation levels[6];
|
||||
u8 signature_salt[0x20];
|
||||
};
|
||||
|
||||
struct IntegrityMetaInfo {
|
||||
u32 magic; // IVFC
|
||||
u32 version;
|
||||
u32 master_hash_size;
|
||||
InfoLevelHash info_level_hash;
|
||||
u8 master_hash[0x20];
|
||||
u8 _0xE0[0x18];
|
||||
};
|
||||
|
||||
static_assert(sizeof(HierarchicalSha256Data) == 0xF8);
|
||||
static_assert(sizeof(IntegrityMetaInfo) == 0xF8);
|
||||
static_assert(sizeof(HierarchicalSha256Data) == sizeof(IntegrityMetaInfo));
|
||||
|
||||
typedef struct romfs_dirent_ctx {
|
||||
u32 entry_offset;
|
||||
struct romfs_dirent_ctx *parent; /* Parent node */
|
||||
@@ -317,70 +171,6 @@ typedef struct {
|
||||
u64 file_partition_size;
|
||||
} romfs_ctx_t;
|
||||
|
||||
struct NcaFsHeader {
|
||||
u16 version; // always 2.
|
||||
u8 fs_type; // see NcaFileSystemType.
|
||||
u8 hash_type; // see NcaHashType.
|
||||
u8 encryption_type; // see NcaEncryptionType.
|
||||
u8 metadata_hash_type;
|
||||
u8 _0x6[0x2]; // empty.
|
||||
|
||||
union {
|
||||
HierarchicalSha256Data hierarchical_sha256_data;
|
||||
IntegrityMetaInfo integrity_meta_info; // used for romfs
|
||||
} hash_data;
|
||||
|
||||
u8 patch_info[0x40];
|
||||
u64 section_ctr;
|
||||
u8 spares_info[0x30];
|
||||
u8 compression_info[0x28];
|
||||
u8 meta_data_hash_data_info[0x30];
|
||||
u8 reserved[0x30];
|
||||
};
|
||||
|
||||
struct NcaSectionHeaderHash {
|
||||
u8 sha256[0x20];
|
||||
};
|
||||
|
||||
struct NcaKeyArea {
|
||||
u8 area[0x10];
|
||||
};
|
||||
|
||||
struct NcaHeader {
|
||||
u8 rsa_fixed_key[0x100];
|
||||
u8 rsa_npdm[0x100]; // key from npdm.
|
||||
u32 magic;
|
||||
u8 distribution_type; // see NcaDistributionType.
|
||||
u8 content_type; // see NcaContentType.
|
||||
u8 old_key_gen; // see NcaOldKeyGeneration.
|
||||
u8 kaek_index; // see NcaKeyAreaEncryptionKeyIndex.
|
||||
u64 size;
|
||||
u64 title_id;
|
||||
u32 context_id;
|
||||
u32 sdk_version;
|
||||
u8 key_gen; // see NcaKeyGeneration.
|
||||
u8 header_1_sig_key_gen;
|
||||
u8 _0x222[0xE]; // empty.
|
||||
FsRightsId rights_id;
|
||||
|
||||
NcaSectionTableEntry fs_table[NCA_SECTION_TOTAL];
|
||||
NcaSectionHeaderHash fs_header_hash[NCA_SECTION_TOTAL];
|
||||
NcaKeyArea key_area[NCA_SECTION_TOTAL];
|
||||
|
||||
u8 _0x340[0xC0]; // empty.
|
||||
|
||||
NcaFsHeader fs_header[NCA_SECTION_TOTAL];
|
||||
};
|
||||
|
||||
constexpr u8 HEADER_KEK_SRC[0x10] = {
|
||||
0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A
|
||||
};
|
||||
|
||||
constexpr u8 HEADER_KEY_SRC[0x20] = {
|
||||
0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0,
|
||||
0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2
|
||||
};
|
||||
|
||||
auto write_padding(BufHelper& buf, u64 off, u64 block) -> u64 {
|
||||
const u64 size = block - (off % block);
|
||||
if (size) {
|
||||
@@ -632,9 +422,9 @@ auto npdm_patch_kc(std::vector<u8>& npdm, u32 off, u32 size, u32 bitmask, u32 va
|
||||
|
||||
// todo: manually build npdm
|
||||
void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
||||
NpdmMeta meta{};
|
||||
NpdmAci0 aci0{};
|
||||
NpdmAcid acid{};
|
||||
npdm::Meta meta{};
|
||||
npdm::Aci0 aci0{};
|
||||
npdm::Acid acid{};
|
||||
std::memcpy(&meta, npdm.data(), sizeof(meta));
|
||||
std::memcpy(&aci0, npdm.data() + meta.aci0_offset, sizeof(aci0));
|
||||
std::memcpy(&acid, npdm.data() + meta.acid_offset, sizeof(acid));
|
||||
@@ -655,7 +445,7 @@ void patch_npdm(std::vector<u8>& npdm, const NpdmPatch& patch) {
|
||||
splGetConfig(SplConfigItem_ExosphereVersion, &ver);
|
||||
ver >>= 40;
|
||||
|
||||
if (ver >= MAKEHOSVERSION(1,7,1)) {
|
||||
if (ver >= MAKEHOSVERSION(1,8,0)) {
|
||||
npdm_patch_kc(npdm, meta.aci0_offset + aci0.kac_offset, aci0.kac_size, 16, BIT(19));
|
||||
npdm_patch_kc(npdm, meta.acid_offset + acid.kac_offset, acid.kac_size, 16, BIT(19));
|
||||
}
|
||||
@@ -805,7 +595,7 @@ void write_nca_padding(BufHelper& buf) {
|
||||
write_padding(buf, buf.tell(), 0x200);
|
||||
}
|
||||
|
||||
void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
|
||||
void nca_encrypt_header(nca::Header* header, std::span<const u8> key) {
|
||||
Aes128XtsContext ctx{};
|
||||
aes128XtsContextCreate(&ctx, key.data(), key.data() + 0x10, true);
|
||||
|
||||
@@ -816,41 +606,41 @@ void nca_encrypt_header(NcaHeader* header, std::span<const u8> key) {
|
||||
}
|
||||
}
|
||||
|
||||
void write_nca_section(NcaHeader& nca_header, u8 index, u64 start, u64 end) {
|
||||
void write_nca_section(nca::Header& nca_header, u8 index, u64 start, u64 end) {
|
||||
auto& section = nca_header.fs_table[index];
|
||||
section.media_start_offset = start / 0x200; // 0xC00 / 0x200
|
||||
section.media_end_offset = end / 0x200; // Section end offset / 200
|
||||
section._0x8[0] = 0x1; // Always 1
|
||||
}
|
||||
|
||||
void write_nca_fs_header_pfs0(NcaHeader& nca_header, u8 index, const std::vector<u8>& master_hash, u64 hash_table_size, u32 block_size) {
|
||||
void write_nca_fs_header_pfs0(nca::Header& nca_header, u8 index, const std::vector<u8>& master_hash, u64 hash_table_size, u32 block_size) {
|
||||
auto& fs_header = nca_header.fs_header[index];
|
||||
fs_header.hash_type = NcaHashType_HierarchicalSha256;
|
||||
fs_header.fs_type = NcaFileSystemType_PFS0;
|
||||
fs_header.hash_type = nca::HashType_HierarchicalSha256;
|
||||
fs_header.fs_type = nca::FileSystemType_PFS0;
|
||||
fs_header.version = 0x2; // Always 2
|
||||
fs_header.hash_data.hierarchical_sha256_data.layer_count = 0x2;
|
||||
fs_header.hash_data.hierarchical_sha256_data.block_size = block_size;
|
||||
fs_header.encryption_type = NcaEncryptionType_None;
|
||||
fs_header.encryption_type = nca::EncryptionType_None;
|
||||
fs_header.hash_data.hierarchical_sha256_data.hash_layer.size = hash_table_size;
|
||||
std::memcpy(fs_header.hash_data.hierarchical_sha256_data.master_hash, master_hash.data(), master_hash.size());
|
||||
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
||||
}
|
||||
|
||||
void write_nca_fs_header_romfs(NcaHeader& nca_header, u8 index) {
|
||||
void write_nca_fs_header_romfs(nca::Header& nca_header, u8 index) {
|
||||
auto& fs_header = nca_header.fs_header[index];
|
||||
fs_header.hash_type = NcaHashType_HierarchicalIntegrity;
|
||||
fs_header.fs_type = NcaFileSystemType_RomFS;
|
||||
fs_header.hash_type = nca::HashType_HierarchicalIntegrity;
|
||||
fs_header.fs_type = nca::FileSystemType_RomFS;
|
||||
fs_header.version = 0x2; // Always 2
|
||||
fs_header.hash_data.integrity_meta_info.magic = 0x43465649;
|
||||
fs_header.hash_data.integrity_meta_info.version = 0x20000; // Always 0x20000
|
||||
fs_header.hash_data.integrity_meta_info.master_hash_size = SHA256_HASH_SIZE;
|
||||
fs_header.hash_data.integrity_meta_info.info_level_hash.max_layers = 0x7;
|
||||
fs_header.encryption_type = NcaEncryptionType_None;
|
||||
fs_header.encryption_type = nca::EncryptionType_None;
|
||||
fs_header.hash_data.integrity_meta_info.info_level_hash.levels[5].block_size = 0x0E; // 0x4000
|
||||
sha256CalculateHash(&nca_header.fs_header_hash[index], &fs_header, sizeof(fs_header));
|
||||
}
|
||||
|
||||
void write_nca_pfs0(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||
void write_nca_pfs0(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||
const auto pfs0 = build_pfs0(entries);
|
||||
const auto pfs0_hash_table = build_pfs0_hash_table(pfs0, block_size);
|
||||
const auto pfs0_master_hash = build_pfs0_master_hash(pfs0_hash_table);
|
||||
@@ -887,7 +677,7 @@ auto ivfc_create_level(const std::vector<u8>& src) -> std::vector<u8> {
|
||||
return buf.buf;
|
||||
}
|
||||
|
||||
void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||
void write_nca_romfs(nca::Header& nca_header, u8 index, const FileEntries& entries, u32 block_size, BufHelper& buf) {
|
||||
auto& fs_header = nca_header.fs_header[index];
|
||||
auto& meta_info = fs_header.hash_data.integrity_meta_info;
|
||||
auto& info_level_hash = meta_info.info_level_hash;
|
||||
@@ -921,22 +711,22 @@ void write_nca_romfs(NcaHeader& nca_header, u8 index, const FileEntries& entries
|
||||
write_nca_fs_header_romfs(nca_header, index);
|
||||
}
|
||||
|
||||
void write_nca_header_encypted(NcaHeader& nca_header, u64 tid, std::span<const u8> key, NcaContentType type, BufHelper& buf) {
|
||||
nca_header.magic = 0x3341434E;
|
||||
nca_header.distribution_type = NcaDistributionType_System;
|
||||
void write_nca_header_encypted(nca::Header& nca_header, u64 tid, const keys::Keys& keys, nca::ContentType type, BufHelper& buf) {
|
||||
nca_header.magic = NCA3_MAGIC;
|
||||
nca_header.distribution_type = nca::DistributionType_System;
|
||||
nca_header.content_type = type;
|
||||
nca_header.title_id = tid;
|
||||
nca_header.sdk_version = 0x000C1100;
|
||||
nca_header.size = buf.tell();
|
||||
|
||||
nca_encrypt_header(&nca_header, key);
|
||||
nca_encrypt_header(&nca_header, keys.header_key);
|
||||
buf.seek(0);
|
||||
buf.write(&nca_header, sizeof(nca_header));
|
||||
}
|
||||
|
||||
auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry {
|
||||
auto create_program_nca(u64 tid, const keys::Keys& keys, const FileEntries& exefs, const FileEntries& romfs, const FileEntries& logo) -> NcaEntry {
|
||||
BufHelper buf;
|
||||
NcaHeader nca_header{};
|
||||
nca::Header nca_header{};
|
||||
buf.write(&nca_header, sizeof(nca_header));
|
||||
|
||||
write_nca_pfs0(nca_header, 0, exefs, PFS0_EXEFS_HASH_BLOCK_SIZE, buf);
|
||||
@@ -945,23 +735,23 @@ auto create_program_nca(u64 tid, std::span<const u8> key, const FileEntries& exe
|
||||
if (logo.size() == 2 && !logo[0].data.empty() && !logo[1].data.empty()) {
|
||||
write_nca_pfs0(nca_header, 2, logo, PFS0_LOGO_HASH_BLOCK_SIZE, buf);
|
||||
}
|
||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Program, buf);
|
||||
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Program, buf);
|
||||
|
||||
return {buf, NcmContentType_Program};
|
||||
}
|
||||
|
||||
auto create_control_nca(u64 tid, std::span<const u8> key, const FileEntries& romfs) -> NcaEntry{
|
||||
NcaHeader nca_header{};
|
||||
auto create_control_nca(u64 tid, const keys::Keys& keys, const FileEntries& romfs) -> NcaEntry{
|
||||
nca::Header nca_header{};
|
||||
BufHelper buf;
|
||||
buf.write(&nca_header, sizeof(nca_header));
|
||||
|
||||
write_nca_romfs(nca_header, 0, romfs, IVFC_HASH_BLOCK_SIZE, buf);
|
||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Control, buf);
|
||||
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Control, buf);
|
||||
|
||||
return {buf, NcmContentType_Control};
|
||||
}
|
||||
|
||||
auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id, const std::vector<NcaEntry>& ncas) -> NcaMetaEntry {
|
||||
auto create_meta_nca(u64 tid, const keys::Keys& keys, NcmStorageId storage_id, const std::vector<NcaEntry>& ncas) -> NcaMetaEntry {
|
||||
CnmtHeader cnmt_header{};
|
||||
NcmApplicationMetaExtendedHeader cnmt_extended{};
|
||||
NcmPackagedContentInfo packaged_content_info[2]{};
|
||||
@@ -997,10 +787,10 @@ auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id,
|
||||
std::snprintf(cnmt_name, sizeof(cnmt_name), "Application_%016lX.cnmt", tid);
|
||||
add_file_entry(cnmt, cnmt_name, cnmt_buf.buf.data(), cnmt_buf.buf.size());
|
||||
|
||||
NcaHeader nca_header{};
|
||||
nca::Header nca_header{};
|
||||
buf.write(&nca_header, sizeof(nca_header));
|
||||
write_nca_pfs0(nca_header, 0, cnmt, PFS0_META_HASH_BLOCK_SIZE, buf);
|
||||
write_nca_header_encypted(nca_header, tid, key, NcaContentType_Meta, buf);
|
||||
write_nca_header_encypted(nca_header, tid, keys, nca::ContentType_Meta, buf);
|
||||
|
||||
// entry
|
||||
NcaMetaEntry entry{buf, NcmContentType_Meta};
|
||||
@@ -1040,26 +830,6 @@ auto create_meta_nca(u64 tid, std::span<const u8> key, NcmStorageId storage_id,
|
||||
return entry;
|
||||
}
|
||||
|
||||
Result nsDeleteApplicationRecord(Service* srv, u64 tid) {
|
||||
return serviceDispatchIn(srv, 27, tid);
|
||||
}
|
||||
|
||||
Result nsPushApplicationRecord(Service* srv, u64 tid, const NcmContentStorageRecord* records, u32 count) {
|
||||
const struct {
|
||||
u8 last_modified_event;
|
||||
u8 padding[0x7];
|
||||
u64 tid;
|
||||
} in = { NsApplicationRecordType_Installed, {0}, tid };
|
||||
|
||||
return serviceDispatchIn(srv, 16, in,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
||||
.buffers = { { records, sizeof(NcmContentStorageRecord) * count } });
|
||||
}
|
||||
|
||||
Result nsInvalidateApplicationControlCache(Service* srv, u64 tid) {
|
||||
return serviceDispatchIn(srv, 404, tid);
|
||||
}
|
||||
|
||||
auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStorageId storage_id) -> Result {
|
||||
R_UNLESS(!config.nro_path.empty(), OwoError_BadArgs);
|
||||
// R_UNLESS(!config.icon.empty(), OwoError_BadArgs);
|
||||
@@ -1073,14 +843,8 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
||||
R_TRY(nsInitialize());
|
||||
ON_SCOPE_EXIT(nsExit());
|
||||
|
||||
// generate header kek
|
||||
u8 header_kek[0x20];
|
||||
R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek));
|
||||
// gen header key 0
|
||||
u8 key[0x20];
|
||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, key));
|
||||
// gen header key 1
|
||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, key + 0x10));
|
||||
keys::Keys keys;
|
||||
R_TRY(keys::parse_keys(keys, false));
|
||||
|
||||
// fix args to include nro path
|
||||
if (config.args.empty()) {
|
||||
@@ -1124,7 +888,7 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
||||
patch_npdm(exefs[1].data, npdm_patch);
|
||||
|
||||
nca_entries.emplace_back(
|
||||
create_program_nca(tid, key, exefs, romfs, logo)
|
||||
create_program_nca(tid, keys, exefs, romfs, logo)
|
||||
);
|
||||
} else {
|
||||
nca_entries.emplace_back(
|
||||
@@ -1147,18 +911,18 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
||||
add_file_entry(romfs, "/icon_AmericanEnglish.dat", config.icon);
|
||||
|
||||
nca_entries.emplace_back(
|
||||
create_control_nca(tid, key, romfs)
|
||||
create_control_nca(tid, keys, romfs)
|
||||
);
|
||||
}
|
||||
|
||||
// create meta
|
||||
NcmContentMetaHeader content_meta_header;
|
||||
NcmContentMetaKey content_meta_key;
|
||||
NcmContentStorageRecord content_storage_record;
|
||||
ncm::ContentStorageRecord content_storage_record;
|
||||
NcmContentMetaData content_meta_data;
|
||||
{
|
||||
pbox->NewTransfer("Creating Meta"_i18n).UpdateTransfer(2, 8);
|
||||
const auto meta_entry = create_meta_nca(tid, key, storage_id, nca_entries);
|
||||
const auto meta_entry = create_meta_nca(tid, keys, storage_id, nca_entries);
|
||||
|
||||
nca_entries.emplace_back(meta_entry.nca_entry);
|
||||
content_meta_header = meta_entry.content_meta_header;
|
||||
@@ -1218,15 +982,15 @@ auto install_forwader_internal(ui::ProgressBox* pbox, OwoConfig& config, NcmStor
|
||||
|
||||
// remove previous application record
|
||||
if (already_installed || hosversionBefore(2,0,0)) {
|
||||
const auto rc = nsDeleteApplicationRecord(srv_ptr, tid);
|
||||
const auto rc = ns::DeleteApplicationRecord(srv_ptr, tid);
|
||||
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
||||
}
|
||||
|
||||
R_TRY(nsPushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
|
||||
R_TRY(ns::PushApplicationRecord(srv_ptr, tid, &content_storage_record, 1));
|
||||
|
||||
// force flush
|
||||
if (already_installed || hosversionBefore(2,0,0)) {
|
||||
const auto rc = nsInvalidateApplicationControlCache(srv_ptr, tid);
|
||||
const auto rc = ns::InvalidateApplicationControlCache(srv_ptr, tid);
|
||||
R_UNLESS(R_SUCCEEDED(rc) || hosversionBefore(2,0,0), rc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,14 +42,15 @@ Result ShowInternal(Config& cfg, const char* guide, const char* initial, s64 len
|
||||
} // namespace
|
||||
|
||||
Result ShowText(std::string& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||
Config cfg;
|
||||
Config cfg{};
|
||||
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
|
||||
out = cfg.out_text;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result ShowNumPad(s64& out, const char* guide, const char* initial, s64 len_min, s64 len_max) {
|
||||
Config cfg;
|
||||
Config cfg{};
|
||||
cfg.numpad = true;
|
||||
R_TRY(ShowInternal(cfg, guide, initial, len_min, len_max));
|
||||
out = std::atoll(cfg.out_text);
|
||||
R_SUCCEED();
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
#include "owo.hpp"
|
||||
#include "swkbd.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "yati/source/file.hpp"
|
||||
|
||||
#include <minIni.h>
|
||||
#include <minizip/unzip.h>
|
||||
@@ -52,6 +54,10 @@ constexpr std::string_view VIDEO_EXTENSIONS[] = {
|
||||
constexpr std::string_view IMAGE_EXTENSIONS[] = {
|
||||
"png", "jpg", "jpeg", "bmp", "gif",
|
||||
};
|
||||
constexpr std::string_view INSTALL_EXTENSIONS[] = {
|
||||
"nsp", "xci", "nsz", "xcz",
|
||||
};
|
||||
|
||||
|
||||
struct RomDatabaseEntry {
|
||||
std::string_view folder;
|
||||
@@ -294,6 +300,8 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
nro_launch(GetNewPathCurrent());
|
||||
}
|
||||
}));
|
||||
} else if (App::GetInstallEnable() && IsExtension(entry.GetExtension(), INSTALL_EXTENSIONS)) {
|
||||
InstallFile(GetEntry());
|
||||
} else {
|
||||
const auto assoc_list = FindFileAssocFor();
|
||||
if (!assoc_list.empty()) {
|
||||
@@ -406,7 +414,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
}
|
||||
log_write("clicked on delete\n");
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
"Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
||||
"Delete Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::PopToMenu();
|
||||
OnDeleteCallback();
|
||||
@@ -421,7 +429,7 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Paste"_i18n, [this](){
|
||||
const std::string buf = "Paste "_i18n + std::to_string(m_selected_files.size()) + " file(s)?"_i18n;
|
||||
App::Push(std::make_shared<OptionBox>(
|
||||
buf, "No"_i18n, "Yes"_i18n, 1, [this](auto op_index){
|
||||
buf, "No"_i18n, "Yes"_i18n, 0, [this](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::PopToMenu();
|
||||
OnPasteCallback();
|
||||
@@ -459,6 +467,32 @@ Menu::Menu(const std::vector<NroEntry>& nro_entries) : MenuBase{"FileBrowser"_i1
|
||||
}));
|
||||
}
|
||||
|
||||
// if install is enabled, check if all currently selected files are installable.
|
||||
if (m_entries_current.size() && App::GetInstallEnable()) {
|
||||
bool should_install = true;
|
||||
if (!m_selected_count) {
|
||||
should_install = IsExtension(GetEntry().GetExtension(), INSTALL_EXTENSIONS);
|
||||
} else {
|
||||
const auto entries = GetSelectedEntries();
|
||||
for (auto&e : entries) {
|
||||
if (!IsExtension(e.GetExtension(), INSTALL_EXTENSIONS)) {
|
||||
should_install = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (should_install) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Install"_i18n, [this](){
|
||||
if (!m_selected_count) {
|
||||
InstallFile(GetEntry());
|
||||
} else {
|
||||
InstallFiles(GetSelectedEntries());
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::RIGHT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
@@ -628,6 +662,9 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
icon = ThemeEntryID_ICON_VIDEO;
|
||||
} else if (IsExtension(ext, IMAGE_EXTENSIONS)) {
|
||||
icon = ThemeEntryID_ICON_IMAGE;
|
||||
} else if (IsExtension(ext, INSTALL_EXTENSIONS)) {
|
||||
// todo: maybe replace this icon with something else?
|
||||
icon = ThemeEntryID_ICON_NRO;
|
||||
} else if (IsExtension(ext, "zip")) {
|
||||
icon = ThemeEntryID_ICON_ZIP;
|
||||
} else if (IsExtension(ext, "nro")) {
|
||||
@@ -775,6 +812,34 @@ void Menu::InstallForwarder() {
|
||||
));
|
||||
}
|
||||
|
||||
void Menu::InstallFile(const FileEntry& target) {
|
||||
std::vector<FileEntry> targets{target};
|
||||
InstallFiles(targets);
|
||||
}
|
||||
|
||||
void Menu::InstallFiles(const std::vector<FileEntry>& targets) {
|
||||
App::Push(std::make_shared<OptionBox>("Install Selected files?"_i18n, "No"_i18n, "Yes"_i18n, 0, [this, targets](auto op_index){
|
||||
if (op_index && *op_index) {
|
||||
App::PopToMenu();
|
||||
|
||||
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this, targets](auto pbox) mutable -> bool {
|
||||
for (auto& e : targets) {
|
||||
const auto rc = yati::InstallFromFile(pbox, &m_fs->m_fs, GetNewPath(e));
|
||||
if (rc == yati::Result_Cancelled) {
|
||||
break;
|
||||
} else if (R_FAILED(rc)) {
|
||||
return false;
|
||||
} else {
|
||||
App::Notify("Installed " + e.GetName());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
||||
log_write("new scan path: %s\n", new_path.s);
|
||||
if (!is_walk_up && !m_path.empty() && !m_entries_current.empty()) {
|
||||
@@ -793,7 +858,7 @@ auto Menu::Scan(const fs::FsPath& new_path, bool is_walk_up) -> Result {
|
||||
}
|
||||
|
||||
FsDir d;
|
||||
R_TRY(m_fs->OpenDirectory(new_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles | FsDirOpenMode_NoFileSize, &d));
|
||||
R_TRY(m_fs->OpenDirectory(new_path, FsDirOpenMode_ReadDirs | FsDirOpenMode_ReadFiles, &d));
|
||||
ON_SCOPE_EXIT(fsDirClose(&d));
|
||||
|
||||
s64 count;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "ui/menus/irs_menu.hpp"
|
||||
#include "ui/menus/themezer.hpp"
|
||||
#include "ui/menus/ghdl.hpp"
|
||||
#include "ui/menus/usb_menu.hpp"
|
||||
|
||||
#include "ui/sidebar.hpp"
|
||||
#include "ui/popup_list.hpp"
|
||||
@@ -246,13 +247,13 @@ MainMenu::MainMenu() {
|
||||
App::SetTheme(index_out);
|
||||
}, App::GetThemeIndex()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Shuffle"_i18n, App::GetThemeShuffleEnable(), [this](bool& enable){
|
||||
App::SetThemeShuffleEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Music"_i18n, App::GetThemeMusicEnable(), [this](bool& enable){
|
||||
App::SetThemeMusicEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("12 Hour Time"_i18n, App::Get12HourTimeEnable(), [this](bool& enable){
|
||||
App::Set12HourTimeEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Network"_i18n, [this](){
|
||||
@@ -317,16 +318,18 @@ MainMenu::MainMenu() {
|
||||
WebShow("https://lite.duckduckgo.com/lite");
|
||||
}));
|
||||
}
|
||||
|
||||
if (App::GetApp()->m_install.Get()) {
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Usb Install"_i18n, [](){
|
||||
App::Push(std::make_shared<menu::usb::Menu>());
|
||||
}));
|
||||
}
|
||||
}));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Advanced"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Advanced Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
SidebarEntryArray::Items install_items;
|
||||
install_items.push_back("System memory"_i18n);
|
||||
install_items.push_back("microSD card"_i18n);
|
||||
|
||||
SidebarEntryArray::Items text_scroll_speed_items;
|
||||
text_scroll_speed_items.push_back("Slow"_i18n);
|
||||
text_scroll_speed_items.push_back("Normal"_i18n);
|
||||
@@ -340,21 +343,94 @@ MainMenu::MainMenu() {
|
||||
App::SetReplaceHbmenuEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Install forwarders"_i18n, App::GetInstallEnable(), [this](bool& enable){
|
||||
App::SetInstallEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
|
||||
App::SetInstallSdEnable(index_out);
|
||||
}, (s64)App::GetInstallSdEnable()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
|
||||
App::SetInstallPrompt(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Text scroll speed"_i18n, text_scroll_speed_items, [this](s64& index_out){
|
||||
App::SetTextScrollSpeed(index_out);
|
||||
}, (s64)App::GetTextScrollSpeed()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryCallback>("Install options"_i18n, [this](){
|
||||
auto options = std::make_shared<Sidebar>("Install Options"_i18n, Sidebar::Side::LEFT);
|
||||
ON_SCOPE_EXIT(App::Push(options));
|
||||
|
||||
SidebarEntryArray::Items install_items;
|
||||
install_items.push_back("System memory"_i18n);
|
||||
install_items.push_back("microSD card"_i18n);
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Enable"_i18n, App::GetInstallEnable(), [this](bool& enable){
|
||||
App::SetInstallEnable(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Show install warning"_i18n, App::GetInstallPrompt(), [this](bool& enable){
|
||||
App::SetInstallPrompt(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryArray>("Install location"_i18n, install_items, [this](s64& index_out){
|
||||
App::SetInstallSdEnable(index_out);
|
||||
}, (s64)App::GetInstallSdEnable()));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Allow downgrade"_i18n, App::GetApp()->m_allow_downgrade.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_allow_downgrade.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Skip if already installed"_i18n, App::GetApp()->m_skip_if_already_installed.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_skip_if_already_installed.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Ticket only"_i18n, App::GetApp()->m_ticket_only.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_ticket_only.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Patch ticket"_i18n, App::GetApp()->m_patch_ticket.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_patch_ticket.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Skip base"_i18n, App::GetApp()->m_skip_base.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_skip_base.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Skip Patch"_i18n, App::GetApp()->m_skip_patch.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_skip_patch.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Skip addon"_i18n, App::GetApp()->m_skip_addon.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_skip_addon.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Skip data patch"_i18n, App::GetApp()->m_skip_data_patch.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_skip_data_patch.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Skip ticket"_i18n, App::GetApp()->m_skip_ticket.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_skip_ticket.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_skip_nca_hash_verify.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_skip_rsa_header_fixed_key_verify.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_skip_rsa_npdm_fixed_key_verify.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_ignore_distribution_bit.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_convert_to_standard_crypto.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Lower master key"_i18n, App::GetApp()->m_lower_master_key.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_lower_master_key.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
|
||||
options->Add(std::make_shared<SidebarEntryBool>("Lower system version"_i18n, App::GetApp()->m_lower_system_version.Get(), [this](bool& enable){
|
||||
App::GetApp()->m_lower_system_version.Set(enable);
|
||||
}, "Enabled"_i18n, "Disabled"_i18n));
|
||||
}));
|
||||
}));
|
||||
}})
|
||||
);
|
||||
|
||||
@@ -38,22 +38,28 @@ void MenuBase::Draw(NVGcontext* vg, Theme* theme) {
|
||||
|
||||
nvgFontSize(vg, font_size);
|
||||
|
||||
#define draw(...) \
|
||||
#define draw(colour, ...) \
|
||||
gfx::textBounds(vg, 0, 0, bounds, __VA_ARGS__); \
|
||||
start_x -= bounds[2] - bounds[0]; \
|
||||
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT), __VA_ARGS__); \
|
||||
gfx::drawTextArgs(vg, start_x, start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_BOTTOM, theme->GetColour(colour), __VA_ARGS__); \
|
||||
start_x -= spacing;
|
||||
|
||||
// draw("version %s", APP_VERSION);
|
||||
draw("%u\uFE6A", m_battery_percetange);
|
||||
draw("%02u:%02u:%02u", m_tm.tm_hour, m_tm.tm_min, m_tm.tm_sec);
|
||||
if (m_ip) {
|
||||
draw("%u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
|
||||
draw(ThemeEntryID_TEXT, "%u\uFE6A", m_battery_percetange);
|
||||
|
||||
if (App::Get12HourTimeEnable()) {
|
||||
draw(ThemeEntryID_TEXT, "%02u:%02u:%02u %s", (m_tm.tm_hour == 0 || m_tm.tm_hour == 12) ? 12 : m_tm.tm_hour % 12, m_tm.tm_min, m_tm.tm_sec, (m_tm.tm_hour < 12) ? "AM" : "PM");
|
||||
} else {
|
||||
draw(("No Internet"_i18n).c_str());
|
||||
draw(ThemeEntryID_TEXT, "%02u:%02u:%02u", m_tm.tm_hour, m_tm.tm_min, m_tm.tm_sec);
|
||||
}
|
||||
|
||||
if (m_ip) {
|
||||
draw(ThemeEntryID_TEXT, "%u.%u.%u.%u", m_ip&0xFF, (m_ip>>8)&0xFF, (m_ip>>16)&0xFF, (m_ip>>24)&0xFF);
|
||||
} else {
|
||||
draw(ThemeEntryID_TEXT, ("No Internet"_i18n).c_str());
|
||||
}
|
||||
if (!App::IsApplication()) {
|
||||
draw(("[Applet Mode]"_i18n).c_str());
|
||||
draw(ThemeEntryID_ERROR, ("[Applet Mode]"_i18n).c_str());
|
||||
}
|
||||
|
||||
#undef draw
|
||||
|
||||
@@ -68,7 +68,7 @@ auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
|
||||
if (is_pack) {
|
||||
cmd = "packList";
|
||||
// fields += ",themes{id,creator{display_name},details{name,description},last_updated,dl_count,like_count,target,preview{original,thumb}}";
|
||||
fields += ",themes{id, preview{thumb}}";
|
||||
fields += ",themes{id,preview{thumb}}";
|
||||
} else {
|
||||
cmd = "themeList";
|
||||
p0 += ",$target:String";
|
||||
@@ -92,7 +92,9 @@ auto apiBuildUrlListInternal(const Config& e, bool is_pack) -> std::string {
|
||||
json += ",\"query\":\"" + e.query + "\"";
|
||||
}
|
||||
|
||||
return api+"("+p0+"){"+cmd+"("+p1+")"+fields+"}}&variables={"+json+"}";
|
||||
json = curl::EscapeString('{'+json+'}');
|
||||
|
||||
return api+"("+p0+"){"+cmd+"("+p1+")"+fields+"}}&variables="+json;
|
||||
}
|
||||
|
||||
auto apiBuildUrlDownloadInternal(const std::string& id, bool is_pack) -> std::string {
|
||||
@@ -363,7 +365,13 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
fs::FsNativeSd().CreateDirectoryRecursively(CACHE_PATH);
|
||||
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this]{
|
||||
SetPop();
|
||||
// if search is valid, then we are in search mode, return back to normal.
|
||||
if (!m_search.empty()) {
|
||||
m_search.clear();
|
||||
InvalidateAllPages();
|
||||
} else {
|
||||
SetPop();
|
||||
}
|
||||
}});
|
||||
|
||||
this->SetActions(
|
||||
@@ -478,6 +486,8 @@ Menu::Menu() : MenuBase{"Themezer"_i18n} {
|
||||
std::string out;
|
||||
if (R_SUCCEEDED(swkbd::ShowText(out)) && !out.empty()) {
|
||||
m_search = out;
|
||||
// PackListDownload();
|
||||
InvalidateAllPages();
|
||||
}
|
||||
}));
|
||||
}}),
|
||||
@@ -650,11 +660,9 @@ void Menu::OnFocusGained() {
|
||||
}
|
||||
|
||||
void Menu::InvalidateAllPages() {
|
||||
for (auto& e : m_pages) {
|
||||
e.m_packList.clear();
|
||||
e.m_ready = PageLoadState::None;
|
||||
}
|
||||
|
||||
m_pages.clear();
|
||||
m_pages.resize(1);
|
||||
m_page_index = 0;
|
||||
PackListDownload();
|
||||
}
|
||||
|
||||
|
||||
170
sphaira/source/ui/menus/usb_menu.cpp
Normal file
@@ -0,0 +1,170 @@
|
||||
#include "ui/menus/usb_menu.hpp"
|
||||
#include "yati/yati.hpp"
|
||||
#include "app.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include "ui/nvg_util.hpp"
|
||||
#include "i18n.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::ui::menu::usb {
|
||||
namespace {
|
||||
|
||||
constexpr u64 CONNECTION_TIMEOUT = 1e+9 * 3;
|
||||
constexpr u64 TRANSFER_TIMEOUT = 1e+9 * 5;
|
||||
|
||||
void thread_func(void* user) {
|
||||
auto app = static_cast<Menu*>(user);
|
||||
|
||||
for (;;) {
|
||||
if (app->GetToken().stop_requested()) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto rc = app->m_usb_source->WaitForConnection(CONNECTION_TIMEOUT, app->m_usb_speed, app->m_usb_count);
|
||||
mutexLock(&app->m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&app->m_mutex));
|
||||
|
||||
if (R_SUCCEEDED(rc)) {
|
||||
app->m_state = State::Connected;
|
||||
break;
|
||||
} else if (R_FAILED(rc) && R_VALUE(rc) != 0xEA01) {
|
||||
log_write("got: 0x%X value: 0x%X\n", rc, R_VALUE(rc));
|
||||
app->m_state = State::Failed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Menu::Menu() : MenuBase{"USB"_i18n} {
|
||||
SetAction(Button::B, Action{"Back"_i18n, [this](){
|
||||
SetPop();
|
||||
}});
|
||||
|
||||
// if mtp is enabled, disable it for now.
|
||||
m_was_mtp_enabled = App::GetMtpEnable();
|
||||
if (m_was_mtp_enabled) {
|
||||
App::Notify("Disable MTP for usb install"_i18n);
|
||||
App::SetMtpEnable(false);
|
||||
}
|
||||
|
||||
// 3 second timeout for transfers.
|
||||
m_usb_source = std::make_shared<yati::source::Usb>(TRANSFER_TIMEOUT);
|
||||
if (R_FAILED(m_usb_source->GetOpenResult())) {
|
||||
log_write("usb init open\n");
|
||||
m_state = State::Failed;
|
||||
} else {
|
||||
if (R_FAILED(m_usb_source->Init())) {
|
||||
log_write("usb init failed\n");
|
||||
m_state = State::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
mutexInit(&m_mutex);
|
||||
if (m_state != State::Failed) {
|
||||
threadCreate(&m_thread, thread_func, this, nullptr, 1024*32, 0x2C, 1);
|
||||
threadStart(&m_thread);
|
||||
}
|
||||
}
|
||||
|
||||
Menu::~Menu() {
|
||||
// signal for thread to exit and wait.
|
||||
m_stop_source.request_stop();
|
||||
threadWaitForExit(&m_thread);
|
||||
threadClose(&m_thread);
|
||||
|
||||
// free usb source before re-enabling mtp.
|
||||
log_write("closing data!!!!\n");
|
||||
m_usb_source.reset();
|
||||
|
||||
if (m_was_mtp_enabled) {
|
||||
App::Notify("Re-enabled MTP"_i18n);
|
||||
App::SetMtpEnable(true);
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Update(Controller* controller, TouchInfo* touch) {
|
||||
MenuBase::Update(controller, touch);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
break;
|
||||
|
||||
case State::Connected:
|
||||
log_write("set to progress\n");
|
||||
m_state = State::Progress;
|
||||
log_write("got connection\n");
|
||||
App::Push(std::make_shared<ui::ProgressBox>("Installing App"_i18n, [this](auto pbox) mutable -> bool {
|
||||
log_write("inside progress box\n");
|
||||
for (u32 i = 0; i < m_usb_count; i++) {
|
||||
const auto rc = yati::InstallFromSource(pbox, m_usb_source);
|
||||
if (R_FAILED(rc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
App::Notify("Installed via usb"_i18n);
|
||||
m_usb_source->Finished();
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [this](bool result){
|
||||
if (result) {
|
||||
App::Notify("Usb install success!"_i18n);
|
||||
} else {
|
||||
App::Notify("Usb install failed!"_i18n);
|
||||
}
|
||||
m_state = State::Done;
|
||||
this->SetPop();
|
||||
}));
|
||||
break;
|
||||
|
||||
case State::Progress:
|
||||
break;
|
||||
|
||||
case State::Done:
|
||||
break;
|
||||
|
||||
case State::Failed:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::Draw(NVGcontext* vg, Theme* theme) {
|
||||
MenuBase::Draw(vg, theme);
|
||||
|
||||
mutexLock(&m_mutex);
|
||||
ON_SCOPE_EXIT(mutexUnlock(&m_mutex));
|
||||
|
||||
switch (m_state) {
|
||||
case State::None:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Waiting for connection..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Connected:
|
||||
break;
|
||||
|
||||
case State::Progress:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Transferring data..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Done:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Press B to Exit..."_i18n.c_str());
|
||||
break;
|
||||
|
||||
case State::Failed:
|
||||
gfx::drawTextArgs(vg, SCREEN_WIDTH / 2.f, SCREEN_HEIGHT / 2.f, 36.f, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "Failed to init usb..."_i18n.c_str());
|
||||
this->SetPop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Menu::OnFocusGained() {
|
||||
MenuBase::OnFocusGained();
|
||||
}
|
||||
|
||||
} // namespace sphaira::ui::menu::usb
|
||||
@@ -102,6 +102,14 @@ auto ProgressBox::Draw(NVGcontext* vg, Theme* theme) -> void {
|
||||
}
|
||||
}
|
||||
|
||||
auto ProgressBox::SetTitle(const std::string& title) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
m_title = title;
|
||||
mutexUnlock(&m_mutex);
|
||||
Yield();
|
||||
return *this;
|
||||
}
|
||||
|
||||
auto ProgressBox::NewTransfer(const std::string& transfer) -> ProgressBox& {
|
||||
mutexLock(&m_mutex);
|
||||
m_transfer = transfer;
|
||||
|
||||
67
sphaira/source/yati/container/nsp.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "yati/container/nsp.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
namespace {
|
||||
|
||||
#define PFS0_MAGIC 0x30534650
|
||||
|
||||
struct Pfs0Header {
|
||||
u32 magic;
|
||||
u32 total_files;
|
||||
u32 string_table_size;
|
||||
u32 padding;
|
||||
};
|
||||
|
||||
struct Pfs0FileTableEntry {
|
||||
u64 data_offset;
|
||||
u64 data_size;
|
||||
u32 name_offset;
|
||||
u32 padding;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Result Nsp::Validate(source::Base* source) {
|
||||
u32 magic;
|
||||
u64 bytes_read;
|
||||
R_TRY(source->Read(std::addressof(magic), 0, sizeof(magic), std::addressof(bytes_read)));
|
||||
R_UNLESS(magic == PFS0_MAGIC, 0x1);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Nsp::GetCollections(Collections& out) {
|
||||
u64 bytes_read;
|
||||
s64 off = 0;
|
||||
|
||||
// get header
|
||||
Pfs0Header header{};
|
||||
R_TRY(m_source->Read(std::addressof(header), off, sizeof(header), std::addressof(bytes_read)));
|
||||
R_UNLESS(header.magic == PFS0_MAGIC, 0x1);
|
||||
off += bytes_read;
|
||||
|
||||
// get file table
|
||||
std::vector<Pfs0FileTableEntry> file_table(header.total_files);
|
||||
R_TRY(m_source->Read(file_table.data(), off, file_table.size() * sizeof(Pfs0FileTableEntry), std::addressof(bytes_read)))
|
||||
off += bytes_read;
|
||||
|
||||
// get string table
|
||||
std::vector<char> string_table(header.string_table_size);
|
||||
R_TRY(m_source->Read(string_table.data(), off, string_table.size(), std::addressof(bytes_read)))
|
||||
off += bytes_read;
|
||||
|
||||
out.reserve(header.total_files);
|
||||
for (u32 i = 0; i < header.total_files; i++) {
|
||||
CollectionEntry entry;
|
||||
entry.name = string_table.data() + file_table[i].name_offset;
|
||||
entry.offset = off + file_table[i].data_offset;
|
||||
entry.size = file_table[i].data_size;
|
||||
out.emplace_back(entry);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
95
sphaira/source/yati/container/xci.cpp
Normal file
@@ -0,0 +1,95 @@
|
||||
#include "yati/container/xci.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::yati::container {
|
||||
namespace {
|
||||
|
||||
#define XCI_MAGIC std::byteswap(0x48454144)
|
||||
#define HFS0_MAGIC 0x30534648
|
||||
#define HFS0_HEADER_OFFSET 0xF000
|
||||
|
||||
struct Hfs0Header {
|
||||
u32 magic;
|
||||
u32 total_files;
|
||||
u32 string_table_size;
|
||||
u32 padding;
|
||||
};
|
||||
|
||||
struct Hfs0FileTableEntry {
|
||||
u64 data_offset;
|
||||
u64 data_size;
|
||||
u32 name_offset;
|
||||
u32 hash_size;
|
||||
u64 padding;
|
||||
u8 hash[0x20];
|
||||
};
|
||||
|
||||
struct Hfs0 {
|
||||
Hfs0Header header{};
|
||||
std::vector<Hfs0FileTableEntry> file_table{};
|
||||
std::vector<std::string> string_table{};
|
||||
s64 data_offset{};
|
||||
};
|
||||
|
||||
Result Hfs0GetPartition(source::Base* source, s64 off, Hfs0& out) {
|
||||
u64 bytes_read;
|
||||
|
||||
// get header
|
||||
R_TRY(source->Read(std::addressof(out.header), off, sizeof(out.header), std::addressof(bytes_read)));
|
||||
R_UNLESS(out.header.magic == HFS0_MAGIC, 0x1);
|
||||
off += bytes_read;
|
||||
|
||||
// get file table
|
||||
out.file_table.resize(out.header.total_files);
|
||||
R_TRY(source->Read(out.file_table.data(), off, out.file_table.size() * sizeof(Hfs0FileTableEntry), std::addressof(bytes_read)))
|
||||
off += bytes_read;
|
||||
|
||||
// get string table
|
||||
std::vector<char> string_table(out.header.string_table_size);
|
||||
R_TRY(source->Read(string_table.data(), off, string_table.size(), std::addressof(bytes_read)))
|
||||
off += bytes_read;
|
||||
|
||||
for (u32 i = 0; i < out.header.total_files; i++) {
|
||||
out.string_table.emplace_back(string_table.data() + out.file_table[i].name_offset);
|
||||
}
|
||||
|
||||
out.data_offset = off;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result Xci::Validate(source::Base* source) {
|
||||
u32 magic;
|
||||
u64 bytes_read;
|
||||
R_TRY(source->Read(std::addressof(magic), 0x100, sizeof(magic), std::addressof(bytes_read)));
|
||||
R_UNLESS(magic == XCI_MAGIC, 0x1);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Xci::GetCollections(Collections& out) {
|
||||
Hfs0 root{};
|
||||
R_TRY(Hfs0GetPartition(m_source, HFS0_HEADER_OFFSET, root));
|
||||
|
||||
for (u32 i = 0; i < root.header.total_files; i++) {
|
||||
if (root.string_table[i] == "secure") {
|
||||
Hfs0 secure{};
|
||||
R_TRY(Hfs0GetPartition(m_source, root.data_offset + root.file_table[i].data_offset, secure));
|
||||
|
||||
for (u32 i = 0; i < secure.header.total_files; i++) {
|
||||
CollectionEntry entry;
|
||||
entry.name = secure.string_table[i];
|
||||
entry.offset = secure.data_offset + secure.file_table[i].data_offset;
|
||||
entry.size = secure.file_table[i].data_size;
|
||||
out.emplace_back(entry);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
return 0x1;
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::container
|
||||
122
sphaira/source/yati/nx/es.cpp
Normal file
@@ -0,0 +1,122 @@
|
||||
#include "yati/nx/es.hpp"
|
||||
#include "yati/nx/crypto.hpp"
|
||||
#include "yati/nx/nxdumptool_rsa.h"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::es {
|
||||
namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
Result ImportTicket(Service* srv, const void* tik_buf, u64 tik_size, const void* cert_buf, u64 cert_size) {
|
||||
return serviceDispatch(srv, 1,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In, SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
||||
.buffers = { { tik_buf, tik_size }, { cert_buf, cert_size } });
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
TikPropertyMask_None = 0,
|
||||
TikPropertyMask_PreInstallation = BIT(0), ///< Determines if the title comes pre-installed on the device. Most likely unused -- a remnant from previous ticket formats.
|
||||
TikPropertyMask_SharedTitle = BIT(1), ///< Determines if the title holds shared contents only. Most likely unused -- a remnant from previous ticket formats.
|
||||
TikPropertyMask_AllContents = BIT(2), ///< Determines if the content index mask shall be bypassed. Most likely unused -- a remnant from previous ticket formats.
|
||||
TikPropertyMask_DeviceLinkIndepedent = BIT(3), ///< Determines if the console should *not* connect to the Internet to verify if the title's being used by the primary console.
|
||||
TikPropertyMask_Volatile = BIT(4), ///< Determines if the ticket copy inside ticket.bin is available after reboot. Can be encrypted.
|
||||
TikPropertyMask_ELicenseRequired = BIT(5), ///< Determines if the console should connect to the Internet to perform license verification.
|
||||
TikPropertyMask_Count = 6 ///< Total values supported by this enum.
|
||||
} TikPropertyMask;
|
||||
|
||||
Result GetTicketDataOffset(std::span<const u8> ticket, u64& out) {
|
||||
log_write("inside es\n");
|
||||
u32 signature_type;
|
||||
std::memcpy(std::addressof(signature_type), ticket.data(), sizeof(signature_type));
|
||||
|
||||
u32 signature_size;
|
||||
switch (signature_type) {
|
||||
case es::TicketSigantureType_RSA_4096_SHA1: log_write("RSA-4096 PKCS#1 v1.5 with SHA-1\n"); signature_size = 0x200; break;
|
||||
case es::TicketSigantureType_RSA_2048_SHA1: log_write("RSA-2048 PKCS#1 v1.5 with SHA-1\n"); signature_size = 0x100; break;
|
||||
case es::TicketSigantureType_ECDSA_SHA1: log_write("ECDSA with SHA-1\n"); signature_size = 0x3C; break;
|
||||
case es::TicketSigantureType_RSA_4096_SHA256: log_write("RSA-4096 PKCS#1 v1.5 with SHA-256\n"); signature_size = 0x200; break;
|
||||
case es::TicketSigantureType_RSA_2048_SHA256: log_write("RSA-2048 PKCS#1 v1.5 with SHA-256\n"); signature_size = 0x100; break;
|
||||
case es::TicketSigantureType_ECDSA_SHA256: log_write("ECDSA with SHA-256\n"); signature_size = 0x3C; break;
|
||||
case es::TicketSigantureType_HMAC_SHA1_160: log_write("HMAC-SHA1-160\n"); signature_size = 0x14; break;
|
||||
default: log_write("unknown ticket\n"); return 0x1;
|
||||
}
|
||||
|
||||
// align-up to 0x40.
|
||||
out = ((signature_size + sizeof(signature_type)) + 0x3F) & ~0x3F;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetTicketData(std::span<const u8> ticket, es::TicketData* out) {
|
||||
u64 data_off;
|
||||
R_TRY(GetTicketDataOffset(ticket, data_off));
|
||||
std::memcpy(out, ticket.data() + data_off, sizeof(*out));
|
||||
|
||||
// validate ticket data.
|
||||
R_UNLESS(out->ticket_version1 == 0x2, Result_InvalidTicketVersion); // must be version 2.
|
||||
R_UNLESS(out->title_key_type == es::TicketTitleKeyType_Common || out->title_key_type == es::TicketTitleKeyType_Personalized, Result_InvalidTicketKeyType);
|
||||
R_UNLESS(out->master_key_revision <= 0x20, Result_InvalidTicketKeyRevision);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result SetTicketData(std::span<u8> ticket, const es::TicketData* in) {
|
||||
u64 data_off;
|
||||
R_TRY(GetTicketDataOffset(ticket, data_off));
|
||||
std::memcpy(ticket.data() + data_off, in, sizeof(*in));
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result GetTitleKey(keys::KeyEntry& out, const TicketData& data, const keys::Keys& keys) {
|
||||
if (data.title_key_type == es::TicketTitleKeyType_Common) {
|
||||
std::memcpy(std::addressof(out), data.title_key_block, sizeof(out));
|
||||
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized) {
|
||||
auto rsa_key = (const es::EticketRsaDeviceKey*)keys.eticket_device_key.key;
|
||||
log_write("personalised ticket\n");
|
||||
log_write("master_key_revision: %u\n", data.master_key_revision);
|
||||
log_write("license_type: %u\n", data.license_type);
|
||||
log_write("properties_bitfield: 0x%X\n", data.properties_bitfield);
|
||||
log_write("device_id: 0x%lX vs 0x%lX\n", data.device_id, std::byteswap(rsa_key->device_id));
|
||||
|
||||
R_UNLESS(data.device_id == std::byteswap(rsa_key->device_id), 0x1);
|
||||
log_write("device id is same\n");
|
||||
|
||||
u8 out_keydata[RSA2048_BYTES]{};
|
||||
size_t out_keydata_size;
|
||||
R_UNLESS(rsa2048OaepDecrypt(out_keydata, sizeof(out_keydata), data.title_key_block, rsa_key->modulus, &rsa_key->public_exponent, sizeof(rsa_key->public_exponent), rsa_key->private_exponent, sizeof(rsa_key->private_exponent), NULL, 0, &out_keydata_size), 0x1);
|
||||
R_UNLESS(out_keydata_size >= sizeof(out), 0x1);
|
||||
std::memcpy(std::addressof(out), out_keydata, sizeof(out));
|
||||
} else {
|
||||
R_THROW(0x1);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result DecryptTitleKey(keys::KeyEntry& out, u8 key_gen, const keys::Keys& keys) {
|
||||
keys::KeyEntry title_kek;
|
||||
R_TRY(keys.GetTitleKek(std::addressof(title_kek), key_gen));
|
||||
crypto::cryptoAes128(std::addressof(out), std::addressof(out), std::addressof(title_kek), false);
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
// todo: i thought i already wrote the code for this??
|
||||
// todo: patch the ticket.
|
||||
Result PatchTicket(std::span<u8> ticket, const keys::Keys& keys, bool convert_personalised) {
|
||||
TicketData data;
|
||||
R_TRY(GetTicketData(ticket, &data));
|
||||
|
||||
if (data.title_key_type == es::TicketTitleKeyType_Common) {
|
||||
// todo: verify common signature
|
||||
} else if (data.title_key_type == es::TicketTitleKeyType_Personalized && convert_personalised) {
|
||||
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::es
|
||||
130
sphaira/source/yati/nx/keys.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "yati/nx/keys.hpp"
|
||||
#include "yati/nx/nca.hpp"
|
||||
#include "yati/nx/es.hpp"
|
||||
#include "yati/nx/crypto.hpp"
|
||||
#include "defines.hpp"
|
||||
#include "log.hpp"
|
||||
#include <minIni.h>
|
||||
#include <memory>
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
|
||||
namespace sphaira::keys {
|
||||
namespace {
|
||||
|
||||
constexpr u8 HEADER_KEK_SRC[0x10] = {
|
||||
0x1F, 0x12, 0x91, 0x3A, 0x4A, 0xCB, 0xF0, 0x0D, 0x4C, 0xDE, 0x3A, 0xF6, 0xD5, 0x23, 0x88, 0x2A
|
||||
};
|
||||
|
||||
constexpr u8 HEADER_KEY_SRC[0x20] = {
|
||||
0x5A, 0x3E, 0xD8, 0x4F, 0xDE, 0xC0, 0xD8, 0x26, 0x31, 0xF7, 0xE2, 0x5D, 0x19, 0x7B, 0xF5, 0xD0,
|
||||
0x1C, 0x9B, 0x7B, 0xFA, 0xF6, 0x28, 0x18, 0x3D, 0x71, 0xF6, 0x4D, 0x73, 0xF1, 0x50, 0xB9, 0xD2
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void parse_hex_key(void* key, const char* hex) {
|
||||
char low[0x11]{};
|
||||
char upp[0x11]{};
|
||||
std::memcpy(low, hex, 0x10);
|
||||
std::memcpy(upp, hex + 0x10, 0x10);
|
||||
*(u64*)key = std::byteswap(std::strtoul(low, nullptr, 0x10));
|
||||
*(u64*)((u8*)key + 8) = std::byteswap(std::strtoul(upp, nullptr, 0x10));
|
||||
}
|
||||
|
||||
Result parse_keys(Keys& out, bool read_from_file) {
|
||||
static constexpr auto find_key = [](const char* key, const char* value, const char* search_key, KeySection& key_section) -> bool {
|
||||
if (!std::strncmp(key, search_key, std::strlen(search_key))) {
|
||||
// get key index.
|
||||
char* end;
|
||||
const auto key_value_str = key + std::strlen(search_key);
|
||||
const auto index = std::strtoul(key_value_str, &end, 0x10);
|
||||
if (end && end != key_value_str && index < 0x20) {
|
||||
KeyEntry keak;
|
||||
parse_hex_key(std::addressof(keak), value);
|
||||
key_section[index] = keak;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
static constexpr auto find_key_single = [](const char* key, const char* value, const char* search_key, KeyEntry& key_entry) -> bool {
|
||||
if (!std::strcmp(key, search_key)) {
|
||||
parse_hex_key(std::addressof(key_entry), value);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
static constexpr auto cb = [](const mTCHAR *Section, const mTCHAR *Key, const mTCHAR *Value, void *UserData) -> int {
|
||||
auto keys = static_cast<Keys*>(UserData);
|
||||
|
||||
auto key_text_key_area_key_app = "key_area_key_application_";
|
||||
auto key_text_key_area_key_oce = "key_area_key_ocean_";
|
||||
auto key_text_key_area_key_sys = "key_area_key_system_";
|
||||
auto key_text_titlekek = "titlekek_";
|
||||
auto key_text_master_key = "master_key_";
|
||||
auto key_text_eticket_rsa_kek = keys->eticket_device_key.generation ? "eticket_rsa_kek_personalized" : "eticket_rsa_kek";
|
||||
|
||||
if (find_key(Key, Value, key_text_key_area_key_app, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_Application])) {
|
||||
return 1;
|
||||
} else if (find_key(Key, Value, key_text_key_area_key_oce, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_Ocean])) {
|
||||
return 1;
|
||||
} else if (find_key(Key, Value, key_text_key_area_key_sys, keys->key_area_key[nca::KeyAreaEncryptionKeyIndex_System])) {
|
||||
return 1;
|
||||
} else if (find_key(Key, Value, key_text_titlekek, keys->titlekek)) {
|
||||
return 1;
|
||||
} else if (find_key(Key, Value, key_text_master_key, keys->master_key)) {
|
||||
return 1;
|
||||
} else if (find_key_single(Key, Value, key_text_eticket_rsa_kek, keys->eticket_rsa_kek)) {
|
||||
log_write("found key single: key: %s value %s\n", Key, Value);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
};
|
||||
|
||||
R_TRY(splCryptoInitialize());
|
||||
ON_SCOPE_EXIT(splCryptoExit());
|
||||
|
||||
u8 header_kek[0x20];
|
||||
R_TRY(splCryptoGenerateAesKek(HEADER_KEK_SRC, 0, 0, header_kek));
|
||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC, out.header_key));
|
||||
R_TRY(splCryptoGenerateAesKey(header_kek, HEADER_KEY_SRC + 0x10, out.header_key + 0x10));
|
||||
|
||||
if (read_from_file) {
|
||||
// get eticket device key, needed for decrypting personalised tickets.
|
||||
R_TRY(setcalInitialize());
|
||||
ON_SCOPE_EXIT(setcalExit());
|
||||
R_TRY(setcalGetEticketDeviceKey(std::addressof(out.eticket_device_key)));
|
||||
|
||||
R_UNLESS(ini_browse(cb, std::addressof(out), "/switch/prod.keys"), 0x1);
|
||||
|
||||
// decrypt eticket device key.
|
||||
if (out.eticket_rsa_kek.IsValid()) {
|
||||
auto rsa_key = (es::EticketRsaDeviceKey*)out.eticket_device_key.key;
|
||||
|
||||
Aes128CtrContext eticket_aes_ctx{};
|
||||
aes128CtrContextCreate(&eticket_aes_ctx, &out.eticket_rsa_kek, rsa_key->ctr);
|
||||
aes128CtrCrypt(&eticket_aes_ctx, &(rsa_key->private_exponent), &(rsa_key->private_exponent), sizeof(es::EticketRsaDeviceKey) - sizeof(rsa_key->ctr));
|
||||
|
||||
const auto public_exponent = std::byteswap(rsa_key->public_exponent);
|
||||
if (public_exponent != 0x10001) {
|
||||
log_write("etick decryption fail: 0x%X\n", public_exponent);
|
||||
if (public_exponent == 0) {
|
||||
log_write("eticket device id is NULL\n");
|
||||
}
|
||||
R_THROW(0x1);
|
||||
} else {
|
||||
log_write("eticket match\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::keys
|
||||
154
sphaira/source/yati/nx/nca.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
#include "yati/nx/nca.hpp"
|
||||
#include "yati/nx/crypto.hpp"
|
||||
#include "yati/nx/nxdumptool_rsa.h"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::nca {
|
||||
namespace {
|
||||
|
||||
constexpr u8 g_key_area_key_application_source[0x10] = { 0x7F, 0x59, 0x97, 0x1E, 0x62, 0x9F, 0x36, 0xA1, 0x30, 0x98, 0x06, 0x6F, 0x21, 0x44, 0xC3, 0x0D };
|
||||
constexpr u8 g_key_area_key_ocean_source[0x10] = { 0x32, 0x7D, 0x36, 0x08, 0x5A, 0xD1, 0x75, 0x8D, 0xAB, 0x4E, 0x6F, 0xBA, 0xA5, 0x55, 0xD8, 0x82 };
|
||||
constexpr u8 g_key_area_key_system_source[0x10] = { 0x87, 0x45, 0xF1, 0xBB, 0xA6, 0xBE, 0x79, 0x64, 0x7D, 0x04, 0x8B, 0xA6, 0x7B, 0x5F, 0xDA, 0x4A };
|
||||
|
||||
constexpr const u8* g_key_area_key[] = {
|
||||
g_key_area_key_application_source,
|
||||
g_key_area_key_ocean_source,
|
||||
g_key_area_key_system_source
|
||||
};
|
||||
|
||||
const unsigned char nca_hdr_fixed_key_moduli_retail[0x2][0x100] = { /* Fixed RSA key used to validate NCA signature 0. */
|
||||
{
|
||||
0xBF, 0xBE, 0x40, 0x6C, 0xF4, 0xA7, 0x80, 0xE9, 0xF0, 0x7D, 0x0C, 0x99, 0x61, 0x1D, 0x77, 0x2F,
|
||||
0x96, 0xBC, 0x4B, 0x9E, 0x58, 0x38, 0x1B, 0x03, 0xAB, 0xB1, 0x75, 0x49, 0x9F, 0x2B, 0x4D, 0x58,
|
||||
0x34, 0xB0, 0x05, 0xA3, 0x75, 0x22, 0xBE, 0x1A, 0x3F, 0x03, 0x73, 0xAC, 0x70, 0x68, 0xD1, 0x16,
|
||||
0xB9, 0x04, 0x46, 0x5E, 0xB7, 0x07, 0x91, 0x2F, 0x07, 0x8B, 0x26, 0xDE, 0xF6, 0x00, 0x07, 0xB2,
|
||||
0xB4, 0x51, 0xF8, 0x0D, 0x0A, 0x5E, 0x58, 0xAD, 0xEB, 0xBC, 0x9A, 0xD6, 0x49, 0xB9, 0x64, 0xEF,
|
||||
0xA7, 0x82, 0xB5, 0xCF, 0x6D, 0x70, 0x13, 0xB0, 0x0F, 0x85, 0xF6, 0xA9, 0x08, 0xAA, 0x4D, 0x67,
|
||||
0x66, 0x87, 0xFA, 0x89, 0xFF, 0x75, 0x90, 0x18, 0x1E, 0x6B, 0x3D, 0xE9, 0x8A, 0x68, 0xC9, 0x26,
|
||||
0x04, 0xD9, 0x80, 0xCE, 0x3F, 0x5E, 0x92, 0xCE, 0x01, 0xFF, 0x06, 0x3B, 0xF2, 0xC1, 0xA9, 0x0C,
|
||||
0xCE, 0x02, 0x6F, 0x16, 0xBC, 0x92, 0x42, 0x0A, 0x41, 0x64, 0xCD, 0x52, 0xB6, 0x34, 0x4D, 0xAE,
|
||||
0xC0, 0x2E, 0xDE, 0xA4, 0xDF, 0x27, 0x68, 0x3C, 0xC1, 0xA0, 0x60, 0xAD, 0x43, 0xF3, 0xFC, 0x86,
|
||||
0xC1, 0x3E, 0x6C, 0x46, 0xF7, 0x7C, 0x29, 0x9F, 0xFA, 0xFD, 0xF0, 0xE3, 0xCE, 0x64, 0xE7, 0x35,
|
||||
0xF2, 0xF6, 0x56, 0x56, 0x6F, 0x6D, 0xF1, 0xE2, 0x42, 0xB0, 0x83, 0x40, 0xA5, 0xC3, 0x20, 0x2B,
|
||||
0xCC, 0x9A, 0xAE, 0xCA, 0xED, 0x4D, 0x70, 0x30, 0xA8, 0x70, 0x1C, 0x70, 0xFD, 0x13, 0x63, 0x29,
|
||||
0x02, 0x79, 0xEA, 0xD2, 0xA7, 0xAF, 0x35, 0x28, 0x32, 0x1C, 0x7B, 0xE6, 0x2F, 0x1A, 0xAA, 0x40,
|
||||
0x7E, 0x32, 0x8C, 0x27, 0x42, 0xFE, 0x82, 0x78, 0xEC, 0x0D, 0xEB, 0xE6, 0x83, 0x4B, 0x6D, 0x81,
|
||||
0x04, 0x40, 0x1A, 0x9E, 0x9A, 0x67, 0xF6, 0x72, 0x29, 0xFA, 0x04, 0xF0, 0x9D, 0xE4, 0xF4, 0x03,
|
||||
},
|
||||
{
|
||||
0xAD, 0xE3, 0xE1, 0xFA, 0x04, 0x35, 0xE5, 0xB6, 0xDD, 0x49, 0xEA, 0x89, 0x29, 0xB1, 0xFF, 0xB6,
|
||||
0x43, 0xDF, 0xCA, 0x96, 0xA0, 0x4A, 0x13, 0xDF, 0x43, 0xD9, 0x94, 0x97, 0x96, 0x43, 0x65, 0x48,
|
||||
0x70, 0x58, 0x33, 0xA2, 0x7D, 0x35, 0x7B, 0x96, 0x74, 0x5E, 0x0B, 0x5C, 0x32, 0x18, 0x14, 0x24,
|
||||
0xC2, 0x58, 0xB3, 0x6C, 0x22, 0x7A, 0xA1, 0xB7, 0xCB, 0x90, 0xA7, 0xA3, 0xF9, 0x7D, 0x45, 0x16,
|
||||
0xA5, 0xC8, 0xED, 0x8F, 0xAD, 0x39, 0x5E, 0x9E, 0x4B, 0x51, 0x68, 0x7D, 0xF8, 0x0C, 0x35, 0xC6,
|
||||
0x3F, 0x91, 0xAE, 0x44, 0xA5, 0x92, 0x30, 0x0D, 0x46, 0xF8, 0x40, 0xFF, 0xD0, 0xFF, 0x06, 0xD2,
|
||||
0x1C, 0x7F, 0x96, 0x18, 0xDC, 0xB7, 0x1D, 0x66, 0x3E, 0xD1, 0x73, 0xBC, 0x15, 0x8A, 0x2F, 0x94,
|
||||
0xF3, 0x00, 0xC1, 0x83, 0xF1, 0xCD, 0xD7, 0x81, 0x88, 0xAB, 0xDF, 0x8C, 0xEF, 0x97, 0xDD, 0x1B,
|
||||
0x17, 0x5F, 0x58, 0xF6, 0x9A, 0xE9, 0xE8, 0xC2, 0x2F, 0x38, 0x15, 0xF5, 0x21, 0x07, 0xF8, 0x37,
|
||||
0x90, 0x5D, 0x2E, 0x02, 0x40, 0x24, 0x15, 0x0D, 0x25, 0xB7, 0x26, 0x5D, 0x09, 0xCC, 0x4C, 0xF4,
|
||||
0xF2, 0x1B, 0x94, 0x70, 0x5A, 0x9E, 0xEE, 0xED, 0x77, 0x77, 0xD4, 0x51, 0x99, 0xF5, 0xDC, 0x76,
|
||||
0x1E, 0xE3, 0x6C, 0x8C, 0xD1, 0x12, 0xD4, 0x57, 0xD1, 0xB6, 0x83, 0xE4, 0xE4, 0xFE, 0xDA, 0xE9,
|
||||
0xB4, 0x3B, 0x33, 0xE5, 0x37, 0x8A, 0xDF, 0xB5, 0x7F, 0x89, 0xF1, 0x9B, 0x9E, 0xB0, 0x15, 0xB2,
|
||||
0x3A, 0xFE, 0xEA, 0x61, 0x84, 0x5B, 0x7D, 0x4B, 0x23, 0x12, 0x0B, 0x83, 0x12, 0xF2, 0x22, 0x6B,
|
||||
0xB9, 0x22, 0x96, 0x4B, 0x26, 0x0B, 0x63, 0x5E, 0x96, 0x57, 0x52, 0xA3, 0x67, 0x64, 0x22, 0xCA,
|
||||
0xD0, 0x56, 0x3E, 0x74, 0xB5, 0x98, 0x1F, 0x0D, 0xF8, 0xB3, 0x34, 0xE6, 0x98, 0x68, 0x5A, 0xAD,
|
||||
}
|
||||
};
|
||||
|
||||
const unsigned char acid_fixed_key_moduli_retail[0x2][0x100] = { /* Fixed RSA keys used to validate ACID signatures. */
|
||||
{
|
||||
0xDD, 0xC8, 0xDD, 0xF2, 0x4E, 0x6D, 0xF0, 0xCA, 0x9E, 0xC7, 0x5D, 0xC7, 0x7B, 0xAD, 0xFE, 0x7D,
|
||||
0x23, 0x89, 0x69, 0xB6, 0xF2, 0x06, 0xA2, 0x02, 0x88, 0xE1, 0x55, 0x91, 0xAB, 0xCB, 0x4D, 0x50,
|
||||
0x2E, 0xFC, 0x9D, 0x94, 0x76, 0xD6, 0x4C, 0xD8, 0xFF, 0x10, 0xFA, 0x5E, 0x93, 0x0A, 0xB4, 0x57,
|
||||
0xAC, 0x51, 0xC7, 0x16, 0x66, 0xF4, 0x1A, 0x54, 0xC2, 0xC5, 0x04, 0x3D, 0x1B, 0xFE, 0x30, 0x20,
|
||||
0x8A, 0xAC, 0x6F, 0x6F, 0xF5, 0xC7, 0xB6, 0x68, 0xB8, 0xC9, 0x40, 0x6B, 0x42, 0xAD, 0x11, 0x21,
|
||||
0xE7, 0x8B, 0xE9, 0x75, 0x01, 0x86, 0xE4, 0x48, 0x9B, 0x0A, 0x0A, 0xF8, 0x7F, 0xE8, 0x87, 0xF2,
|
||||
0x82, 0x01, 0xE6, 0xA3, 0x0F, 0xE4, 0x66, 0xAE, 0x83, 0x3F, 0x4E, 0x9F, 0x5E, 0x01, 0x30, 0xA4,
|
||||
0x00, 0xB9, 0x9A, 0xAE, 0x5F, 0x03, 0xCC, 0x18, 0x60, 0xE5, 0xEF, 0x3B, 0x5E, 0x15, 0x16, 0xFE,
|
||||
0x1C, 0x82, 0x78, 0xB5, 0x2F, 0x47, 0x7C, 0x06, 0x66, 0x88, 0x5D, 0x35, 0xA2, 0x67, 0x20, 0x10,
|
||||
0xE7, 0x6C, 0x43, 0x68, 0xD3, 0xE4, 0x5A, 0x68, 0x2A, 0x5A, 0xE2, 0x6D, 0x73, 0xB0, 0x31, 0x53,
|
||||
0x1C, 0x20, 0x09, 0x44, 0xF5, 0x1A, 0x9D, 0x22, 0xBE, 0x12, 0xA1, 0x77, 0x11, 0xE2, 0xA1, 0xCD,
|
||||
0x40, 0x9A, 0xA2, 0x8B, 0x60, 0x9B, 0xEF, 0xA0, 0xD3, 0x48, 0x63, 0xA2, 0xF8, 0xA3, 0x2C, 0x08,
|
||||
0x56, 0x52, 0x2E, 0x60, 0x19, 0x67, 0x5A, 0xA7, 0x9F, 0xDC, 0x3F, 0x3F, 0x69, 0x2B, 0x31, 0x6A,
|
||||
0xB7, 0x88, 0x4A, 0x14, 0x84, 0x80, 0x33, 0x3C, 0x9D, 0x44, 0xB7, 0x3F, 0x4C, 0xE1, 0x75, 0xEA,
|
||||
0x37, 0xEA, 0xE8, 0x1E, 0x7C, 0x77, 0xB7, 0xC6, 0x1A, 0xA2, 0xF0, 0x9F, 0x10, 0x61, 0xCD, 0x7B,
|
||||
0x5B, 0x32, 0x4C, 0x37, 0xEF, 0xB1, 0x71, 0x68, 0x53, 0x0A, 0xED, 0x51, 0x7D, 0x35, 0x22, 0xFD,
|
||||
},
|
||||
{
|
||||
0xE7, 0xAA, 0x25, 0xC8, 0x01, 0xA5, 0x14, 0x6B, 0x01, 0x60, 0x3E, 0xD9, 0x96, 0x5A, 0xBF, 0x90,
|
||||
0xAC, 0xA7, 0xFD, 0x9B, 0x5B, 0xBD, 0x8A, 0x26, 0xB0, 0xCB, 0x20, 0x28, 0x9A, 0x72, 0x12, 0xF5,
|
||||
0x20, 0x65, 0xB3, 0xB9, 0x84, 0x58, 0x1F, 0x27, 0xBC, 0x7C, 0xA2, 0xC9, 0x9E, 0x18, 0x95, 0xCF,
|
||||
0xC2, 0x73, 0x2E, 0x74, 0x8C, 0x66, 0xE5, 0x9E, 0x79, 0x2B, 0xB8, 0x07, 0x0C, 0xB0, 0x4E, 0x8E,
|
||||
0xAB, 0x85, 0x21, 0x42, 0xC4, 0xC5, 0x6D, 0x88, 0x9C, 0xDB, 0x15, 0x95, 0x3F, 0x80, 0xDB, 0x7A,
|
||||
0x9A, 0x7D, 0x41, 0x56, 0x25, 0x17, 0x18, 0x42, 0x4D, 0x8C, 0xAC, 0xA5, 0x7B, 0xDB, 0x42, 0x5D,
|
||||
0x59, 0x35, 0x45, 0x5D, 0x8A, 0x02, 0xB5, 0x70, 0xC0, 0x72, 0x35, 0x46, 0xD0, 0x1D, 0x60, 0x01,
|
||||
0x4A, 0xCC, 0x1C, 0x46, 0xD3, 0xD6, 0x35, 0x52, 0xD6, 0xE1, 0xF8, 0x3B, 0x5D, 0xEA, 0xDD, 0xB8,
|
||||
0xFE, 0x7D, 0x50, 0xCB, 0x35, 0x23, 0x67, 0x8B, 0xB6, 0xE4, 0x74, 0xD2, 0x60, 0xFC, 0xFD, 0x43,
|
||||
0xBF, 0x91, 0x08, 0x81, 0xC5, 0x4F, 0x5D, 0x16, 0x9A, 0xC4, 0x9A, 0xC6, 0xF6, 0xF3, 0xE1, 0xF6,
|
||||
0x5C, 0x07, 0xAA, 0x71, 0x6C, 0x13, 0xA4, 0xB1, 0xB3, 0x66, 0xBF, 0x90, 0x4C, 0x3D, 0xA2, 0xC4,
|
||||
0x0B, 0xB8, 0x3D, 0x7A, 0x8C, 0x19, 0xFA, 0xFF, 0x6B, 0xB9, 0x1F, 0x02, 0xCC, 0xB6, 0xD3, 0x0C,
|
||||
0x7D, 0x19, 0x1F, 0x47, 0xF9, 0xC7, 0x40, 0x01, 0xFA, 0x46, 0xEA, 0x0B, 0xD4, 0x02, 0xE0, 0x3D,
|
||||
0x30, 0x9A, 0x1A, 0x0F, 0xEA, 0xA7, 0x66, 0x55, 0xF7, 0xCB, 0x28, 0xE2, 0xBB, 0x99, 0xE4, 0x83,
|
||||
0xC3, 0x43, 0x03, 0xEE, 0xDC, 0x1F, 0x02, 0x23, 0xDD, 0xD1, 0x2D, 0x39, 0xA4, 0x65, 0x75, 0x03,
|
||||
0xEF, 0x37, 0x9C, 0x06, 0xD6, 0xFA, 0xA1, 0x15, 0xF0, 0xDB, 0x17, 0x47, 0x26, 0x4F, 0x49, 0x03
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Result DecryptKeak(const keys::Keys& keys, Header& header) {
|
||||
const auto key_generation = header.GetKeyGeneration();
|
||||
|
||||
// try with spl.
|
||||
keys::KeyEntry keak;
|
||||
if (R_SUCCEEDED(splCryptoGenerateAesKek(g_key_area_key[header.kaek_index], key_generation, 0, &keak))) {
|
||||
for (auto& key_area : header.key_area) {
|
||||
R_TRY(splCryptoGenerateAesKey(&keak, std::addressof(key_area), std::addressof(key_area)));
|
||||
}
|
||||
} else {
|
||||
// failed with spl, try using keys.
|
||||
R_TRY(keys.GetNcaKeyArea(&keak, key_generation, header.kaek_index));
|
||||
for (auto& key_area : header.key_area) {
|
||||
crypto::cryptoAes128(std::addressof(key_area), std::addressof(key_area), std::addressof(keak), false);
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result EncryptKeak(const keys::Keys& keys, Header& header, u8 key_generation) {
|
||||
header.SetKeyGeneration(key_generation);
|
||||
|
||||
keys::KeyEntry keak;
|
||||
R_TRY(keys.GetNcaKeyArea(&keak, key_generation, header.kaek_index));
|
||||
log_write("re-encrypting with: 0x%X\n", key_generation);
|
||||
|
||||
for (auto& key_area : header.key_area) {
|
||||
crypto::cryptoAes128(std::addressof(key_area), std::addressof(key_area), std::addressof(keak), true);
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result VerifyFixedKey(const Header& header) {
|
||||
R_UNLESS(header.sig_key_gen < std::size(nca_hdr_fixed_key_moduli_retail), 0x1);
|
||||
auto mod = nca_hdr_fixed_key_moduli_retail[header.sig_key_gen];
|
||||
|
||||
const u8 E[3] = { 1, 0, 1 };
|
||||
if (!rsa2048VerifySha256BasedPssSignature(&header.magic, 0x200, header.rsa_fixed_key, mod, E, sizeof(E))) {
|
||||
auto new_header = header;
|
||||
// if failed, detect if this is a eshop/xci convert.
|
||||
new_header.distribution_type ^= 1;
|
||||
if (!rsa2048VerifySha256BasedPssSignature(&new_header.magic, 0x200, new_header.rsa_fixed_key, mod, E, sizeof(E))) {
|
||||
log_write("FAILED nca header hash\n");
|
||||
R_THROW(0x1);
|
||||
} else {
|
||||
log_write("WARNING! nca is converted! distribution_type: %u\n", new_header.distribution_type);
|
||||
R_SUCCEED();
|
||||
}
|
||||
}
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::nca
|
||||
34
sphaira/source/yati/nx/ncm.cpp
Normal file
@@ -0,0 +1,34 @@
|
||||
#include "yati/nx/ncm.hpp"
|
||||
#include "defines.hpp"
|
||||
#include <memory>
|
||||
|
||||
namespace sphaira::ncm {
|
||||
namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
auto GetAppId(const NcmContentMetaKey& key) -> u64 {
|
||||
if (key.type == NcmContentMetaType_Patch) {
|
||||
return key.id ^ 0x800;
|
||||
} else if (key.type == NcmContentMetaType_AddOnContent) {
|
||||
return (key.id ^ 0x1000) & ~0xFFF;
|
||||
} else {
|
||||
return key.id;
|
||||
}
|
||||
}
|
||||
|
||||
Result Delete(NcmContentStorage* cs, const NcmContentId *content_id) {
|
||||
bool has;
|
||||
R_TRY(ncmContentStorageHas(cs, std::addressof(has), content_id));
|
||||
if (has) {
|
||||
R_TRY(ncmContentStorageDelete(cs, content_id));
|
||||
}
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Register(NcmContentStorage* cs, const NcmContentId *content_id, const NcmPlaceHolderId *placeholder_id) {
|
||||
R_TRY(Delete(cs, content_id));
|
||||
return ncmContentStorageRegister(cs, content_id, placeholder_id);
|
||||
}
|
||||
|
||||
} // namespace sphaira::ncm
|
||||
39
sphaira/source/yati/nx/ns.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "yati/nx/ns.hpp"
|
||||
|
||||
namespace sphaira::ns {
|
||||
namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
Result PushApplicationRecord(Service* srv, u64 tid, const ncm::ContentStorageRecord* records, u32 count) {
|
||||
const struct {
|
||||
u8 last_modified_event;
|
||||
u8 padding[0x7];
|
||||
u64 tid;
|
||||
} in = { ApplicationRecordType_Installed, {0}, tid };
|
||||
|
||||
return serviceDispatchIn(srv, 16, in,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_In },
|
||||
.buffers = { { records, sizeof(*records) * count } });
|
||||
}
|
||||
|
||||
Result ListApplicationRecordContentMeta(Service* srv, u64 offset, u64 tid, ncm::ContentStorageRecord* out_records, u32 count, s32* entries_read) {
|
||||
struct {
|
||||
u64 offset;
|
||||
u64 tid;
|
||||
} in = { offset, tid };
|
||||
|
||||
return serviceDispatchInOut(srv, 17, in, *entries_read,
|
||||
.buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
|
||||
.buffers = { { out_records, sizeof(*out_records) * count } });
|
||||
}
|
||||
|
||||
Result DeleteApplicationRecord(Service* srv, u64 tid) {
|
||||
return serviceDispatchIn(srv, 27, tid);
|
||||
}
|
||||
|
||||
Result InvalidateApplicationControlCache(Service* srv, u64 tid) {
|
||||
return serviceDispatchIn(srv, 404, tid);
|
||||
}
|
||||
|
||||
} // namespace sphaira::ns
|
||||
158
sphaira/source/yati/nx/nxdumptool_rsa.c
Normal file
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* rsa.c
|
||||
*
|
||||
* Copyright (c) 2018-2019, SciresM.
|
||||
* Copyright (c) 2020-2024, DarkMatterCore <pabloacurielz@gmail.com>.
|
||||
*
|
||||
* This file is part of nxdumptool (https://github.com/DarkMatterCore/nxdumptool).
|
||||
*
|
||||
* nxdumptool is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* nxdumptool is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "yati/nx/nxdumptool_rsa.h"
|
||||
#include "log.hpp"
|
||||
#include <switch.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <mbedtls/rsa.h>
|
||||
#include <mbedtls/entropy.h>
|
||||
#include <mbedtls/ctr_drbg.h>
|
||||
#include <mbedtls/pk.h>
|
||||
|
||||
#define LOG_MSG_ERROR(...) log_write(__VA_ARGS__)
|
||||
|
||||
/* Function prototypes. */
|
||||
|
||||
static bool rsa2048VerifySha256BasedSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, \
|
||||
bool use_pss);
|
||||
|
||||
bool rsa2048VerifySha256BasedPssSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size)
|
||||
{
|
||||
return rsa2048VerifySha256BasedSignature(data, data_size, signature, modulus, public_exponent, public_exponent_size, true);
|
||||
}
|
||||
|
||||
bool rsa2048VerifySha256BasedPkcs1v15Signature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size)
|
||||
{
|
||||
return rsa2048VerifySha256BasedSignature(data, data_size, signature, modulus, public_exponent, public_exponent_size, false);
|
||||
}
|
||||
|
||||
bool rsa2048OaepDecrypt(void *dst, size_t dst_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, const void *private_exponent, \
|
||||
size_t private_exponent_size, const void *label, size_t label_size, size_t *out_size)
|
||||
{
|
||||
if (!dst || !dst_size || !signature || !modulus || !public_exponent || !public_exponent_size || !private_exponent || !private_exponent_size || (!label && label_size) || (label && !label_size) || \
|
||||
!out_size)
|
||||
{
|
||||
LOG_MSG_ERROR("Invalid parameters!");
|
||||
return false;
|
||||
}
|
||||
|
||||
mbedtls_entropy_context entropy = {0};
|
||||
mbedtls_ctr_drbg_context ctr_drbg = {0};
|
||||
mbedtls_rsa_context rsa = {0};
|
||||
|
||||
const char *pers = __func__;
|
||||
int mbedtls_ret = 0;
|
||||
bool ret = false;
|
||||
|
||||
/* Initialize contexts. */
|
||||
mbedtls_entropy_init(&entropy);
|
||||
mbedtls_ctr_drbg_init(&ctr_drbg);
|
||||
mbedtls_rsa_init(&rsa, MBEDTLS_RSA_PKCS_V21, MBEDTLS_MD_SHA256);
|
||||
|
||||
/* Seed the random number generator. */
|
||||
mbedtls_ret = mbedtls_ctr_drbg_seed(&ctr_drbg, mbedtls_entropy_func, &entropy, (const u8*)pers, strlen(pers));
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_ctr_drbg_seed failed! (%d).", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Import RSA parameters. */
|
||||
mbedtls_ret = mbedtls_rsa_import_raw(&rsa, (const u8*)modulus, RSA2048_BYTES, NULL, 0, NULL, 0, (const u8*)private_exponent, private_exponent_size, (const u8*)public_exponent, public_exponent_size);
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_rsa_import_raw failed! (%d).", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Derive RSA prime factors. */
|
||||
mbedtls_ret = mbedtls_rsa_complete(&rsa);
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_rsa_complete failed! (%d).", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Perform RSA-OAEP decryption. */
|
||||
mbedtls_ret = mbedtls_rsa_rsaes_oaep_decrypt(&rsa, mbedtls_ctr_drbg_random, &ctr_drbg, MBEDTLS_RSA_PRIVATE, (const u8*)label, label_size, out_size, (const u8*)signature, (u8*)dst, dst_size);
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_rsa_rsaes_oaep_decrypt failed! (%d).", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ret = true;
|
||||
|
||||
end:
|
||||
mbedtls_rsa_free(&rsa);
|
||||
mbedtls_ctr_drbg_free(&ctr_drbg);
|
||||
mbedtls_entropy_free(&entropy);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool rsa2048VerifySha256BasedSignature(const void *data, size_t data_size, const void *signature, const void *modulus, const void *public_exponent, size_t public_exponent_size, \
|
||||
bool use_pss)
|
||||
{
|
||||
if (!data || !data_size || !signature || !modulus || !public_exponent || !public_exponent_size)
|
||||
{
|
||||
LOG_MSG_ERROR("Invalid parameters!");
|
||||
return false;
|
||||
}
|
||||
|
||||
int mbedtls_ret = 0;
|
||||
mbedtls_rsa_context rsa = {0};
|
||||
u8 hash[SHA256_HASH_SIZE] = {0};
|
||||
bool ret = false;
|
||||
|
||||
/* Initialize RSA context. */
|
||||
mbedtls_rsa_init(&rsa, use_pss ? MBEDTLS_RSA_PKCS_V21 : MBEDTLS_RSA_PKCS_V15, MBEDTLS_MD_SHA256);
|
||||
|
||||
/* Import RSA parameters. */
|
||||
mbedtls_ret = mbedtls_rsa_import_raw(&rsa, (const u8*)modulus, RSA2048_BYTES, NULL, 0, NULL, 0, NULL, 0, (const u8*)public_exponent, public_exponent_size);
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_rsa_import_raw failed! (%d).", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
/* Calculate SHA-256 checksum for the input data. */
|
||||
sha256CalculateHash(hash, data, data_size);
|
||||
|
||||
/* Verify signature. */
|
||||
mbedtls_ret = (use_pss ? mbedtls_rsa_rsassa_pss_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, SHA256_HASH_SIZE, hash, (const u8*)signature) : \
|
||||
mbedtls_rsa_rsassa_pkcs1_v15_verify(&rsa, NULL, NULL, MBEDTLS_RSA_PUBLIC, MBEDTLS_MD_SHA256, SHA256_HASH_SIZE, hash, (const u8*)signature));
|
||||
if (mbedtls_ret != 0)
|
||||
{
|
||||
LOG_MSG_ERROR("mbedtls_rsa_rsassa_%s_verify failed! (%d).", use_pss ? "pss" : "pkcs1_v15", mbedtls_ret);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ret = true;
|
||||
|
||||
end:
|
||||
mbedtls_rsa_free(&rsa);
|
||||
|
||||
return ret;
|
||||
}
|
||||
20
sphaira/source/yati/source/file.cpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#include "yati/source/file.hpp"
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
File::File(FsFileSystem* fs, const fs::FsPath& path) {
|
||||
m_open_result = fsFsOpenFile(fs, path, FsOpenMode_Read, std::addressof(m_file));
|
||||
}
|
||||
|
||||
File::~File() {
|
||||
if (R_SUCCEEDED(GetOpenResult())) {
|
||||
fsFileClose(std::addressof(m_file));
|
||||
}
|
||||
}
|
||||
|
||||
Result File::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
R_TRY(GetOpenResult());
|
||||
return fsFileRead(std::addressof(m_file), off, buf, size, 0, bytes_read);
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
28
sphaira/source/yati/source/stdio.cpp
Normal file
@@ -0,0 +1,28 @@
|
||||
#include "yati/source/stdio.hpp"
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
|
||||
Stdio::Stdio(const char* path) {
|
||||
m_file = std::fopen(path, "rb");
|
||||
if (!m_file) {
|
||||
m_open_result = fsdevGetLastResult();
|
||||
}
|
||||
}
|
||||
|
||||
Stdio::~Stdio() {
|
||||
if (R_SUCCEEDED(GetOpenResult())) {
|
||||
std::fclose(m_file);
|
||||
}
|
||||
}
|
||||
|
||||
Result Stdio::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
R_TRY(GetOpenResult());
|
||||
|
||||
std::fseek(m_file, off, SEEK_SET);
|
||||
R_TRY(fsdevGetLastResult());
|
||||
|
||||
*bytes_read = std::fread(buf, 1, size, m_file);
|
||||
return fsdevGetLastResult();
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
361
sphaira/source/yati/source/usb.cpp
Normal file
@@ -0,0 +1,361 @@
|
||||
/*
|
||||
* Copyright (c) Atmosphère-NX
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it
|
||||
* under the terms and conditions of the GNU General Public License,
|
||||
* version 2, as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Most of the usb transfer code was taken from Haze.
|
||||
#include "yati/source/usb.hpp"
|
||||
#include "log.hpp"
|
||||
|
||||
namespace sphaira::yati::source {
|
||||
namespace {
|
||||
|
||||
constexpr u32 MAGIC = 0x53504841;
|
||||
constexpr u32 VERSION = 1;
|
||||
|
||||
struct SendHeader {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
};
|
||||
|
||||
struct RecvHeader {
|
||||
u32 magic;
|
||||
u32 version;
|
||||
u32 bcdUSB;
|
||||
u32 count;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Usb::Usb(u64 transfer_timeout) {
|
||||
m_open_result = usbDsInitialize();
|
||||
m_transfer_timeout = transfer_timeout;
|
||||
}
|
||||
|
||||
Usb::~Usb() {
|
||||
if (R_SUCCEEDED(GetOpenResult())) {
|
||||
usbDsExit();
|
||||
}
|
||||
}
|
||||
|
||||
Result Usb::Init() {
|
||||
log_write("doing USB init\n");
|
||||
R_TRY(m_open_result);
|
||||
|
||||
u8 iManufacturer, iProduct, iSerialNumber;
|
||||
static const u16 supported_langs[1] = {0x0409};
|
||||
// Send language descriptor
|
||||
R_TRY(usbDsAddUsbLanguageStringDescriptor(NULL, supported_langs, sizeof(supported_langs)/sizeof(u16)));
|
||||
// Send manufacturer
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iManufacturer, "Nintendo"));
|
||||
// Send product
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iProduct, "Nintendo Switch"));
|
||||
// Send serial number
|
||||
R_TRY(usbDsAddUsbStringDescriptor(&iSerialNumber, "SerialNumber"));
|
||||
|
||||
// Send device descriptors
|
||||
struct usb_device_descriptor device_descriptor = {
|
||||
.bLength = USB_DT_DEVICE_SIZE,
|
||||
.bDescriptorType = USB_DT_DEVICE,
|
||||
.bcdUSB = 0x0110,
|
||||
.bDeviceClass = 0x00,
|
||||
.bDeviceSubClass = 0x00,
|
||||
.bDeviceProtocol = 0x00,
|
||||
.bMaxPacketSize0 = 0x40,
|
||||
.idVendor = 0x057e,
|
||||
.idProduct = 0x3000,
|
||||
.bcdDevice = 0x0100,
|
||||
.iManufacturer = iManufacturer,
|
||||
.iProduct = iProduct,
|
||||
.iSerialNumber = iSerialNumber,
|
||||
.bNumConfigurations = 0x01
|
||||
};
|
||||
|
||||
// Full Speed is USB 1.1
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Full, &device_descriptor));
|
||||
|
||||
// High Speed is USB 2.0
|
||||
device_descriptor.bcdUSB = 0x0200;
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_High, &device_descriptor));
|
||||
|
||||
// Super Speed is USB 3.0
|
||||
device_descriptor.bcdUSB = 0x0300;
|
||||
// Upgrade packet size to 512
|
||||
device_descriptor.bMaxPacketSize0 = 0x09;
|
||||
R_TRY(usbDsSetUsbDeviceDescriptor(UsbDeviceSpeed_Super, &device_descriptor));
|
||||
|
||||
// Define Binary Object Store
|
||||
const u8 bos[0x16] = {
|
||||
0x05, // .bLength
|
||||
USB_DT_BOS, // .bDescriptorType
|
||||
0x16, 0x00, // .wTotalLength
|
||||
0x02, // .bNumDeviceCaps
|
||||
|
||||
// USB 2.0
|
||||
0x07, // .bLength
|
||||
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
|
||||
0x02, // .bDevCapabilityType
|
||||
0x02, 0x00, 0x00, 0x00, // dev_capability_data
|
||||
|
||||
// USB 3.0
|
||||
0x0A, // .bLength
|
||||
USB_DT_DEVICE_CAPABILITY, // .bDescriptorType
|
||||
0x03, /* .bDevCapabilityType */
|
||||
0x00, /* .bmAttributes */
|
||||
0x0E, 0x00, /* .wSpeedSupported */
|
||||
0x03, /* .bFunctionalitySupport */
|
||||
0x00, /* .bU1DevExitLat */
|
||||
0x00, 0x00 /* .bU2DevExitLat */
|
||||
};
|
||||
|
||||
R_TRY(usbDsSetBinaryObjectStore(bos, sizeof(bos)));
|
||||
|
||||
struct usb_interface_descriptor interface_descriptor = {
|
||||
.bLength = USB_DT_INTERFACE_SIZE,
|
||||
.bDescriptorType = USB_DT_INTERFACE,
|
||||
.bInterfaceNumber = USBDS_DEFAULT_InterfaceNumber, // set below
|
||||
.bNumEndpoints = static_cast<u8>(std::size(m_endpoints)),
|
||||
.bInterfaceClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceSubClass = USB_CLASS_VENDOR_SPEC,
|
||||
.bInterfaceProtocol = USB_CLASS_VENDOR_SPEC,
|
||||
};
|
||||
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_in = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_IN,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
.wMaxPacketSize = 0x40,
|
||||
};
|
||||
|
||||
struct usb_endpoint_descriptor endpoint_descriptor_out = {
|
||||
.bLength = USB_DT_ENDPOINT_SIZE,
|
||||
.bDescriptorType = USB_DT_ENDPOINT,
|
||||
.bEndpointAddress = USB_ENDPOINT_OUT,
|
||||
.bmAttributes = USB_TRANSFER_TYPE_BULK,
|
||||
.wMaxPacketSize = 0x40,
|
||||
};
|
||||
|
||||
const struct usb_ss_endpoint_companion_descriptor endpoint_companion = {
|
||||
.bLength = sizeof(struct usb_ss_endpoint_companion_descriptor),
|
||||
.bDescriptorType = USB_DT_SS_ENDPOINT_COMPANION,
|
||||
.bMaxBurst = 0x0F,
|
||||
.bmAttributes = 0x00,
|
||||
.wBytesPerInterval = 0x00,
|
||||
};
|
||||
|
||||
R_TRY(usbDsRegisterInterface(&m_interface));
|
||||
|
||||
interface_descriptor.bInterfaceNumber = m_interface->interface_index;
|
||||
endpoint_descriptor_in.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
endpoint_descriptor_out.bEndpointAddress += interface_descriptor.bInterfaceNumber + 1;
|
||||
|
||||
// Full Speed Config
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Full, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// High Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x200;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x200;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_High, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
|
||||
// Super Speed Config
|
||||
endpoint_descriptor_in.wMaxPacketSize = 0x400;
|
||||
endpoint_descriptor_out.wMaxPacketSize = 0x400;
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &interface_descriptor, USB_DT_INTERFACE_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_in, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_descriptor_out, USB_DT_ENDPOINT_SIZE));
|
||||
R_TRY(usbDsInterface_AppendConfigurationData(m_interface, UsbDeviceSpeed_Super, &endpoint_companion, USB_DT_SS_ENDPOINT_COMPANION_SIZE));
|
||||
|
||||
//Setup endpoints.
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_In], endpoint_descriptor_in.bEndpointAddress));
|
||||
R_TRY(usbDsInterface_RegisterEndpoint(m_interface, &m_endpoints[UsbSessionEndpoint_Out], endpoint_descriptor_out.bEndpointAddress));
|
||||
|
||||
R_TRY(usbDsInterface_EnableInterface(m_interface));
|
||||
R_TRY(usbDsEnable());
|
||||
|
||||
log_write("success USB init\n");
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::WaitForConnection(u64 timeout, u32& speed, u32& count) {
|
||||
const SendHeader send_header{
|
||||
.magic = MAGIC,
|
||||
.version = VERSION,
|
||||
};
|
||||
|
||||
alignas(0x1000) u8 aligned[0x1000]{};
|
||||
std::memcpy(aligned, std::addressof(send_header), sizeof(send_header));
|
||||
|
||||
// send header.
|
||||
u32 transferredSize;
|
||||
R_TRY(TransferPacketImpl(false, aligned, sizeof(send_header), &transferredSize, timeout));
|
||||
|
||||
// receive header.
|
||||
struct RecvHeader recv_header{};
|
||||
R_TRY(TransferPacketImpl(true, aligned, sizeof(recv_header), &transferredSize, timeout));
|
||||
|
||||
// copy data into header struct.
|
||||
std::memcpy(&recv_header, aligned, sizeof(recv_header));
|
||||
|
||||
// validate received header.
|
||||
R_UNLESS(recv_header.magic == MAGIC, Result_BadMagic);
|
||||
R_UNLESS(recv_header.version == VERSION, Result_BadVersion);
|
||||
R_UNLESS(recv_header.count > 0, Result_BadCount);
|
||||
|
||||
count = recv_header.count;
|
||||
speed = recv_header.bcdUSB;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
bool Usb::GetConfigured() const {
|
||||
UsbState usb_state;
|
||||
usbDsGetState(std::addressof(usb_state));
|
||||
return usb_state == UsbState_Configured;
|
||||
}
|
||||
|
||||
Event *Usb::GetCompletionEvent(UsbSessionEndpoint ep) const {
|
||||
return std::addressof(m_endpoints[ep]->CompletionEvent);
|
||||
}
|
||||
|
||||
Result Usb::WaitTransferCompletion(UsbSessionEndpoint ep, u64 timeout) const {
|
||||
auto event = GetCompletionEvent(ep);
|
||||
const auto rc = eventWait(event, timeout);
|
||||
|
||||
if (R_FAILED(rc)) {
|
||||
R_TRY(usbDsEndpoint_Cancel(m_endpoints[ep]));
|
||||
eventClear(event);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
Result Usb::TransferAsync(UsbSessionEndpoint ep, void *buffer, u32 size, u32 *out_urb_id) const {
|
||||
return usbDsEndpoint_PostBufferAsync(m_endpoints[ep], buffer, size, out_urb_id);
|
||||
}
|
||||
|
||||
Result Usb::GetTransferResult(UsbSessionEndpoint ep, u32 urb_id, u32 *out_requested_size, u32 *out_transferred_size) const {
|
||||
UsbDsReportData report_data;
|
||||
|
||||
R_TRY(eventClear(std::addressof(m_endpoints[ep]->CompletionEvent)));
|
||||
R_TRY(usbDsEndpoint_GetReportData(m_endpoints[ep], std::addressof(report_data)));
|
||||
R_TRY(usbDsParseReportData(std::addressof(report_data), urb_id, out_requested_size, out_transferred_size));
|
||||
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::TransferPacketImpl(bool read, void *page, u32 size, u32 *out_size_transferred, u64 timeout) const {
|
||||
u32 urb_id;
|
||||
|
||||
/* If we're not configured yet, wait to become configured first. */
|
||||
// R_TRY(usbDsWaitReady(timeout));
|
||||
if (!GetConfigured()) {
|
||||
R_TRY(eventWait(usbDsGetStateChangeEvent(), timeout));
|
||||
R_TRY(eventClear(usbDsGetStateChangeEvent()));
|
||||
R_THROW(0xEA01);
|
||||
}
|
||||
|
||||
/* Select the appropriate endpoint and begin a transfer. */
|
||||
const auto ep = read ? UsbSessionEndpoint_Out : UsbSessionEndpoint_In;
|
||||
R_TRY(TransferAsync(ep, page, size, std::addressof(urb_id)));
|
||||
|
||||
/* Try to wait for the event. */
|
||||
R_TRY(WaitTransferCompletion(ep, timeout));
|
||||
|
||||
/* Return what we transferred. */
|
||||
return GetTransferResult(ep, urb_id, nullptr, out_size_transferred);
|
||||
}
|
||||
|
||||
Result Usb::SendCommand(s64 off, s64 size) const {
|
||||
struct {
|
||||
u32 hash;
|
||||
u32 magic;
|
||||
s64 off;
|
||||
s64 size;
|
||||
} meta{0, 0, off, size};
|
||||
|
||||
alignas(0x1000) static u8 aligned[0x1000]{};
|
||||
std::memcpy(aligned, std::addressof(meta), sizeof(meta));
|
||||
|
||||
u32 transferredSize;
|
||||
return TransferPacketImpl(false, aligned, sizeof(meta), &transferredSize, m_transfer_timeout);
|
||||
}
|
||||
|
||||
Result Usb::Finished() const {
|
||||
return SendCommand(0, 0);
|
||||
}
|
||||
|
||||
Result Usb::InternalRead(void* _buf, s64 off, s64 size) const {
|
||||
u8* buf = (u8*)_buf;
|
||||
alignas(0x1000) u8 aligned[0x1000]{};
|
||||
const auto stored_size = size;
|
||||
s64 total = 0;
|
||||
|
||||
while (size) {
|
||||
auto read_size = size;
|
||||
auto read_buf = buf;
|
||||
|
||||
if (u64(buf) & 0xFFF) {
|
||||
read_size = std::min<u64>(size, sizeof(aligned) - (u64(buf) & 0xFFF));
|
||||
read_buf = aligned;
|
||||
log_write("unaligned read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
} else if (read_size & 0xFFF) {
|
||||
if (read_size <= 0xFFF) {
|
||||
log_write("unaligned small read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
read_buf = aligned;
|
||||
} else {
|
||||
log_write("unaligned big read %zd %zd read_size: %zd align: %zd\n", off, size, read_size, u64(buf) & 0xFFF);
|
||||
// read as much as possible into buffer, the rest will
|
||||
// be handled in a second read which will be aligned size aligned.
|
||||
read_size = read_size & ~0xFFF;
|
||||
}
|
||||
}
|
||||
|
||||
R_TRY(SendCommand(off, read_size));
|
||||
|
||||
u32 transferredSize{};
|
||||
R_TRY(TransferPacketImpl(true, read_buf, read_size, &transferredSize, m_transfer_timeout));
|
||||
R_UNLESS(transferredSize <= read_size, Result_BadTransferSize);
|
||||
|
||||
if (read_buf == aligned) {
|
||||
std::memcpy(buf, aligned, transferredSize);
|
||||
}
|
||||
|
||||
if (transferredSize < read_size) {
|
||||
log_write("reading less than expected! %u vs %zd stored: %zd\n", transferredSize, read_size, stored_size);
|
||||
}
|
||||
|
||||
off += transferredSize;
|
||||
buf += transferredSize;
|
||||
size -= transferredSize;
|
||||
total += transferredSize;
|
||||
}
|
||||
|
||||
R_UNLESS(total == stored_size, Result_BadTotalSize);
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
Result Usb::Read(void* buf, s64 off, s64 size, u64* bytes_read) {
|
||||
R_TRY(GetOpenResult());
|
||||
R_TRY(InternalRead(buf, off, size));
|
||||
*bytes_read = size;
|
||||
R_SUCCEED();
|
||||
}
|
||||
|
||||
} // namespace sphaira::yati::source
|
||||
1317
sphaira/source/yati/yati.cpp
Normal file
130
tools/usb_total.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# based on usb.py from Tinfoil, by Adubbz.
|
||||
import struct
|
||||
import sys
|
||||
import os
|
||||
import usb.core
|
||||
import usb.util
|
||||
import time
|
||||
import glob
|
||||
|
||||
# magic number (SPHA) for the script and switch.
|
||||
MAGIC = 0x53504841
|
||||
# version of the usb script.
|
||||
VERSION = 1
|
||||
# list of supported extensions.
|
||||
EXTS = (".nsp", ".xci", ".nsz", ".xcz")
|
||||
|
||||
def verify_switch(bcdUSB, count, in_ep, out_ep):
|
||||
header = in_ep.read(8, timeout=0)
|
||||
switch_magic = struct.unpack('<I', header[0:4])[0]
|
||||
switch_version = struct.unpack('<I', header[4:8])[0]
|
||||
|
||||
if switch_magic != MAGIC:
|
||||
raise Exception("Unexpected magic {}".format(switch_magic))
|
||||
|
||||
if switch_version != VERSION:
|
||||
raise Exception("Unexpected version {}".format(switch_version))
|
||||
|
||||
send_data = struct.pack('<IIII', MAGIC, VERSION, bcdUSB, count)
|
||||
out_ep.write(data=send_data, timeout=0)
|
||||
|
||||
def wait_for_input(path, in_ep, out_ep):
|
||||
buf = None
|
||||
predicted_off = 0
|
||||
print("now waiting for intput\n")
|
||||
|
||||
with open(path, "rb") as file:
|
||||
while True:
|
||||
header = in_ep.read(24, timeout=0)
|
||||
|
||||
range_offset = struct.unpack('<Q', header[8:16])[0]
|
||||
range_size = struct.unpack('<Q', header[16:24])[0]
|
||||
|
||||
if (range_offset == 0 and range_size == 0):
|
||||
break
|
||||
|
||||
if (buf != None and range_offset == predicted_off and range_size == len(buf)):
|
||||
# print("predicted the read off {} size {}".format(predicted_off, len(buf)))
|
||||
pass
|
||||
else:
|
||||
file.seek(range_offset)
|
||||
buf = file.read(range_size)
|
||||
|
||||
if (len(buf) != range_size):
|
||||
# print("off: {} size: {}".format(range_offset, range_size))
|
||||
raise ValueError('bad buf size!!!!!')
|
||||
|
||||
result = out_ep.write(data=buf, timeout=0)
|
||||
if (len(buf) != result):
|
||||
print("off: {} size: {}".format(range_offset, range_size))
|
||||
raise ValueError('bad result!!!!!')
|
||||
|
||||
predicted_off = range_offset + range_size
|
||||
buf = file.read(range_size)
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("hello world")
|
||||
|
||||
# check which mode the user has selected.
|
||||
args = len(sys.argv)
|
||||
if (args != 2):
|
||||
print("either run python usb_total.py game.nsp OR drag and drop the game onto the python file (if python is in your path)")
|
||||
sys.exit(1)
|
||||
|
||||
path = sys.argv[1]
|
||||
files = []
|
||||
|
||||
if os.path.isfile(path) and path.endswith(EXTS):
|
||||
files.append(path)
|
||||
elif os.path.isdir(path):
|
||||
for f in glob.glob(path + "/**/*.*", recursive=True):
|
||||
if os.path.isfile(f) and f.endswith(EXTS):
|
||||
files.append(f)
|
||||
else:
|
||||
raise ValueError('must be a file!')
|
||||
|
||||
# for file in files:
|
||||
# print("found file: {}".format(file))
|
||||
|
||||
# Find the switch
|
||||
print("waiting for switch...\n")
|
||||
dev = None
|
||||
|
||||
while (dev is None):
|
||||
dev = usb.core.find(idVendor=0x057E, idProduct=0x3000)
|
||||
time.sleep(0.5)
|
||||
|
||||
print("found the switch!\n")
|
||||
|
||||
cfg = None
|
||||
|
||||
try:
|
||||
cfg = dev.get_active_configuration()
|
||||
print("found active config")
|
||||
except usb.core.USBError:
|
||||
print("no currently active config")
|
||||
cfg = None
|
||||
|
||||
if cfg is None:
|
||||
dev.set_configuration()
|
||||
cfg = dev.get_active_configuration()
|
||||
|
||||
is_out_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_OUT
|
||||
is_in_ep = lambda ep: usb.util.endpoint_direction(ep.bEndpointAddress) == usb.util.ENDPOINT_IN
|
||||
out_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_out_ep)
|
||||
in_ep = usb.util.find_descriptor(cfg[(0,0)], custom_match=is_in_ep)
|
||||
assert out_ep is not None
|
||||
assert in_ep is not None
|
||||
|
||||
print("iManufacturer: {} iProduct: {} iSerialNumber: {}".format(dev.manufacturer, dev.product, dev.serial_number))
|
||||
print("bcdUSB: {} bMaxPacketSize0: {}".format(hex(dev.bcdUSB), dev.bMaxPacketSize0))
|
||||
|
||||
try:
|
||||
verify_switch(dev.bcdUSB, len(files), in_ep, out_ep)
|
||||
|
||||
for file in files:
|
||||
print("installing file: {}".format(file))
|
||||
wait_for_input(file, in_ep, out_ep)
|
||||
dev.reset()
|
||||
except Exception as inst:
|
||||
print("An exception occurred " + str(inst))
|
||||