Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Some refactoring of hwid handling #136

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 88 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],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: big goal of this PR will be to encapsulate device information in a more human-friendly format

) -> 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,50 @@ 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 +891,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 +920,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