diff --git a/core/adb_handlers/felica.py b/core/adb_handlers/felica.py index 479e84d..22c9f05 100644 --- a/core/adb_handlers/felica.py +++ b/core/adb_handlers/felica.py @@ -10,13 +10,14 @@ class ADBFelicaLookupRequest(ADBBaseRequest): self.pmm = hex(pmm)[2:].upper() class ADBFelicaLookupResponse(ADBBaseResponse): - def __init__(self, access_code: str = None, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None: + def __init__(self, access_code: str = None, idx: int = 0, game_id: str = "SXXX", store_id: int = 1, keychip_id: str = "A69E01A8888", code: int = 0x03, length: int = 0x30, status: int = 1) -> None: super().__init__(code, length, status, game_id, store_id, keychip_id) self.access_code = access_code if access_code is not None else "00000000000000000000" + self.idx = idx @classmethod - def from_req(cls, req: ADBHeader, access_code: str = None) -> "ADBFelicaLookupResponse": - c = cls(access_code, req.game_id, req.store_id, req.keychip_id) + def from_req(cls, req: ADBHeader, access_code: str = None, idx: int = 0) -> "ADBFelicaLookupResponse": + c = cls(access_code, idx, req.game_id, req.store_id, req.keychip_id) c.head.protocol_ver = req.protocol_ver return c @@ -26,7 +27,7 @@ class ADBFelicaLookupResponse(ADBBaseResponse): "access_code" / Int8ub[10], Padding(2) ).build(dict( - felica_idx = 0, + felica_idx = self.idx, access_code = bytes.fromhex(self.access_code) )) diff --git a/core/aimedb.py b/core/aimedb.py index 08a1b65..c6887d3 100644 --- a/core/aimedb.py +++ b/core/aimedb.py @@ -194,6 +194,9 @@ class AimedbServlette(): if user_id and user_id > 0: await self.data.card.update_card_last_login(req.access_code) + if req.access_code.startswith("010") or req.access_code.startswith("3"): + await self.data.card.set_chip_id_by_access_code(req.access_code, req.serial_number) + self.logger.info(f"Attempt to set chip id to {req.serial_number} for access code {req.access_code}") return ret async def handle_lookup_ex(self, data: bytes, resp_code: int) -> ADBBaseResponse: @@ -229,15 +232,24 @@ class AimedbServlette(): async def handle_felica_lookup(self, data: bytes, resp_code: int) -> bytes: """ - On official, I think a card has to be registered for this to actually work, but - I'm making the executive decision to not implement that and just kick back our - faux generated access code. The real felica IDm -> access code conversion is done - on the ADB server, which we do not and will not ever have access to. Because we can - assure that all IDms will be unique, this basic 0-padded hex -> int conversion will - be fine. + On official, the IDm is used as a key to look up the stored access code in a large + database. We do not have access to that database so we have to make due with what we got. + Interestingly, namco games are able to read S_PAD0 and send the server the correct access + code, but aimedb doesn't. Until somebody either enters the correct code manually, or scans + on a game that reads it correctly from the card, this will have to do. It's the same conversion + used on the big boy networks. """ req = ADBFelicaLookupRequest(data) - ac = self.data.card.to_access_code(req.idm) + card = await self.data.card.get_card_by_idm(req.idm) + if not card: + ac = self.data.card.to_access_code(req.idm) + test = await self.data.card.get_card_by_access_code(ac) + if test: + await self.data.card.set_idm_by_access_code(ac, req.idm) + + else: + ac = card['access_code'] + self.logger.info( f"idm {req.idm} ipm {req.pmm} -> access_code {ac}" ) @@ -245,7 +257,8 @@ class AimedbServlette(): async def handle_felica_register(self, data: bytes, resp_code: int) -> bytes: """ - I've never seen this used. + Used to register felica moble access codes. Will never be used on our network + because we don't implement felica_lookup properly. """ req = ADBFelicaLookupRequest(data) ac = self.data.card.to_access_code(req.idm) @@ -279,8 +292,18 @@ class AimedbServlette(): async def handle_felica_lookup_ex(self, data: bytes, resp_code: int) -> bytes: req = ADBFelicaLookup2Request(data) - access_code = self.data.card.to_access_code(req.idm) - user_id = await self.data.card.get_user_id_from_card(access_code=access_code) + user_id = None + card = await self.data.card.get_card_by_idm(req.idm) + if not card: + access_code = self.data.card.to_access_code(req.idm) + card = await self.data.card.get_card_by_access_code(access_code) + if card: + user_id = card['user'] + await self.data.card.set_idm_by_access_code(access_code, req.idm) + + else: + user_id = card['user'] + access_code = card['access_code'] if user_id is None: user_id = -1 @@ -290,6 +313,14 @@ class AimedbServlette(): ) resp = ADBFelicaLookup2Response.from_req(req.head, user_id, access_code) + + if user_id > 0: + if card['is_banned'] and card['is_locked']: + resp.head.status = ADBStatus.BAN_SYS_USER + elif card['is_banned']: + resp.head.status = ADBStatus.BAN_SYS + elif card['is_locked']: + resp.head.status = ADBStatus.LOCK_USER if user_id and user_id > 0 and self.config.aimedb.id_secret: auth_key = create_sega_auth_key(user_id, req.head.game_id, req.head.store_id, req.head.keychip_id, self.config.aimedb.id_secret, self.config.aimedb.id_lifetime_seconds) @@ -337,6 +368,16 @@ class AimedbServlette(): self.logger.info( f"Registration blocked!: access code {req.access_code}" ) + + if user_id > 0: + if req.access_code.startswith("010") or req.access_code.startswith("3"): + await self.data.card.set_chip_id_by_access_code(req.access_code, req.serial_number) + self.logger.info(f"Attempt to set chip id to {req.serial_number} for access code {req.access_code}") + + elif req.access_code.startswith("0008"): + idm = self.data.card.to_idm(req.access_code) + await self.data.card.set_idm_by_access_code(req.access_code, idm) + self.logger.info(f"Attempt to set IDm to {idm} for access code {req.access_code}") resp = ADBLookupResponse.from_req(req.head, user_id) if resp.user_id <= 0: diff --git a/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py b/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py new file mode 100644 index 0000000..12fca3a --- /dev/null +++ b/core/data/alembic/versions/48f4acc43a7e_card_add_idm_chip_id.py @@ -0,0 +1,50 @@ +"""card_add_idm_chip_id + +Revision ID: 48f4acc43a7e +Revises: 1e150d16ab6b +Create Date: 2024-06-21 23:53:34.369134 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = '48f4acc43a7e' +down_revision = '1e150d16ab6b' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('aime_card', sa.Column('idm', sa.String(length=16), nullable=True)) + op.add_column('aime_card', sa.Column('chip_id', sa.BIGINT(), nullable=True)) + op.alter_column('aime_card', 'access_code', + existing_type=mysql.VARCHAR(length=20), + nullable=False) + op.alter_column('aime_card', 'created_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('now()'), + existing_nullable=True) + op.create_unique_constraint(None, 'aime_card', ['chip_id']) + op.create_unique_constraint(None, 'aime_card', ['idm']) + op.create_unique_constraint(None, 'aime_card', ['access_code']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'aime_card', type_='unique') + op.drop_constraint(None, 'aime_card', type_='unique') + op.drop_constraint(None, 'aime_card', type_='unique') + op.alter_column('aime_card', 'created_date', + existing_type=mysql.TIMESTAMP(), + server_default=sa.text('CURRENT_TIMESTAMP'), + existing_nullable=True) + op.alter_column('aime_card', 'access_code', + existing_type=mysql.VARCHAR(length=20), + nullable=True) + op.drop_column('aime_card', 'chip_id') + op.drop_column('aime_card', 'idm') + # ### end Alembic commands ### diff --git a/core/data/schema/card.py b/core/data/schema/card.py index cd5c647..798dd03 100644 --- a/core/data/schema/card.py +++ b/core/data/schema/card.py @@ -1,6 +1,6 @@ from typing import Dict, List, Optional from sqlalchemy import Table, Column, UniqueConstraint -from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP +from sqlalchemy.types import Integer, String, Boolean, TIMESTAMP, BIGINT from sqlalchemy.sql.schema import ForeignKey from sqlalchemy.sql import func from sqlalchemy.engine import Row @@ -11,12 +11,10 @@ aime_card = Table( "aime_card", metadata, Column("id", Integer, primary_key=True, nullable=False), - Column( - "user", - ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), - nullable=False, - ), - Column("access_code", String(20)), + Column("user", ForeignKey("aime_user.id", ondelete="cascade", onupdate="cascade"), nullable=False), + Column("access_code", String(20), nullable=False, unique=True), + Column("idm", String(16), unique=True), + Column("chip_id", BIGINT, unique=True), Column("created_date", TIMESTAMP, server_default=func.now()), Column("last_login_date", TIMESTAMP, onupdate=func.now()), Column("is_locked", Boolean, server_default="0"), @@ -122,6 +120,26 @@ class CardData(BaseData): if result is None: self.logger.warn(f"Failed to update last login time for {access_code}") + async def get_card_by_idm(self, idm: str) -> Optional[Row]: + result = await self.execute(aime_card.select(aime_card.c.idm == idm)) + if result: + return result.fetchone() + + async def get_card_by_chip_id(self, chip_id: int) -> Optional[Row]: + result = await self.execute(aime_card.select(aime_card.c.chip_id == chip_id)) + if result: + return result.fetchone() + + async def set_chip_id_by_access_code(self, access_code: str, chip_id: int) -> Optional[Row]: + result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(chip_id=chip_id)) + if not result: + self.logger.error(f"Failed to update chip ID to {chip_id} for {access_code}") + + async def set_idm_by_access_code(self, access_code: str, idm: str) -> Optional[Row]: + result = await self.execute(aime_card.update(aime_card.c.access_code == access_code).values(idm=idm)) + if not result: + self.logger.error(f"Failed to update IDm to {idm} for {access_code}") + def to_access_code(self, luid: str) -> str: """ Given a felica cards internal 16 hex character luid, convert it to a 0-padded 20 digit access code as a string @@ -132,4 +150,4 @@ class CardData(BaseData): """ Given a 20 digit access code as a string, return the 16 hex character luid """ - return f"{int(access_code):0{16}x}" + return f"{int(access_code):0{16}X}" diff --git a/titles/pokken/base.py b/titles/pokken/base.py index f864824..ee0d134 100644 --- a/titles/pokken/base.py +++ b/titles/pokken/base.py @@ -165,6 +165,8 @@ class PokkenBase: f"Register new card {access_code} (UserId {user_id}, CardId {card_id})" ) + await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16)) + elif card is None: self.logger.info(f"Registration of card {access_code} blocked!") res.load_user.CopyFrom(load_usr) @@ -173,6 +175,8 @@ class PokkenBase: else: user_id = card['user'] card_id = card['id'] + if not card['chip_id']: + await self.data.card.set_chip_id_by_access_code(access_code, int(request.load_user.chip_id[:8], 16)) """ TODO: Unlock all supports? Probably diff --git a/titles/sao/base.py b/titles/sao/base.py index 80f91a2..76a1def 100644 --- a/titles/sao/base.py +++ b/titles/sao/base.py @@ -83,6 +83,10 @@ class SaoBase: if not user_id: user_id = await self.data.user.create_user() #works card_id = await self.data.card.create_card(user_id, req.access_code) + if req.access_code.startswith("5"): + await self.data.card.set_idm_by_access_code(card_id, req.chip_id[:16]) + elif req.access_code.startswith("010") or req.access_code.startswith("3"): + await self.data.card.set_chip_id_by_access_code(card_id, int(req.chip_id[:8], 16)) if card_id is None: user_id = -1