Skip to content

Instantly share code, notes, and snippets.

@ThePedroo
Last active May 18, 2025 20:33
Show Gist options
  • Save ThePedroo/b04f209696953b9a144455fb12b90265 to your computer and use it in GitHub Desktop.
Save ThePedroo/b04f209696953b9a144455fb12b90265 to your computer and use it in GitHub Desktop.
SignSee, a tool to check if a ReZygisk build has been tampered with and if it uses official public key.
#!/usr/bin/env node
/*
SignSee 0.0.2 by ThePedroo
Licensed under AGPL 3.0 by ThePedroo,
read about in Open Source Initiative: https://opensource.org/license/agpl-v3
*/
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
import * as ed from '@noble/ed25519'
/* INFO: Polyfill for etc.sha512Sync */
ed.etc.sha512Sync = (...m) => crypto.createHash('sha512').update(ed.etc.concatBytes(...m)).digest()
const SIGNATURE_LENGTH = 64
const PUBLIC_KEY_LENGTH = 32
/* INFO: "Magic" string, created based on PerformanC's private key */
const PERFORMANC_PUBLIC_KEY = 'ddf4a98c0f7b121f3e1281a7de957ed3e5b528aaf03b13e7f04a561054d9187f'
/* INFO: Misaki structure
<digest/signature> + <public key>
<public key> = 32 bytes
<digest/signature> = 64 bytes
The digest of the entire project is based on both filename, file content size
and file content. We create a buffer with all of those datas, in an alphabetical order,
then ed25519 algorithm with SHA512 is used to verify if the digest in misaki file
is valid using the information from the files in the folder, made by SignSee.
OBS: The path to the file is not included in the digest, only the filename.
*/
async function signFiles(files) {
let toSign = Buffer.alloc(0)
for await (const file of files) {
const fileNameBytes = Buffer.from(path.basename(file), 'utf-8')
toSign = Buffer.concat([ toSign, fileNameBytes ])
toSign = Buffer.concat([ toSign, Buffer.from([0]) ])
const stats = fs.statSync(file)
const sizeBuffer = Buffer.alloc(8)
sizeBuffer.writeBigInt64LE(BigInt(stats.size), 0)
toSign = Buffer.concat([ toSign, sizeBuffer ])
const stream = fs.createReadStream(file)
for await (const chunk of stream) {
toSign = Buffer.concat([ toSign, chunk ])
}
}
return toSign
}
async function recursiveReadDir(dir) {
const files = []
for await (const file of fs.readdirSync(dir)) {
const fullPath = path.join(dir, file)
if (fs.statSync(fullPath).isDirectory()) {
files.push(...await recursiveReadDir(fullPath))
} else {
files.push(fullPath)
}
}
return files
}
async function verifyMisaki(root) {
let verification = null
let publicKey = null
{
const misakiBuffer = fs.readFileSync(`${root}/misaki.sig`)
const signature = misakiBuffer.subarray(0, SIGNATURE_LENGTH)
publicKey = misakiBuffer.subarray(SIGNATURE_LENGTH)
if (publicKey.length === 0) throw new Error('Empty public key')
if (publicKey.length !== PUBLIC_KEY_LENGTH) throw new Error('Invalid public key length')
let allFiles = await recursiveReadDir(root)
/* INFO: Alphabetical order of files is important, as the digest is based on the order of the files */
allFiles.sort()
/* INFO: misaki.sig file must be ignored for the digest part */
allFiles = allFiles.filter((file) => file !== `${root}/misaki.sig`)
const signedData = await signFiles(allFiles)
verification = ed.verify(signature, signedData, publicKey)
}
return {
IsNotTampered: verification,
isOfficial: publicKey.equals(Buffer.from(PERFORMANC_PUBLIC_KEY, 'hex'))
}
}
(async () => {
console.log('SignSee 0.0.2 by ThePedroo')
console.log('Licensed under AGPL 3.0 by ThePedroo')
console.log('Read about in Open Source Initiative: https://opensource.org/license/agpl-v3')
console.log()
if (process.argv.length < 3) {
console.error('Usage: node verify-zip.js <path-to-unzipped-folder>')
process.exit(1)
}
if (!fs.existsSync(process.argv[2])) {
console.error('Invalid path to unzipped folder')
process.exit(1)
}
if (!fs.statSync(process.argv[2]).isDirectory()) {
console.error('Invalid path to unzipped folder, not a directory')
process.exit(1)
}
if (!fs.existsSync(`${process.argv[2]}/misaki.sig`)) {
console.error('Invalid path to unzipped folder, missing misaki.sig')
console.error('The build is either old or has been tampered with. Do not trust.')
process.exit(1)
}
if (!fs.statSync(`${process.argv[2]}/misaki.sig`).size === SIGNATURE_LENGTH + PUBLIC_KEY_LENGTH) {
console.error('Invalid misaki.sig file, invalid size')
console.error('The build is either old or has been tampered with. Do not trust.')
process.exit(1)
}
const timeInit = Date.now()
const buildIntegrity = await verifyMisaki(process.argv[2])
console.log(`* Time taken to verify: ${Date.now() - timeInit}ms`)
console.log()
console.log('Code Integrity Check:')
console.log(` - Build Integrity: ${buildIntegrity.IsNotTampered ? 'OK' : 'Tampered'}`)
console.log(` - Is Official: ${buildIntegrity.isOfficial ? 'Yes' : 'No'}`)
console.log()
console.log()
console.log('OBS: Official means that the zip contains the public key generated by PerformanC members\'s\n' +
'private key to create the build, certifying that the build is not malicious.')
console.log()
console.log('OBS: Is Official must be taken into consideration together with Build Integrity.\n' +
'If Build Integrity is OK but Is Official is not, it means that the build was made and signed\n' +
'by someone else and MAY be malicious.\n' +
'If Build Integrity is not OK, it means that the build was tampered with and something, malicious or\n' +
'not, was modified. Those shouldn\'t be used without extreme care.')
})()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment