mirror of
https://github.com/martravi/wiiqt6.git
synced 2024-11-22 21:29:15 +01:00
7cbba0738e
* add more "known updates" to the NUS class. now your can specify "3.4u" or similar instead of a TID and it will try to download all the titles that would have been in that update. the lists of titles were taken from wiimpersonator logs and when those were missing, from disc update partitions. thanks to markhemus and rduke for helping make the lists * NUS tool will now make sure there is a valid setting.txt in the nand after it is done installing titles to it git-svn-id: http://wiiqt.googlecode.com/svn/trunk@7 389f4c8b-5dfe-645f-db0e-df882bc27289
489 lines
12 KiB
C++
489 lines
12 KiB
C++
#include "wad.h"
|
|
#include "tools.h"
|
|
#include "tiktmd.h"
|
|
|
|
static QByteArray globalCert;
|
|
|
|
Wad::Wad( const QByteArray stuff )
|
|
{
|
|
ok = false;
|
|
if( stuff.size() < 0x80 )//less than this and there is definitely nothing there
|
|
{
|
|
Err( "Size is < 0x80" );
|
|
return;
|
|
}
|
|
|
|
QByteArray copy = stuff;
|
|
QBuffer b( © );
|
|
b.open( QIODevice::ReadOnly );
|
|
|
|
quint32 tmp;
|
|
b.read( (char*)&tmp, 4 );
|
|
if( qFromBigEndian( tmp ) != 0x20 )
|
|
{
|
|
b.close();
|
|
Err( "Bad header size" );
|
|
return;
|
|
}
|
|
b.read( (char*)&tmp, 4 );
|
|
tmp = qFromBigEndian( tmp );
|
|
if( tmp != 0x49730000 && tmp != 0x69620000 && tmp != 0x426b0000 )
|
|
{
|
|
b.close();
|
|
hexdump( stuff, 0, 0x40 );
|
|
Err( "Bad file magic word" );
|
|
return;
|
|
}
|
|
|
|
quint32 certSize;
|
|
quint32 tikSize;
|
|
quint32 tmdSize;
|
|
quint32 appSize;
|
|
quint32 footerSize;
|
|
|
|
b.read( (char*)&certSize, 4 );
|
|
certSize = qFromBigEndian( certSize );
|
|
|
|
b.seek( 0x10 );
|
|
b.read( (char*)&tikSize, 4 );
|
|
tikSize = qFromBigEndian( tikSize );
|
|
b.read( (char*)&tmdSize, 4 );
|
|
tmdSize = qFromBigEndian( tmdSize );
|
|
b.read( (char*)&appSize, 4 );
|
|
appSize = qFromBigEndian( appSize );
|
|
b.read( (char*)&footerSize, 4 );
|
|
footerSize = qFromBigEndian( footerSize );
|
|
|
|
b.close();//close the buffer, the rest of the data can be checked without it
|
|
|
|
//sanity check this thing
|
|
quint32 s = stuff.size();
|
|
if( s < ( RU( 0x40, certSize ) + RU( 0x40, tikSize ) + RU( 0x40, tmdSize ) + RU( 0x40, appSize ) + RU( 0x40, footerSize ) ) )
|
|
{
|
|
Err( "Total size is less than the combined sizes of all the parts that it is supposed to contain" );
|
|
return;
|
|
}
|
|
|
|
quint32 pos = 0x40;
|
|
certData = stuff.mid( pos, certSize );
|
|
pos += RU( 0x40, certSize );
|
|
tikData = stuff.mid( pos, tikSize );
|
|
pos += RU( 0x40, tikSize );
|
|
tmdData = stuff.mid( pos, tmdSize );
|
|
pos += RU( 0x40, tmdSize );
|
|
|
|
Ticket ticket( tikData );
|
|
Tmd t( tmdData );
|
|
|
|
//hexdump( tikData );
|
|
//hexdump( tmdData );
|
|
|
|
if( ticket.Tid() != t.Tid() )
|
|
qWarning() << "wad contains 2 different TIDs";
|
|
|
|
quint32 cnt = t.Count();
|
|
//qDebug() << "Wad contains" << hex << cnt << "contents";
|
|
|
|
//another quick sanity check
|
|
quint32 totalSize = 0;
|
|
for( quint32 i = 0; i < cnt; i++ )
|
|
totalSize += t.Size( i );
|
|
|
|
if( totalSize > appSize )
|
|
{
|
|
Err( "Size of all the apps in the tmd is greater than the size in the wad header" );
|
|
return;
|
|
}
|
|
//read all the contents, check the hash, and remember the data ( still encrypted )
|
|
for( quint32 i = 0; i < cnt; i++ )
|
|
{
|
|
|
|
quint32 s = RU( 0x40, t.Size( i ) );
|
|
//qDebug() << "content" << i << "is at" << hex << pos << "with size" << s;
|
|
QByteArray encData = stuff.mid( pos, s );
|
|
pos += s;
|
|
|
|
//doing this here in case there is some other object that is using the AES that would change the key on us
|
|
AesSetKey( ticket.DecryptedKey() );
|
|
|
|
QByteArray decData = AesDecrypt( i, encData );
|
|
decData.resize( t.Size( i ) );
|
|
QByteArray realHash = GetSha1( decData );
|
|
if( realHash != t.Hash( i ) )
|
|
{
|
|
Err( QString( "hash doesnt match for content %1" ).arg( i ) );
|
|
}
|
|
partsEnc << encData;
|
|
}
|
|
//wtf? some VC titles have a full banner as the footer? maybe somebody's wad packer is busted
|
|
//QByteArray footer = stuff.mid( pos, stuff.size() - pos );
|
|
//qDebug() << "footer";
|
|
//hexdump( footer );
|
|
ok = true;
|
|
}
|
|
|
|
Wad::Wad( QList< QByteArray > stuff, bool encrypted )
|
|
{
|
|
if( stuff.size() < 3 )
|
|
{
|
|
Err( "Cant treate a wad with < 3 items" );
|
|
return;
|
|
}
|
|
|
|
tmdData = stuff.at( 0 );
|
|
tikData = stuff.at( 1 );
|
|
|
|
Ticket ticket( tikData );
|
|
Tmd t( tmdData );
|
|
|
|
quint16 cnt = stuff.size() - 2;
|
|
if( cnt != t.Count() )
|
|
{
|
|
Err( "The number of items given doesnt match the number in the tmd" );
|
|
return;
|
|
}
|
|
for( quint16 i = 0; i < cnt; i++ )
|
|
{
|
|
QByteArray encData;
|
|
|
|
if( encrypted )
|
|
{
|
|
encData = stuff.at( i + 2 );
|
|
}
|
|
else
|
|
{
|
|
QByteArray decDataPadded = PaddedByteArray( stuff.at( i + 2 ), 0x40 );
|
|
//doing this here in case there is some other object that is using the AES that would change the key on us
|
|
AesSetKey( ticket.DecryptedKey() );
|
|
encData = AesEncrypt( i, decDataPadded );
|
|
}
|
|
partsEnc << encData;
|
|
}
|
|
ok = true;
|
|
|
|
}
|
|
|
|
void Wad::SetCert( const QByteArray stuff )
|
|
{
|
|
certData = stuff;
|
|
}
|
|
|
|
quint64 Wad::Tid()
|
|
{
|
|
if( !tmdData.size() )
|
|
{
|
|
Err( "There is no data in the TMD" );
|
|
return 0;
|
|
}
|
|
Tmd t( tmdData );
|
|
return t.Tid();
|
|
}
|
|
|
|
void Wad::SetGlobalCert( const QByteArray &stuff )
|
|
{
|
|
globalCert = stuff;
|
|
}
|
|
|
|
const QByteArray Wad::Content( quint16 i )
|
|
{
|
|
if( tmdData.isEmpty() || tikData.isEmpty() )
|
|
{
|
|
Err( "Can't decryte data without a TMD and ticket" );
|
|
return QByteArray();
|
|
}
|
|
Ticket ticket( tikData );
|
|
Tmd t( tmdData );
|
|
if( partsEnc.size() != t.Count() || i >= partsEnc.size() )
|
|
{
|
|
Err( "I dont know whats going on some number is out of range and i dont like it" );
|
|
return QByteArray();
|
|
}
|
|
QByteArray encData = partsEnc.at( i );
|
|
|
|
AesSetKey( ticket.DecryptedKey() );
|
|
|
|
QByteArray decData = AesDecrypt( i, encData );
|
|
decData.resize( t.Size( i ) );
|
|
QByteArray realHash = GetSha1( decData );
|
|
if( realHash != t.Hash( i ) )
|
|
{
|
|
Err( QString( "hash doesnt match for content %1" ).arg( i ) );
|
|
return QByteArray();
|
|
}
|
|
return decData;
|
|
}
|
|
|
|
void Wad::Err( QString str )
|
|
{
|
|
ok = false;
|
|
errStr = str;
|
|
qWarning() << "Wad::Error" << str;
|
|
}
|
|
|
|
const QByteArray Wad::Data( quint32 magicWord, const QByteArray footer )
|
|
{
|
|
//qDebug() << "Wad::Data" << hex << magicWord << footer.size();
|
|
if( !partsEnc.size() || tmdData.isEmpty() || tikData.isEmpty() || ( certData.isEmpty() && globalCert.isEmpty() ) )
|
|
{
|
|
Err( "Dont have all the parts to make a wad" );
|
|
return QByteArray();
|
|
}
|
|
|
|
//do some brief checks to make sure that wad is good
|
|
Ticket ticket( tikData );
|
|
Tmd t( tmdData );
|
|
if( t.Tid() != ticket.Tid() )
|
|
{
|
|
Err( "Ticket and TMD have different TID" );
|
|
return QByteArray();
|
|
}
|
|
if( partsEnc.size() != t.Count() )
|
|
{
|
|
Err( "Dont have enough contents according to the TMD" );
|
|
return QByteArray();
|
|
}
|
|
|
|
//everything seems in order, try to make the thing
|
|
QByteArray cert = certData.isEmpty() ? globalCert : certData;
|
|
quint32 certSize = cert.size();
|
|
quint32 tikSize = ticket.SignedSize();
|
|
quint32 tmdSize = t.SignedSize();
|
|
quint32 appSize = 0;
|
|
quint32 footerSize = footer.size();
|
|
|
|
//add all the app sizes together and check that they match the TMD
|
|
quint16 cnt = t.Count();
|
|
for( quint16 i = 0; i < cnt; i++ )
|
|
{
|
|
quint32 s = RU( 0x40, partsEnc.at( i ).size() );
|
|
if( RU( 0x40, t.Size( i ) ) != s )
|
|
{
|
|
Err( QString( "Size of content %1 is bad ( %2, %3, %4 )" ).arg( i )
|
|
.arg( t.Size( i ), 0, 16 )
|
|
.arg( RU( 0x40, t.Size( i ) ), 0, 16 )
|
|
.arg( s, 0, 16 ) );
|
|
return QByteArray();
|
|
}
|
|
appSize += s;
|
|
}
|
|
|
|
QByteArray header( 0x20, '\0' );
|
|
QBuffer buf( &header );
|
|
buf.open( QIODevice::WriteOnly );
|
|
|
|
quint32 tmp = qFromBigEndian( 0x20 );//header size
|
|
buf.write( (const char*)&tmp, 4 );
|
|
tmp = qFromBigEndian( magicWord );//magic word
|
|
buf.write( (const char*)&tmp, 4 );
|
|
tmp = qFromBigEndian( certSize );
|
|
buf.write( (const char*)&tmp, 4 );
|
|
buf.seek( 0x10 );
|
|
tmp = qFromBigEndian( tikSize );
|
|
buf.write( (const char*)&tmp, 4 );
|
|
tmp = qFromBigEndian( tmdSize );
|
|
buf.write( (const char*)&tmp, 4 );
|
|
tmp = qFromBigEndian( appSize );
|
|
buf.write( (const char*)&tmp, 4 );
|
|
tmp = qFromBigEndian( footerSize );
|
|
buf.write( (const char*)&tmp, 4 );
|
|
buf.close();
|
|
//hexdump( header, 0, 0x20 );
|
|
|
|
QByteArray ret = PaddedByteArray( header, 0x40 ) + PaddedByteArray( cert, 0x40 );
|
|
//make sure we dont have the huge ticket and TMD that come when downloading from NUS
|
|
QByteArray tik = tikData;
|
|
tik.resize( tikSize );
|
|
tik = PaddedByteArray( tik, 0x40 );
|
|
|
|
QByteArray tm = tmdData;
|
|
tm.resize( tmdSize );
|
|
tm = PaddedByteArray( tm, 0x40 );
|
|
|
|
//hexdump( tik );
|
|
//hexdump( tm );
|
|
|
|
ret += tik + tm;
|
|
for( quint16 i = 0; i < cnt; i++ )
|
|
{
|
|
ret += PaddedByteArray( partsEnc.at( i ), 0x40 );
|
|
}
|
|
ret += footer;
|
|
return ret;
|
|
}
|
|
|
|
QString Wad::WadName( QString path )
|
|
{
|
|
if( !tmdData.size() )
|
|
{
|
|
Err( "There is no data in the TMD" );
|
|
return QString();
|
|
}
|
|
Tmd t( tmdData );
|
|
return WadName( t.Tid(), t.Version(), path );
|
|
}
|
|
|
|
QString Wad::WadName( quint64 tid, quint16 version, QString path )
|
|
{
|
|
quint32 type = (quint32)( ( tid >> 32 ) & 0xffffffff );
|
|
quint32 base = (quint32)( tid & 0xffffffff );
|
|
quint8 reg = (quint8)( tid & 0xff );
|
|
quint32 regionFree = ( ( base >> 8 ) & 0xffffff ) << 8;
|
|
QString region;
|
|
switch( reg )
|
|
{
|
|
case 0x45:
|
|
region = "US";
|
|
break;
|
|
case 0x50:
|
|
region = "EU";
|
|
break;
|
|
case 0x4A:
|
|
region = "JP";
|
|
break;
|
|
case 0x4B:
|
|
region = "KO";//is this correct?? i have no korean games
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
QString name;
|
|
switch( type )
|
|
{
|
|
case 1:
|
|
switch( base )
|
|
{
|
|
case 1:
|
|
name = QString( "BOOT2-v%1-64" ).arg( version );
|
|
break;
|
|
case 2:
|
|
name = QString( "RVL-WiiSystemmenu-v%1" ).arg( version );
|
|
break;
|
|
case 0x100:
|
|
name = QString( "RVL-bc-v%1" ).arg( version );
|
|
break;
|
|
case 0x101:
|
|
name = QString( "RVL-mios-v%1" ).arg( version );
|
|
break;
|
|
default:
|
|
if( base > 0xff )
|
|
break;
|
|
name = QString( "IOS%1-64-v%2" ).arg( base ).arg( version );
|
|
break;
|
|
}
|
|
break;
|
|
case 0x10002:
|
|
switch( base )
|
|
{
|
|
case 0x48414141://HAAA
|
|
name = QString( "RVL-photo-v%1" ).arg( version );
|
|
break;
|
|
case 0x48415941://HAYA
|
|
name = QString( "RVL-photo2-v%1" ).arg( version );
|
|
break;
|
|
case 0x48414241://HABA
|
|
name = QString( "RVL-Shopping-v%1" ).arg( version );
|
|
break;
|
|
case 0x48414341://HACA
|
|
name = QString( "RVL-NigaoeNR-v%1" ).arg( version );//mii channel
|
|
break;
|
|
case 0x48414741://HAGA
|
|
name = QString( "RVL-News-v%1" ).arg( version );
|
|
break;
|
|
case 0x48414641://HAFA
|
|
name = QString( "RVL-Weather-v%1" ).arg( version );
|
|
break;
|
|
default:
|
|
switch( regionFree )
|
|
{
|
|
case 0x48414600://HAF?
|
|
name = QString( "RVL-Forecast_%1-v%2" ).arg( region ).arg( version );
|
|
break;
|
|
case 0x48414700://HAG?
|
|
name = QString( "RVL-News_%1-v%2" ).arg( region ).arg( version );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case 0x10008:
|
|
switch( regionFree )
|
|
{
|
|
case 0x48414b00://HAK?
|
|
name = QString( "RVL-Eulav_%1-v%2" ).arg( region ).arg( version );
|
|
break;
|
|
case 0x48414c00://HAL?
|
|
name = QString( "RVL-Rgnsel_%1-v%2" ).arg( region ).arg( version );
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
if( name.isEmpty() )
|
|
return QString();
|
|
|
|
if( path.isEmpty() )
|
|
return name + ".wad.out.wad";
|
|
|
|
QString ret = name + ".wad.out.wad";
|
|
int i = 1;
|
|
while( QFile::exists( path + "/" + ret ) )
|
|
{
|
|
ret = name + QString( "(copy %1)" ).arg( i ) + ".wad.out.wad";
|
|
i++;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
QByteArray Wad::FromDirectory( QDir dir )
|
|
{
|
|
QFileInfoList tmds = dir.entryInfoList( QStringList() << "*.tmd", QDir::Files );
|
|
if( tmds.isEmpty() )
|
|
{
|
|
qWarning() << "Wad::FromDirectory -> no tmd found in" << dir.absolutePath();
|
|
return QByteArray();
|
|
}
|
|
QByteArray tmdD = ReadFile( tmds.at( 0 ).absoluteFilePath() );
|
|
if( tmdD.isEmpty() )
|
|
return QByteArray();
|
|
QFileInfoList tiks = dir.entryInfoList( QStringList() << "*.tik", QDir::Files );
|
|
if( tiks.isEmpty() )
|
|
{
|
|
qWarning() << "Wad::FromDirectory -> no tik found in" << dir.absolutePath();
|
|
return QByteArray();
|
|
}
|
|
QByteArray tikD = ReadFile( tiks.at( 0 ).absoluteFilePath() );
|
|
if( tikD.isEmpty() )
|
|
return QByteArray();
|
|
Tmd t(tmdD );
|
|
QList<QByteArray> datas = QList<QByteArray>()<< tmdD << tikD;
|
|
|
|
quint16 cnt = t.Count();
|
|
for( quint16 i = 0; i < cnt; i++ )
|
|
{
|
|
QByteArray appD = ReadFile( dir.absoluteFilePath( t.Cid( i ) + ".app" ) );
|
|
if( appD.isEmpty() )
|
|
return QByteArray();
|
|
|
|
datas << appD;
|
|
}
|
|
Wad wad( datas, false );
|
|
if( !wad.IsOk() )
|
|
return QByteArray();
|
|
|
|
QFileInfoList certs = dir.entryInfoList( QStringList() << "*.cert", QDir::Files );
|
|
if( !certs.isEmpty() )
|
|
{
|
|
QByteArray certD = ReadFile( certs.at( 0 ).absoluteFilePath() );
|
|
if( !certD.isEmpty() )
|
|
wad.SetCert( certD );
|
|
}
|
|
|
|
QByteArray ret = wad.Data();
|
|
return ret;
|
|
}
|