Core: Add SHA1 hashing for ROMs

This commit is contained in:
Vicki Pfau 2025-03-30 16:44:33 -07:00
parent 62247f0dce
commit eb781d290b
12 changed files with 442 additions and 6 deletions

View File

@ -37,6 +37,7 @@ Misc:
- Core: Improve rumble emulation by averaging state over entire frame (fixes mgba.io/i/3232)
- Core: Add MD5 hashing for ROMs
- Core: Add support for specifying an arbitrary portable directory
- Core: Add SHA1 hashing for ROMs
- FFmpeg: Add Ut Video option
- GB: Prevent incompatible BIOSes from being used on differing models
- GB Serialize: Add missing savestate support for MBC6 and NT (newer)

33
include/mgba-util/sha1.h Normal file
View File

@ -0,0 +1,33 @@
/* Copyright (c) 2013-2025 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Based on https://github.com/clibs/sha1
*/
#ifndef SHA1_H
#define SHA1_H
#include <mgba-util/common.h>
CXX_GUARD_START
struct SHA1Context {
uint32_t state[5];
uint32_t count[2];
unsigned char buffer[64];
};
void sha1Init(struct SHA1Context* ctx);
void sha1Update(struct SHA1Context* ctx, const void* input, size_t len);
void sha1Finalize(uint8_t digest[20], struct SHA1Context* ctx);
void sha1Buffer(const void* input, size_t len, uint8_t* result);
struct VFile;
bool sha1File(struct VFile* vf, uint8_t* result);
CXX_GUARD_END
#endif

View File

@ -28,11 +28,6 @@ enum mPlatform {
mPLATFORM_GB = 1,
};
enum mCoreChecksumType {
mCHECKSUM_CRC32,
mCHECKSUM_MD5,
};
struct mAudioBuffer;
struct mCoreConfig;
struct mCoreSync;

View File

@ -188,6 +188,12 @@ struct mCoreRegisterInfo {
enum mCoreRegisterType type;
};
enum mCoreChecksumType {
mCHECKSUM_CRC32,
mCHECKSUM_MD5,
mCHECKSUM_SHA1,
};
CXX_GUARD_END
#endif

View File

@ -353,6 +353,9 @@ static struct mScriptValue* _mScriptCoreChecksum(const struct mCore* core, int t
case mCHECKSUM_MD5:
size = 16;
break;
case mCHECKSUM_SHA1:
size = 20;
break;
}
if (!size) {
return &mScriptValueNull;

View File

@ -24,6 +24,7 @@
#include <mgba-util/md5.h>
#include <mgba-util/memory.h>
#include <mgba-util/patch.h>
#include <mgba-util/sha1.h>
#include <mgba-util/vfs.h>
static const struct mCoreChannelInfo _GBVideoLayers[] = {
@ -539,6 +540,15 @@ static void _GBCoreChecksum(const struct mCore* core, void* data, enum mCoreChec
md5Buffer("", 0, data);
}
break;
case mCHECKSUM_SHA1:
if (gb->romVf) {
sha1File(gb->romVf, data);
} else if (gb->memory.rom && gb->isPristine) {
sha1Buffer(gb->memory.rom, gb->pristineRomSize, data);
} else {
sha1Buffer("", 0, data);
}
break;
}
return;
}

View File

@ -31,6 +31,7 @@
#include <mgba-util/elf-read.h>
#endif
#include <mgba-util/md5.h>
#include <mgba-util/sha1.h>
#include <mgba-util/memory.h>
#include <mgba-util/patch.h>
#include <mgba-util/vfs.h>
@ -701,6 +702,19 @@ static void _GBACoreChecksum(const struct mCore* core, void* data, enum mCoreChe
md5Buffer("", 0, data);
}
break;
case mCHECKSUM_SHA1:
if (gba->romVf) {
sha1File(gba->romVf, data);
} else if (gba->mbVf) {
sha1File(gba->mbVf, data);
} else if (gba->memory.rom && gba->isPristine) {
sha1Buffer(gba->memory.rom, gba->pristineRomSize, data);
} else if (gba->memory.rom) {
sha1Buffer(gba->memory.rom, gba->memory.romSize, data);
} else {
sha1Buffer("", 0, data);
}
break;
}
return;
}

View File

@ -25,6 +25,7 @@ ROMInfo::ROMInfo(std::shared_ptr<CoreController> controller, QWidget* parent)
#endif
uint32_t crc32 = 0;
uint8_t md5[16]{};
uint8_t sha1[20]{};
CoreController::Interrupter interrupter(controller);
mCore* core = controller->thread()->core;
@ -41,6 +42,7 @@ ROMInfo::ROMInfo(std::shared_ptr<CoreController> controller, QWidget* parent)
core->checksum(core, &crc32, mCHECKSUM_CRC32);
core->checksum(core, &md5, mCHECKSUM_MD5);
core->checksum(core, &sha1, mCHECKSUM_SHA1);
m_ui.size->setText(QString::number(core->romSize(core)) + tr(" bytes"));
@ -69,6 +71,10 @@ ROMInfo::ROMInfo(std::shared_ptr<CoreController> controller, QWidget* parent)
md5[0x0], md5[0x1], md5[0x2], md5[0x3], md5[0x4], md5[0x5], md5[0x6], md5[0x7],
md5[0x8], md5[0x9], md5[0xA], md5[0xB], md5[0xC], md5[0xD], md5[0xE], md5[0xF]));
m_ui.sha1->setText(QString::asprintf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x",
sha1[ 0], sha1[ 1], sha1[ 2], sha1[ 3], sha1[ 4], sha1[ 5], sha1[ 6], sha1[ 7], sha1[ 8], sha1[ 9],
sha1[10], sha1[11], sha1[12], sha1[13], sha1[14], sha1[15], sha1[16], sha1[17], sha1[18], sha1[19]));
QString savePath = controller->savePath();
if (!savePath.isEmpty()) {
m_ui.savefile->setText(savePath);

View File

@ -95,13 +95,30 @@
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>SHA-1</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLabel" name="sha1">
<property name="text">
<string notr="true">{SHA1}</string>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Save file:</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="5" column="1">
<widget class="QLabel" name="savefile">
<property name="text">
<string notr="true">{SAVEFILE}</string>

View File

@ -7,6 +7,7 @@ set(BASE_SOURCE_FILES
gbk-table.c
hash.c
md5.c
sha1.c
string.c
table.c
vector.c

258
src/util/sha1.c Normal file
View File

@ -0,0 +1,258 @@
/* Copyright (c) 2013-2025 Jeffrey Pfau
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*
* Based on https://github.com/clibs/sha1
*
* Test Vectors (from FIPS PUB 180-1)
* "abc"
* A9993E36 4706816A BA3E2571 7850C26C 9CD0D89D
* "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"
* 84983E44 1C3BD26E BAAE4AA1 F95129E5 E54670F1
* A million repetitions of "a"
* 34AA973C D4C4DAA4 F61EEB2B DBAD2731 6534016F
*/
#include <mgba-util/sha1.h>
#include <mgba-util/vfs.h>
/* #define SHA1HANDSOFF * Copies data before messing with it. */
#define SHA1HANDSOFF
#define rol(value, bits) (((value) << (bits)) | ((value) >> (32 - (bits))))
/* blk0() and blk() perform the initial expand. */
/* I got the idea of expanding during the round function from SSLeay */
#ifndef __BIG_ENDIAN__
#define blk0(i) (block->l[i] = (rol(block->l[i],24)&0xFF00FF00) \
|(rol(block->l[i],8)&0x00FF00FF))
#else
#define blk0(i) block->l[i]
#endif
#define blk(i) (block->l[i&15] = rol(block->l[(i+13)&15]^block->l[(i+8)&15] \
^block->l[(i+2)&15]^block->l[i&15],1))
/* (R0+R1), R2, R3, R4 are the different operations used in SHA1 */
#define R0(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk0(i)+0x5A827999+rol(v,5);w=rol(w,30);
#define R1(v,w,x,y,z,i) z+=((w&(x^y))^y)+blk(i)+0x5A827999+rol(v,5);w=rol(w,30);
#define R2(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0x6ED9EBA1+rol(v,5);w=rol(w,30);
#define R3(v,w,x,y,z,i) z+=(((w|x)&y)|(w&x))+blk(i)+0x8F1BBCDC+rol(v,5);w=rol(w,30);
#define R4(v,w,x,y,z,i) z+=(w^x^y)+blk(i)+0xCA62C1D6+rol(v,5);w=rol(w,30);
/* Hash a single 512-bit block. This is the core of the algorithm. */
static void sha1Transform(uint32_t state[5], const uint8_t buffer[64]) {
uint32_t a, b, c, d, e;
typedef union {
unsigned char c[64];
uint32_t l[16];
} CHAR64LONG16;
#ifdef SHA1HANDSOFF
CHAR64LONG16 block[1]; /* use array to appear as a pointer */
memcpy(block, buffer, 64);
#else
/* The following had better never be used because it causes the
* pointer-to-const buffer to be cast into a pointer to non-const.
* And the result is written through. I threw a "const" in, hoping
* this will cause a diagnostic.
*/
CHAR64LONG16 *block = (const CHAR64LONG16 *) buffer;
#endif
/* Copy context->state[] to working vars */
a = state[0];
b = state[1];
c = state[2];
d = state[3];
e = state[4];
/* 4 rounds of 20 operations each. Loop unrolled. */
R0(a, b, c, d, e, 0);
R0(e, a, b, c, d, 1);
R0(d, e, a, b, c, 2);
R0(c, d, e, a, b, 3);
R0(b, c, d, e, a, 4);
R0(a, b, c, d, e, 5);
R0(e, a, b, c, d, 6);
R0(d, e, a, b, c, 7);
R0(c, d, e, a, b, 8);
R0(b, c, d, e, a, 9);
R0(a, b, c, d, e, 10);
R0(e, a, b, c, d, 11);
R0(d, e, a, b, c, 12);
R0(c, d, e, a, b, 13);
R0(b, c, d, e, a, 14);
R0(a, b, c, d, e, 15);
R1(e, a, b, c, d, 16);
R1(d, e, a, b, c, 17);
R1(c, d, e, a, b, 18);
R1(b, c, d, e, a, 19);
R2(a, b, c, d, e, 20);
R2(e, a, b, c, d, 21);
R2(d, e, a, b, c, 22);
R2(c, d, e, a, b, 23);
R2(b, c, d, e, a, 24);
R2(a, b, c, d, e, 25);
R2(e, a, b, c, d, 26);
R2(d, e, a, b, c, 27);
R2(c, d, e, a, b, 28);
R2(b, c, d, e, a, 29);
R2(a, b, c, d, e, 30);
R2(e, a, b, c, d, 31);
R2(d, e, a, b, c, 32);
R2(c, d, e, a, b, 33);
R2(b, c, d, e, a, 34);
R2(a, b, c, d, e, 35);
R2(e, a, b, c, d, 36);
R2(d, e, a, b, c, 37);
R2(c, d, e, a, b, 38);
R2(b, c, d, e, a, 39);
R3(a, b, c, d, e, 40);
R3(e, a, b, c, d, 41);
R3(d, e, a, b, c, 42);
R3(c, d, e, a, b, 43);
R3(b, c, d, e, a, 44);
R3(a, b, c, d, e, 45);
R3(e, a, b, c, d, 46);
R3(d, e, a, b, c, 47);
R3(c, d, e, a, b, 48);
R3(b, c, d, e, a, 49);
R3(a, b, c, d, e, 50);
R3(e, a, b, c, d, 51);
R3(d, e, a, b, c, 52);
R3(c, d, e, a, b, 53);
R3(b, c, d, e, a, 54);
R3(a, b, c, d, e, 55);
R3(e, a, b, c, d, 56);
R3(d, e, a, b, c, 57);
R3(c, d, e, a, b, 58);
R3(b, c, d, e, a, 59);
R4(a, b, c, d, e, 60);
R4(e, a, b, c, d, 61);
R4(d, e, a, b, c, 62);
R4(c, d, e, a, b, 63);
R4(b, c, d, e, a, 64);
R4(a, b, c, d, e, 65);
R4(e, a, b, c, d, 66);
R4(d, e, a, b, c, 67);
R4(c, d, e, a, b, 68);
R4(b, c, d, e, a, 69);
R4(a, b, c, d, e, 70);
R4(e, a, b, c, d, 71);
R4(d, e, a, b, c, 72);
R4(c, d, e, a, b, 73);
R4(b, c, d, e, a, 74);
R4(a, b, c, d, e, 75);
R4(e, a, b, c, d, 76);
R4(d, e, a, b, c, 77);
R4(c, d, e, a, b, 78);
R4(b, c, d, e, a, 79);
/* Add the working vars back into context.state[] */
state[0] += a;
state[1] += b;
state[2] += c;
state[3] += d;
state[4] += e;
/* Wipe variables */
a = b = c = d = e = 0;
#ifdef SHA1HANDSOFF
memset(block, '\0', sizeof(block));
#endif
}
/* shaInit - Initialize new context */
void sha1Init(struct SHA1Context* context) {
/* SHA1 initialization constants */
context->state[0] = 0x67452301;
context->state[1] = 0xEFCDAB89;
context->state[2] = 0x98BADCFE;
context->state[3] = 0x10325476;
context->state[4] = 0xC3D2E1F0;
context->count[0] = context->count[1] = 0;
}
/* Run your data through this. */
void sha1Update(struct SHA1Context* context, const void* data, size_t len) {
size_t i;
size_t j;
j = context->count[0];
if ((context->count[0] += len << 3) < j) {
++context->count[1];
}
context->count[1] += (len >> 29);
j = (j >> 3) & 63;
if ((j + len) > 63) {
memcpy(&context->buffer[j], data, (i = 64 - j));
sha1Transform(context->state, context->buffer);
for (; i + 63 < len; i += 64) {
sha1Transform(context->state, &((uint8_t*) data)[i]);
}
j = 0;
} else {
i = 0;
}
memcpy(&context->buffer[j], &((uint8_t*) data)[i], len - i);
}
/* Add padding and return the message digest. */
void sha1Finalize(uint8_t digest[20], struct SHA1Context* context) {
unsigned i;
uint8_t finalcount[8];
uint8_t c;
for (i = 0; i < 8; ++i) {
finalcount[i] = (uint8_t) ((context->count[(i >= 4 ? 0 : 1)] >> ((3 - (i & 3)) * 8)) & 255); /* Endian independent */
}
c = 0200;
sha1Update(context, &c, 1);
while ((context->count[0] & 504) != 448) {
c = 0000;
sha1Update(context, &c, 1);
}
sha1Update(context, finalcount, 8); /* Should cause a SHA1Transform() */
for (i = 0; i < 20; ++i) {
digest[i] = (uint8_t) ((context->state[i >> 2] >> ((3 - (i & 3)) * 8)) & 255);
}
/* Wipe variables */
memset(context, '\0', sizeof(*context));
memset(&finalcount, '\0', sizeof(finalcount));
}
void sha1Buffer(const void* input, size_t len, uint8_t* result) {
struct SHA1Context ctx;
size_t i;
sha1Init(&ctx);
for (i = 0; i + 63 < len; i += 64) {
sha1Update(&ctx, &((const uint8_t*) input)[i], 64);
}
for (; i < len; ++i) {
sha1Update(&ctx, &((const uint8_t*) input)[i], 1);
}
sha1Finalize(result, &ctx);
}
bool sha1File(struct VFile* vf, uint8_t* result) {
struct SHA1Context ctx;
uint8_t buffer[2048];
sha1Init(&ctx);
ssize_t read;
ssize_t position = vf->seek(vf, 0, SEEK_CUR);
if (vf->seek(vf, 0, SEEK_SET) < 0) {
return false;
}
while ((read = vf->read(vf, buffer, sizeof(buffer))) > 0) {
sha1Update(&ctx, buffer, read);
}
vf->seek(vf, position, SEEK_SET);
if (read < 0) {
return false;
}
sha1Finalize(result, &ctx);
return true;
}

