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