Password analyzer/creator

Discuss PoP1 for SNES here.
Post Reply
User avatar
Norbert
The Prince of Persia
The Prince of Persia
Posts: 5743
Joined: April 9th, 2009, 10:58 pm

Password analyzer/creator

Post by Norbert »

David, in April 2007 you created a SNES password analyzer/creator, which was available at your Freeweb.hu website.
I think it's no longer available anywhere. Maybe you can attach it to a post in this thread. Thanks.
David
The Prince of Persia
The Prince of Persia
Posts: 2846
Joined: December 11th, 2008, 9:48 pm
Location: Hungary

Re: Password analyzer/creator

Post by David »

I attached it to this post.

The ZIP contains two files:
snes_pwd_gen.htm is the generator/decoder itself.
snes_pwd.htm is a description of how the SNES passwords work.
Attachments
snes_password.zip
(3.34 KiB) Downloaded 321 times
User avatar
spartacus735
Wise Scribe
Wise Scribe
Posts: 217
Joined: November 20th, 2011, 12:41 pm
Location: France

Re: Password analyzer/creator

Post by spartacus735 »

Great tool ! thank
>>>>>>>>>>>>>>>>>>>> http://www.youtube.com/user/spartacus735 <<<<<<<<<<<<<<<<<<<< the channel of prince of persia snes mod
User avatar
Norbert
The Prince of Persia
The Prince of Persia
Posts: 5743
Joined: April 9th, 2009, 10:58 pm

Re: Password analyzer/creator

Post by Norbert »

David
The Prince of Persia
The Prince of Persia
Posts: 2846
Joined: December 11th, 2008, 9:48 pm
Location: Hungary

Re: Password analyzer/creator

Post by David »

