Skip to content

Add NASC uidhmac repair#308

Open
jonbarrow wants to merge 4 commits into
devfrom
feat/repair-uidhmac
Open

Add NASC uidhmac repair#308
jonbarrow wants to merge 4 commits into
devfrom
feat/repair-uidhmac

Conversation

@jonbarrow
Copy link
Copy Markdown
Member

@jonbarrow jonbarrow commented Dec 15, 2025

Resolves #XXX

Changes:

Adds the ability to remake the users uidhmac when 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?

adds the ability to remake the users uidhmac when needed
Comment on lines +12 to +75
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

This route handler performs
a database access
, but is not rate-limited.
@jonbarrow
Copy link
Copy Markdown
Member Author

@DaniElectra poke

Copy link
Copy Markdown
Member

@DaniElectra DaniElectra left a comment

Choose a reason for hiding this comment

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

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.

Comment thread src/middleware/nasc.ts

let pid = 0; // * Real PIDs are always positive and non-zero
let pidHmac = '';
let uidhmac = '';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a specific reason for changing this name?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Just lining it up with the name from NASC is all

Comment thread src/services/api/routes/v1/repair-uidhmac.ts Outdated
Comment thread src/services/api/routes/v1/repair-uidhmac.ts Outdated
@jonbarrow
Copy link
Copy Markdown
Member Author

jonbarrow commented Dec 20, 2025

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.

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 (\x21-\x5B and \x5D-\x7D) would make this effectively impossible to bruteforce

Also to keep in mind, we have to change the PID HMAC generation/assignment on the friends server too for new accounts.

I know, I just want to nail down the system here first before moving to the friends server

@DaniElectra
Copy link
Copy Markdown
Member

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 (\x21-\x5B and \x5D-\x7D) would make this effectively impossible to bruteforce

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 👍

@jonbarrow
Copy link
Copy Markdown
Member Author

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 (\x21-\x5B and \x5D-\x7D) would make this effectively impossible to bruteforce

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"M9

And 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
}

@DaniElectra
Copy link
Copy Markdown
Member

That should be good yeah

@jonbarrow jonbarrow mentioned this pull request Dec 31, 2025
4 tasks
@jonbarrow
Copy link
Copy Markdown
Member Author

jonbarrow commented Dec 31, 2025

(at least on the NASC login phase, unsure on account registration)

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants