diff --git a/.gitignore b/.gitignore
index 31e40848..d69d476a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -84,3 +84,6 @@ src/main/resources/meta/*/*.json
test-diff
htmlReport
docs/logs
+
+# Sensitive keys
+game_encryption.json
\ No newline at end of file
diff --git a/AquaNet/src/libs/i18n/en_ref.ts b/AquaNet/src/libs/i18n/en_ref.ts
index f08fe877..0fb0f272 100644
--- a/AquaNet/src/libs/i18n/en_ref.ts
+++ b/AquaNet/src/libs/i18n/en_ref.ts
@@ -144,10 +144,9 @@ export const EN_REF_SETUP = {
'setup.welcome': `Welcome! If you have a game set up, please follow the instructions below to set up the connection with AquaDX.`,
'setup.keychip-warning': `Your keychip(s) are linked to your account and should be kept secure.`,
'setup.steps.one': `Pick a method of setting up network communications. Some browsers may not be able to do automatic setup.`,
- 'setup.steps.two': `Ensure your game has encryption disabled as AquaDX does not support encryption. This may be via a patch or setting.`,
- 'setup.steps.three': `Link your Aime card to your AquaDX account using the Cards page via it's access code or serial number.`,
- 'setup.steps.four': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`,
- 'setup.support-info': `If you need assistance, feel free to make an inquiry in a support channel.`,
+ 'setup.steps.two': `Link your Aime card to your AquaDX account using the Cards page via it's access code or serial number.`,
+ 'setup.steps.three': `Start the game. Upon reaching the title screen, the network icon in the corner should now show green instead of grey.`,
+ 'setup.support-info': `If you need additional assistance, feel free to make an inquiry in a support channel.`,
'setup.reveal-keychip': `Reveal keychip`,
'setup.type.automatic': `Automatic Setup`,
'setup.type.manual': `Manual Setup`,
@@ -156,6 +155,15 @@ export const EN_REF_SETUP = {
'setup.automatic.success': `Success, data has been updated`,
'setup.automatic.failure': `Failed to update information, please ensure access is not blocked and try again.`,
'setup.automatic.select': `Pick file`,
+ 'setup.troubleshooting.header': 'Setup Troubleshooting (FAQ)',
+ 'setup.troubleshooting.items.one': `» It displays DNS BAD on Network Test
+ You likely misconfigured your segatools.ini. Ensure it's pointing to the DNS shown above (under Manual Setup).`,
+ 'setup.troubleshooting.items.two': `» It displays タイトル サーバー BAD (Title Server BAD) on Network Test
+ You should check your segatools.ini to ensure your keychip is set correctly.
+ Additionally, Card Maker users and players using a game version higher than what's listed may need to disable encryption
+ (instructions for Card Maker)`,
+ 'setup.troubleshooting.items.three': `» I can't scan my card
+ The built-in emulated card reader, by default, is configured to use the Enter / Return key to scan in (hold the key).`,
'setup.keychip': 'Keychip Management',
'setup.keychip.warning': 'Keychips can be manually set here for cabinet owners with physical keychips trying to connect to AquaDX. Selecting a keychip will autofill it for the setup below.',
'setup.keychip.add': `Add keychip`,
diff --git a/AquaNet/src/libs/i18n/zh.ts b/AquaNet/src/libs/i18n/zh.ts
index e5ee5412..b997bf7a 100644
--- a/AquaNet/src/libs/i18n/zh.ts
+++ b/AquaNet/src/libs/i18n/zh.ts
@@ -5,6 +5,7 @@ import {
EN_REF_LEADERBOARD,
EN_REF_MAI_PHOTO,
EN_REF_SETTINGS,
+ EN_REF_SETUP,
EN_REF_USER,
EN_REF_USERBOX,
type EN_REF_Welcome
@@ -59,15 +60,15 @@ const zhWelcome: typeof EN_REF_Welcome = {
'welcome.username-missing': '用户名/邮箱必须填哦',
'welcome.email-password-missing': '邮箱和密码必须填哦',
'welcome.waiting-turnstile': '正在验证网络环境…',
- 'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试',
- 'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
- 'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
- 'welcome.reset-password-sent': '重置邮件已发送至 ${email},请翻翻收件箱',
- 'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
- 'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱',
- 'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
- 'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦,请翻翻收件箱!',
- 'welcome.reset-state-1': '邮件发送次数过多,暂时不会再发送新的重置邮件了',
+ 'welcome.turnstile-error': '验证网络环境出错了,请关闭 VPN 后重试',
+ 'welcome.turnstile-timeout': '验证网络环境超时了,请重试',
+ 'welcome.verification-sent': '验证邮件已发送至 ${email},请翻翻收件箱',
+ 'welcome.reset-password-sent': '重置邮件已发送至 ${email},请翻翻收件箱',
+ 'welcome.verify-state-0': '您还没有验证邮箱哦!验证邮件一分钟内刚刚发到您的邮箱,请翻翻收件箱',
+ 'welcome.verify-state-1': '您还没有验证邮箱哦!我们在过去的 24 小时内已经发送了 3 封验证邮件,所以我们不会再发送了,请翻翻收件箱',
+ 'welcome.verify-state-2': '您还没有验证邮箱哦!我们刚刚又发送了一封验证邮件,请翻翻收件箱',
+ 'welcome.reset-state-0': '重置邮件刚刚发送到你的邮箱啦,请翻翻收件箱!',
+ 'welcome.reset-state-1': '邮件发送次数过多,暂时不会再发送新的重置邮件了',
'welcome.verifying': '正在验证邮箱…请稍等',
'welcome.verified': '您的邮箱已经验证成功!您现在可以登录了',
'welcome.verification-failed': '验证失败:${message}。请重试',
@@ -90,7 +91,7 @@ const zhGeneral: typeof EN_REF_GENERAL = {
'game.ongeki': "音击",
'game.wacca': "华卡",
"status.error": "发生错误",
- "status.error.hint": "出了一些问题,请稍后刷新重试或者",
+ "status.error.hint": "出了一些问题,请稍后刷新重试或者",
"status.error.hint.link": "加我们的 Discord 群问一问",
"status.detail": "详细信息:${detail}",
"action.refresh": "刷新",
@@ -112,21 +113,21 @@ const zhHome: typeof EN_REF_HOME = {
'home.link-card': '绑定游戏卡',
'home.link-cards-description':'绑定游戏数据卡(Amusement IC 或 Aime 卡)后才可以访问游戏存档哦',
'home.join-community': '加入群组',
- 'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助',
+ 'home.join-community-description': '加入我们的聊天群组,与其他玩家聊天、获取帮助',
'home.setup': '连接到 AquaDX',
- 'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接',
+ 'home.setup-description': '如果您有街机框体或者手台,点击这里设置服务器的连接',
'home.import': '导入玩家数据',
- 'home.import-description': '如果你来自其他在线服,可以点击这里导入从其他服务器导出的数据',
+ 'home.import-description': '如果你来自其他在线服,可以点击这里导入从其他服务器导出的数据',
'home.linkcard.cards': "已绑卡片",
'home.linkcard.description': "这些是您绑定到帐户的卡",
'home.linkcard.account-card': "账户卡",
'home.linkcard.registered': "注册于",
'home.linkcard.lastused': "上次使用",
- 'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域",
- 'home.linkcard.access-code': "卡背面的 20 位卡号(如果提示找不到卡,请尝试使用游戏内置的显示卡号功能,输入游戏读取到的卡号)",
+ 'home.linkcard.enter-info': "请输入以下信息,或将 aime.txt / felica.txt 文件拖放到此区域",
+ 'home.linkcard.access-code': "卡背面的 20 位卡号(如果提示找不到卡,请尝试使用游戏内置的显示卡号功能,输入游戏读取到的卡号)",
'home.linkcard.enter-sn1': "在您的手机",
'home.linkcard.enter-sn2': "上下载 NFC Tools 并扫描您的卡。然后输入显示的 SN 号。",
- 'home.linkcard.kdx-notice': "如果你在玩 KanadeDX,请在这里输入虚拟卡号(可以在设置 > 卡号中找到卡号)",
+ 'home.linkcard.kdx-notice': "如果你在玩 KanadeDX,请在这里输入虚拟卡号(可以在设置 > 卡号中找到卡号)",
'home.linkcard.link': "绑定",
'home.linkcard.data-conflict': "卡号冲突",
'home.linkcard.name': "名称",
@@ -137,15 +138,15 @@ const zhHome: typeof EN_REF_HOME = {
'home.linkcard.notfound': "找不到卡",
'home.linkcard.unlink': "取消链接",
'home.linkcard.unlink-notice': "你确定要取消此卡的链接吗?",
- 'home.linkcard.felica-ac-warning': "该 Access Code 是一张 FeliCa AIC 卡。\n如果你使用实体卡(而非 aime.txt 模拟)刷卡登录游戏,与官方服务器不同,你需要绑定该卡的 FeliCa SN(或与之对应的,游戏界面中查看得到的 00 开头的卡号)而非此号码。\n如果你使用 aime.txt 模拟登录,请忽略本警告继续绑定。",
- 'home.setup.welcome': "欢迎! 如果您有街机框体或者手台,请按照以下说明设置以连接到 AquaDX。",
- 'home.setup.blockquote': "我们假设您已经拥有所需的文件,并且可以启动机台或手台附带的游戏(例如 ROM 和 segatools)。如果没有,请联系您设备的卖家以获取所需的文件,因为出于版权原因,我们不会提供这些文件。",
+ 'home.linkcard.felica-ac-warning': "该 Access Code 是一张 FeliCa AIC 卡。\n如果你使用实体卡(而非 aime.txt 模拟)刷卡登录游戏,与官方服务器不同,你需要绑定该卡的 FeliCa SN(或与之对应的,游戏界面中查看得到的 00 开头的卡号)而非此号码。\n如果你使用 aime.txt 模拟登录,请忽略本警告继续绑定。",
+ 'home.setup.welcome': "欢迎! 如果您有街机框体或者手台,请按照以下说明设置以连接到 AquaDX。",
+ 'home.setup.blockquote': "我们假设您已经拥有所需的文件,并且可以启动机台或手台附带的游戏(例如 ROM 和 segatools)。如果没有,请联系您设备的卖家以获取所需的文件,因为出于版权原因,我们不会提供这些文件。",
'home.setup.get': "开始",
'home.setup.edit': "请打开您的 segatools.ini 文件并修改以下行",
- 'home.setup.test': "在您重新启动游戏后,应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。",
- 'home.setup.ask': "如果您有任何问题,请加入我们的",
+ 'home.setup.test': "在您重新启动游戏后,应该能够连接到 AquaDX。可以验证测试菜单中的网络测试测试连接是否全部良好。",
+ 'home.setup.ask': "如果您有任何问题,请加入我们的",
'home.setup.support': "以获取支持",
- 'home.setup.keychip-tips': "这是你的狗号,不要与任何人分享",
+ 'home.setup.keychip-tips': "这是你的狗号,不要与任何人分享",
'home.community.discord': 'Discord',
'home.community.telegram': 'Telegram (中文)',
'home.community.qq': 'QQ (中文)',
@@ -154,6 +155,37 @@ const zhHome: typeof EN_REF_HOME = {
'home.import.data-conflict': '继续导入将覆盖现有数据',
}
+const zhSetup: typeof EN_REF_SETUP = {
+ 'setup.welcome': `欢迎!如果您已准备好游戏,请按照以下说明设置与 AquaDX 的连接。`,
+ 'setup.keychip-warning': `您的密钥芯片已与您的账户绑定,请妥善保管。`,
+ 'setup.steps.one': `选择一种网络通信设置方法。某些浏览器可能无法进行自动设置。`,
+ 'setup.steps.two': `请通过 卡片 页面,使用访问码或序列号将您的 Aime 卡与 AquaDX 账户绑定。`,
+ 'setup.steps.three': `启动游戏。进入标题画面后,角落处的网络图标应变为绿色,而非灰色。`,
+ 'setup.support-info': `如需进一步协助,欢迎在 支持频道 中咨询。`,
+ 'setup.reveal-keychip': `显示密钥芯片`,
+ 'setup.type.automatic': `自动设置`,
+ 'setup.type.manual': `手动设置`,
+ 'setup.manual': `请使用以下信息修改您的 segatools.ini 文件。`,
+ 'setup.automatic': `请在下方的 segatools.ini 文件中选择,以自动填写网络信息。`,
+ 'setup.automatic.select': `选择文件`,
+ 'setup.automatic.success': `成功,数据已更新`,
+ 'setup.automatic.failure': `更新信息失败,请确保访问未被阻止,然后重试。`,
+ 'setup.troubleshooting.header': '设置故障排除(常见问题解答)',
+ 'setup.troubleshooting.items.one': `» 网络测试显示 DNS BAD
+ 您可能配置了错误的 segatools.ini。请确保其指向上述(手动设置部分)显示的 DNS。`,
+ 'setup.troubleshooting.items.two': `» 网络测试显示 タイトル サーバー BAD (Title Server BAD)
+ 请检查您的 segatools.ini 文件,确保密钥芯片设置正确。
+ 此外, Card Maker 用户以及使用高于列表中版本的游戏版本的玩家,可能需要禁用加密
+ (Card Maker 操作指南)`,
+ 'setup.troubleshooting.items.three': `» 无法扫描卡片
+ 内置的模拟读卡器默认配置为使用回车键进行扫描(长按该键)。`,
+ 'setup.keychip': '钥匙芯片管理',
+ 'setup.keychip.warning': '对于拥有实体钥匙芯片并试图连接到 AquaDX 的机柜所有者,可在此手动设置钥匙芯片。选择钥匙芯片后,下方设置项将自动填充。',
+ 'setup.keychip.add': `添加钥匙芯片`,
+ 'setup.keychip.delete': `删除`,
+ 'setup.keychip.placeholder': `新钥匙芯片 ID`
+}
+
const zhSettings: typeof EN_REF_SETTINGS = {
'settings.title': '用户设置',
'settings.tabs.profile': '个人资料',
@@ -163,7 +195,7 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.tabs.ongeki': '音击',
'settings.tabs.wacca': '华卡',
- "settings.options.all.preset-warning": "您正在使用预设选项配置,部分选项可能显示不正确。",
+ "settings.options.all.preset-warning": "您正在使用预设选项配置,部分选项可能显示不正确。",
"settings.options.all.personal-best": "记录",
"settings.options.all.rival-score": "对手",
"settings.options.all.life": "生命值",
@@ -220,11 +252,11 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.fields.optOutOfLeaderboard.name': '不参与排行榜',
'settings.fields.optOutOfLeaderboard.desc': '登录之后还是可以在排行榜上看到自己',
'settings.fields.enableMusicRank.name': '在你的机台上启用「推荐乐曲排行榜」',
- 'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话,可以关闭这个(会影响你自己的机器)。',
+ 'settings.fields.enableMusicRank.desc': '如果你自己设计了排行榜的话,可以关闭这个(会影响你自己的机器)。',
'settings.mai2.name': '玩家名字',
'settings.profile.picture': '头像',
'settings.profile.upload-new': '上传',
- 'settings.profile.bad-format': '无效的图片格式,支持的格式有 PNG、JPG、JPEG、WEBP 和 GIF。',
+ 'settings.profile.bad-format': '无效的图片格式,支持的格式有 PNG、JPG、JPEG、WEBP 和 GIF。',
'settings.profile.save': '保存',
'settings.profile.name': '昵称',
'settings.profile.username': '用户名',
@@ -237,7 +269,7 @@ const zhSettings: typeof EN_REF_SETTINGS = {
'settings.profile.unchanged': '未更改',
'settings.export': '导出玩家数据',
'settings.batchManualExport': "导出 Batch Manual 格式(用于 Tachi)",
- 'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
+ 'settings.cabNotice': '注意:下面这些设置只会影响你自己的机器,如果你是在其他人的机器上玩的话,请联系机主来改设置',
// AI
'settings.siteNotice': "这些设置仅适用于网站。",
// AI
@@ -284,7 +316,7 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.matching.symbolChat.default': '默认',
'userbox.new.name': 'AquaBox',
- 'userbox.new.setup': '将中二(Lumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。',
+ 'userbox.new.setup': '将中二(Lumi 或更高版本)的游戏文件夹拖放到下方区域,以显示带有名牌和头像的 UserBox。所有文件都在浏览器中处理。',
'userbox.new.setup.notice': '选择包含游戏数据的最外层文件夹。',
'userbox.new.setup.processing_file': '正在处理文件',
'userbox.new.setup.finalizing': '正在保存到内部存储',
@@ -298,20 +330,20 @@ export const zhUserbox: typeof EN_REF_USERBOX = {
'userbox.new.activate_desc': '启用后可显示带有名牌和头像的 UserBox',
'userbox.new.activate_profile': '在用户页面启用 AquaBox',
'userbox.new.activate_profile_desc': '启用后可在个人页面显示带有名牌和头像的 UserBox',
- 'userbox.new.error.invalidFolder': '所选文件夹无效。请确认游戏版本为 Lumi 或更新,并且包含 “A000” 选项包。',
+ 'userbox.new.error.invalidFolder': '所选文件夹无效。请确认游戏版本为 Lumi 或更新,并且包含 “A000” 选项包。',
'userbox.new.error.invalidUrl': '输入的 URL 无效。'
};
export const zhMaiPhoto: typeof EN_REF_MAI_PHOTO = {
'maiphoto.title': 'Mai 纪念照片库',
- 'maiphoto.url_warning': '注意:如果想与朋友分享图片的话,请先保存照片再发出去。不要复制图片 URL,因为 URL 中包含 AquaDX 账号信息。',
+ 'maiphoto.url_warning': '注意:如果想与朋友分享图片的话,请先保存照片再发出去。不要复制图片 URL,因为 URL 中包含 AquaDX 账号信息。',
'maiphoto.none': '还没有图片哦~ 可以在每次游戏结束的时候点击上传来上传照片。',
}
export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
'trans.title': '🏳️⚧️ AquaTrans™ 数据迁移工具',
'trans.confirm.unbackuped.title': '确认迁移',
- 'trans.confirm.unbackuped.msg': '似乎还没有备份目标服务器的数据,真的要继续吗?(推荐先备份一下,因为迁移的时候会覆盖数据)',
+ 'trans.confirm.unbackuped.msg': '似乎还没有备份目标服务器的数据,真的要继续吗?(推荐先备份一下,因为迁移的时候会覆盖数据)',
'trans.confirm.untested.title': '不太聪明喵',
'trans.confirm.untested.msg': '在两个服务器上都测试完连接之后才能进行数据迁移哦!',
'trans.confirm.done.title': '完成!',
@@ -319,8 +351,8 @@ export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
'trans.alert.in-progress': '在迁移了在迁移了',
'trans.prompt-html': `
👋 欢迎使用 AquaTrans™ 服务器游玩数据迁移工具!
- 这个工具可以导出任意服务器的数据,并使用连接凭证(卡号、服务器地址和 Keychip ID)将数据导入任何其他服务器。
- 我将模拟游戏客户端,从源服务器拉取游戏数据并推送到目标服务器。
+ 这个工具可以导出任意服务器的数据,并使用连接凭证(卡号、服务器地址和 Keychip ID)将数据导入任何其他服务器。
+ 我将模拟游戏客户端,从源服务器拉取游戏数据并推送到目标服务器。
填写下面的表格开始迁移吧!
`,
'trans.error.empty': '请填写所有字段。',
@@ -336,11 +368,11 @@ export const zhAquaTrans: typeof EN_REF_AQUATRANS = {
'trans.btn.test': '测试连接',
'trans.btn.export': '导出数据',
'trans.btn.import': '导入数据',
- 'trans.blacklist': "这个服务器的服主把这个导出工具 ban 了,所以不能从这里导出",
+ 'trans.blacklist': "这个服务器的服主把这个导出工具 ban 了,所以不能从这里导出",
}
export const ZH = { ...zhUser, ...zhWelcome, ...zhGeneral,
...zhLeaderboard, ...zhHome, ...zhSettings, ...zhUserbox, ...zhMaiPhoto,
- ...zhAquaTrans
+ ...zhAquaTrans, ...zhSetup
}
diff --git a/AquaNet/src/pages/Home/SetupInstructions.svelte b/AquaNet/src/pages/Home/SetupInstructions.svelte
index 11141c9a..6f5ba7f0 100644
--- a/AquaNet/src/pages/Home/SetupInstructions.svelte
+++ b/AquaNet/src/pages/Home/SetupInstructions.svelte
@@ -201,10 +201,18 @@ ${keychipLines}`.trim(), {
3.
{@html t('setup.steps.three')}
-
- 4.
{@html t('setup.steps.four')}
-
-
+ {t('setup.troubleshooting.header')}
+
+ -
+ {@html t('setup.troubleshooting.items.one')}
+
+ -
+ {@html t('setup.troubleshooting.items.two')}
+
+ -
+ {@html t('setup.troubleshooting.items.three')}
+
+
{@html t('setup.support-info')}
@@ -217,6 +225,9 @@ ${keychipLines}`.trim(), {
.code
overflow-x: auto
+ ul
+ li
+ margin: 0.75em 0
.divider
width: 90%
height: 1px
diff --git a/README.md b/README.md
index a099a9ee..0f655f45 100644
--- a/README.md
+++ b/README.md
@@ -32,21 +32,22 @@ Multipurpose game server for ALL.Net games.
> [!WARNING]
> CHUNITHM pre-NEW!! and maimai pre-DX are no longer supported after February 6th, 2026 and all associated data will be removed.
-| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Data Import |
-|------------------------|---------------------|---------------------|-------------------------------------------------------------|--------|-------------|
-| SDHD: CHUNITHM | 2.45 (X-VERSE-X) | 2.00 (NEW) | | ✅ | ❗ |
-| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Missing circle (teams) support | ✅ | ✅ |
-| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ |
-| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ |
-| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ❌ |
-| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ |
-| SDFE: Wacca | 3.07 (Reverse) | N/A | Later versions are EOS patches, network will not work | ✅ | ❌ |
+| Game | Latest Ver. | Initial Ver. | Notes | Web UI | Import | Encrypt |
+|------------------------|---------------------|---------------------|-------------------------------------------------------------|--------|-------------|---------|
+| SDHD: CHUNITHM | 2.45 (X-VERSE-X) | 2.00 (NEW) | | ✅ | ❗ | ✅ |
+| SDEZ: maimai DX | 1.65 (CiRCLE PLUS) | 1.00 (DX) | Missing circle (teams) support | ✅ | ✅ | ✅ |
+| SDGA: maimai DX (Intl) | 1.60 (CiRCLE) | 1.00 (DX) | Thanks [@Clansty](https://github.com/clansty) | ✅ | ✅ | ✅ |
+| SDED: Card Maker | 1.39 | N/A | Thanks [@Becods](https://github.com/Becods) | ❌ | ❌ | ❌ |
+| SDDT: O.N.G.E.K.I. | 1.50 (Re:Fresh) | N/A | Thanks [@PenguinCaptain](https://github.com/PenguinCaptain) | ✅ | ❌ | ✅ |
+| SBZV: Project DIVA | 7.10 | N/A | Minimal support | ❌ | ❌ | 🔓 |
+| SDFE: Wacca | 3.07 (REVERSE) | N/A | Later versions are EOS patches | ✅ | ❌ | 🔓 |
Check out these docs for more information.
* [Game specific notes](docs/game_specific_notes.md)
* [Frequently asked questions](docs/frequently_asked_questions.md)
+* [Encryption notes](docs/encryption.md)
## Usage
diff --git a/config/application.properties b/config/application.properties
index 2a55c13a..7b63286f 100644
--- a/config/application.properties
+++ b/config/application.properties
@@ -142,4 +142,4 @@ aqua-net.bot.enabled=true
aqua-net.bot.secret=hunter2
## OpenAI Settings (For content moderation)
-aqua-net.openai.api-key=sk-1234567890abcdef1234567890abcdef
+aqua-net.openai.api-key=sk-1234567890abcdef1234567890abcdef
\ No newline at end of file
diff --git a/docs/encryption.md b/docs/encryption.md
new file mode 100644
index 00000000..50437860
--- /dev/null
+++ b/docs/encryption.md
@@ -0,0 +1,35 @@
+# Encryption Support
+
+AquaDX supports encryption for Chunithm, Maimai and Ongeki.
+It is not required to connect and keys are NOT provided by default.
+
+Card Maker has encryption features in the client, but they are not supported due to it's complexity.
+
+## Adding new keys to AquaDX
+
+> [!IMPORTANT]
+> Ensure you're using AquaDX from source. Docker will work, as long as you use `docker-compose up --build`
+
+1. Create a new file at `src/main/resources/data/game//game_encryption.json`
+2. Fill it out using the following schema:
+```json
+{
+ "code": "SDHD",
+ "versions": [215, 216],
+ "key": "DECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBADDECAFBAD",
+ "iv": "DECAFBADDECAFBADDECAFBADDECAFBAD",
+ "salt": "DECAFBADDECAFBAD",
+ "iterations": 36
+},
+```
+
+**`key`, `salt`, and `iv` MUST be hex strings. Do NOT insert raw ASCII strings.**
+
+Replace `code` with the correct code for your game (`SDHD` for Chunithm JP, `SDEZ` for Maimai JP, etc.).
+Set `versions` to be an array of game versions without the decimal (215 for 2.15, 150 for 1.50, etc.).
+You MUST include rom patch versions in this array or it will not encrypt correctly.
+
+- Maimai DX does NOT use iterations, you do not need to include it
+- Ongeki always uses 64 iterations (you must include it)
+- If using a specific Python script to rip Chunithm keys, focus on the `=>` entries.
+- `key` is 32 bytes long. `iv` is 16 bytes long. `salt` is 8 bytes long. (hex strings should be twice as long as they are in bytes)
\ No newline at end of file
diff --git a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt
index ef977362..bc89868f 100644
--- a/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/allnet/AllNet.kt
@@ -185,20 +185,7 @@ class AllNet(
// If keychip authentication is enabled, the game URLs will be set to /gs/{token}/{game}/...
val base = if (session != null) "gs/$session" else "g"
- return "http://$addr/$base/" + when (gameId) {
- "SDBT" -> "chu2/$ver/$session/"
- "SDHD" -> "chu3/$ver/"
- "SDGS" -> "chu3/$ver/" // International (c3exp)
- "SBZV" -> "diva/"
- "SDDT" -> "ongeki/$ver/"
- "SDEY" -> "mai/"
- "SDGA" -> "mai2/" // International (Exp)
- "SDGB" -> "mai2/" // International (China) - TODO: Test it
- "SDEZ" -> "mai2/"
- "SDFE" -> "wacca" // Note: Wacca must not end with a trailing slash
- "SDED" -> "card/"
- else -> ""
- }
+ return "http://$addr/$base/$gameId/$ver/"
}
companion object {
diff --git a/src/main/java/icu/samnyan/aqua/sega/cardmaker/CardMakerController.kt b/src/main/java/icu/samnyan/aqua/sega/cardmaker/CardMakerController.kt
index 489b07cb..6f06f38d 100644
--- a/src/main/java/icu/samnyan/aqua/sega/cardmaker/CardMakerController.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/cardmaker/CardMakerController.kt
@@ -19,7 +19,7 @@ import java.time.format.DateTimeFormatter
* @author samnyan (privateamusement@protonmail.com)
*/
@RestController
-@RequestMapping("/g/card")
+@RequestMapping("/g/SDED/{version}")
class CardMakerController(
val mapper: BasicMapper,
@param:Value("\${allnet.server.host:}") val ALLNET_HOST: String,
diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt
index 714580eb..4bbaa203 100644
--- a/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/chusan/ChusanController.kt
@@ -17,13 +17,18 @@ import icu.samnyan.aqua.spring.Metrics
import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory
import org.springframework.web.bind.annotation.RestController
+import javax.crypto.SecretKeyFactory
+import javax.crypto.spec.PBEKeySpec
/**
* @author samnyan (privateamusement@protonmail.com)
*/
@Suppress("unused")
@RestController
-@API(value = ["/g/chu3/{version}/ChuniServlet", "/g/chu3/{version}"])
+@API(value = [
+ "/g/SDHD/{version}/ChuniServlet", "/g/SDHD/{version}",
+ "/g/SDGS/{version}/ChuniServlet", "/g/SDGS/{version}",
+])
class ChusanController(
val mapper: StringMapper,
val cmMapper: BasicMapper,
@@ -37,14 +42,19 @@ class ChusanController(
): MeowApi({ api, resp ->
if (resp is String) resp
else (if ("CM" in api || api == "GetUserItemApi" ) cmMapper else mapper).write(resp)
+},
+@kotlin.ExperimentalStdlibApi
+{ endpoint: String ->
+ db.gameData.chu3GameEncryption.map{
+ val suffix = when (it.code) { "SDGS" -> "C3Exp" else -> "" }
+ SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
+ .generateSecret(
+ PBEKeySpec((endpoint + suffix).toCharArray(), it.salt!!.hexToByteArray(), it.iterations, 128)
+ ).encoded.toHexString().substring(0, 32)
+ }
}) {
val log = LoggerFactory.getLogger(ChusanController::class.java)
- val noopEndpoint = setOf("UpsertClientBookkeepingApi", "UpsertClientDevelopApi", "UpsertClientErrorApi",
- "UpsertClientSettingApi", "UpsertClientTestmodeApi", "CreateTokenApi", "RemoveTokenApi", "UpsertClientUploadApi",
- "PrinterLoginApi", "PrinterLogoutApi", "Ping", "GameLogoutApi", "RemoveMatchingMemberApi",
- "UpsertClientPlayTimeApi", "UpsertClientGameStartApi", "UpsertClientGameEndApi")
-
init { chusanInit() }
val handlers = initH
@@ -60,28 +70,27 @@ class ChusanController(
data["c3exp"] = true
}
+ val apiFriendlyName = if (endpoint.lowercase() == endpoint && endpoint.length == 32)
+ hashes.filter { it.value.contains(api) }.keys.first().str + " (encrypt)" else api
+
if (api.startsWith("CM") && api !in handlers) api = api.removePrefix("CM")
val token = TokenChecker.tokenShort()
- log.info("$token : $api < ${data.toJson()}")
+ log.info("$token : $apiFriendlyName < ${data.toJson()}")
val noop = """{"returnCode":"1","apiName":"$api"}"""
- if (api !in noopEndpoint && !handlers.containsKey(api)) {
- log.warn("$token : $api > not found")
+ if (!handlers.containsKey(api)) {
+ log.warn("$token : $apiFriendlyName > not found")
return noop
}
// Only record the counter metrics if the API is known.
Metrics.counter("aquadx_chusan_api_call", "api" to api).increment()
- if (api in noopEndpoint) {
- log.info("$token : $api > no-op")
- return noop
- }
return try {
Metrics.timer("aquadx_chusan_api_latency", "api" to api).recordCallable {
serialize(api, handlers[api]!!(ctx) ?: noop).also {
if (api !in setOf("GetUserItemApi", "GetGameEventApi"))
- log.info("$token : $api > ${it.truncate(500)}")
+ log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
}
}
} catch (e: Exception) {
diff --git a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt
index 07371a45..45485d6a 100644
--- a/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/chusan/handler/ChusanApis.kt
@@ -431,5 +431,23 @@ fun ChusanController.chusanInit() {
// process()
"""{"returnCode":"1"}"""
+
+ // NOTE: no-op APIs moved to respect encryption
+ "UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"UpsertClientBookkeepingApi"}""" }
+ "UpsertClientDevelop" static { """{returnCode":1, "apiName":"UpsertClientDevelopApi"}""" }
+ "UpsertClientError" static { """{returnCode":1, "apiName":"UpsertClientErrorApi"}""" }
+ "UpsertClientSetting" static { """{returnCode":1, "apiName":"UpsertClientSettingApi"}""" }
+ "UpsertClientTestmode" static { """{returnCode":1, "apiName":"UpsertClientTestmodeApi"}""" }
+ "CreateToken" static { """{returnCode":1, "apiName":"CreateTokenApi"}""" }
+ "RemoveToken" static { """{returnCode":1, "apiName":"RemoveTokenApi"}""" }
+ "UpsertClientUpload" static { """{returnCode":1, "apiName":"UpsertClientUploadApi"}""" }
+ "PrinterLogin" static { """{returnCode":1, "apiName":"PrinterLoginApi"}""" }
+ "PrinterLogout" static { """{returnCode":1, "apiName":"PrinterLogoutApi"}""" }
+ "Ping" static { """{returnCode":1, "apiName":"Ping"}""" }
+ "GameLogout" static { """{returnCode":1, "apiName":"GameLogoutApi"}""" }
+ "RemoveMatchingMember" static { """{returnCode":1, "apiName":"RemoveMatchingMemberApi"}""" }
+ "UpsertClientPlayTime" static { """{returnCode":1, "apiName":"UpsertClientPlayTimeApi"}""" }
+ "UpsertClientGameStart" static { """{returnCode":1, "apiName":"UpsertClientGameStartApi"}""" }
+ "UpsertClientGameEnd" static { """{returnCode":1, "apiName":"UpsertClientGameEndApi"}""" }
}
}
\ No newline at end of file
diff --git a/src/main/java/icu/samnyan/aqua/sega/diva/DivaController.kt b/src/main/java/icu/samnyan/aqua/sega/diva/DivaController.kt
index 7e349215..1cab32d8 100644
--- a/src/main/java/icu/samnyan/aqua/sega/diva/DivaController.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/diva/DivaController.kt
@@ -35,7 +35,7 @@ val DIVA_INIT = mapOf("db_close" to "0,0", "retry_time" to "FFFF")
* @author samnyan (privateamusement@protonmail.com)
*/
@RestController
-@RequestMapping("/g/diva")
+@RequestMapping("/g/SBZV/{version}")
class DivaController(
val attendHandler: AttendHandler,
val cardProcedureHandler: CardProcedureHandler,
diff --git a/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt b/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt
index 6bda26e1..34bd81f0 100644
--- a/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/general/BaseHandler.kt
@@ -31,11 +31,23 @@ typealias PagePost = (MutJDict) -> Unit
data class PagedProcessor(val add: JDict?, val fn: PagedHandler, var post: PagePost? = null)
// A very :3 way of declaring APIs
-abstract class MeowApi(val serialize: (String, Any) -> String) {
+abstract class MeowApi(
+ val serialize: (String, Any) -> String,
+ val endpointHash: ((String) -> List)? = null
+) {
val initH = mutableMapOf()
+ val hashes = mutableMapOf>()
infix operator fun String.invoke(fn: SpecialHandler) {
if (initH.containsKey("${this}Api")) error("Duplicate API $this found! Someone is not smart 👀")
initH["${this}Api"] = fn
+ if (endpointHash != null) {
+ // Keep track of hashes for logging; they're not necessary for anything else
+ hashes["${this}Api"] = mutableListOf()
+ for (hashedEndpoint in endpointHash("${this}Api")) {
+ initH[hashedEndpoint] = fn
+ hashes["${this}Api"]?.add(hashedEndpoint)
+ }
+ }
}
infix fun String.static(fn: () -> Any) = serialize(this, fn()).let { resp -> this { resp } }
diff --git a/src/main/java/icu/samnyan/aqua/sega/general/filter/CompressionFilter.kt b/src/main/java/icu/samnyan/aqua/sega/general/filter/CompressionFilter.kt
index a4242e2c..2e8594df 100644
--- a/src/main/java/icu/samnyan/aqua/sega/general/filter/CompressionFilter.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/general/filter/CompressionFilter.kt
@@ -5,6 +5,8 @@ import ext.logger
import ext.toJson
import icu.samnyan.aqua.net.components.GeoIP
import icu.samnyan.aqua.sega.allnet.TokenChecker
+import icu.samnyan.aqua.sega.general.model.GameEncryptionKey
+import icu.samnyan.aqua.sega.util.GameDataService
import icu.samnyan.aqua.sega.util.ZLib
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
@@ -14,6 +16,9 @@ import org.springframework.stereotype.Component
import org.springframework.web.filter.OncePerRequestFilter
import org.springframework.web.util.ContentCachingResponseWrapper
import java.util.*
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
/**
@@ -21,21 +26,46 @@ import java.util.*
*/
@Component
class CompressionFilter(
- val geoip: GeoIP
+ val geoip: GeoIP,
+ val gameData: GameDataService
) : OncePerRequestFilter() {
companion object {
val log = logger()
val b64d = Base64.getMimeDecoder()
val b64e = Base64.getMimeEncoder()
+
}
+ var keys = gameData.chu3GameEncryption + gameData.mai2GameEncryption + gameData.ogkGameEncryption
+ fun getEncryptionKeys(path: String): GameEncryptionKey? {
+ val endpoint = path.split("/").last()
+ if (endpoint.lowercase() != endpoint || endpoint.length != 32) return null
+
+ val game = path.split("/")[2]
+ val version = path.split("/")[3].filterNot { it == '.' }.toInt()
+ return keys.find { it.versions.contains(version) && it.code == game }
+ }
+
+ @OptIn(ExperimentalStdlibApi::class)
override fun doFilterInternal(req: HttpServletRequest, resp: HttpServletResponse, chain: FilterChain) {
val isDeflate = req.getHeader("content-encoding") == "deflate"
val isDfi = req.getHeader("pragma") == "DFI"
+ val keys = getEncryptionKeys(req.servletPath)
+
// Decode input
val reqSrc = try {
req.inputStream.readAllBytes().let {
+ if (keys != null) {
+ val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
+ cipher.init(
+ Cipher.DECRYPT_MODE,
+ SecretKeySpec(keys.key!!.hexToByteArray(), "AES"),
+ IvParameterSpec(keys.iv!!.hexToByteArray())
+ )
+ cipher.doFinal(it)
+ } else it
+ }.let {
if (isDeflate) ZLib.decompress(it)
else if (isDfi) ZLib.decompress(b64d.decode(it))
else it
@@ -50,7 +80,19 @@ class CompressionFilter(
val respW = ContentCachingResponseWrapper(resp)
val result = try {
chain.doFilter(CompressRequestWrapper(req, reqSrc), respW)
- ZLib.compress(respW.contentAsByteArray).let { if (isDfi) b64e.encode(it) else it }
+ ZLib.compress(respW.contentAsByteArray)
+ .let { if (isDfi) b64e.encode(it) else it }
+ .let {
+ if (keys != null) {
+ val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
+ cipher.init(
+ Cipher.ENCRYPT_MODE,
+ SecretKeySpec(keys.key!!.hexToByteArray(), "AES"),
+ IvParameterSpec(keys.iv!!.hexToByteArray())
+ )
+ cipher.doFinal(it)
+ } else it
+ }
} finally {
if (respW.status != 200) {
val details = mapOf(
diff --git a/src/main/java/icu/samnyan/aqua/sega/general/model/EncryptionKey.kt b/src/main/java/icu/samnyan/aqua/sega/general/model/EncryptionKey.kt
new file mode 100644
index 00000000..78123f70
--- /dev/null
+++ b/src/main/java/icu/samnyan/aqua/sega/general/model/EncryptionKey.kt
@@ -0,0 +1,14 @@
+package icu.samnyan.aqua.sega.general.model
+
+import com.fasterxml.jackson.annotation.JsonIgnore
+import com.fasterxml.jackson.annotation.JsonProperty
+import java.time.LocalDateTime
+
+class GameEncryptionKey {
+ var code: String? = null
+ var versions: List = listOf()
+ var key: String? = null
+ var salt: String? = null
+ var iv: String? = null
+ var iterations = 0
+}
\ No newline at end of file
diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt
index a8406721..e547ba4e 100644
--- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2Apis.kt
@@ -424,8 +424,20 @@ fun Maimai2ServletController.initApis() {
)
) }
- "UpsertUserPlaceCircleRegist" static { mapOf(
- "returnCode" to 0,
- "apiName" to "UpsertUserPlaceCircleRegistApi"
- ) }
+ // NOTE: no-op APIs moved to respect encryption
+ "UpsertUserPlaceCircleRegist" static { mapOf( "returnCode" to 0, "apiName" to "com.sega.maimai2servlet.api.UpsertUserPlaceCircleRegistApi") }
+ "GetUserScoreRanking" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.GetUserScoreRankingApi"}""" }
+ "UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientBookkeepingApi"}""" }
+ "UpsertClientSetting" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientSettingApi"}""" }
+ "UpsertClientTestmode" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientTestmodeApi"}""" }
+ "UpsertClientUpload" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientUploadApi"}""" }
+ "Ping" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.Ping"}""" }
+ "RemoveToken" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.RemoveTokenApi"}""" }
+ "CMLogin" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMLoginApi"}""" }
+ "CMLogout" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMLogoutApi"}""" }
+ "CMUpsertBuyCard" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.CMUpsertBuyCardApi"}""" }
+ "UserLogout" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UserLogoutApi"}""" }
+ "GetGameMapAreaCondition" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.GetGameMapAreaConditionApi"}""" }
+ "UpsertUserChargelog" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertUserChargelogApi"}""" }
+ "UpsertClientPlayTime" static { """{returnCode":1, "apiName":"com.sega.maimai2servlet.api.UpsertClientPlayTimeApi"}""" }
}
diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt
index 3cbf80e3..50df3a08 100644
--- a/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/Maimai2ServletController.kt
@@ -11,12 +11,14 @@ import icu.samnyan.aqua.sega.maimai2.handler.*
import icu.samnyan.aqua.sega.maimai2.model.Mai2Repos
import icu.samnyan.aqua.spring.Metrics
import jakarta.servlet.http.HttpServletRequest
+import kotlinx.io.bytestring.hexToByteString
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
+import java.security.MessageDigest
import java.time.format.DateTimeFormatter
import kotlin.reflect.full.declaredMemberProperties
@@ -25,7 +27,11 @@ import kotlin.reflect.full.declaredMemberProperties
*/
@Suppress("unused")
@RestController
-@RequestMapping(path = ["/g/mai2/Maimai2Servlet/", "/g/mai2/"])
+@RequestMapping(path = [
+ "/g/SDGA/{version}/Maimai2Servlet/", "/g/SDGA/{version}",
+ "/g/SDEZ/{version}/Maimai2Servlet/", "/g/SDEZ/{version}",
+ //"/g/SDGB/{version}/Maimai2Servlet/", "/g/SDGB/{version}",
+])
class Maimai2ServletController(
val upsertUserAll: UpsertUserAllHandler,
val getUserItem: GetUserItemHandler,
@@ -40,7 +46,18 @@ class Maimai2ServletController(
val getGameRanking: GetGameRankingHandler,
val db: Mai2Repos,
val net: Maimai2,
-): MeowApi(serialize = { _, resp -> if (resp is String) resp else resp.toJson() }) {
+): MeowApi(
+ serialize = { _, resp -> if (resp is String) resp else resp.toJson() },
+ @kotlin.ExperimentalStdlibApi
+ { endpoint: String ->
+ db.gameData.mai2GameEncryption.map{
+ val suffix = when (it.code) { "SDGA" -> "MaimaiExp" "SDGB" -> "MaimaiChn" else -> "" }
+ MessageDigest.getInstance("MD5")
+ .digest((endpoint + suffix).toByteArray() + it.salt!!.hexToByteArray())
+ .toHexString()
+ }
+ }
+) {
companion object {
private val log = logger()
private val empty = listOf()
@@ -55,11 +72,6 @@ class Maimai2ServletController(
"CMGetUserCardApi","CMGetUserCardPrintErrorApi","CMGetUserDataApi","CMGetUserItemApi","CMUpsertUserPrintApi",
"GetUserFavoriteItemApi")
- val noopEndpoint = setOf("GetUserScoreRankingApi", "UpsertClientBookkeepingApi",
- "UpsertClientSettingApi", "UpsertClientTestmodeApi", "UpsertClientUploadApi", "Ping", "RemoveTokenApi",
- "CMLoginApi", "CMLogoutApi", "CMUpsertBuyCardApi", "UserLogoutApi", "GetGameMapAreaConditionApi",
- "UpsertUserChargelogApi","UpsertClientPlayTimeApi")
-
val members = this::class.declaredMemberProperties
val handlers: Map = initH + endpointList.associateWith { api ->
val name = api.replace("Api", "").lowercase()
@@ -72,27 +84,25 @@ class Maimai2ServletController(
@API("/{api}")
fun handle(@PathVariable api: String, @RequestBody data: Map, req: HttpServletRequest): Any {
val token = TokenChecker.tokenShort()
- log.info("$token : $api < ${data.toJson()}")
+
+ val apiFriendlyName = if (api.lowercase() == api && api.length == 32)
+ ((hashes.filter { it.value.contains(api) }.keys.firstOrNull()?.str + " (encrypt)") ?: api) else "$api (encrypt)"
+ log.info("$token : $apiFriendlyName < ${data.toJson()}")
val noop = """{"returnCode":1,"apiName":"com.sega.maimai2servlet.api.$api"}"""
- if (api !in noopEndpoint && !handlers.containsKey(api)) {
- log.warn("$token : $api > not found")
+ if (!handlers.containsKey(api)) {
+ log.warn("$token : $apiFriendlyName > not found")
return noop
}
// Only record the counter metrics if the API is known.
Metrics.counter("aquadx_maimai2_api_call", "api" to api).increment()
- if (api in noopEndpoint) {
- log.info("$token : $api > no-op")
- return noop
- }
-
return try {
Metrics.timer("aquadx_maimai2_api_latency", "api" to api).recordCallable {
val ctx = RequestContext(req, data.mut)
serialize(api, handlers[api]!!(ctx) ?: noop).also {
- log.info("$token : $api > ${it.truncate(500)}")
+ log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
}
}
} catch (e: Exception) {
diff --git a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt
index 7f6fb1f3..b828c7a1 100644
--- a/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/maimai2/model/Repos.kt
@@ -147,7 +147,7 @@ class Mai2Repos(
val userKaleidx: MAi2UserKaleidxRepo,
val userIntimate: MAi2UserIntimateRepo,
val userRegions: Mai2UserRegionsRepo,
- gameData: GameDataService
+ val gameData: GameDataService
) {
val gameCharge = StaticRepo(gameData.mai2Charges) { it.orderId }
val gameEvent = StaticRepo(gameData.mai2Events) { it.id }
diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt
index 32e477a0..698812d5 100644
--- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiApis.kt
@@ -70,4 +70,16 @@ fun OngekiController.ongekiInit() {
"GetClientTestmode" {
empty.staticLst("clientTestmodeList") + mapOf("placeId" to data["placeId"])
}
+
+ // NOTE: no-op APIs moved to respect encryption
+ "ExtendLockTime" static { """{returnCode":1, "apiName":"ExtendLockTimeApi"}""" }
+ "GameLogout" static { """{returnCode":1, "apiName":"GameLogoutApi"}""" }
+ "RegisterPromotionCard" static { """{returnCode":1, "apiName":"RegisterPromotionCardApi"}""" }
+ "UpsertClientBookkeeping" static { """{returnCode":1, "apiName":"UpsertClientBookkeepingApi"}""" }
+ "UpsertClientDevelop" static { """{returnCode":1, "apiName":"UpsertClientDevelopApi"}""" }
+ "UpsertClientError" static { """{returnCode":1, "apiName":"UpsertClientErrorApi"}""" }
+ "UpsertClientSetting" static { """{returnCode":1, "apiName":"UpsertClientSettingApi"}""" }
+ "UpsertClientTestmode" static { """{returnCode":1, "apiName":"UpsertClientTestmodeApi"}""" }
+ "UpsertUserGplog" static { """{returnCode":1, "apiName":"UpsertUserGplogApi"}""" }
+ "Ping" static { """{returnCode":1, "apiName":"Ping"}""" }
}
\ No newline at end of file
diff --git a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiController.kt b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiController.kt
index a82bb06c..b8192fe0 100644
--- a/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiController.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/ongeki/OngekiController.kt
@@ -8,14 +8,18 @@ import icu.samnyan.aqua.sega.general.GameMusicPopularity
import icu.samnyan.aqua.sega.general.MeowApi
import icu.samnyan.aqua.sega.general.RequestContext
import icu.samnyan.aqua.sega.general.service.CardService
+import icu.samnyan.aqua.sega.maimai2.Maimai2ServletController
import icu.samnyan.aqua.sega.util.BasicMapper
import icu.samnyan.aqua.spring.Metrics
import jakarta.servlet.http.HttpServletRequest
import org.springframework.web.bind.annotation.RestController
+import javax.crypto.SecretKeyFactory
+import javax.crypto.spec.PBEKeySpec
+
@Suppress("unused")
@RestController
-@API("/g/ongeki/{version}", "/g/ongeki")
+@API("/g/SDDT/{version}", "/g/SDDT")
class OngekiController(
val mapper: BasicMapper,
val db: OngekiUserRepos,
@@ -23,14 +27,23 @@ class OngekiController(
val us: AquaUserServices,
val pop: GameMusicPopularity,
val cardService: CardService,
-): MeowApi({ _, resp -> if (resp is String) resp else mapper.write(resp) }) {
+): MeowApi(
+ { _, resp -> if (resp is String) resp else mapper.write(resp) },
+ @OptIn(ExperimentalStdlibApi::class)
+ { endpoint: String ->
+ gdb.gameData.ogkGameEncryption.map{
+ SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1")
+ .generateSecret(PBEKeySpec(
+ endpoint.toCharArray(),
+ it.salt!!.hexToByteArray(),
+ it.iterations, 128
+ )).encoded.toHexString().substring(0, 32)
+ }
+ }
+) {
val log = logger()
- val noopEndpoint = setOf("ExtendLockTimeApi", "GameLogoutApi", "RegisterPromotionCardApi",
- "UpsertClientBookkeepingApi", "UpsertClientDevelopApi", "UpsertClientErrorApi", "UpsertClientSettingApi",
- "UpsertClientTestmodeApi", "UpsertUserGplogApi", "Ping")
-
init { ongekiInit() }
val handlers = initH
@@ -40,26 +53,25 @@ class OngekiController(
version?.let { data["version"] = it }
val token = TokenChecker.tokenShort()
- log.info("$token : $api < ${data.toJson()}")
+
+ val apiFriendlyName = if (api.lowercase() == api && api.length == 32)
+ ((hashes.filter { it.value.contains(api) }.keys.firstOrNull()?.str + " (encrypt)") ?: api) else "$api (encrypt)"
+ log.info("$token : $apiFriendlyName < ${data.toJson()}")
val noop = """{"returnCode":1,"apiName":"${api.substringBefore("Api").firstCharLower()}"}"""
- if (api !in noopEndpoint && !handlers.containsKey(api)) {
- log.warn("$token : $api > not found")
+ if (!handlers.containsKey(api)) {
+ log.warn("$token : $apiFriendlyName > not found")
return noop
}
// Only record the counter metrics if the API is known.
Metrics.counter("aquadx_ongeki_api_call", "api" to api).increment()
- if (api in noopEndpoint) {
- log.info("$token : $api > no-op")
- return noop
- }
return try {
Metrics.timer("aquadx_ongeki_api_latency", "api" to api).recordCallable {
serialize(api, handlers[api]!!(ctx) ?: noop).also {
if (api !in setOf("GetUserItemApi", "GetGameEventApi"))
- log.info("$token : $api > ${it.truncate(500)}")
+ log.info("$token : $apiFriendlyName > ${it.truncate(500)}")
}
}
} catch (e: Exception) {
diff --git a/src/main/java/icu/samnyan/aqua/sega/util/GameDataService.kt b/src/main/java/icu/samnyan/aqua/sega/util/GameDataService.kt
index b2322465..285866d9 100644
--- a/src/main/java/icu/samnyan/aqua/sega/util/GameDataService.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/util/GameDataService.kt
@@ -3,6 +3,7 @@ package icu.samnyan.aqua.sega.util
import com.fasterxml.jackson.core.type.TypeReference
import ext.logger
import ext.toJson
+import icu.samnyan.aqua.sega.general.model.GameEncryptionKey
import icu.samnyan.aqua.sega.chusan.model.GameCharge as Chu3GameCharge
import icu.samnyan.aqua.sega.chusan.model.GameEvent as Chu3GameEvent
import icu.samnyan.aqua.sega.chusan.model.GameGacha as Chu3GameGacha
@@ -45,7 +46,8 @@ class GameDataService() {
return mapper.read(resStream.bufferedReader().use { it.readText() }, object : TypeReference>() {})
}
- log.warn("Game data file $f or resource $resPath not found, using empty list")
+ if (file != "game_encryption.json")
+ log.warn("Game data file $f or resource $resPath not found, using empty list")
return emptyList()
}
}
@@ -54,6 +56,7 @@ class GameDataService() {
lateinit var mai2Events: List
lateinit var mai2Charges: List
lateinit var mai2SellingCards: List
+ lateinit var mai2GameEncryption: List
// chusan
lateinit var chu3GameLinkedVerses: List
@@ -63,6 +66,7 @@ class GameDataService() {
lateinit var chu3GameGachas: List
lateinit var chu3GameLoginBonusPresets: List
lateinit var chu3GameLoginBonuses: List
+ lateinit var chu3GameEncryption: List
// ongeki
lateinit var ogkGameCards: List
@@ -75,6 +79,7 @@ class GameDataService() {
lateinit var ogkGameSkills: List
lateinit var ogkGameGachaCards: List
lateinit var ogkGameGachas: List
+ lateinit var ogkGameEncryption: List
init {
load()
@@ -84,6 +89,7 @@ class GameDataService() {
mai2Events = load("maimai2", "game_event.json")
mai2Charges = load("maimai2", "game_charge.json")
mai2SellingCards = load("maimai2", "game_selling_card.json")
+ mai2GameEncryption = load(game = "maimai2", file = "game_encryption.json")
chu3GameLinkedVerses = load("chusan", "game_linked_verse.json")
chu3GameCharges = load("chusan", "game_charge.json")
@@ -92,6 +98,7 @@ class GameDataService() {
chu3GameGachas = load("chusan", "game_gacha.json")
chu3GameLoginBonusPresets = load("chusan", "game_login_bonus_preset.json")
chu3GameLoginBonuses = load("chusan", "game_login_bonus.json")
+ chu3GameEncryption = load(game = "chusan", file = "game_encryption.json")
ogkGameCards = load("ongeki", "game_card.json")
ogkGameCharas = load("ongeki", "game_chara.json")
@@ -103,6 +110,7 @@ class GameDataService() {
ogkGameSkills = load("ongeki", "game_skill.json")
ogkGameGachaCards = load("ongeki", "game_gacha_card.json")
ogkGameGachas = load("ongeki", "game_gacha.json")
+ ogkGameEncryption = load(game = "ongeki", file = "game_encryption.json")
}
}
diff --git a/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt b/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt
index 3c5e28a4..87178ac0 100644
--- a/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt
+++ b/src/main/java/icu/samnyan/aqua/sega/wacca/WaccaServer.kt
@@ -25,7 +25,7 @@ import kotlin.math.max
import kotlin.math.min
@RestController
-@API("/g/wacca/")
+@API("/g/SDFE/{version}")
class WaccaServer {
// These are lateinit autowired instead of constructor injection because the tests depend on creating
// an instance of this class with no arguments.