Here is a detailed disassembly of the password-related code.
It has some new info about the xor checksum bits (plus that always-zero bit) [they're calculated using a CRC] and the unused "x" bit [it seems it was meant for the checkpoint flag].
Attachments
SNES-password-disassembly.txt
(22.36 KiB) Downloaded 550 times
kupfeli
Scholar Scribe
Scholar Scribe
Posts: 1
Joined: August 1st, 2019, 7:44 am

Re: Password analyzer/creator

Post by kupfeli »

Hi people,

Using information from this forum (credit where credit is due), while on holiday I wrote a password generator/analyzer on my mobile phone.
Also, in the comments you will find a thorough explanation of the elapsed time counter, also put together using various sources.

Here it is, I don't need any credits, you can do anything and everything with it.

Kind regards,
Kupfeli

Code: Select all

import java.util.*;

 class PoP
 {
 	//USA/EU version
 	private static final String euAlphabet="BTL3GY7Q9CVM4HZ8R+DWN5J12S!FXP6K";
 	
 	//Japan version
 	private static final String jpnAlphabet="AIQYEMU34BJRZFNVW5CKS1GOPX6DLT2H";
 	
 	private static Vector<int[]> grid=new Vector<int[]>();
 	
 	public static void main(String args[])
 	{ 
		Scanner s=new Scanner(System.in);
		
		grid.add(new int[]{0,0,0,0,0});
		grid.add(new int[]{0,0,0,0,0});
		grid.add(new int[]{0,0,0,0,0});
		grid.add(new int[]{0,0,0,0,0});
		grid.add(new int[]{0,0,0,0,0});
		grid.add(new int[]{0,0,0,0,0});
		grid.add(new int[]{0,0,0,0,0});
		
		
		String[] parsedInput=s.nextLine().split(" ");
			
		if(parsedInput.length<2)
		{
			printUsage();
			return;
		}
		String command=parsedInput[0].toLowerCase();

		if(command.equals("generate"))
		{
			generate(parsedInput);
		}
		else if(command.equals("analyze"))
		{
			analyze(parsedInput);
		}
		else
		{
			printUsage();
		}
 	}
 	
 	public static void generate(String[] input)
 	{
 		try
 		{
 			if(input.length<4)
 			{
 				printUsage();
 				return;
 			}
 		   int level=Integer.parseInt(input[1]);
 		   int health=Integer.parseInt(input[2]);
 		   String[] timeParts=input[3].split(":");
			int minutesLeft=Integer.parseInt(timeParts[0]);
			int secondsLeft=0;
			if(timeParts.length>1)secondsLeft=Integer.parseInt(timeParts[1]);
			if(input.length>4)grid.get(4)[2]=1;
			
			level--;
			if(level>19)level=19;
			if(level<0)level=0;
			if(health>15)health=15;
			if(health<0)health=0;
			if(minutesLeft>120)minutesLeft=120;
			if(minutesLeft<0)minutesLeft=0;
			if(secondsLeft>59)secondsLeft=59;
			if(secondsLeft<0)secondsLeft=0;
			if(minutesLeft==120)secondsLeft=0;
			
			int[] levelBits=getBits(level,5);
			int[] healthBits=getBits(health,4);
			
			/*
Calculation of elapsed time counter
PAL=50 FPS
NTSC=60 FPS

Password stores elapsed time counter.
This counter is increased per eighth frame, regardless of framerate.
Timer in the game takes 425 counter ticks as a minute.
Now the total time in game you get to complete the game are 120 minutes, but those are game minutes, so in respect to the 425 ticks per minute where a tick happens each eighth frame and thus is dependent of the snes framerate.

Now, for PAL, the exact time you get to play therefore:
gametime is 425×120=51000 ticks
fps=50, so there are 50/8=6,25 ticks in a second in realworld time on PAL.
That means in realworld time we have 51000/6,25=8160 seconds to complete the game which is 136 minutes exactly.
For NTSC it would be 113,33 minutes (51000/(60/8)/60)

Now the password stores the elapsed time counter, ok. We want to enter the remaining time as input for generating the password, so we first calculate the number of remaining time ticks, where we assume PAL now:

remaining time ticks is input seconds × 6,25.
NO!! Well technically yes, but then we need to calculate different passwords for PAL and NTSC. To make things easy, we take the GAME time. In the game 425 ticks are in a minute, so 425/60=7,08 ticks in a game second, so for our password generation it doesnt matter if we use PAL or NTSC in this case, we just need to be aware that our input is treated as game time. So, if you enter 120 minutes as remaining time, you would see 120 in the counter in the game, but just be aware that the actual real world time in that case is more in the PAL version and less in the NTSC version.
Now lets say as remaining time we enter 120 minutes. For the counter in the password we calculate the following value:

remaining time in seconds=120x60=7200
in game counter ticks this is 7200x7,08, but then we use the rounded value. More accurately we would use 7200x(425/60)=51000

elapsed time counter in password is then: 51000 - 51000 = 0, which is correct of course, as we have the full time remaining and thus did not spend any game ticks :-)

Another example: remaining time as input is 106 minutes and 12 seconds.
Counter value in password would then be: 
51000-(((106x60)+12)x(425/60))=5865
			*/
			double dMinutes=(double)minutesLeft;
			double dSeconds=(double)secondsLeft;
			double dMaxTicks=(double)51000;
			double dGameTicksPerSecond=((double)425)/((double)60);
			int elapsedTimeCounter =(int) Math.round(dMaxTicks-(((dMinutes*((double)60))+dSeconds)*dGameTicksPerSecond));
			int[] counterBits=getBits(elapsedTimeCounter,16);
			
			//Set the level bits
			grid.get(0)[3]=levelBits[0];
			grid.get(1)[3]=levelBits[1];
			grid.get(2)[3]=levelBits[2];
			grid.get(3)[3]=levelBits[3];
			grid.get(4)[3]=levelBits[4];
			
			//Set the health bits
			grid.get(0)[2]=healthBits[3];
			grid.get(1)[2]=healthBits[2];
			grid.get(2)[2]=healthBits[1];
			grid.get(3)[2]=healthBits[0];
			
			//Set the counter bits
			grid.get(0)[1]=counterBits[0];
			grid.get(1)[1]=counterBits[1];
			grid.get(2)[1]=counterBits[2];
			grid.get(3)[1]=counterBits[3];
			grid.get(3)[0]=counterBits[4];
			grid.get(2)[0]=counterBits[5];
			grid.get(1)[0]=counterBits[6];
			grid.get(0)[0]=counterBits[7];
			grid.get(4)[1]=counterBits[8];
			grid.get(4)[0]=counterBits[9];
			grid.get(5)[3]=counterBits[10];
			grid.get(5)[2]=counterBits[11];
			grid.get(5)[1]=counterBits[12];
			grid.get(5)[0]=counterBits[13];
			grid.get(6)[1]=counterBits[14];
			grid.get(6)[0]=counterBits[15];
			
			int addChecksum=0;
			for(int i=0;i<7;i++)
			{
				addChecksum+=bitsToInt(grid.get(i));
			}
			addChecksum&=0x1f;
			
			int[] addBits=getBits(addChecksum,5);
			
			//Set the add checksum bits
			grid.get(4)[4]=addBits[0];
			grid.get(5)[4]=addBits[1];
			grid.get(6)[4]=addBits[2];
			grid.get(6)[3]=addBits[3];
			grid.get(6)[2]=addBits[4];
			
			//Set the XOR checksum bits
			int x1=grid.get(0)[0];
			x1^=grid.get(0)[1];
			x1^=grid.get(1)[1];
			x1^=grid.get(1)[3];
			x1^=grid.get(2)[0];
			x1^=grid.get(2)[1];
			x1^=grid.get(2)[2];
			x1^=grid.get(3)[2];
			x1^=grid.get(4)[1];
			x1^=grid.get(4)[2];
			x1^=grid.get(4)[3];
			x1^=grid.get(5)[0];
			x1^=grid.get(5)[3];
			x1^=grid.get(6)[0];
			x1^=1;
			grid.get(1)[4]=x1;
			
			int x2=grid.get(0)[2];
			x2^=grid.get(1)[1];
			x2^=grid.get(1)[2];
			x2^=grid.get(1)[3];
			x2^=grid.get(2)[0];
			x2^=grid.get(2)[3];
			x2^=grid.get(3)[0];
			x2^=grid.get(3)[2];
			x2^=grid.get(3)[3];
			x2^=grid.get(4)[0];
			x2^=grid.get(4)[1];
			x2^=grid.get(5)[1];
			x2^=grid.get(5)[3];
			x2^=grid.get(6)[0];
			x2^=grid.get(6)[1];
			grid.get(2)[4]=x2;
			
			int x3=grid.get(0)[0];
			x3^=grid.get(0)[3];
			x3^=grid.get(1)[0];
			x3^=grid.get(1)[2];
			x3^=grid.get(1)[3];
			x3^=grid.get(2)[0];
			x3^=grid.get(2)[1];
			x3^=grid.get(3)[1];
			x3^=grid.get(3)[3];
			x3^=grid.get(4)[0];
			x3^=grid.get(4)[1];
			x3^=grid.get(4)[2];
			x3^=grid.get(5)[2];
			x3^=grid.get(6)[1];
			grid.get(3)[4]=x3;

			
			//Print passwords
			System.out.print("The EU/USA password is ");
			printPassword(euAlphabet);
			System.out.print("The Japan  password is ");
			printPassword(jpnAlphabet);
			System.out.println("\nGenerated bit grid:");
			printBitGrid();
		}
		catch(Exception ex)
		{
			System.out.println("Something went wrong: "+ex.getMessage());
			System.out.println("\n");
			printUsage();
		}
 	}
 	
 	public static void printBitGrid()
 	{
		//Print bit grid
		for(int i=0;i<grid.size();i++)
		{
			printBits(grid.get(i));
		}
 	}
	public static void printPassword(String alphabet)
	{
		for(int i=0;i<grid.size();i++)
		{
			int idx=bitsToInt(grid.get(i));
			System.out.print(alphabet.charAt(idx));
		}
		System.out.println("");
	}
	
	public static void analyze(String[] input)
	{
		try
		{
			String pass=input[1];
			String alphabet=euAlphabet;
			if(input.length>2)
			{
				String country=input[2].toUpperCase();
				if(country.equals("JPN"))
				{
					alphabet=jpnAlphabet;
				}
				else if(!country.equals("EU"))
				{
					System.out.println("Invalid country '"+country+"' given for analyze command, valid values are EU or JPN");
					return;
				}
			}
			if(!(pass.length()==7))
			{
				System.out.println("Invalid password length for analyze command, length must be 7!");
				return;
			}
			
			for(int i=0;i<pass.length();i++)
			{
				int idx=alphabet.indexOf(pass.charAt(i));
				if(idx<0)
				{
					System.out.println("Invalid character '"+pass.charAt(i)+"' in password!\nOnly valid characters are '"+alphabet+"'");
					return;
				}
				grid.set(i,getBits(idx,5));
			}
			System.out.println("Analyzed bit grid:\n");
			printBitGrid();
			System.out.println("");
			
			boolean bValid=true;
			int[] levelBits=new int[5];
			levelBits[0]=grid.get(0)[3];
			levelBits[1]=grid.get(1)[3];
			levelBits[2]=grid.get(2)[3];
			levelBits[3]=grid.get(3)[3];
			levelBits[4]=grid.get(4)[3];
			int level=bitsToInt(levelBits);
			if(level<0 || level>19)
			{
				System.out.println("Invalid level "+level+": valid value must be in range [0-19]");
				bValid=false;
			}
			else
			{
				System.out.println("Level is "+level+" (actual level in game is then "+(level+1)+")");
			}
			
			int[] healthBits=new int[4];
			healthBits[0]=grid.get(3)[2];
			healthBits[1]=grid.get(2)[2];
			healthBits[2]=grid.get(1)[2];
			healthBits[3]=grid.get(0)[2];
			
			int health=bitsToInt(healthBits);
			if(level<0 || health>15)
			{
				System.out.println("Invalid health "+health+": valid value must be in range [0-15]");
				bValid=false;
			}
			else
			{
				System.out.println("Health is "+health);
			}
			
			int[] counterBits=new int[16];
			
			counterBits[0]=grid.get(0)[1];
			counterBits[1]=grid.get(1)[1];
			counterBits[2]=grid.get(2)[1];
			counterBits[3]=grid.get(3)[1];
			counterBits[4]=grid.get(3)[0];
			counterBits[5]=grid.get(2)[0];
			counterBits[6]=grid.get(1)[0];
			counterBits[7]=grid.get(0)[0];
			counterBits[8]=grid.get(4)[1];
			counterBits[9]=grid.get(4)[0];
			counterBits[10]=grid.get(5)[3];
			counterBits[11]=grid.get(5)[2];
			counterBits[12]=grid.get(5)[1];
			counterBits[13]=grid.get(5)[0];
			counterBits[14]=grid.get(6)[1];
			counterBits[15]=grid.get(6)[0];
			
			int counter=bitsToInt(counterBits);
			System.out.println("Elapsed time counter is "+counter);
			double dGameTicksPerSecond=((double)425)/((double)60);
			counter=51000-counter;
			int remainingSeconds=(int)Math.round(((double)counter)/dGameTicksPerSecond);
			int remainingMinutes=remainingSeconds/60;
			remainingSeconds%=60;
			System.out.println("Remaining time: "+remainingMinutes+" minutes and "+remainingSeconds+" seconds (in game time)");
			
			System.out.println("x bit is "+grid.get(4)[2]);
			
			System.out.println("\n---------");
			System.out.println("Checksums");
			System.out.println("---------");
			
			//Validate the XOR checksum bits
			if(grid.get(0)[4]!=0)
			{
				System.out.println("XOR checksum bit 0 is invalid, must be 0, but it is "+grid.get(0)[4]);
				bValid=false;
			}
			
			int x1=grid.get(0)[0];
			x1^=grid.get(0)[1];
			x1^=grid.get(1)[1];
			x1^=grid.get(1)[3];
			x1^=grid.get(2)[0];
			x1^=grid.get(2)[1];
			x1^=grid.get(2)[2];
			x1^=grid.get(3)[2];
			x1^=grid.get(4)[1];
			x1^=grid.get(4)[2];
			x1^=grid.get(4)[3];
			x1^=grid.get(5)[0];
			x1^=grid.get(5)[3];
			x1^=grid.get(6)[0];
			x1^=1;
			if(grid.get(1)[4]!=x1)
			{
				System.out.println("XOR checksum bit 1 is invalid, must be "+x1+", but it is "+grid.get(1)[4]);
				bValid=false;
			}
			
			int x2=grid.get(0)[2];
			x2^=grid.get(1)[1];
			x2^=grid.get(1)[2];
			x2^=grid.get(1)[3];
			x2^=grid.get(2)[0];
			x2^=grid.get(2)[3];
			x2^=grid.get(3)[0];
			x2^=grid.get(3)[2];
			x2^=grid.get(3)[3];
			x2^=grid.get(4)[0];
			x2^=grid.get(4)[1];
			x2^=grid.get(5)[1];
			x2^=grid.get(5)[3];
			x2^=grid.get(6)[0];
			x2^=grid.get(6)[1];
			if(grid.get(2)[4]!=x2)
			{
				System.out.println("XOR checksum bit 2 is invalid, must be "+x2+", but it is "+grid.get(2)[4]);
				bValid=false;
			}
			
			int x3=grid.get(0)[0];
			x3^=grid.get(0)[3];
			x3^=grid.get(1)[0];
			x3^=grid.get(1)[2];
			x3^=grid.get(1)[3];
			x3^=grid.get(2)[0];
			x3^=grid.get(2)[1];
			x3^=grid.get(3)[1];
			x3^=grid.get(3)[3];
			x3^=grid.get(4)[0];
			x3^=grid.get(4)[1];
			x3^=grid.get(4)[2];
			x3^=grid.get(5)[2];
			x3^=grid.get(6)[1];
			if(grid.get(3)[4]!=x3)
			{
				System.out.println("XOR checksum bit 3 is invalid, must be "+x3+", but it is "+grid.get(3)[4]);
				bValid=false;
			}
			
			if(bValid)
			{
				System.out.println("XOR checksum is valid (binary "+x3+""+x2+""+x1+"0)");
			}
			
			//Validate the add checksum bits
			
			int[] addBits=new int[5];
			addBits[0]=grid.get(4)[4];
			addBits[1]=grid.get(5)[4];
			addBits[2]=grid.get(6)[4];
			addBits[3]=grid.get(6)[3];
			addBits[4]=grid.get(6)[2];
			
			int addSum=bitsToInt(addBits);
			
			//Clear checksum bits for validation
			grid.get(4)[4]=0;
			grid.get(5)[4]=0;
			grid.get(6)[4]=0;
			grid.get(6)[3]=0;
			grid.get(6)[2]=0;
			
			grid.get(0)[4]=0;
			grid.get(1)[4]=0;
			grid.get(2)[4]=0;
			grid.get(3)[4]=0;
			
			int addChecksumValid=0;
			for(int i=0;i<7;i++)
			{
				addChecksumValid+=bitsToInt(grid.get(i));
			}
			addChecksumValid&=0x1f;
			
			int[] addBitsValid=getBits(addChecksumValid,5);
			
			for(int i=0;i<addBits.length;i++)
			{
				if(addBits[i]!=addBitsValid[i])
				{
					System.out.println("ADD checksum bit "+i+" is invalid, must be "+addBitsValid[i]+", but it is "+addBits[i]);
					bValid=false;
				}
			}
			
			if(bValid)
			{
				System.out.println("ADD checksum is valid (binary "+addBits[4]+""+addBits[3]+""+addBits[2]+""+addBits[1]+""+addBits[0]+")");
			}
			
			
			System.out.println("\nPassword is "+(bValid?"VALID!":"INVALID :("));
				
		}
		catch(Exception ex)
		{
			System.out.println("Something went wrong: "+ex.getMessage());
			System.out.println("\n");
			printUsage();
		}
	}
 	
	public static int[] getBits(int num,int bits)
	{
 		int[] result=new int[bits];
 		for(int i=0;i<bits;i++)
 		{
 			result[i]=num&1;
 			num>>=1;
 		}
 		return result;
	}
 	
	public static int bitsToInt(int[] bits)
	{
 		int result=0;
 		for(int i=0;i<bits.length;i++)
 		{
 			result<<=1;
 			result|=bits[bits.length-i-1];
 		}
 		return result;
	}
 	
	public static void printBits(int[] bits)
	{
 		for(int i=0;i<bits.length;i++)
 		{
 			System.out.print(bits[bits.length-i-1]);
 		}
 		System.out.println("");
	}
	
	public static void printUsage()
	{
		System.out.println("SNES Prince of Persia password generator/analyzer\n\n"+
			"Input format:\n"+
			"<command> <param>...\n"+
			"Command=generate or analyze\n"+
			"generate parameters:\n"+
			" <level> <health> <time_left> [x bit]\n"+
			"  level     = [1-20]\n"+
			"  health    = [0-15]\n"+
			"  time left = time left in format minutes:seconds\n   Only minutes is also allowed.\n   Max=120 minutes\n   NOTE: Minutes are game minutes,\n   so what the game counter displays.\n"+
			"  x bit     = optional value, default is 0\n   when this parameter is set\n   the x bit will be set to 1,\n   no matter what value you enter.\n   The x bit was originally supposed to be used\n   to differentiate between starting at\n   the beginning of a level, or at a checkpoint.\n   The checkpoint mechanism does not seem to be implemented\n   in the game, but you can fiddle around\n   with it to see what happens.\n\n"+
			"analyze parameters:\n"+
			" <password> [country]\n"+
			"  password = password to analyze\n"+
			"  country  = optional value for which game version\n   the password was generated.\n   Valid values are EU or JPN.\n   When not given, EU is used.");
			
	}
}
David
The Prince of Persia
The Prince of Persia
Posts: 2846
Joined: December 11th, 2008, 9:48 pm
Location: Hungary

Re: Password analyzer/creator

Post by David »

I think I've found some logic in the alphabets used by PoP1 SNES passwords.

If you break them into rows of 9 characters, they will look like this:

Code: Select all

(JP)
A I Q Y E M U 3 4
B J R Z F N V W 5
C K S 1 G O P X 6
D L T 2 H

(US/EU)
B T L 3 G Y 7 Q 9
C V M 4 H Z 8 R +
D W N 5 J 1 2 S !
F X P 6 K
If you read these vertically, then the letters are mostly in alphabetic order.
Post Reply