commit b8d84dba6aa184f432630f27f61478a842bc94bf Author: Knut Ahlers Date: Tue Nov 5 16:47:30 2019 +0100 Initial setup of library / decrypter diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4ba68a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +cmd/sii-decrypt/sii-decrypt diff --git a/cmd/sii-decrypt/go.mod b/cmd/sii-decrypt/go.mod new file mode 100644 index 0000000..9d8f01d --- /dev/null +++ b/cmd/sii-decrypt/go.mod @@ -0,0 +1,12 @@ +module github.com/Luzifer/sii/cmd/sii-decrypt + +go 1.13 + +replace github.com/Luzifer/sii => ../../ + +require ( + github.com/Luzifer/rconfig/v2 v2.2.1 + github.com/Luzifer/sii v0.0.0 + github.com/pkg/errors v0.8.1 + github.com/sirupsen/logrus v1.4.2 +) diff --git a/cmd/sii-decrypt/go.sum b/cmd/sii-decrypt/go.sum new file mode 100644 index 0000000..c02d9ce --- /dev/null +++ b/cmd/sii-decrypt/go.sum @@ -0,0 +1,22 @@ +github.com/Luzifer/rconfig v2.2.0+incompatible h1:Kle3+rshPM7LxciOheaR4EfHUzibkDDGws04sefQ5m8= +github.com/Luzifer/rconfig/v2 v2.2.1 h1:zcDdLQlnlzwcBJ8E0WFzOkQE1pCMn3EbX0dFYkeTczg= +github.com/Luzifer/rconfig/v2 v2.2.1/go.mod h1:OKIX0/JRZrPJ/ZXXWklQEFXA6tBfWaljZbW37w+sqBw= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19 h1:WB265cn5OpO+hK3pikC9hpP1zI/KTwmyMFKloW9eOVc= +gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/cmd/sii-decrypt/main.go b/cmd/sii-decrypt/main.go new file mode 100644 index 0000000..c5fc0f9 --- /dev/null +++ b/cmd/sii-decrypt/main.go @@ -0,0 +1,81 @@ +package main + +import ( + "encoding/hex" + "fmt" + "io" + "os" + + "github.com/Luzifer/rconfig/v2" + "github.com/Luzifer/sii" + log "github.com/sirupsen/logrus" +) + +var ( + cfg = struct { + DecryptKey string `flag:"decrypt-key" default:"" description:"Hex formated decryption key" validate:"nonzero"` + LogLevel string `flag:"log-level" default:"info" description:"Log level (debug, info, warn, error, fatal)"` + VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"` + }{} + + decryptKey []byte + version = "dev" +) + +func init() { + rconfig.AutoEnv(true) + if err := rconfig.ParseAndValidate(&cfg); err != nil { + log.Fatalf("Unable to parse commandline options: %s", err) + } + + if cfg.VersionAndExit { + fmt.Printf("sii-decrypt %s\n", version) + os.Exit(0) + } + + if l, err := log.ParseLevel(cfg.LogLevel); err != nil { + log.WithError(err).Fatal("Unable to parse log level") + } else { + log.SetLevel(l) + } +} + +func main() { + var err error + decryptKey, err = hex.DecodeString(cfg.DecryptKey) + if err != nil { + log.WithError(err).Fatal("Unable to read encryption key") + } + + if len(rconfig.Args()) != 2 { + log.Fatal("Expecting exactly one SII file as an argument") + } + + stat, err := os.Stat(rconfig.Args()[1]) + if err != nil { + log.WithError(err).Fatal("Unable to read info of save-file") + } + + f, err := os.Open(rconfig.Args()[1]) + if err != nil { + log.WithError(err).Fatal("Unable to open encrypted file") + } + defer f.Close() + + sii.SetEncryptionKey(decryptKey) + + contentReader, err := sii.DecryptRaw(f, stat.Size()) + if err != nil { + log.WithError(err).Fatal("Unable to decrypt file") + } + + tf, err := os.Create("/tmp/output") + if err != nil { + log.WithError(err).Fatal("Unable to create output file") + } + defer tf.Close() + + if _, err := io.Copy(tf, contentReader); err != nil { + log.WithError(err).Fatal("Unable to copy content") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d0ac67a --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/Luzifer/sii + +go 1.13 + +require github.com/pkg/errors v0.8.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f29ab35 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/sii.go b/sii.go new file mode 100644 index 0000000..241201a --- /dev/null +++ b/sii.go @@ -0,0 +1,139 @@ +package sii + +import ( + "bytes" + "compress/flate" + "crypto/aes" + "crypto/cipher" + "encoding/binary" + "io" + "os" + "reflect" + + "github.com/pkg/errors" +) + +var encryptionKey []byte + +var ( + sigBinary = []byte{0x42, 0x53, 0x49, 0x49} // BSII (unencrypted binary format) + sigEncrypted = []byte{0x53, 0x63, 0x73, 0x43} // ScsC (compressed and encrypted SiiN with headers) + sigPlain = []byte{0x53, 0x69, 0x69, 0x4e} // SiiN (plain-text unit file) +) + +var ( + ErrNoEncryptionKeySet = errors.New("No encryption key set") +) + +type scscHeader struct { + Signature [4]byte + HMAC [32]byte + InitVector [16]byte + DataSize uint32 +} + +// DecryptRaw takes a reader of a ScsC file and extracts the SiiN raw content +func DecryptRaw(file io.ReaderAt, size int64) (io.Reader, error) { + if encryptionKey == nil { + return nil, ErrNoEncryptionKeySet + } + + ftHeader, err := readFTHeader(file) + if err != nil { + return nil, errors.Wrap(err, "Unable to read file header") + } + + if !reflect.DeepEqual(ftHeader, sigEncrypted) { + return nil, errors.New("Input file does not contain ScsC header") + } + + h := scscHeader{} + if err := binary.Read(io.NewSectionReader(file, 0, int64(binary.Size(h))), binary.LittleEndian, &h); err != nil { + return nil, errors.Wrap(err, "Unable to read header from file") + } + + var content = make([]byte, size-int64(binary.Size(h))) + if _, err := file.ReadAt(content, int64(binary.Size(h))); err != nil { + return nil, errors.Wrap(err, "Unable to read encrypted content") + } + + block, err := aes.NewCipher(encryptionKey) + if err != nil { + return nil, errors.Wrap(err, "Invalid encryption key") + } + decrypter := cipher.NewCBCDecrypter(block, h.InitVector[:]) + decrypter.CryptBlocks(content, content) + + return flate.NewReader(bytes.NewReader(content[2:])), nil +} + +// ReadUnitFile reads the file, decrypts it if required and parses it into the Unit struct +func ReadUnitFile(filename string) (*Unit, error) { + stat, err := os.Stat(filename) + if err != nil { + return nil, errors.Wrap(err, "Unable to read info of save-file") + } + + f, err := os.Open(filename) + if err != nil { + return nil, errors.Wrap(err, "Unable to open encrypted file") + } + defer f.Close() + + // Check what type of file we do have here + ftHeader, err := readFTHeader(f) + if err != nil { + return nil, errors.Wrap(err, "Unable to read file header") + } + + var r io.Reader + switch { + case reflect.DeepEqual(ftHeader, sigBinary): + return nil, errors.New("File has unsupported Binary-SII format") + + case reflect.DeepEqual(ftHeader, sigEncrypted): + r, err = DecryptRaw(f, stat.Size()) + if err != nil { + return nil, errors.New("Unable to decrypt file") + } + + case reflect.DeepEqual(ftHeader, sigPlain): + // We already got the plain file: We can just read it + r = f + + default: + return nil, errors.New("Invalid / unknown file type header found") + } + + return parseSIIPlainFile(r) +} + +// SetEncryptionKey sets the 32-byte key to encrypt / decrypt ScsC files. +// The key is not included for legal reasons and you need to obtain it from other sources. +func SetEncryptionKey(key []byte) { encryptionKey = key } + +func WriteUnitFile(filename string, encrypt bool, data *Unit) error { + if encrypt && encryptionKey == nil { + return ErrNoEncryptionKeySet + } + + // FIXME: Implement this + return errors.New("Not implemented") +} + +func parseSIIPlainFile(r io.Reader) (*Unit, error) { + // FIXME: Implement this + return nil, errors.New("Not implemented") +} + +func readFTHeader(f io.ReaderAt) ([]byte, error) { + var ftHeader = make([]byte, 4) + if n, err := f.ReadAt(ftHeader, 0); err != nil || n != 4 { + if err != nil { + err = errors.Errorf("Received %d / 4 byte header") + } + return nil, errors.Wrap(err, "Unable to read 4-byte file header") + } + + return ftHeader, nil +} diff --git a/unit.go b/unit.go new file mode 100644 index 0000000..6f3e12d --- /dev/null +++ b/unit.go @@ -0,0 +1,33 @@ +package sii + +type Block interface { + Name() string + Type() string +} + +type Marshaler interface { + MarshalSII() ([]byte, error) +} + +type Unmarshaler interface { + UnmarshalSII([]byte) error +} + +type RawBlock struct { + Data []byte + + blockName string + blockType string +} + +func (r RawBlock) MarshalSII() []byte { return r.Data } +func (r RawBlock) Name() string { return r.blockName } +func (r RawBlock) Type() string { return r.blockType } +func (r *RawBlock) UnmarshalSII(blockName, blockType string, in []byte) error { + r.Data = in + return nil +} + +type Unit struct { + Entries []Block +}