View File

@ -7,6 +7,7 @@
#include <mgba-util/crc32.h>
#include <mgba-util/md5.h>
#include <mgba-util/sha1.h>
M_TEST_DEFINE(emptyCrc32) {
uint8_t buffer[1] = {0};
@ -115,6 +116,92 @@ M_TEST_DEFINE(twoBlockMd5) {
}), 16);
}
M_TEST_DEFINE(emptySha1) {
uint8_t buffer[1] = {0};
uint8_t digest[20] = {0};
sha1Buffer(buffer, 0, digest);
assert_memory_equal(digest, ((uint8_t[]) {
0xDA, 0x39, 0xA3, 0xEE, 0x5E, 0x6B, 0x4B, 0x0D, 0x32, 0x55,
0xBF, 0xEF, 0x95, 0x60, 0x18, 0x90, 0xAF, 0xD8, 0x07, 0x09
}), 16);
}
M_TEST_DEFINE(newlineSha1) {
uint8_t buffer[1] = { '\n' };
uint8_t digest[20] = {0};
sha1Buffer(buffer, 1, digest);
assert_memory_equal(digest, ((uint8_t[]) {
0xAD, 0xC8, 0x3B, 0x19, 0xE7, 0x93, 0x49, 0x1B, 0x1C, 0x6E,
0xA0, 0xFD, 0x8B, 0x46, 0xCD, 0x9F, 0x32, 0xE5, 0x92, 0xFC
}), 20);
}
M_TEST_DEFINE(fullBlockSha1) {
uint8_t buffer[64] = {
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
};
uint8_t digest[20] = {0};
sha1Buffer(buffer, 64, digest);
assert_memory_equal(digest, ((uint8_t[]) {
0xCB, 0x4D, 0xD3, 0xDA, 0xCA, 0x2D, 0x6F, 0x25, 0x44, 0xBC,
0x0D, 0xAA, 0x6B, 0xEB, 0xB7, 0x8A, 0xED, 0x0B, 0xD0, 0x34
}), 20);
}
M_TEST_DEFINE(overflowBlockSha1) {
uint8_t buffer[65] = {
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x0a,
};
uint8_t digest[20] = {0};
sha1Buffer(buffer, 65, digest);
assert_memory_equal(digest, ((uint8_t[]) {
0xA3, 0x96, 0x68, 0x5E, 0xF7, 0x73, 0x87, 0x13, 0x2C, 0x43,
0x64, 0x42, 0x2D, 0x16, 0x65, 0x39, 0x65, 0x6F, 0xB8, 0x93
}), 20);
}
M_TEST_DEFINE(twoBlockSha1) {
uint8_t buffer[128] = {
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20,
};
uint8_t digest[20] = {0};
sha1Buffer(buffer, 128, digest);
assert_memory_equal(digest, ((uint8_t[]) {
0xFF, 0xB5, 0xE5, 0xD9, 0x6E, 0x19, 0x71, 0x4F, 0xFE, 0xF6,
0x0A, 0xC8, 0x74, 0x9E, 0xCA, 0xEF, 0xBE, 0xC9, 0xD2, 0x95
}), 20);
}
M_TEST_SUITE_DEFINE(Hashes,
cmocka_unit_test(emptyCrc32),
cmocka_unit_test(newlineCrc32),
@ -127,4 +214,9 @@ M_TEST_SUITE_DEFINE(Hashes,
cmocka_unit_test(fullBlockMd5),
cmocka_unit_test(overflowBlockMd5),
cmocka_unit_test(twoBlockMd5),
cmocka_unit_test(emptySha1),
cmocka_unit_test(newlineSha1),
cmocka_unit_test(fullBlockSha1),
cmocka_unit_test(overflowBlockSha1),
cmocka_unit_test(twoBlockSha1),
)