Skip to content

Commit

Permalink
Some refactoring of hwid handling
Browse files Browse the repository at this point in the history
  • Loading branch information
cmyui committed Jul 9, 2024
1 parent 39b914c commit 9f4f29b
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 43 deletions.
129 changes: 87 additions & 42 deletions common/ripple/user_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ async def ban(user_id: int) -> None:
~(
privileges.USER_NORMAL
| privileges.USER_PUBLIC
# Seems appropriate to remove pending verification here
| privileges.USER_PENDING_VERIFICATION
),
user_id,
Expand Down Expand Up @@ -580,11 +581,14 @@ async def associate_user_with_hwids_and_restrict_if_multiaccounting(
# Running under wine, check by unique id
logger.debug("Logging Linux/Mac hardware")
banned = await glob.db.fetchAll(
"""SELECT users.id as userid, hw_user.occurencies, users.username FROM hw_user
"""
SELECT users.id as userid, hw_user.occurencies, users.username
FROM hw_user
LEFT JOIN users ON users.id = hw_user.userid
WHERE hw_user.userid != %(userid)s
AND hw_user.unique_id = %(uid)s
AND (users.privileges & 3 != 3)""",
AND (users.privileges & 3 != 3)
""",
{
"userid": user_id,
"uid": hwid_set[3],
Expand All @@ -594,13 +598,16 @@ async def associate_user_with_hwids_and_restrict_if_multiaccounting(
# Running under windows, do all checks
logger.debug("Logging Windows hardware")
banned = await glob.db.fetchAll(
"""SELECT users.id as userid, hw_user.occurencies, users.username FROM hw_user
"""
SELECT users.id as userid, hw_user.occurencies, users.username
FROM hw_user
LEFT JOIN users ON users.id = hw_user.userid
WHERE hw_user.userid != %(userid)s
AND hw_user.mac = %(mac)s
AND hw_user.unique_id = %(uid)s
AND hw_user.disk_id = %(diskid)s
AND (users.privileges & 3 != 3)""",
AND (users.privileges & 3 != 3)
""",
{
"userid": user_id,
"mac": hwid_set[2],
Expand All @@ -616,7 +623,11 @@ async def associate_user_with_hwids_and_restrict_if_multiaccounting(

# Get the total numbers of logins
user_hwids_count_rec = await glob.db.fetch(
"SELECT COUNT(*) AS count FROM hw_user WHERE userid = %s",
"""
SELECT COUNT(*) AS count
FROM hw_user
WHERE userid = %s
""",
[user_id],
)
# and make sure it is valid
Expand All @@ -641,9 +652,10 @@ async def associate_user_with_hwids_and_restrict_if_multiaccounting(
# Update hash set occurencies
await glob.db.execute(
"""
INSERT INTO hw_user (id, userid, mac, unique_id, disk_id, occurencies) VALUES (NULL, %s, %s, %s, %s, 1)
ON DUPLICATE KEY UPDATE occurencies = occurencies + 1
""",
INSERT INTO hw_user (id, userid, mac, unique_id, disk_id, occurencies)
VALUES (NULL, %s, %s, %s, %s, 1)
ON DUPLICATE KEY UPDATE occurencies = occurencies + 1
""",
[user_id, hwid_set[2], hwid_set[3], hwid_set[4]],
)

Expand Down Expand Up @@ -674,37 +686,51 @@ async def grant_user_default_privileges(user_id: int) -> None:
)


async def authorize_login_and_activate_new_account(
WINE_STATIC_MAC_ADDRESS = "b4ec3c4334a0249dae95c284ec5983df"
WINE_STATIC_DISK_ID = "ffae06fb022871fe9beb58b005c5e21d"


def _hwid_set_running_under_wine(hwid_set: list[str]) -> bool:
return hwid_set[2] == WINE_STATIC_MAC_ADDRESS or hwid_set[4] == WINE_STATIC_DISK_ID


async def find_user_ids_with_hwid_matches(
user_id: int,
# TODO: refactor hwid sets into an object across the codebase
hwid_set: list[str],
) -> bool:
"""
Check for multi-accounts, authorize the login, activate the account (if new),
and grant them default user privileges (publicity & regular access).
"""
username = await get_username_from_id(user_id)
) -> list[int]:
"""\
Find any users using the same set of hardware identifiers.
# Make sure there are no other accounts activated with this exact mac/unique id/hwid
if (
hwid_set[2] == "b4ec3c4334a0249dae95c284ec5983df"
or hwid_set[4] == "ffae06fb022871fe9beb58b005c5e21d"
):
In practice, we use this to determine users re-using the same hardware.
This often a common sign of multi-accounting, which is against our ToS.
"""
if _hwid_set_running_under_wine(hwid_set):
# Running under wine, check only by uniqueid
await audit_logs.send_log_as_discord_webhook(
message=f"[{username}](https://akatsuki.gg/u/{user_id}) running under wine:\n**Full data:** {hwid_set}\n**Usual wine mac address hash:** b4ec3c4334a0249dae95c284ec5983df\n**Usual wine disk id:** ffae06fb022871fe9beb58b005c5e21d",
discord_channel="ac_confidential",
)
logger.debug("Veryfing with Linux/Mac hardware")
match = await glob.db.fetchAll(
"SELECT userid FROM hw_user WHERE unique_id = %(uid)s AND userid != %(userid)s AND activated = 1 LIMIT 1",
matching_records = await glob.db.fetchAll(
"""
SELECT userid
FROM hw_user
WHERE unique_id = %(uid)s
AND userid != %(userid)s
AND activated = 1
LIMIT 1
""",
{"uid": hwid_set[3], "userid": user_id},
)
else:
# Running under windows, full check
logger.debug("Veryfing with Windows hardware")
match = await glob.db.fetchAll(
"SELECT userid FROM hw_user WHERE mac = %(mac)s AND unique_id = %(uid)s AND disk_id = %(diskid)s AND userid != %(userid)s AND activated = 1 LIMIT 1",
matching_records = await glob.db.fetchAll(
"""
SELECT userid
FROM hw_user
WHERE mac = %(mac)s
AND unique_id = %(uid)s
AND disk_id = %(diskid)s
AND userid != %(userid)s
AND activated = 1
LIMIT 1
""",
{
"mac": hwid_set[2],
"uid": hwid_set[3],
Expand All @@ -713,30 +739,49 @@ async def authorize_login_and_activate_new_account(
},
)

if match:
# This is a multiaccount, restrict other account and ban this account
return [rec["userid"] for rec in matching_records]


async def authorize_login_and_activate_new_account(
user_id: int,
# TODO: refactor hwid sets into an object across the codebase
hwid_set: list[str],
) -> bool:
"""
Check for multi-accounts, authorize the login, activate the account (if new),
and grant them default user privileges (publicity & regular access).
"""
username = await get_username_from_id(user_id)

user_ids_associated_with_device = await find_user_ids_with_hwid_matches(
user_id, hwid_set
)

if user_ids_associated_with_device:
# There are other users associated with this set of hardware identifiers.
# We'll consider this a multi-account; restrict original account; ban this account.

# Get original user_id and username (lowest ID)
originalUserID = match[0]["userid"]
originalUsername: str | None = await get_username_from_id(originalUserID)
# Fetch their original account's information
original_user_id = user_ids_associated_with_device[0]
original_username = await get_username_from_id(original_user_id)

# Ban this user and append notes
await ban(user_id) # this removes the USER_PENDING_VERIFICATION flag too
await append_cm_notes(
user_id,
f"{originalUsername}'s multiaccount ({originalUserID}), found HWID match while verifying account.",
f"{original_username}'s multiaccount ({original_user_id}), found HWID match while verifying account.",
)
await append_cm_notes(
originalUserID,
original_user_id,
f"Has created multiaccount {username} ({user_id}).",
)

# Restrict the original
await restrict(originalUserID)
await restrict(original_user_id)

# Discord message
await audit_logs.send_log_as_discord_webhook(
message=f"[{originalUsername}](https://akatsuki.gg/u/{originalUserID}) has been restricted because they have created the multiaccount [{username}](https://akatsuki.gg/u/{user_id}). The multiaccount has been banned.",
message=f"[{original_username}](https://akatsuki.gg/u/{original_user_id}) has been restricted because they have created the multiaccount [{username}](https://akatsuki.gg/u/{user_id}). The multiaccount has been banned.",
discord_channel="ac_general",
)

Expand Down Expand Up @@ -845,7 +890,7 @@ async def remove_from_leaderboard(user_id: int) -> None:
for board in ("leaderboard", "relaxboard"):
for mode in ("std", "taiko", "ctb", "mania"):
await pipe.zrem(f"ripple:{board}:{mode}", str(user_id))
if country and country != "xx":
if country != "xx":
await pipe.zrem(f"ripple:{board}:{mode}:{country}", str(user_id))

await pipe.execute()
Expand Down Expand Up @@ -874,7 +919,7 @@ async def remove_from_specified_leaderboard(

async with glob.redis.pipeline() as pipe:
await pipe.zrem(redis_board, str(user_id))
if country and country != "xx":
if country != "xx":
await pipe.zrem(f"{redis_board}:{country}", str(user_id))

await pipe.execute()
Expand Down
8 changes: 8 additions & 0 deletions events/loginEvent.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,14 @@ async def handle(web_handler: AsyncRequestHandler) -> tuple[str, bytes]: # toke
else:
# The user's freeze has expired; restrict them.
await user_utils.restrict(userID)

maybe_token = await osuToken.update_token(
userToken["token_id"],
privileges=userToken["privileges"] & ~privileges.USER_PUBLIC,
)
assert maybe_token is not None
userToken = maybe_token

await user_utils.unfreeze(
userID,
should_log_to_cm_notes_and_discord=False,
Expand Down
1 change: 0 additions & 1 deletion objects/osuToken.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,6 @@ async def get_all_tokens_by_username(username: str) -> list[Token]:
return [token for token in tokens if token["username"] == username]


# TODO: the things that can actually be Optional need to have different defaults
async def update_token(
token_id: str,
*,
Expand Down

0 comments on commit 9f4f29b

Please sign in to comment.