-
-
Save andrewrk/4425843 to your computer and use it in GitHub Desktop.
var crypto = require('crypto'); | |
var assert = require('assert'); | |
function mcHexDigest(str) { | |
var hash = new Buffer(crypto.createHash('sha1').update(str).digest(), 'binary'); | |
// check for negative hashes | |
var negative = hash.readInt8(0) < 0; | |
if (negative) performTwosCompliment(hash); | |
var digest = hash.toString('hex'); | |
// trim leading zeroes | |
digest = digest.replace(/^0+/g, ''); | |
if (negative) digest = '-' + digest; | |
return digest; | |
} | |
function performTwosCompliment(buffer) { | |
var carry = true; | |
var i, newByte, value; | |
for (i = buffer.length - 1; i >= 0; --i) { | |
value = buffer.readUInt8(i); | |
newByte = ~value & 0xff; | |
if (carry) { | |
carry = newByte === 0xff; | |
buffer.writeUInt8(newByte + 1, i); | |
} else { | |
buffer.writeUInt8(newByte, i); | |
} | |
} | |
} | |
assert.strictEqual(mcHexDigest('Notch'), "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"); | |
assert.strictEqual(mcHexDigest('jeb_'), "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"); | |
assert.strictEqual(mcHexDigest('simon'), "88e16a1019277b15d58faf0541e11910eb756f6"); |
To make sure I understood minecraft's hexdigests correctly I implemented this myself. I'm dropping my code here in the hopes of it helping someone, I can remove it again if needed. I hope the comments are helpful. This example only generates the hex digest of the hash I coded in, if you need it for other things (like the code snippet above does) then you should simply change the hash
export function generateHexDigest (secret: Buffer, pubKey: Buffer) {
// The hex digest is the hash made below.
// However, when this hash is negative (meaning its MSB is 1, as it is in two's complement), instead of leaving it
// like that, we make it positive and simply put a '-' in front of it. This is a simple process: as you always do
// with 2's complement you simply flip all bits and add 1
let hash = crypto.createHash('sha1')
.update('') // serverId = just an empty string
.update(secret)
.update(pubKey)
.digest()
// Negative check: check if the most significant bit of the hash is a 1.
const isNegative = (hash.readUInt8(0) & (1 << 7)) !== 0 // when 0, it is positive
if (isNegative) {
// Flip all bits and add one. Start at the right to make sure the carry works
const inverted = Buffer.allocUnsafe(hash.length)
let carry = 0
for (let i = hash.length - 1; i >= 0; i--) {
let num = (hash.readUInt8(i) ^ 0b11111111) // a byte XOR a byte of 1's = the inverse of the byte
if (i === hash.length - 1) num++
num += carry
carry = Math.max(0, num - 0b11111111)
num = Math.min(0b11111111, num)
inverted.writeUInt8(num, i)
}
hash = inverted
}
let result = hash.toString('hex').replace(/^0+/, '')
// If the result was negative, add a '-' sign
if (isNegative) result = `-${result}`
return result
}
Replying to timvandam
To make sure I understood minecraft's hexdigests correctly I implemented this myself. I'm dropping my code here in the hopes of it helping someone, I can remove it again if needed. I hope the comments are helpful. This example only generates the hex digest of the hash I coded in, if you need it for other things (like the code snippet above does) then you should simply change the hash
export function generateHexDigest (secret: Buffer, pubKey: Buffer) { // The hex digest is the hash made below. // However, when this hash is negative (meaning its MSB is 1, as it is in two's complement), instead of leaving it // like that, we make it positive and simply put a '-' in front of it. This is a simple process: as you always do // with 2's complement you simply flip all bits and add 1let hash = crypto.createHash('sha1')
.update('') // serverId = just an empty string
.update(secret)
.update(pubKey)
.digest()// Negative check: check if the most significant bit of the hash is a 1.
const isNegative = (hash.readUInt8(0) & (1 << 7)) !== 0 // when 0, it is positiveif (isNegative) {
// Flip all bits and add one. Start at the right to make sure the carry works
const inverted = Buffer.allocUnsafe(hash.length)
let carry = 0
for (let i = hash.length - 1; i >= 0; i--) {
let num = (hash.readUInt8(i) ^ 0b11111111) // a byte XOR a byte of 1's = the inverse of the byte
if (i === hash.length - 1) num++
num += carry
carry = Math.max(0, num - 0b11111111)
num = Math.min(0b11111111, num)
inverted.writeUInt8(num, i)
}
hash = inverted
}
let result = hash.toString('hex').replace(/^0+/, '')
// If the result was negative, add a '-' sign
if (isNegative) result =-${result}
return result
}
I smell ECMAscript... delicious!
This is much simpler using modern javascript:
function hexDigest(text) {
const hash = crypto.createHash("sha1")
.update(text)
.digest()
return BigInt.asIntN( // performs two's compliment
160, // hash size in bits
hash.reduce( // convert buffer to bigint using reduce
(a, x) =>
a << 8n | // bit-shift the accumulator 8 bits (one byte) to the left
BigInt(x), // fill lower byte just freed up by bit-shifting using bitwise or-operator
0n // start with accumulator value of 0
)
).toString(16) // display the result with base 16 (hex)
}
Apparently, you can do this, which is much simpler to read and also internally optimized (unlike the bitwise or reduce approach):
BigInt(`0x${digest.toString('hex')}`)
So we get this, and I'm fine with how it looks:
export function minecraftSha1(
serverId: string,
sharedSecret: Buffer,
publicKey: Buffer,
): string {
const sha1 = createHash('sha1');
sha1.update(Buffer.from(serverId, 'ascii'));
sha1.update(sharedSecret);
sha1.update(publicKey);
return mcHexDigest(sha1.digest());
}
function mcHexDigest(digest: Buffer | Uint8Array): string {
const bigint = BigInt(`0x${digest.toString('hex')}`);
return BigInt.asIntN(digest.length * 8, bigint).toString(16);
}
Please note that there are cases where this code tries to write
256
as a byte andwriteUInt8
rejects it. Line 25 should read like this (or something equivalent):