WiiUDownloader/decryption.go
2025-04-19 20:14:24 +02:00

639 lines
16 KiB
Go

package wiiudownloader
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha1"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
)
var commonKey = []byte{0xD7, 0xB0, 0x04, 0x02, 0x65, 0x9B, 0xA2, 0xAB, 0xD2, 0xCB, 0x0D, 0xB2, 0x7F, 0xA2, 0xB6, 0x56}
var (
encryptedHashedContentBuffer = make([]byte, BLOCK_SIZE_HASHED)
decryptedHashedContentBuffer = make([]byte, BLOCK_SIZE_HASHED)
decryptedDataBuffer = make([]byte, 0xFC00)
)
var (
encryptedContentBuffer = make([]byte, BLOCK_SIZE)
decryptedContentBuffer = make([]byte, BLOCK_SIZE)
)
var (
iv = make([]byte, aes.BlockSize)
hashes = make([]byte, HASHES_SIZE)
hashesBuffer = make([]byte, HASHES_SIZE)
)
const (
BLOCK_SIZE = 0x8000
BLOCK_SIZE_HASHED = 0x10000
HASH_BLOCK_SIZE = 0xFC00
HASHES_SIZE = 0x0400
MAX_LEVELS = 0x10
)
const READ_SIZE = 8 * 1024 * 1024
var readSizedBuffer = make([]byte, READ_SIZE)
type Content struct {
ID uint32
Index []byte
Type uint16
Size uint64
Hash []byte
CIDStr string
}
type FEntry struct {
Type byte // 0 = file, 1 = directory
NameOffset uint32 // 3 bytes
Offset uint32 // 4 bytes
Length uint32 // 4 bytes
Flags uint16 // 2 bytes
ContentID uint16 // 2 bytes
}
type FSTData struct {
FSTReader *bytes.Reader
EntryCount uint32
Entries uint32
NamesOffset uint32
FSTEntries []FEntry
}
func extractFileHash(src *os.File, partDataOffset uint64, fileOffset uint64, size uint64, path string, contentId uint16, cipherHashTree cipher.Block) error {
writeSize := HASH_BLOCK_SIZE
blockNumber := (fileOffset / HASH_BLOCK_SIZE) & 0x0F
dst, err := os.Create(path)
if err != nil {
return fmt.Errorf("could not create '%s': %w", path, err)
}
defer dst.Close()
roffset := fileOffset / HASH_BLOCK_SIZE * BLOCK_SIZE_HASHED
soffset := fileOffset - (fileOffset / HASH_BLOCK_SIZE * HASH_BLOCK_SIZE)
if soffset+size > uint64(writeSize) {
writeSize = writeSize - int(soffset)
}
_, err = src.Seek(int64(partDataOffset+roffset), io.SeekStart)
if err != nil {
return err
}
for size > 0 {
if uint64(writeSize) > size {
writeSize = int(size)
}
if _, err := io.ReadFull(src, encryptedHashedContentBuffer); err != nil {
return fmt.Errorf("could not read %d bytes from '%s': %w", BLOCK_SIZE_HASHED, path, err)
}
clear(hashes)
clear(iv)
iv[1] = byte(contentId)
cipher.NewCBCDecrypter(cipherHashTree, iv).CryptBlocks(hashes, encryptedHashedContentBuffer[:HASHES_SIZE])
h0Hash := hashes[0x14*blockNumber : 0x14*blockNumber+sha1.Size]
iv = hashes[0x14*blockNumber : 0x14*blockNumber+aes.BlockSize]
if blockNumber == 0 {
iv[1] ^= byte(contentId)
}
cipher.NewCBCDecrypter(cipherHashTree, iv).CryptBlocks(decryptedHashedContentBuffer, encryptedHashedContentBuffer[HASHES_SIZE:])
hash := sha1.Sum(decryptedHashedContentBuffer[:HASH_BLOCK_SIZE])
if !bytes.Equal(hash[:], h0Hash) {
return errors.New("h0 hash mismatch")
}
size -= uint64(writeSize)
_, err = dst.Write(decryptedHashedContentBuffer[soffset : soffset+uint64(writeSize)])
if err != nil {
return err
}
blockNumber++
if blockNumber >= 16 {
blockNumber = 0
}
if soffset != 0 {
writeSize = HASH_BLOCK_SIZE
soffset = 0
}
}
return nil
}
func extractFile(src *os.File, partDataOffset uint64, fileOffset uint64, size uint64, path string, contentId uint16, cipherHashTree cipher.Block) error {
writeSize := BLOCK_SIZE
dst, err := os.Create(path)
if err != nil {
return fmt.Errorf("could not create '%s': %w", path, err)
}
defer dst.Close()
roffset := fileOffset / BLOCK_SIZE * BLOCK_SIZE
soffset := fileOffset - (fileOffset / BLOCK_SIZE * BLOCK_SIZE)
if soffset+size > uint64(writeSize) {
writeSize = writeSize - int(soffset)
}
_, err = src.Seek(int64(partDataOffset+roffset), io.SeekStart)
if err != nil {
return err
}
clear(iv)
iv[1] = byte(contentId)
aesCipher := cipher.NewCBCDecrypter(cipherHashTree, iv)
for size > 0 {
if uint64(writeSize) > size {
writeSize = int(size)
}
if n, err := io.ReadFull(src, encryptedContentBuffer); err != nil && n != BLOCK_SIZE {
return fmt.Errorf("could not read %d bytes from '%s': %w", BLOCK_SIZE, path, err)
}
aesCipher.CryptBlocks(decryptedContentBuffer, encryptedContentBuffer)
n, err := dst.Write(decryptedContentBuffer[soffset : soffset+uint64(writeSize)])
if err != nil {
return err
}
size -= uint64(n)
if soffset != 0 {
writeSize = BLOCK_SIZE
soffset = 0
}
}
return nil
}
func (fst *FSTData) Parse() error {
var err error // Hack to avoid shadowing
if _, err := fst.FSTReader.Seek(0x8, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek to entry count: %w", err)
}
if fst.EntryCount, err = readInt(fst.FSTReader, 4); err != nil {
return fmt.Errorf("failed to read entry count: %w", err)
}
if _, err := fst.FSTReader.Seek(int64(0x20+fst.EntryCount*0x20+8), io.SeekStart); err != nil {
return fmt.Errorf("failed to seek to entries: %w", err)
}
if fst.Entries, err = readInt(fst.FSTReader, 4); err != nil {
return fmt.Errorf("failed to read entries: %w", err)
}
fst.NamesOffset = 0x20 + fst.EntryCount*0x20 + fst.Entries*0x10
if _, err := fst.FSTReader.Seek(4, io.SeekCurrent); err != nil {
return fmt.Errorf("failed to seek to names offset: %w", err)
}
for i := uint32(0); i < fst.Entries; i++ {
entry := FEntry{}
if entry.Type, err = readByte(fst.FSTReader); err != nil {
return fmt.Errorf("failed to read entry type for entry %d: %w", i, err)
}
nameOffset, err := read3BytesBE(fst.FSTReader)
if err != nil {
return fmt.Errorf("failed to read name offset for entry %d: %w", i, err)
}
entry.NameOffset = uint32(nameOffset)
if entry.Offset, err = readInt(fst.FSTReader, 4); err != nil {
return fmt.Errorf("failed to read entry offset for entry %d: %w", i, err)
}
if entry.Length, err = readInt(fst.FSTReader, 4); err != nil {
return fmt.Errorf("failed to read entry length for entry %d: %w", i, err)
}
flags, err := readInt16(fst.FSTReader, 2)
if err != nil {
return fmt.Errorf("failed to read flags for entry %d: %w", i, err)
}
entry.Flags = flags
contentID, err := readInt16(fst.FSTReader, 2)
if err != nil {
return fmt.Errorf("failed to read content ID for entry %d: %w", i, err)
}
entry.ContentID = contentID
fst.FSTEntries = append(fst.FSTEntries, entry)
}
return nil
}
func readByte(f io.ReadSeeker) (byte, error) {
buf := make([]byte, 1)
n, err := f.Read(buf)
if err != nil {
return 0, err
}
if n < 1 {
return 0, io.ErrUnexpectedEOF
}
return buf[0], nil
}
func readInt(f io.ReadSeeker, s int) (uint32, error) {
buf := make([]byte, 4) // Buffer size is always 4 for uint32
n, err := f.Read(buf[:s])
if err != nil {
return 0, err
}
if n < s {
// If we didn't read the expected number of bytes, seek back to the
// previous position in the file and return an error.
if _, err := f.Seek(int64(-n), io.SeekCurrent); err != nil {
return 0, err
}
return 0, io.ErrUnexpectedEOF
}
return binary.BigEndian.Uint32(buf), nil
}
func readInt16(f io.ReadSeeker, s int) (uint16, error) {
buf := make([]byte, 2) // Buffer size is always 2 for uint16
n, err := f.Read(buf[:s])
if err != nil {
return 0, err
}
if n < s {
// If we didn't read the expected number of bytes, seek back to the
// previous position in the file and return an error.
if _, err := f.Seek(int64(-n), io.SeekCurrent); err != nil {
return 0, err
}
return 0, io.ErrUnexpectedEOF
}
return binary.BigEndian.Uint16(buf), nil
}
func readString(f io.ReadSeeker) (string, error) {
buffer := bytes.NewBuffer(nil)
chunk := make([]byte, 64) // Read in chunks of 64 bytes
for {
n, err := f.Read(chunk)
if err != nil && err != io.EOF {
return "", err
}
if n == 0 {
break
}
// Look for null terminator in this chunk
nullIndex := bytes.IndexByte(chunk[:n], 0)
if nullIndex != -1 {
// Found the null terminator
buffer.Write(chunk[:nullIndex])
// Seek back to position right after the null terminator
_, err = f.Seek(int64(-(n - nullIndex - 1)), io.SeekCurrent)
if err != nil {
return "", err
}
return buffer.String(), nil
}
// No null terminator found, append the whole chunk
buffer.Write(chunk[:n])
if err == io.EOF {
break
}
}
// If we get here without finding a null terminator
if buffer.Len() == 0 {
return "", io.EOF
}
return buffer.String(), nil
}
func read3BytesBE(f io.ReadSeeker) (uint32, error) {
b := make([]byte, 3)
if _, err := f.Read(b); err != nil {
return 0, err
}
return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16, nil
}
func decryptContentToBuffer(encryptedFile *os.File, decryptedBuffer *bytes.Buffer, cipherHashTree cipher.Block, content Content) error {
hasHashTree := content.Type&2 != 0
encryptedStat, err := encryptedFile.Stat()
if err != nil {
return err
}
encryptedSize := encryptedStat.Size()
path := filepath.Dir(encryptedFile.Name())
if hasHashTree { // if has a hash tree
chunkCount := encryptedSize / 0x10000
h3Data, err := os.ReadFile(filepath.Join(path, fmt.Sprintf("%s.h3", content.CIDStr)))
if err != nil {
return err
}
h3BytesSHASum := sha1.Sum(h3Data)
if hex.EncodeToString(h3BytesSHASum[:]) != hex.EncodeToString(content.Hash) {
return errors.New("H3 Hash mismatch")
}
h0HashNum := int64(0)
h1HashNum := int64(0)
h2HashNum := int64(0)
h3HashNum := int64(0)
clear(hashes)
clear(hashesBuffer)
for chunkNum := int64(0); chunkNum < chunkCount; chunkNum++ {
encryptedFile.Read(hashesBuffer)
clear(iv)
cipher.NewCBCDecrypter(cipherHashTree, iv).CryptBlocks(hashes, hashesBuffer)
h0Hashes := hashes[0:0x140]
h1Hashes := hashes[0x140:0x280]
h2Hashes := hashes[0x280:0x3c0]
h0Hash := h0Hashes[(h0HashNum * 0x14):((h0HashNum + 1) * 0x14)]
h1Hash := h1Hashes[(h1HashNum * 0x14):((h1HashNum + 1) * 0x14)]
h2Hash := h2Hashes[(h2HashNum * 0x14):((h2HashNum + 1) * 0x14)]
h3Hash := h3Data[(h3HashNum * 0x14):((h3HashNum + 1) * 0x14)]
h0HashesHash := sha1.Sum(h0Hashes)
h1HashesHash := sha1.Sum(h1Hashes)
h2HashesHash := sha1.Sum(h2Hashes)
if !bytes.Equal(h0HashesHash[:], h1Hash) {
return errors.New("h0 Hashes Hash mismatch")
}
if !bytes.Equal(h1HashesHash[:], h2Hash) {
return errors.New("h1 Hashes Hash mismatch")
}
if !bytes.Equal(h2HashesHash[:], h3Hash) {
return errors.New("h2 Hashes Hash mismatch")
}
encryptedFile.Read(decryptedDataBuffer)
cipher.NewCBCDecrypter(cipherHashTree, h0Hash[:16]).CryptBlocks(decryptedDataBuffer, decryptedDataBuffer)
decryptedDataHash := sha1.Sum(decryptedDataBuffer)
if !bytes.Equal(decryptedDataHash[:], h0Hash) {
return errors.New("data block hash invalid")
}
_, err = decryptedBuffer.Write(hashes)
if err != nil {
return err
}
_, err = decryptedBuffer.Write(decryptedDataBuffer)
if err != nil {
return err
}
h0HashNum++
if h0HashNum >= 16 {
h0HashNum = 0
h1HashNum++
}
if h1HashNum >= 16 {
h1HashNum = 0
h2HashNum++
}
if h2HashNum >= 16 {
h2HashNum = 0
h3HashNum++
}
}
} else {
cipherContent := cipher.NewCBCDecrypter(cipherHashTree, append(content.Index, make([]byte, 14)...))
contentHash := sha1.New()
left := content.Size
leftHash := content.Size
for i := 0; i <= int(content.Size/READ_SIZE)+1; i++ {
toRead := min(READ_SIZE, left)
toReadHash := min(READ_SIZE, leftHash)
_, err = io.ReadFull(encryptedFile, readSizedBuffer[:toRead])
if err != nil {
return err
}
decryptedContent := make([]byte, toRead)
cipherContent.CryptBlocks(decryptedContent, readSizedBuffer[:toRead])
contentHash.Write(decryptedContent[:toReadHash])
_, err = decryptedBuffer.Write(decryptedContent)
if err != nil {
return err
}
left -= toRead
leftHash -= toRead
if left == 0 {
break
}
}
if !bytes.Equal(content.Hash[:sha1.Size], contentHash.Sum(nil)) {
return errors.New("content hash mismatch")
}
}
return nil
}
func DecryptContents(path string, progressReporter ProgressReporter, deleteEncryptedContents bool) error {
tmdPath := filepath.Join(path, "title.tmd")
if _, err := os.Stat(tmdPath); os.IsNotExist(err) {
return err
}
tmdData, err := os.ReadFile(tmdPath)
if err != nil {
return err
}
tmd, err := ParseTMD(tmdData)
if err != nil {
return err
}
// Check if all contents are present and how they are named
for i := range tmd.Contents {
tmd.Contents[i].CIDStr = fmt.Sprintf("%08X", tmd.Contents[i].ID)
_, err := os.Stat(filepath.Join(path, tmd.Contents[i].CIDStr+".app"))
if err != nil {
tmd.Contents[i].CIDStr = fmt.Sprintf("%08x", tmd.Contents[i].ID)
_, err = os.Stat(filepath.Join(path, tmd.Contents[i].CIDStr+".app"))
if err != nil {
return errors.New("content not found")
}
}
}
// Find the encrypted titlekey
var encryptedTitleKey []byte
ticketPath := filepath.Join(path, "title.tik")
if _, err := os.Stat(ticketPath); err == nil {
cetk, err := os.Open(ticketPath)
if err == nil {
cetk.Seek(0x1BF, 0)
encryptedTitleKey = make([]byte, 0x10)
cetk.Read(encryptedTitleKey)
if err := cetk.Close(); err != nil {
return err
}
}
}
c, err := aes.NewCipher(commonKey)
if err != nil {
return err
}
titleIDBytes := make([]byte, 8)
binary.BigEndian.PutUint64(titleIDBytes, tmd.TitleID)
cbc := cipher.NewCBCDecrypter(c, append(titleIDBytes, make([]byte, 8)...))
decryptedTitleKey := make([]byte, len(encryptedTitleKey))
cbc.CryptBlocks(decryptedTitleKey, encryptedTitleKey)
cipherHashTree, err := aes.NewCipher(decryptedTitleKey)
if err != nil {
return fmt.Errorf("failed to create AES cipher: %w", err)
}
fstEncFile, err := os.Open(filepath.Join(path, tmd.Contents[0].CIDStr+".app"))
if err != nil {
return err
}
decryptedBuffer := bytes.Buffer{}
if err := decryptContentToBuffer(fstEncFile, &decryptedBuffer, cipherHashTree, tmd.Contents[0]); err != nil {
if err := fstEncFile.Close(); err != nil {
return err
}
return err
}
if err := fstEncFile.Close(); err != nil {
return err
}
fst := FSTData{FSTReader: bytes.NewReader(decryptedBuffer.Bytes()), FSTEntries: make([]FEntry, 0), EntryCount: 0, Entries: 0, NamesOffset: 0}
if err := fst.Parse(); err != nil {
return err
}
outputPath := path
entry := make([]uint32, 0x10)
lEntry := make([]uint32, 0x10)
level := uint32(0)
for i := uint32(0); i < fst.Entries-1; i++ {
progressReporter.UpdateDecryptionProgress(float64(i) / float64(fst.Entries-1))
if level > 0 {
for (level >= 1) && (lEntry[level-1] == i+1) {
level--
}
}
if fst.FSTEntries[i].Type&1 != 0 {
entry[level] = i
lEntry[level] = fst.FSTEntries[i].Length
level++
if level >= MAX_LEVELS {
return errors.New("level >= MAX_LEVELS")
}
} else {
pathOffset := uint32(0)
outputPath = path
for j := uint32(0); j < level; j++ {
pathOffset = fst.FSTEntries[entry[j]].NameOffset & 0x00FFFFFF
fst.FSTReader.Seek(int64(fst.NamesOffset+pathOffset), io.SeekStart)
directory, err := readString(fst.FSTReader)
if err != nil {
return fmt.Errorf("failed to read directory name: %w", err)
}
outputPath = filepath.Join(outputPath, directory)
os.MkdirAll(outputPath, 0755)
}
pathOffset = fst.FSTEntries[i].NameOffset & 0x00FFFFFF
fst.FSTReader.Seek(int64(fst.NamesOffset+pathOffset), io.SeekStart)
fileName, err := readString(fst.FSTReader)
if err != nil {
return fmt.Errorf("failed to read file name: %w", err)
}
outputPath = filepath.Clean(filepath.Join(outputPath, fileName))
contentOffset := uint64(fst.FSTEntries[i].Offset)
if fst.FSTEntries[i].Flags&4 == 0 {
contentOffset <<= 5
}
if fst.FSTEntries[i].Type&0x80 == 0 {
matchingContent := tmd.Contents[fst.FSTEntries[i].ContentID]
tmdFlags := matchingContent.Type
srcFile, err := os.Open(filepath.Join(path, matchingContent.CIDStr+".app"))
if err != nil {
return err
}
if tmdFlags&0x02 != 0 {
err = extractFileHash(srcFile, 0, contentOffset, uint64(fst.FSTEntries[i].Length), outputPath, fst.FSTEntries[i].ContentID, cipherHashTree)
} else {
err = extractFile(srcFile, 0, contentOffset, uint64(fst.FSTEntries[i].Length), outputPath, fst.FSTEntries[i].ContentID, cipherHashTree)
}
srcFile.Close()
if err != nil {
return err
}
}
}
}
progressReporter.UpdateDecryptionProgress(1.0)
if deleteEncryptedContents {
doDeleteEncryptedContents(path)
}
return nil
}