Add NASC uidhmac repair#308
Conversation
adds the ability to remake the users uidhmac when needed
| router.post('/', async (request: express.Request, response: express.Response): Promise<void> => { | ||
| const pid = request.body.pid?.trim(); // * This has to be forwarded since this request comes from the websites server | ||
| const nexPassword = request.body.password?.trim(); | ||
|
|
||
| if (!pid || pid === '' || !/^\d+$/.test(pid)) { | ||
| response.status(400).json({ | ||
| app: 'api', | ||
| status: 400, | ||
| error: 'Invalid PID format' | ||
| }); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if (!nexPassword || !/^[0-9A-Za-z]{16}$/.test(nexPassword)) { | ||
| response.status(400).json({ | ||
| app: 'api', | ||
| status: 400, | ||
| error: 'Invalid NEX password format' | ||
| }); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const nexAccount = await NEXAccount.findOne({ | ||
| pid: parseInt(pid), | ||
| password: nexPassword | ||
| }); | ||
|
|
||
| if (!nexAccount) { | ||
| response.json({ | ||
| app: 'api', | ||
| status: 400, | ||
| error: 'Invalid NEX account' | ||
| }); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| nexAccount.generateUIDHMAC(); | ||
|
|
||
| response.json({ | ||
| app: 'api', | ||
| status: 200, | ||
| data: { | ||
| uidhmac: nexAccount.uidhmac | ||
| } | ||
| }); | ||
| } catch (error: any) { | ||
| LOG_ERROR('[POST] /v1/register: ' + error); | ||
| if (error.stack) { | ||
| console.error(error.stack); | ||
| } | ||
|
|
||
| response.status(500).json({ | ||
| app: 'api', | ||
| status: 500, | ||
| error: 'Internal server error' | ||
| }); | ||
|
|
||
| return; | ||
| } | ||
| }); |
Check failure
Code scanning / CodeQL
Missing rate limiting High
|
@DaniElectra poke |
DaniElectra
left a comment
There was a problem hiding this comment.
The limit for the PID HMAC is 8 UTF-16 characters, so while we have some limitations there is some room for improving the original algorithm here.
Also to keep in mind, we have to change the PID HMAC generation/assignment on the friends server too for new accounts.
|
|
||
| let pid = 0; // * Real PIDs are always positive and non-zero | ||
| let pidHmac = ''; | ||
| let uidhmac = ''; |
There was a problem hiding this comment.
Is there a specific reason for changing this name?
There was a problem hiding this comment.
Just lining it up with the name from NASC is all
Okay, so we can use any characters then? That does improve things. Currently the way the hmac is handled seems to allow for relatively simple bruteforce attacks. Both us and ourselves currently just use 8 lowercase hex characters, which is effectively just an int32. Granted that's still 4,294,967,295 combinations, so a bruteforce would still take some time but isn't impossible. I've done it before myself when doing archival I checked some dumps that connect to our NASC server and the average request time seems to be about 700ms. Assuming a single IP, and 1000 requests per second (easily reachable when using something like AWS), and rounding 700ms up to 1 second, that's only ~50 days to bruteforce a persons hmac given 8 lowercase hex characters Including uppercase characters does improve things (a little under 2 years to go through every combination) but it's still lower than I'd like Do you see any limitation on the types of characters we can use? Because if we can use special characters too, then using the same range of characters the NEX password uses (
I know, I just want to nail down the system here first before moving to the friends server |
I haven't tested it, but I don't see any limitations on the characters (at least on the NASC login phase, unsure on account registration) That should work 👍 |
How does this look then? It takes the PID as a string and makes a sha256 hmac out of it with a secret key only the server would know, takes the first 8 bytes, and maps them to the charset. I think that should be secure enough? In my testing this seems to work pretty well, I get seemingly random results which is good. Just wanted to make sure I'm not overlooking any glaringly obvious security holes const crypto = require('node:crypto');
const CHAR_SET = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}';
function generateUIDHMAC(pid, key) {
const hmac = crypto.createHmac('sha256', key).update(pid);
const hash = hmac.digest();
return Array.from(hash.slice(0, 8), byte => CHAR_SET[byte % CHAR_SET.length]).join('');
}
const output = generateUIDHMAC('0123456789', 'secret-key');
console.log(output); // * $vT.W"M9And it can be ported to Go pretty easily too: package main
import (
"crypto/hmac"
"crypto/sha256"
"fmt"
)
const CHAR_SET = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{|}"
func generateUIDHMAC(pid, key string) string {
h := hmac.New(sha256.New, []byte(key))
h.Write([]byte(pid))
hash := h.Sum(nil)
result := make([]byte, 8)
for i := 0; i < 8; i++ {
charIndex := hash[i] % byte(len(CHAR_SET))
result[i] = CHAR_SET[charIndex]
}
return string(result)
}
func main() {
output := generateUIDHMAC("0123456789", "secret-key")
fmt.Println(output) // * $vT.W"M9
} |
|
That should be good yeah |
I asked ZeroSkill to take a look at this part since he was working with the 3DS NASC client a while ago. I confirmed that the only limitation is the length of the string, the console just copies from the buffer into the save directly. So this SHOULD be good! Won't merge yet though because of the holiday and because the friends server needs to update as well, and to ask around to make sure the algorithm has no security issues |
Resolves #XXX
Changes:
Adds the ability to remake the users
uidhmacwhen needed. Untested, but should work.@DaniElectra before we decide to merge this I wanted to talk about the actual hmac itself. Nintendo seemed to always use an 8 hex character (4 byte) hmac here. But that seems...not great. 4 bytes isn't a ton. When discussing this with Kinnay and Zak a while ago, I apparently was able to get the 3DS to use a larger string (kinnay/NintendoClients#76 (comment)). But I have not tried this in ages and I'm not sure if maybe I just did something funky?
Can you confirm whether or not we're able to use larger strings here? I'm asking because I've checked some NASC dumps from our users and they seem to only be using the first 4 bytes, despite the fact that our friends server seems to be sending the entire hmac? Which doesn't line up with what I apparently saw happen in 2022, maybe Nintendo changed something in one of the later updates...? I have not looked into the internals of the NASC client, but I figured you might know. If so, that would be ideal. If not, maybe we can use more than just hex characters?