Skip to content

Commit

Permalink
Added an option to decrypt title files
Browse files Browse the repository at this point in the history
Hacky implementation, but should work
  • Loading branch information
Disquse committed Sep 17, 2024
1 parent addfc55 commit fc3baee
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 34 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ This tool was created only for educational and data mining purposes.
Download and install [Go](https://go.dev) (1.18+) toolchain.

```powershell
# Clone and build.
git clone "https://github.com/Disquse/RGLExtractor"
cd RGLExtractor
go build
# Extract Launcher's RPF content.
.\RGLExtractor.exe --rgl "C:\Program Files\Rockstar Games\Launcher" --out "C:\Launcher_rpf"
# Decrypt title.rgl files (recursively).
.\RGLExtractor.exe --titles "C:\Launcher_rpf" --out "C:\titles_rgl"
```

## Thanks
Expand Down
127 changes: 113 additions & 14 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,60 @@ import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
)

const (
cmdInvalid = 0
cmdExtractLauncher = 1
cmdDecryptTitles = 2
)

type cliParams struct {
rglPath string
outPath string
cmdType int
rglPath string
outPath string
titlesPath string
}

const (
helpCommand = "`.\\RGLExtractor.exe --rgl \"C:\\Program Files\\Rockstar Games\\Launcher\" --out \"C:\\Launcher_rpf\"`"
helpCommand = "`.\\RGLExtractor.exe --rgl \"C:\\Program Files\\Rockstar Games\\Launcher\" --out \"C:\\Launcher_rpf\"`" +
"\nor\n`.\\RGLExtractor.exe --titles \"C:\\Launcher_rpf\" --out \"C:\\titles_rgl\"`"
)

