Click here to download the file.
/*
* MIT License
*
* Based on Project Blue Dream
* Originally reverse Engineered by Johnny L. de Alba (Arkonviox), 2020
* Additional reverse engineering by Moduvator, 2026
* Copyright (c) 2026. Novotrade International and Sega Games Co., Ltd.
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or
* sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
* KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
* WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
* OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Project SECCO
* Ecco 2 Password System
*
* Game: Ecco 2: The Tides of Time, US/Europe
* Platform: Sega Genesis/Mega Drive/Sega CD
*
* Summary:
* Reverse engineering of original game's password system
* Proof of concept code with a sample driver
*
*/
#include <stdio.h>
#include <stdbool.h>
#include <stdint.h>
#include <stddef.h>
#include <string.h>
// XOR Salt for Ecco 2
#define ECCO2_US_SALT 0x15b63c7a
#define CRCPOS 3 // Checksum character position in password
#define PWD_SIZE 8 // Password size
#define ASCII_OFFSET 65 // Offset to turn codes into ASCII
// Stored in Easy Flags
#define EASY_FLAG 0x01
#define FORCEEASY_FLAG 0x80
// Stored in Skill Points
#define FORCEHARD_FLAG 0x80
#define DEATHS_MASK 0x0F
#define POINTS_MASK 0x1F
#define PFIELD_MASK 0x7F
#define DEATHS_MAX 0x0F
// Packed password masks
#define PWDMASK_GLOBES 0x1F
#define PWDMASK_STAGEID 0x3F
#define PWDMASK_POINTS 0x0F
#define PWDMASK_TIME 0xFFFE
#define PWDMASK_HARD 0x10000
#define PWDMASK_EASY 0x8000
// Packed password offsets
#define PWDOFF_STAGEID 0x05
#define PWDOFF_POINTS 0x0B
#define PWDOFF_EASYF 0x0F
#define PWDOFF_HARDF 0x10
#define PWDOFF_TIME 0x10
// Maximum valid Stage ID
#define STAGEID_MAX 49
// User input buffer size
#define INPUTBUF_SIZE 10
// Checksum LUT for scrambling password
uint8_t encodeLUT[] = {
0x16, 0x08, 0x01, 0x0F, 0x02, 0x07, 0x14, 0x09,
0x0D, 0x19, 0x17, 0x0A, 0x00, 0x0B, 0x04, 0x0C,
0x06, 0x0E, 0x05, 0x10, 0x13, 0x11, 0x03, 0x15,
0x12, 0x18
};
// Checksum LUT for descrambling password
uint8_t decodeLUT[] = {
0x0C, 0x02, 0x04, 0x16, 0x0E, 0x12, 0x10, 0x05,
0x01, 0x07, 0x0B, 0x0D, 0x0F, 0x08, 0x11, 0x03,
0x13, 0x15, 0x18, 0x14, 0x06, 0x17, 0x00, 0x0A,
0x19, 0x09
};
// Asterite globes audit table
uint8_t gAsteriteTable[] = {
0x01, 0x0e, 0x03, 0x01, 0x17, 0x17, 0x17,
0x17, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x18, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x18, 0x18, 0x18, 0x18,
0x17, 0x17, 0x17, 0x17, 0x18, 0x18, 0x18,
0x18, 0x18, 0x18, 0x18, 0x0d, 0x18, 0x18
};
// Tides of Time Stage Names
const char *ecco2LevelNames[STAGEID_MAX] = {
"Sea of Green", "Deep Ridge", "The Eye", "Sea of Birds", "Secret Cave",
"The Hungry Ones", "Convergence", "The Globe Holder", "Selection Scr",
"Two Tides", "The Lost Orcas", "Vents of Medusa", "Four Islands",
"Maze of Stone", "Home Bay", "Sea of Darkness", "Crystal Springs",
"Fault Zone", "Gateway", "Trellia's Bay", "Sky Way", "Asterite's Cave",
"Eagles Bay", "Fin to Feather", "Skylands", "Tube of Medusa", "Vortex Arrived",
"Aqua Tubeway", "Sky Tides", "Moray Abyss", "Asterite's Home", "Epilogue",
"Atlantis", "Fish City", "City of Forever", "Black Clouds", "Vortex Future",
"Gravitor Box", "Lunar Bay", "Dark Sea", "New Machine", "Inside", "Inter",
"Innuendo", "Trans", "Vortex Queen", "Big Water", "The Pod", "Tmachine"
};
// Wrapper to get user input and remove \n from input buffer
char * s_gets(char *st, int n) {
char * retval;
char * find;
if (retval = fgets(st, n, stdin)) {
if (find = strchr(st, '\n')) {
*find = '\0';
} else {
while (getchar() != '\n') {
continue;
}
}
}
return retval;
}
/* Scramble packed password via bit shuffling */
uint32_t scramblePassword(uint32_t rawPassword) {
uint32_t hmask = 0x80000000;
uint32_t lmask = 0x40000000;
uint32_t key = 0x00000001;
uint32_t result = 0;
for (int i = 16; i > 0; i--) {
if ((rawPassword & hmask) != 0) {
result |= key;
}
if ((rawPassword & lmask) != 0) {
result |= hmask;
}
hmask = hmask >> 2;
lmask = lmask >> 2;
key = key << 2;
}
return result;
}
/* Unscramble packed password via bit shuffling */
uint32_t unscramblePassword(uint32_t rawPassword) {
uint32_t hmask = 0x80000000;
uint32_t lmask = 0x40000000;
uint32_t key = 0x00000001;
uint32_t result = 0;
for (int i = 16; i > 0; i--) {
if ((rawPassword & key) != 0) {
result |= hmask;
}
if ((rawPassword & hmask) != 0) {
result |= lmask;
}
hmask = hmask >> 2;
lmask = lmask >> 2;
key = key << 2;
}
return result;
}
/* Test given stage ID against Asterite Globe pairs */
bool asteriteGlobeAudit(uint8_t stageID, uint8_t globePairs) {
// List of stage IDs to reject
switch (stageID) {
case 8: // Selection scr
case 26: // Vortex arrived
case 47: // The Pod
case 48: // Tmachine
return false;
break;
}
uint8_t expectedGlobes = gAsteriteTable[stageID];
return (expectedGlobes == globePairs);
}
/* Turn a given password string into data */
uint32_t decodeRawPassword(char *srcString) {
int i;
char charCode;
uint16_t computedCRC = 0;
uint16_t givenCRC = 0;
uint32_t product = 0;
uint32_t multiplier = 1;
for (i = 0; i < PWD_SIZE; i++) {
if (i == CRCPOS) i++; // Skip CRC character
charCode = srcString[i];
computedCRC += charCode;
}
computedCRC = computedCRC % 26;
charCode = srcString[CRCPOS];
givenCRC = decodeLUT[charCode];
if (computedCRC == givenCRC) {
for (i = 0; i < PWD_SIZE; i++) {
if (i == CRCPOS) i++; // Skip CRC character
charCode = srcString[i];
product += (charCode * multiplier);
multiplier *= 26;
}
return product;
}
return 0;
}
/*
Wrapper for decodeRawPassword. Password screen already supplies string
as character positions. Here we need to convert it from ASCII first.
*/
uint32_t decodePassword(char *srcString) {
int i;
uint32_t decodedPwd = 0;
// Turn string into character codes
for (i = 0; i < PWD_SIZE; i++) {
srcString[i] = srcString[i] - ASCII_OFFSET;
}
decodedPwd = decodeRawPassword(srcString);
if (decodedPwd > 0) {
printf("Scrambled Password:\t0x%0X\n", decodedPwd);
decodedPwd = unscramblePassword(decodedPwd ^ ECCO2_US_SALT);
printf("Unscrambled Password:\t0x%0X\n", decodedPwd);
}
return decodedPwd;
}
/* Parse a decoded and descrambled password */
int parsePassword(uint32_t decodedPass) {
char *stageName = NULL;
char *auditResult = NULL;
bool globeAudit = false;
uint8_t globePairs, stageID, skillPoints, forcedEasy, forcedHard;
uint16_t timeElapsed;
globePairs = decodedPass & PWDMASK_GLOBES;
stageID = (decodedPass >> PWDOFF_STAGEID) & PWDMASK_STAGEID;
skillPoints = (decodedPass >> PWDOFF_POINTS) & PWDMASK_POINTS;
forcedEasy = (decodedPass >> PWDOFF_EASYF) & 1;
forcedHard = (decodedPass >> PWDOFF_HARDF) & 1;
timeElapsed = (decodedPass >> PWDOFF_TIME) & PWDMASK_TIME;
if (stageID < STAGEID_MAX) {
globeAudit = asteriteGlobeAudit(stageID, globePairs);
if (globeAudit) auditResult = "Audit OK";
else auditResult = "Audit FAILED";
stageName = ecco2LevelNames[stageID];
printf(
"Globe Pairs:\t%d (%s)\nStage ID:\t%d - %s\nPoints/Deaths:\t%d\nForce Easy:\t%d\nForce Hard:\t%d\nTime Elapsed:\t0x%0X\n",
globePairs, auditResult, stageID, stageName, skillPoints, forcedEasy, forcedHard, timeElapsed
);
return 0;
}
printf("Ivalid Stage ID: %d\n", stageID);
return -1;
}
/* Wrapper that accepts a password string and spits out the result */
void processPass(char *pwdString) {
uint32_t decodedPass = 0;
printf("Trying to decode: %s\n", pwdString);
decodedPass = decodePassword(pwdString);
if (decodedPass > 0) {
parsePassword(decodedPass);
} else {
puts("CRC Error");
}
}
int main(void) {
char inputBuf[INPUTBUF_SIZE] = { 0 };
char *result = NULL;
do {
// Clear the password buffer
memset(inputBuf, 0, sizeof(inputBuf));
// Request and read new input
printf("Input password: ");
result = s_gets(inputBuf, sizeof(inputBuf));
// Process password or break if empty
if(strlen(inputBuf) > 0)
processPass(_strupr(inputBuf));
} while (strlen(inputBuf) > 0);
return 0;
}