feat: Hash user authentication tickets for enhanced security

Implement SHA256 hashing for user authentication tickets before storage. This prevents sensitive tokens from being stored in plain text, significantly improving security.

Changes include:
*   Hashing tickets in `AccountRouteHandler` and `UbiservicesRouteHandler` during profile updates and login flows.
*   Introducing dedicated `updateUserTicket` methods in `AccountRepository` and `AccountService`.
*   Adjusting the `Account` model to handle tickets separately.
*   Adding a new `config` table to the database schema.
This commit is contained in:
ibratabian17
2025-07-17 17:52:51 +07:00
parent 3ccfb22a03
commit 103d8259b3
6 changed files with 35 additions and 102 deletions

View File

@@ -2,6 +2,7 @@
* Account Route Handler for OpenParty
* Handles user account-related routes
*/
const crypto = require('crypto');
const axios = require('axios');
const RouteHandler = require('./RouteHandler'); // Assuming RouteHandler is in the same directory
const MostPlayedService = require('../../services/MostPlayedService');
@@ -418,7 +419,8 @@ class AccountRouteHandler extends RouteHandler {
// Update only the fields present in the request body, preserving other fields
// Ensure the ticket is updated if present in the header
const updateData = { ...req.body };
updateData.ticket = ticket; // Always update ticket from header
const hashedTicket = crypto.createHash('sha256').update(ticket).digest('hex');
updateData.ticket = hashedTicket; // Always update ticket from header
const updatedProfile = await AccountService.updateUser(profileId, updateData);
return res.send({
@@ -698,4 +700,4 @@ class AccountRouteHandler extends RouteHandler {
}
}
module.exports = new AccountRouteHandler();
module.exports = new AccountRouteHandler();

View File

@@ -2,6 +2,7 @@
* Ubiservices Route Handler for OpenParty
* Handles Ubisoft services related routes
*/
const crypto = require('crypto');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const RouteHandler = require('./RouteHandler'); // Assuming RouteHandler is in the same directory
@@ -158,7 +159,8 @@ class UbiservicesRouteHandler extends RouteHandler {
// Update user mappings
AccountService.addUserId(response.data.profileId, response.data.userId);
AccountService.updateUserTicket(response.data.profileId, `Ubi_v1 ${response.data.ticket}`);
const hashedTicket = crypto.createHash('sha256').update(`Ubi_v1 ${response.data.ticket}`).digest('hex');
AccountService.updateUserTicket(response.data.profileId, hashedTicket);
} catch (error) {
console.log("[ACC] Error fetching from Ubisoft services", error.message);
@@ -496,4 +498,4 @@ class UbiservicesRouteHandler extends RouteHandler {
}
// Export an instance of the route handler
module.exports = new UbiservicesRouteHandler();
module.exports = new UbiservicesRouteHandler();

View File

@@ -140,11 +140,21 @@ class DatabaseManager {
if (err) {
this.logger.error('Error creating user_profiles table:', err.message);
reject(err);
} else {
this.logger.info('All tables created. Resolving initialize promise.');
resolve(this._db);
}
});
// Create config table
this._db.run(`CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT
)`, (err) => {
if (err) {
this.logger.error('Error creating config table:', err.message);
return reject(err);
}
this.logger.info('All tables created. Resolving initialize promise.');
resolve(this._db);
});
});
}
});

View File

@@ -89,7 +89,7 @@ class Account {
};
const simpleOverwriteKeys = [
'profileId', 'userId', 'username', 'nickname', 'name', 'email', 'password', 'ticket',
'profileId', 'userId', 'username', 'nickname', 'name', 'email', 'password',
'avatar', 'country', 'platformId', 'alias', 'aliasGender', 'jdPoints',
'portraitBorder', 'rank', 'skin', 'diamondPoints', 'wdfRank', 'stars',
'unlocks', 'language', 'firstPartyEnv'
@@ -212,49 +212,9 @@ class Account {
* @returns {Object} Plain object representation
*/
toJSON() {
return {
profileId: this.profileId,
userId: this.userId,
username: this.username,
nickname: this.nickname,
name: this.name,
email: this.email,
password: this.password,
ticket: this.ticket,
avatar: this.avatar,
country: this.country,
platformId: this.platformId,
alias: this.alias,
aliasGender: this.aliasGender,
jdPoints: this.jdPoints,
portraitBorder: this.portraitBorder,
rank: this.rank,
scores: this.scores,
favorites: this.favorites,
songsPlayed: this.songsPlayed,
progression: this.progression,
history: this.history,
// New fields
skin: this.skin,
diamondPoints: this.diamondPoints,
unlockedAvatars: this.unlockedAvatars,
unlockedSkins: this.unlockedSkins,
unlockedAliases: this.unlockedAliases,
unlockedPortraitBorders: this.unlockedPortraitBorders,
wdfRank: this.wdfRank,
stars: this.stars,
unlocks: this.unlocks,
populations: this.populations,
inProgressAliases: this.inProgressAliases,
language: this.language,
firstPartyEnv: this.firstPartyEnv,
syncVersions: this.syncVersions,
otherPids: this.otherPids,
stats: this.stats,
mapHistory: this.mapHistory,
createdAt: this.createdAt,
updatedAt: this.updatedAt
};
const data = { ...this };
delete data.ticket;
return data;
}
/**
@@ -262,46 +222,7 @@ class Account {
* @returns {Object} Sanitized plain object representation
*/
toPublicJSON() {
const publicData = {
profileId: this.profileId,
userId: this.userId,
username: this.username,
nickname: this.nickname,
name: this.name,
avatar: this.avatar,
country: this.country,
platformId: this.platformId,
alias: this.alias,
aliasGender: this.aliasGender,
jdPoints: this.jdPoints,
portraitBorder: this.portraitBorder,
rank: this.rank,
scores: this.scores,
favorites: this.favorites,
songsPlayed: this.songsPlayed,
progression: this.progression,
history: this.history,
// New fields
skin: this.skin,
diamondPoints: this.diamondPoints,
unlockedAvatars: this.unlockedAvatars,
unlockedSkins: this.unlockedSkins,
unlockedAliases: this.unlockedAliases,
unlockedPortraitBorders: this.unlockedPortraitBorders,
wdfRank: this.wdfRank,
stars: this.stars,
unlocks: this.unlocks,
populations: this.populations,
inProgressAliases: this.inProgressAliases,
language: this.language,
firstPartyEnv: this.firstPartyEnv,
syncVersions: this.syncVersions,
otherPids: this.otherPids,
stats: this.stats,
mapHistory: this.mapHistory,
createdAt: this.createdAt,
updatedAt: this.updatedAt
};
const publicData = this.toJSON();
// Explicitly remove sensitive fields if they were somehow added
delete publicData.email;
delete publicData.password;
@@ -310,4 +231,4 @@ class Account {
}
}
module.exports = Account;
module.exports = Account;

View File

@@ -38,7 +38,6 @@ class AccountRepository {
name: row.name,
email: row.email,
password: row.password, // Should be handled securely if stored
ticket: row.ticket,
alias: row.alias,
aliasGender: row.aliasGender,
avatar: row.avatar,
@@ -119,14 +118,14 @@ class AccountRepository {
return new Promise((resolve, reject) => {
db.run(`INSERT OR REPLACE INTO user_profiles (
profileId, userId, username, nickname, name, email, password, ticket,
profileId, userId, username, nickname, name, email, password,
alias, aliasGender, avatar, country, platformId, jdPoints, portraitBorder, rank,
scores, songsPlayed, favorites, progression, history,
skin, diamondPoints, unlockedAvatars, unlockedSkins, unlockedAliases, unlockedPortraitBorders,
wdfRank, stars, unlocks, populations, inProgressAliases, language, firstPartyEnv,
syncVersions, otherPids, stats, mapHistory,
createdAt, updatedAt
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
accountData.profileId,
accountData.userId,
@@ -135,7 +134,6 @@ class AccountRepository {
accountData.name,
accountData.email,
accountData.password, // Ensure this is handled securely (e.g., hashed) if stored
accountData.ticket,
accountData.alias,
accountData.aliasGender,
accountData.avatar,
@@ -206,7 +204,6 @@ class AccountRepository {
name: row.name,
email: row.email,
password: row.password,
ticket: row.ticket,
alias: row.alias,
aliasGender: row.aliasGender,
avatar: row.avatar,
@@ -276,7 +273,6 @@ class AccountRepository {
name: row.name,
email: row.email,
password: row.password,
ticket: row.ticket,
alias: row.alias,
aliasGender: row.aliasGender,
avatar: row.avatar,
@@ -346,7 +342,6 @@ class AccountRepository {
name: row.name,
email: row.email,
password: row.password,
ticket: row.ticket,
alias: row.alias,
aliasGender: row.aliasGender,
avatar: row.avatar,
@@ -433,4 +428,4 @@ class AccountRepository {
}
}
module.exports = new AccountRepository(); // Export a singleton instance
module.exports = new AccountRepository(); // Export a singleton instance

View File

@@ -1,6 +1,7 @@
/**
* Service for handling account-related business logic
*/
const crypto = require('crypto');
const Account = require('../models/Account');
const AccountRepository = require('../repositories/AccountRepository');
const Logger = require('../utils/logger');
@@ -27,7 +28,8 @@ class AccountService {
*/
async findUserFromTicket(ticket) {
this.logger.info(`Finding user from ticket`);
const account = await AccountRepository.findByTicket(ticket);
const hashedTicket = crypto.createHash('sha256').update(ticket).digest('hex');
const account = await AccountRepository.findByTicket(hashedTicket);
return account ? account.profileId : null;
}
@@ -73,7 +75,8 @@ class AccountService {
account = new Account({ profileId });
}
account.update({ ticket });
const hashedTicket = crypto.createHash('sha256').update(ticket).digest('hex');
account.update({ ticket: hashedTicket });
return AccountRepository.save(account);
}