func parseParams() *cliParams {
rglPath := flag.String("rgl", "", "Path to root folder of RGL installation")
outPath := flag.String("out", "", "Path to output folder for extraction")
titlesPath := flag.String("titles", "", "Path to folder with title.rgl files to decrypt")

flag.Parse()

if *rglPath == "" {
fmt.Printf("You need to specify launcher path. Example: %s\n", helpCommand)
return nil
}
cmdType := cmdInvalid

if *outPath == "" {
fmt.Printf("You need to specify output path. Example: %s\n", helpCommand)
if *titlesPath != "" {
cmdType = cmdDecryptTitles

pathStat, err := os.Stat(*titlesPath)
if err == os.ErrNotExist || !pathStat.IsDir() {
fmt.Printf("Invalid titles path: \"%s\"\n", *rglPath)
return nil
}
} else if *rglPath != "" {
cmdType = cmdExtractLauncher

pathStat, err := os.Stat(*rglPath)
if err == os.ErrNotExist || !pathStat.IsDir() {
fmt.Printf("Invalid launcher path: \"%s\"\n", *rglPath)
return nil
}
} else {
fmt.Printf("You need to specify more arguments. Example:\n%s\n", helpCommand)
return nil
}

rglPathStat, err := os.Stat(*rglPath)
if err == os.ErrNotExist || !rglPathStat.IsDir() {
fmt.Printf("Invalid launcher path: \"%s\"\n", *rglPath)
if *outPath == "" {
fmt.Printf("You need to specify output path. Example:\n%s\n", helpCommand)
return nil
}

Expand All @@ -51,7 +75,82 @@ func parseParams() *cliParams {
}

return &cliParams{
rglPath: *rglPath,
outPath: *outPath,
cmdType: cmdType,
rglPath: *rglPath,
outPath: *outPath,
titlesPath: *titlesPath,
}
}

func extractLauncher(params *cliParams) error {
rgl, err := LoadLauncher(params.rglPath)

if err != nil {
return err
}

logFunc := func(log string) {
fmt.Println(log)
}

for _, packFile := range rgl.Files {
err = packFile.extractPackFile(params.outPath, logFunc)

if err != nil {
return err
}
}

fmt.Printf("Done! Extracted into %s\n", params.outPath)
return nil
}

func decryptTitles(params *cliParams) error {
decryptFile := func(filePath string) error {
title, err := ReadTitleFromFile(filePath)
if err != nil {
return err
}

pathParts := strings.Split(filePath, "\\")
outPath := filepath.Join(params.outPath, pathParts[len(pathParts)-2]+"-title.rgl.json")
directory := filepath.Dir(outPath)

if _, err := os.Stat(directory); os.IsNotExist(err) {
err := os.MkdirAll(directory, 0755)

if err != nil {
return err
}
}

file, err := os.OpenFile(outPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
if err != nil {
return err
}

defer file.Close()

content := title.decrypt()
if _, err = file.Write([]byte(content)); err != nil {
return err
}

return nil
}

err := filepath.Walk(params.titlesPath, func(path string, info os.FileInfo, err error) error {
if err == nil && filepath.Ext(info.Name()) == ".rgl" {
decryptFile(path)

}
return nil
})

if err != nil {
panic(err)
}

fmt.Printf("Done! Decrypted into %s\n", params.outPath)
return nil
}
28 changes: 8 additions & 20 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,21 @@
package main

import (
"fmt"
)

func main() {
params := parseParams()

if params == nil {
return
}

rgl, err := LoadLauncher(params.rglPath)

if err != nil {
panic(err)
}
var err error

logFunc := func(log string) {
fmt.Println(log)
switch params.cmdType {
case cmdDecryptTitles:
err = decryptTitles(params)
case cmdExtractLauncher:
err = extractLauncher(params)
}

for _, packFile := range rgl.Files {
err = packFile.extractPackFile(params.outPath, logFunc)

if err != nil {
panic(err)
}
if err != nil {
panic(err)
}

fmt.Printf("Done! Extracted into %s\n", params.outPath)
}
120 changes: 120 additions & 0 deletions title.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package main

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"errors"
"io/ioutil"
"os"
)

var (
errInvalidMagic = errors.New("title: invalid file magic")
errUnknownVersion = errors.New("title: unknown version")
errSizeMismatch = errors.New("title: buffer size mismatch")
)

type rglTitle struct {
Magic []byte
Version uint32
Length uint32
Data []byte
}

func ReadTitleFromFile(filePath string) (*rglTitle, error) {
file, err := os.Open(filePath)
if err != nil {
return nil, err
}

defer file.Close()

content, err := ioutil.ReadAll(file)
if err != nil {
return nil, err
}

return ReadTitleFromBuffer(content)
}

func ReadTitleFromBuffer(content []byte) (*rglTitle, error) {
buffer := bytes.NewBuffer(content)
reader := NewReader(buffer)

magic := make([]byte, 4)
_, err := reader.Read(magic)
if err != nil {
return nil, err
}

if string(magic) != "RGLM" {
return nil, errInvalidMagic
}

version, err := reader.ReadUint32()
if err != nil {
return nil, err
}

length, err := reader.ReadUint32()
if err != nil {
return nil, err
}

if version != 1 || length > uint32(len(content)) {
return nil, errUnknownVersion
}

// Hardcoded offset?
reader.SetOffset(0x50)

data := make([]byte, length)
size, err := reader.Read(data)
if err != nil {
return nil, err
}

if size != int(length) {
return nil, errSizeMismatch
}

return &rglTitle{
Magic: magic,
Version: version,
Length: length,
Data: data,
}, nil
}

func (title *rglTitle) decrypt() string {
// Key and IV are both empty
key := make([]byte, 32)
iv := make([]byte, 16)

block, err := aes.NewCipher(key)
if err != nil {
return ""
}

buffer := make([]byte, title.Length)
mode := cipher.NewCBCDecrypter(block, iv)
mode.CryptBlocks(buffer, title.Data)
content := string(buffer)

// FIXME: hacky trimming
for i := 0; i < len(content); i += 1 {
if content[i] == '{' {
content = content[i : len(content)-1]
break
}
}
for i := len(content) - 1; i > 0; i -= 1 {
if content[i] == '}' {
content = content[:i+1]
break
}
}

return content
}

0 comments on commit fc3baee

Please sign in to comment.