Skip to content

Commit

Permalink
Implement title contents decryption
Browse files Browse the repository at this point in the history
For various reasons, this does not properly work when contents sizes are not multiples of 16 (read: the AES block size in use).
  • Loading branch information
spotlightishere committed Sep 20, 2020
1 parent 4f9e3d0 commit d3970c3
Show file tree
Hide file tree
Showing 2 changed files with 72 additions and 6 deletions.
69 changes: 66 additions & 3 deletions file.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,73 @@
package wadlib

import "crypto/sha1"

var sha = sha1.New()
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/sha1"
"encoding/binary"
"errors"
"fmt"
)

type WADFile struct {
ContentRecord
RawData []byte
}

func readData(data []byte, contents []ContentRecord, titleKey [16]byte) ([]WADFile, error) {
// Each content within the data section is aligned to a 0x40/64-byte boundary.
r := readable{
data: data,
}

// TODO: We naively assume that the index will be accurately indexed from 0.
// All observed Nintendo files follow this, but external tools may not follow this format.
// We should most likely apply max index validation and sort,
// otherwise data will read out of order in these cases.

// All data contents will be the same amount as the number of contents per TMD.
wads := make([]WADFile, len(contents))
for _, content := range contents {
// It's okay to cast this from a uint64 as the WAD file format
// cannot exceed the maximum uint32 value within the data section.
encryptedData := r.getRange(uint32(content.Size))

// The title's decrypted key will be what we'll decrypt with.
block, err := aes.NewCipher(titleKey[:])
if err != nil {
return nil, err
}

// The IV we'll use will be the two bytes sourced from the content's index,
// padded with 14 null bytes.
var indexBytes [2]byte
binary.BigEndian.PutUint16(indexBytes[:], content.Index)

iv := make([]byte, 16)
iv[0] = indexBytes[0]
iv[1] = indexBytes[1]

blockMode := cipher.NewCBCDecrypter(block, iv)

// The resulting decrypted contents is the same size as the input, including padding.
decryptedData := make([]byte, content.Size)

// ...and we're off!
blockMode.CryptBlocks(decryptedData, encryptedData)

// Ensure that the decrypted data matches the SHA-1 hash given in the contents list.
sha := sha1.Sum(decryptedData)
if bytes.Compare(sha[:], content.Hash[:]) != 0 {
return nil, errors.New(fmt.Sprintf("content %08x did not match the noted hash when decrypted", content.ID))
}

// We're all set!
wads[content.Index] = WADFile{
content,
decryptedData,
}
}

return wads, nil
}
9 changes: 6 additions & 3 deletions wad.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ type WAD struct {
CertificateRevocationList []byte
Ticket Ticket
TMD *TMD
RawData []byte
Data []WADFile
Meta []byte
}

Expand Down Expand Up @@ -133,7 +133,10 @@ func LoadWAD(contents []byte) (*WAD, error) {
}

// For each content, we want to separate the raw data.
data := r.getRange(header.DataSize)
data, err := readData(r.getRange(header.DataSize), tmd.Contents, ticket.TitleKey)
if err != nil {
return nil, err
}

// We're at the very end and can safely read to the very end of meta, ignoring subsequent data.
meta := r.getRange(header.MetaSize)
Expand All @@ -144,7 +147,7 @@ func LoadWAD(contents []byte) (*WAD, error) {
CertificateRevocationList: crl,
Ticket: ticket,
TMD: tmd,
RawData: data,
Data: data,
Meta: meta,
}, nil
}

0 comments on commit d3970c3

Please sign in to comment.