├── eapki ├── main.go └── cmd │ ├── path.go │ ├── pins.go │ ├── root.go │ ├── proxy.go │ ├── fcheck.go │ ├── p7e.go │ ├── bruteforce.go │ ├── keyring.go │ ├── obfuscate.go │ └── dump.go ├── obfuscate ├── states │ ├── bootstrap-1.5.1_release_32_MDX │ ├── bootstrap-1.5.3_release_64_M32 │ ├── bootstrap-1.7.1_release_64_MDX │ ├── bootstrap-1.8.2_release_32_L44 │ ├── bootstrap-1.8.2_release_64_KFC │ ├── bootstrap-2.0.0_debug_64_TDJ │ ├── bootstrap-2.0.0_release_64_KFC │ └── bootstrap-2.0.0_release_64_TBS ├── bruteforce.go └── obfuscate.go ├── tools └── save-obfuscator-state │ └── main.go ├── go.mod ├── keyring ├── key_source.go └── keyring.go ├── dongle ├── pin_test.go ├── pin.go └── dongle.go ├── p7e └── p7e.go ├── drmfs ├── path.go ├── fcheck.go └── dump.go ├── LICENSE ├── README.md ├── go.sum └── proxy └── proxy.go /eapki/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/YoshihikoAbe/eapki/eapki/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | -------------------------------------------------------------------------------- /obfuscate/states/bootstrap-1.5.1_release_32_MDX: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoshihikoAbe/eapki/HEAD/obfuscate/states/bootstrap-1.5.1_release_32_MDX -------------------------------------------------------------------------------- /obfuscate/states/bootstrap-1.5.3_release_64_M32: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoshihikoAbe/eapki/HEAD/obfuscate/states/bootstrap-1.5.3_release_64_M32 -------------------------------------------------------------------------------- /obfuscate/states/bootstrap-1.7.1_release_64_MDX: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoshihikoAbe/eapki/HEAD/obfuscate/states/bootstrap-1.7.1_release_64_MDX -------------------------------------------------------------------------------- /obfuscate/states/bootstrap-1.8.2_release_32_L44: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoshihikoAbe/eapki/HEAD/obfuscate/states/bootstrap-1.8.2_release_32_L44 -------------------------------------------------------------------------------- /obfuscate/states/bootstrap-1.8.2_release_64_KFC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoshihikoAbe/eapki/HEAD/obfuscate/states/bootstrap-1.8.2_release_64_KFC -------------------------------------------------------------------------------- /obfuscate/states/bootstrap-2.0.0_debug_64_TDJ: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoshihikoAbe/eapki/HEAD/obfuscate/states/bootstrap-2.0.0_debug_64_TDJ -------------------------------------------------------------------------------- /obfuscate/states/bootstrap-2.0.0_release_64_KFC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoshihikoAbe/eapki/HEAD/obfuscate/states/bootstrap-2.0.0_release_64_KFC -------------------------------------------------------------------------------- /obfuscate/states/bootstrap-2.0.0_release_64_TBS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YoshihikoAbe/eapki/HEAD/obfuscate/states/bootstrap-2.0.0_release_64_TBS -------------------------------------------------------------------------------- /tools/save-obfuscator-state/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/YoshihikoAbe/eapki/obfuscate" 7 | ) 8 | 9 | func main() { 10 | b, _ := os.ReadFile(os.Args[1]) 11 | obfus, _ := obfuscate.NewObfuscator(b) 12 | os.WriteFile(os.Args[2], []byte(obfus), 0644) 13 | } 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/YoshihikoAbe/eapki 2 | 3 | go 1.24.2 4 | 5 | require ( 6 | github.com/YoshihikoAbe/avsproperty v0.0.1 7 | github.com/YoshihikoAbe/fsdump v0.0.1 8 | github.com/miekg/pkcs11 v1.1.1 9 | github.com/spf13/cobra v1.8.1 10 | ) 11 | 12 | require ( 13 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 14 | github.com/spf13/pflag v1.0.5 // indirect 15 | golang.org/x/text v0.16.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /keyring/key_source.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | type KeySource interface { 4 | ContentsCode() string 5 | DecryptKey(b []byte) ([]byte, error) 6 | } 7 | 8 | type MemoryKeySource struct { 9 | Code string `json:"code"` 10 | Version string `json:"version"` 11 | Master []byte `json:"master"` 12 | } 13 | 14 | func (ks MemoryKeySource) ContentsCode() string { 15 | return ks.Code 16 | } 17 | 18 | func (ks MemoryKeySource) DecryptKey(b []byte) ([]byte, error) { 19 | return ks.Master, nil 20 | } 21 | -------------------------------------------------------------------------------- /dongle/pin_test.go: -------------------------------------------------------------------------------- 1 | package dongle 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "testing" 7 | ) 8 | 9 | func TestPin(t *testing.T) { 10 | pins := [3][]byte{} 11 | pins[0], _ = base64.StdEncoding.DecodeString("v0wNBOdwWybQ/5Gm3FRl1A==") 12 | pins[1], _ = base64.StdEncoding.DecodeString("fqa3fqUnIY7hAZQfY8bnTg==") 13 | pins[2], _ = base64.StdEncoding.DecodeString("fiY3fiUnIQ5hARQfY0ZnTg==") 14 | 15 | pg, _ := NewPinGenerator([]byte("05")) 16 | for i, want := range pins { 17 | if !bytes.Equal(pg.Generate(), want) { 18 | t.Fatalf("(%d): invalid pin", i) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /eapki/cmd/path.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/YoshihikoAbe/eapki/drmfs" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // pathCmd represents the path command 11 | var pathCmd = &cobra.Command{ 12 | Use: "path CODE PATH", 13 | Short: "Convert a path/filename to an obfuscated drmfs path", 14 | Args: cobra.MinimumNArgs(2), 15 | 16 | Run: func(cmd *cobra.Command, args []string) { 17 | p := drmfs.PathObfuscator{} 18 | p.Init(args[0]) 19 | fmt.Println(p.Obfuscate(args[1])) 20 | }, 21 | } 22 | 23 | func init() { 24 | rootCmd.AddCommand(pathCmd) 25 | 26 | // Here you will define your flags and configuration settings. 27 | 28 | // Cobra supports Persistent Flags which will work for this command 29 | // and all subcommands, e.g.: 30 | // pathCmd.PersistentFlags().String("foo", "", "A help for foo") 31 | 32 | // Cobra supports local flags which will only run when this command 33 | // is called directly, e.g.: 34 | // pathCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 35 | } 36 | -------------------------------------------------------------------------------- /p7e/p7e.go: -------------------------------------------------------------------------------- 1 | package p7e 2 | 3 | import ( 4 | "crypto" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | ) 8 | 9 | type p7eError string 10 | 11 | func (e p7eError) Error() string { 12 | return "eapki/p7e: " + string(e) 13 | } 14 | 15 | func Decrypt(data []byte, decrypter crypto.Decrypter) ([]byte, error) { 16 | const blockSize = 16 17 | 18 | if size := len(data); size < 260+blockSize { 19 | return nil, p7eError("file too small") 20 | } 21 | 22 | key := data[80 : 80+128] 23 | iv := data[239 : 239+16] 24 | content := data[260:] 25 | 26 | key, err := decrypter.Decrypt(nil, key, nil) 27 | if err != nil { 28 | return nil, err 29 | } 30 | block, _ := aes.NewCipher(key) 31 | cbc := cipher.NewCBCDecrypter(block, iv) 32 | 33 | if len(content)%blockSize != 0 { 34 | return nil, p7eError("size of content is not a multiple of the cipher's block size") 35 | } 36 | cbc.CryptBlocks(content, content) 37 | 38 | padding := content[len(content)-1] 39 | if padding > blockSize { 40 | return nil, p7eError("invalid padding") 41 | } 42 | return content[:len(content)-int(padding)], nil 43 | } 44 | -------------------------------------------------------------------------------- /drmfs/path.go: -------------------------------------------------------------------------------- 1 | package drmfs 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "hash" 7 | "strconv" 8 | ) 9 | 10 | type PathObfuscator struct { 11 | hash hash.Hash 12 | } 13 | 14 | func (po *PathObfuscator) Init(contentsCode string) { 15 | key := sha1.Sum([]byte(contentsCode + "test")) 16 | po.hash = hmac.New(sha1.New, key[:]) 17 | } 18 | 19 | func (po PathObfuscator) Obfuscate(path string) string { 20 | po.hash.Write([]byte(path)) 21 | sum := po.hash.Sum(nil) 22 | po.hash.Reset() 23 | 24 | out, _ := formatHashPath(sum) 25 | return out 26 | } 27 | 28 | func formatHashPath(b []byte) (string, error) { 29 | const tbl = "0123456789abcdef" 30 | 31 | if size := len(b); size != sha1.Size { 32 | return "", drmError("invalid path size: " + strconv.Itoa(size)) 33 | } 34 | 35 | out := make([]byte, 43) 36 | out[0] = tbl[b[0]>>4] 37 | out[1] = '/' 38 | out[2] = tbl[b[0]&15] 39 | out[3] = '/' 40 | out[4] = tbl[b[1]>>4] 41 | out[5] = '/' 42 | out[6] = tbl[b[1]&15] 43 | 44 | for i, b := range b[2:] { 45 | i *= 2 46 | out[i+7] = tbl[b>>4] 47 | out[i+8] = tbl[b&15] 48 | } 49 | 50 | return string(out), nil 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Yoshihiko ABE 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /eapki/cmd/pins.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | 8 | "github.com/YoshihikoAbe/eapki/dongle" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // pinCmd represents the pin command 13 | var pinsCmd = &cobra.Command{ 14 | Use: "pins SERIAL", 15 | Short: "List possible dongle pins", 16 | Args: cobra.MinimumNArgs(1), 17 | 18 | Run: func(cmd *cobra.Command, args []string) { 19 | pg, err := dongle.NewPinGenerator(bytes.ToLower([]byte(args[0]))) 20 | if err != nil { 21 | fatal(err) 22 | } 23 | for i := 0; i < dongle.NumberOfPins; i++ { 24 | fmt.Println(base64.StdEncoding.EncodeToString(pg.Generate())) 25 | } 26 | }, 27 | } 28 | 29 | func init() { 30 | rootCmd.AddCommand(pinsCmd) 31 | 32 | // Here you will define your flags and configuration settings. 33 | 34 | // Cobra supports Persistent Flags which will work for this command 35 | // and all subcommands, e.g.: 36 | // pinCmd.PersistentFlags().String("foo", "", "A help for foo") 37 | 38 | // Cobra supports local flags which will only run when this command 39 | // is called directly, e.g.: 40 | // pinCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 41 | } 42 | -------------------------------------------------------------------------------- /eapki/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // rootCmd represents the base command when called without any subcommands 11 | var rootCmd = &cobra.Command{ 12 | Use: "eapki", 13 | Short: "EAPKI toolkit", 14 | // Uncomment the following line if your bare application 15 | // has an action associated with it: 16 | // Run: func(cmd *cobra.Command, args []string) { }, 17 | } 18 | 19 | // Execute adds all child commands to the root command and sets flags appropriately. 20 | // This is called by main.main(). It only needs to happen once to the rootCmd. 21 | func Execute() { 22 | err := rootCmd.Execute() 23 | if err != nil { 24 | os.Exit(1) 25 | } 26 | } 27 | 28 | func init() { 29 | // Here you will define your flags and configuration settings. 30 | // Cobra supports persistent flags, which, if defined here, 31 | // will be global for your application. 32 | 33 | // rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.eapki.yaml)") 34 | 35 | // Cobra also supports local flags, which will only run 36 | // when this action is called directly. 37 | } 38 | 39 | func fatal(v ...any) { 40 | fmt.Fprintln(os.Stderr, v...) 41 | os.Exit(1) 42 | } 43 | -------------------------------------------------------------------------------- /eapki/cmd/proxy.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/YoshihikoAbe/eapki/dongle" 7 | "github.com/YoshihikoAbe/eapki/proxy" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // proxyCmd represents the proxy command 12 | var proxyCmd = &cobra.Command{ 13 | Use: "proxy ADDRESS REMOTE", 14 | Short: "Start authentication proxy", 15 | Long: `A TLS/SSL proxy that performs client certificate authentication on behalf of its clients. 16 | It uses the newest client certificate from the connected account key.`, 17 | Args: cobra.MinimumNArgs(2), 18 | Run: runProxy, 19 | } 20 | 21 | func init() { 22 | rootCmd.AddCommand(proxyCmd) 23 | 24 | // Here you will define your flags and configuration settings. 25 | 26 | // Cobra supports Persistent Flags which will work for this command 27 | // and all subcommands, e.g.: 28 | // proxyCmd.PersistentFlags().String("foo", "", "A help for foo") 29 | 30 | // Cobra supports local flags which will only run when this command 31 | // is called directly, e.g.: 32 | // proxyCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 33 | } 34 | 35 | func runProxy(cmd *cobra.Command, args []string) { 36 | dongle, err := dongle.Find(dongle.AccountKey) 37 | if err != nil { 38 | log.Fatalln(err) 39 | } 40 | log.Fatalln(proxy.Listen(args[0], args[1], dongle)) 41 | } 42 | -------------------------------------------------------------------------------- /obfuscate/bruteforce.go: -------------------------------------------------------------------------------- 1 | package obfuscate 2 | 3 | import ( 4 | "bytes" 5 | "embed" 6 | "strconv" 7 | ) 8 | 9 | //go:embed states/* 10 | var states embed.FS 11 | 12 | var obfuscators []Obfuscator 13 | 14 | func init() { 15 | entries, err := states.ReadDir("states") 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | obfuscators = make([]Obfuscator, len(entries)) 21 | for i, entry := range entries { 22 | b, err := states.ReadFile("states/" + entry.Name()) 23 | if err != nil { 24 | panic(err) 25 | } 26 | obfuscators[i] = b 27 | } 28 | } 29 | 30 | func Bruteforce(in []byte) ([]byte, error) { 31 | if len := len(in); len < 5 { 32 | return nil, obfuscateError("file smaller than the minimum allowed size: " + strconv.Itoa(int(len)) + " < 5") 33 | } 34 | rd := bytes.NewReader(in) 35 | buf := bytes.NewBuffer(nil) 36 | 37 | for _, obfus := range obfuscators { 38 | rd.Seek(0, 0) 39 | if err := obfus.Deobfuscate(buf, rd); err != nil { 40 | return nil, err 41 | } 42 | switch string(buf.Bytes()[:4]) { 43 | case "\xef\xbb\xbf<": 44 | fallthrough 45 | case "", outName) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eapki 2 | 3 | EAPKI toolkit 4 | 5 | # Usage 6 | 7 | ``` 8 | EAPKI toolkit 9 | 10 | Usage: 11 | eapki [command] 12 | 13 | Available Commands: 14 | dump Dump the contents of an encrypted filesystem 15 | fcheck Perform file integrity check 16 | help Help about any command 17 | keyring Create keyring dump 18 | obfuscate Obfuscate or deobfuscate files used early in the eapki client's boot process (kbt.dll, etc...) 19 | p7e Decrypt PKCS #7 encrypted files (kdm.dll, etc...) 20 | path Convert a path/filename to an obfuscated drmfs path 21 | pins List possible dongle pins 22 | proxy Start authentication proxy 23 | 24 | Flags: 25 | -h, --help help for eapki 26 | 27 | Use "eapki [command] --help" for more information about a command. 28 | ``` 29 | 30 | ## Environment Variables 31 | 32 | `PKCS11_MODULE`: Path to the PKCS11 module. If left blank, a platform specific default will be used instead. 33 | 34 | ## Dumping drmfs 35 | 36 | After connecting your license key, you can dump the contents of an encrypted filesystem by running `eapki dump SOURCE DESTINATION`. 37 | 38 | After a successful dump, you may also perform a file check by running `eapki fcheck DESTINATION DESTINATION/prop/filepath.xml`. 39 | 40 | ## Decrypting Other Files 41 | 42 | Some files are encrypted outside the context of drmfs, with the most notable of these being avs2-core.dll, avs2-ea3.dll, and bootstrap.xml. 43 | 44 | Assuming that you have their respective bootstrap.exe file, you can decrypt these files by running `eapki obfuscate BOOTSTRAP FILES...` -------------------------------------------------------------------------------- /eapki/cmd/bruteforce.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/YoshihikoAbe/eapki/obfuscate" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | // obfuscateCmd represents the obfuscate command 12 | var bruteforceCmd = &cobra.Command{ 13 | Use: "bruteforce FILES...", 14 | Short: "Deobfuscate files used early in the eapki client's boot process using precomputed obfuscator states", 15 | Args: cobra.MinimumNArgs(1), 16 | Run: runBruteforce, 17 | } 18 | 19 | func init() { 20 | rootCmd.AddCommand(bruteforceCmd) 21 | 22 | // Here you will define your flags and configuration settings. 23 | 24 | // Cobra supports Persistent Flags which will work for this command 25 | // and all subcommands, e.g.: 26 | // obfuscateCmd.PersistentFlags().String("foo", "", "A help for foo") 27 | 28 | // Cobra supports local flags which will only run when this command 29 | // is called directly, e.g.: 30 | // obfuscateCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 31 | } 32 | 33 | func runBruteforce(cmd *cobra.Command, args []string) { 34 | noDec := 0 35 | for _, name := range args { 36 | enc, err := os.ReadFile(name) 37 | if err != nil { 38 | fatal(err) 39 | } 40 | 41 | dec, err := obfuscate.Bruteforce(enc) 42 | if err != nil { 43 | fmt.Fprintln(os.Stderr, name+": decrypt failed:", err) 44 | continue 45 | } 46 | 47 | if err := os.WriteFile(name+".dec", dec, 0644); err != nil { 48 | fatal(err) 49 | } 50 | noDec++ 51 | fmt.Println("successfully decrypted", name) 52 | } 53 | fmt.Printf("decrypted %d/%d files\n", noDec, len(args)) 54 | } 55 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/YoshihikoAbe/avsproperty v0.0.1 h1:EhaxmJoVHY0iXmOOSYC2g8PdlMJ60UOF9DpTqE4g9xo= 2 | github.com/YoshihikoAbe/avsproperty v0.0.1/go.mod h1:QnubObUKj734sRrFtaFzp9Zrk3LKLJHp3+iqIMhqdXM= 3 | github.com/YoshihikoAbe/fsdump v0.0.1 h1:y8puE1fwGisIU2mPUyrkwOC3HlYvhRCsIWDlWCRkKBY= 4 | github.com/YoshihikoAbe/fsdump v0.0.1/go.mod h1:aXvDhFrth+5KPfxZM7xMZ10fPS61fKkeL1Uc1Ui1f2M= 5 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 6 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 7 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 8 | github.com/miekg/pkcs11 v1.1.1 h1:Ugu9pdy6vAYku5DEpVWVFPYnzV+bxB+iRdbuFSu7TvU= 9 | github.com/miekg/pkcs11 v1.1.1/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= 10 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 11 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 12 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 13 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 14 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 15 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 16 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 19 | -------------------------------------------------------------------------------- /eapki/cmd/keyring.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/YoshihikoAbe/eapki/dongle" 8 | "github.com/YoshihikoAbe/eapki/keyring" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // keyringCmd represents the keyring command 13 | var keyringCmd = &cobra.Command{ 14 | Use: "keyring FILENAME", 15 | Short: "Create keyring dump", 16 | Args: cobra.MinimumNArgs(1), 17 | 18 | Run: runKeyring, 19 | } 20 | 21 | func init() { 22 | rootCmd.AddCommand(keyringCmd) 23 | 24 | // Here you will define your flags and configuration settings. 25 | 26 | // Cobra supports Persistent Flags which will work for this command 27 | // and all subcommands, e.g.: 28 | // keyringCmd.PersistentFlags().String("foo", "", "A help for foo") 29 | 30 | // Cobra supports local flags which will only run when this command 31 | // is called directly, e.g.: 32 | // keyringCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 33 | } 34 | 35 | func runKeyring(cmd *cobra.Command, args []string) { 36 | filename := args[0] 37 | 38 | dongle, err := dongle.Find(dongle.LicenseKey) 39 | if err != nil { 40 | fatal(err) 41 | } 42 | 43 | f, err := os.Open(filename) 44 | if err != nil { 45 | fatal(err) 46 | } 47 | kr, err := keyring.New(f, dongle) 48 | if err != nil { 49 | fatal(err) 50 | } 51 | 52 | mks := keyring.MemoryKeySource{ 53 | Code: kr.ContentsCode(), 54 | Version: kr.Version(), 55 | Master: kr.MasterKey(), 56 | } 57 | data, err := json.Marshal(mks) 58 | if err != nil { 59 | fatal(err) 60 | } 61 | if err := os.WriteFile(mks.Code+"_"+mks.Version+".json", data, 0666); err != nil { 62 | fatal(err) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /proxy/proxy.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "io" 7 | "log" 8 | "net" 9 | 10 | "github.com/YoshihikoAbe/eapki/dongle" 11 | ) 12 | 13 | func Listen(address, remote string, account *dongle.Dongle) error { 14 | if account.Type() != dongle.AccountKey { 15 | panic("Invalid dongle type") 16 | } 17 | 18 | l, err := net.Listen("tcp", address) 19 | if err != nil { 20 | return err 21 | } 22 | defer l.Close() 23 | 24 | state := &listenState{ 25 | remote: remote, 26 | tls: &tls.Config{ 27 | InsecureSkipVerify: true, 28 | Certificates: []tls.Certificate{ 29 | { 30 | Certificate: [][]byte{account.Certificate().Raw}, 31 | PrivateKey: account, 32 | }, 33 | }, 34 | }, 35 | } 36 | 37 | for { 38 | conn, err := l.Accept() 39 | if err != nil { 40 | log.Println(err) 41 | continue 42 | } 43 | log.Println("received connection:", conn.RemoteAddr()) 44 | go state.accept(conn) 45 | } 46 | } 47 | 48 | type listenState struct { 49 | remote string 50 | tls *tls.Config 51 | } 52 | 53 | func (state *listenState) accept(conn net.Conn) { 54 | remote, err := tls.Dial("tcp", state.remote, state.tls) 55 | if err != nil { 56 | conn.Close() 57 | log.Println(err) 58 | return 59 | } 60 | 61 | log.Println("established connection with remote server:", conn.RemoteAddr(), "->", remote.RemoteAddr()) 62 | 63 | go state.transmit(conn, remote) 64 | go state.transmit(remote, conn) 65 | } 66 | 67 | func (state *listenState) transmit(in, out net.Conn) { 68 | defer in.Close() 69 | defer out.Close() 70 | if err := state.doTransmit(in, out); err != nil { 71 | if err != io.EOF && !errors.Is(err, net.ErrClosed) { 72 | log.Println(err) 73 | } 74 | } 75 | } 76 | 77 | func (state *listenState) doTransmit(in, out net.Conn) error { 78 | b := make([]byte, 8192) 79 | for { 80 | n, err := in.Read(b) 81 | if err != nil { 82 | return err 83 | } 84 | if _, err := out.Write(b[:n]); err != nil { 85 | return err 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /drmfs/fcheck.go: -------------------------------------------------------------------------------- 1 | package drmfs 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "io" 7 | "os" 8 | "path" 9 | "time" 10 | 11 | "github.com/YoshihikoAbe/avsproperty" 12 | ) 13 | 14 | var ( 15 | md5NodeName, _ = avsproperty.NewNodeName("dst_md5") 16 | sizeNodeName, _ = avsproperty.NewNodeName("dst_size") 17 | ) 18 | 19 | type CheckResult struct { 20 | Time time.Time `json:"time"` 21 | 22 | Broken []string `json:"broken"` 23 | Missing []string `json:"missing"` 24 | TotalBroken int `json:"total_broken"` 25 | TotalMissing int `json:"total_missing"` 26 | TotalFiles int `json:"total_files"` 27 | } 28 | 29 | func CheckContents(list *avsproperty.Node, root string) (*CheckResult, error) { 30 | if list.Name().String() != "list" { 31 | return nil, drmError("invalid root node") 32 | } 33 | 34 | result := &CheckResult{ 35 | Time: time.Now(), 36 | Broken: []string{}, 37 | Missing: []string{}, 38 | } 39 | 40 | hash := md5.New() 41 | for _, entry := range list.Children() { 42 | if !entry.Name().Equals(fileNodeName) { 43 | continue 44 | } 45 | result.TotalFiles++ 46 | 47 | pathNode := entry.SearchChildNodeName(pathNodeName) 48 | md5Node := entry.SearchChildNodeName(md5NodeName) 49 | sizeNode := entry.SearchChildNodeName(sizeNodeName) 50 | if pathNode == nil || md5Node == nil || sizeNode == nil { 51 | return nil, drmError("invalid file node in list") 52 | } 53 | 54 | filename := pathNode.StringValue() 55 | rd, err := os.Open(path.Join(root, filename)) 56 | if err != nil { 57 | result.Missing = append(result.Missing, filename) 58 | result.TotalMissing++ 59 | continue 60 | } 61 | 62 | if _, err := io.CopyN(hash, rd, int64(sizeNode.UintValue())); err != nil { 63 | rd.Close() 64 | return nil, err 65 | } 66 | rd.Close() 67 | if !bytes.Equal(md5Node.BinaryValue(), hash.Sum(nil)) { 68 | result.Broken = append(result.Broken, filename) 69 | result.TotalBroken++ 70 | } 71 | hash.Reset() 72 | } 73 | 74 | return result, nil 75 | } 76 | -------------------------------------------------------------------------------- /eapki/cmd/obfuscate.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/YoshihikoAbe/eapki/obfuscate" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | // obfuscateCmd represents the obfuscate command 13 | var obfuscateCmd = &cobra.Command{ 14 | Use: "obfuscate BOOTSTRAP FILES...", 15 | Short: "Obfuscate or deobfuscate files used early in the eapki client's boot process (kbt.dll, etc...)", 16 | Args: cobra.MinimumNArgs(2), 17 | Run: runObfuscate, 18 | } 19 | 20 | func init() { 21 | rootCmd.AddCommand(obfuscateCmd) 22 | 23 | // Here you will define your flags and configuration settings. 24 | 25 | // Cobra supports Persistent Flags which will work for this command 26 | // and all subcommands, e.g.: 27 | // obfuscateCmd.PersistentFlags().String("foo", "", "A help for foo") 28 | 29 | // Cobra supports local flags which will only run when this command 30 | // is called directly, e.g.: 31 | // obfuscateCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 32 | obfuscateCmd.Flags().BoolP("obfuscate", "o", false, "Perform obfuscation. By default, deobfuscation is performed") 33 | } 34 | 35 | func runObfuscate(cmd *cobra.Command, args []string) { 36 | bootstrapName := args[0] 37 | files := args[1:] 38 | 39 | b, err := os.ReadFile(bootstrapName) 40 | if err != nil { 41 | fatal(err) 42 | } 43 | o, err := obfuscate.NewObfuscator(b) 44 | if err != nil { 45 | fatal(err) 46 | } 47 | 48 | var do func(io.Writer, io.Reader) error 49 | if obfuscate, _ := cmd.Flags().GetBool("obfuscate"); obfuscate { 50 | do = o.Obfuscate 51 | } else { 52 | do = o.Deobfuscate 53 | } 54 | 55 | for _, inName := range files { 56 | in, err := os.Open(inName) 57 | if err != nil { 58 | fatal(err) 59 | } 60 | outName := inName + ".out" 61 | out, err := os.Create(outName) 62 | if err != nil { 63 | fatal(err) 64 | } 65 | 66 | if err := do(out, in); err != nil { 67 | fatal(err) 68 | } 69 | 70 | out.Close() 71 | in.Close() 72 | 73 | fmt.Println(inName, "->", outName) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /eapki/cmd/dump.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "os" 7 | "time" 8 | 9 | "github.com/YoshihikoAbe/eapki/dongle" 10 | "github.com/YoshihikoAbe/eapki/drmfs" 11 | "github.com/YoshihikoAbe/eapki/keyring" 12 | "github.com/YoshihikoAbe/fsdump" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | // dumpCmd represents the dump command 17 | var dumpCmd = &cobra.Command{ 18 | Use: "dump SOURCE DESTINATION", 19 | Short: "Dump the contents of an encrypted filesystem", 20 | Args: cobra.MinimumNArgs(2), 21 | 22 | Run: runDump, 23 | } 24 | 25 | func init() { 26 | rootCmd.AddCommand(dumpCmd) 27 | 28 | // Here you will define your flags and configuration settings. 29 | 30 | // Cobra supports Persistent Flags which will work for this command 31 | // and all subcommands, e.g.: 32 | // dumpCmd.PersistentFlags().String("foo", "", "A help for foo") 33 | 34 | // Cobra supports local flags which will only run when this command 35 | // is called directly, e.g.: 36 | // dumpCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") 37 | dumpCmd.Flags().StringP("key", "k", "", "Keyring dump file") 38 | dumpCmd.Flags().IntP("workers", "w", 0, "Number of workers. Specify a value less than one, and the number of logical CPUs available to the process will be used") 39 | } 40 | 41 | func runDump(cmd *cobra.Command, args []string) { 42 | src := args[0] 43 | dest := args[1] 44 | keyFile, _ := cmd.Flags().GetString("key") 45 | workers, _ := cmd.Flags().GetInt("workers") 46 | 47 | ks, err := getKeySource(keyFile) 48 | if err != nil { 49 | log.Fatalln("failed to initialize key source:", err) 50 | } 51 | 52 | start := time.Now() 53 | 54 | ch, err := drmfs.Dump(src, ks) 55 | if err != nil { 56 | log.Fatalln(err) 57 | } 58 | 59 | dumper := fsdump.Dumper{ 60 | Src: &fsdump.ChannelFileSource{Chan: ch}, 61 | Dest: dest, 62 | NumWorkers: workers, 63 | } 64 | dumper.Run() 65 | 66 | log.Println("time elapsed:", time.Since(start)) 67 | } 68 | 69 | func getKeySource(keyFile string) (keyring.KeySource, error) { 70 | if keyFile != "" { 71 | return loadKeyFile(keyFile) 72 | } 73 | return dongle.Find(dongle.LicenseKey) 74 | } 75 | 76 | func loadKeyFile(name string) (keyring.KeySource, error) { 77 | data, err := os.ReadFile(name) 78 | if err != nil { 79 | return nil, err 80 | } 81 | ks := &keyring.MemoryKeySource{} 82 | if err := json.Unmarshal(data, ks); err != nil { 83 | return nil, err 84 | } 85 | 86 | return ks, nil 87 | } 88 | -------------------------------------------------------------------------------- /dongle/pin.go: -------------------------------------------------------------------------------- 1 | package dongle 2 | 3 | import ( 4 | "crypto/sha1" 5 | "fmt" 6 | ) 7 | 8 | const SerialLength = 16 9 | 10 | const ( 11 | stepOldPin = iota 12 | stepNewPin 13 | stepAsciiPin 14 | 15 | NumberOfPins 16 | ) 17 | 18 | type PinGenerator struct { 19 | serial []byte 20 | step int 21 | } 22 | 23 | func NewPinGenerator(serial []byte) (*PinGenerator, error) { 24 | if len(serial) > SerialLength { 25 | return nil, fmt.Errorf("eapki/dongle: serial number too long: %d > %d", len(serial), SerialLength) 26 | } 27 | return &PinGenerator{serial: serial}, nil 28 | } 29 | 30 | func (pg *PinGenerator) Generate() []byte { 31 | if pg.step >= NumberOfPins { 32 | return nil 33 | } 34 | 35 | pin := pg.hashSerial() 36 | if pg.step == stepAsciiPin { 37 | makeAscii(pin) 38 | } 39 | incZeros(pin) 40 | 41 | pg.step++ 42 | return pin 43 | } 44 | 45 | func (pg *PinGenerator) hashSerial() []byte { 46 | var data []byte 47 | 48 | if pg.step == stepNewPin || pg.step == stepAsciiPin { 49 | data = []byte{ 50 | 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 51 | 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 52 | 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 53 | 0x59, 0x5A, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 54 | 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 55 | 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 56 | 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 57 | 0x59, 0x5A, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 58 | // placeholder for serial number 59 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 60 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 61 | } 62 | } else if pg.step == stepOldPin { 63 | data = []byte{ 64 | 0xC6, 0xEB, 0xF5, 0x84, 0x07, 0x34, 0xD3, 0x32, 65 | 0x4F, 0xA4, 0x93, 0xE3, 0xAA, 0x45, 0x01, 0x4E, 66 | 0x45, 0xAF, 0x93, 0xE3, 0x8A, 0x23, 0x74, 0x02, 67 | 0x45, 0xAF, 0x83, 0x63, 0x23, 0x49, 0x92, 0x45, 68 | // placeholder for serial number 69 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 70 | 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 71 | } 72 | } else { 73 | panic("invalid step") 74 | } 75 | copy(data[len(data)-SerialLength:], pg.serial) 76 | 77 | sum := sha1.Sum(data) 78 | return sum[:16] 79 | } 80 | 81 | func incZeros(buffer []byte) { 82 | j := byte(1) 83 | for i, b := range buffer { 84 | if b == 0 { 85 | buffer[i] = j 86 | j++ 87 | } 88 | } 89 | } 90 | 91 | func makeAscii(buffer []byte) { 92 | for i, b := range buffer { 93 | if b >= 128 { 94 | buffer[i] -= 128 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /obfuscate/obfuscate.go: -------------------------------------------------------------------------------- 1 | package obfuscate 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/sha1" 8 | "crypto/sha512" 9 | "debug/pe" 10 | "encoding" 11 | "encoding/binary" 12 | "io" 13 | "strconv" 14 | "unsafe" 15 | ) 16 | 17 | type obfuscateError string 18 | 19 | func (e obfuscateError) Error() string { 20 | return "eapki/obfuscate: " + string(e) 21 | } 22 | 23 | type Obfuscator []byte 24 | 25 | func NewObfuscator(bootstrap []byte) (Obfuscator, error) { 26 | f, err := pe.NewFile(bytes.NewReader(bootstrap)) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | var ( 32 | checksum uintptr 33 | subsystem uintptr 34 | directory uintptr 35 | size uint32 36 | ) 37 | if header, ok := f.OptionalHeader.(*pe.OptionalHeader64); ok { 38 | size = header.SizeOfHeaders 39 | checksum = unsafe.Offsetof(header.CheckSum) 40 | subsystem = unsafe.Offsetof(header.Subsystem) 41 | directory = unsafe.Offsetof(header.DataDirectory) 42 | } else { 43 | header := f.OptionalHeader.(*pe.OptionalHeader32) 44 | size = header.SizeOfHeaders 45 | checksum = unsafe.Offsetof(header.CheckSum) 46 | subsystem = unsafe.Offsetof(header.Subsystem) 47 | directory = unsafe.Offsetof(header.DataDirectory) 48 | } 49 | optional := uintptr(binary.LittleEndian.Uint32(bootstrap[60:])) + 4 + unsafe.Sizeof(pe.FileHeader{}) 50 | directory = optional + directory + (unsafe.Sizeof(pe.DataDirectory{}) * 4) 51 | 52 | hash := sha1.New() 53 | hash.Write(bootstrap[:optional+checksum]) 54 | hash.Write(bootstrap[optional+subsystem : directory]) 55 | hash.Write(bootstrap[directory+unsafe.Sizeof(pe.DataDirectory{}) : size]) 56 | 57 | for _, s := range f.Sections { 58 | if _, err := io.Copy(hash, s.Open()); err != nil { 59 | return nil, err 60 | } 61 | } 62 | 63 | b, _ := hash.(encoding.BinaryMarshaler).MarshalBinary() 64 | return b, nil 65 | } 66 | 67 | func (o Obfuscator) Deobfuscate(wr io.Writer, rd io.Reader) error { 68 | header := make([]byte, 67) 69 | if _, err := io.ReadFull(rd, header); err != nil { 70 | return err 71 | } 72 | if i := header[0]; i != 0 { 73 | return obfuscateError("invalid magic number in header: " + strconv.Itoa(int(i)) + " != 0") 74 | } 75 | 76 | _, err := io.Copy(wr, cipher.StreamReader{ 77 | S: o.makeCipher(header), 78 | R: rd, 79 | }) 80 | return err 81 | } 82 | 83 | func (o Obfuscator) Obfuscate(wr io.Writer, rd io.Reader) error { 84 | b, err := io.ReadAll(rd) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | header := make([]byte, 3, 67) 90 | hash := sha512.New() 91 | hash.Write(b) 92 | header = hash.Sum(header) 93 | header[1] = header[3] ^ 'O' 94 | header[2] = header[4] ^ 'D' 95 | if _, err := wr.Write(header); err != nil { 96 | return err 97 | } 98 | 99 | _, err = cipher.StreamWriter{ 100 | S: o.makeCipher(header), 101 | W: wr, 102 | }.Write(b) 103 | return err 104 | } 105 | 106 | func (o Obfuscator) makeCipher(header []byte) cipher.Stream { 107 | hash := sha1.New() 108 | hash.(encoding.BinaryUnmarshaler).UnmarshalBinary(o) 109 | hash.Write(header[19 : 19+16]) 110 | block, _ := aes.NewCipher(hash.Sum(nil)[:16]) 111 | return cipher.NewCTR(block, header[3:3+16]) 112 | } 113 | -------------------------------------------------------------------------------- /drmfs/dump.go: -------------------------------------------------------------------------------- 1 | package drmfs 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "log" 7 | "os" 8 | "path" 9 | 10 | "github.com/YoshihikoAbe/avsproperty" 11 | "github.com/YoshihikoAbe/eapki/keyring" 12 | "github.com/YoshihikoAbe/fsdump" 13 | ) 14 | 15 | var ( 16 | dirNodeName, _ = avsproperty.NewNodeName("dir") 17 | fileNodeName, _ = avsproperty.NewNodeName("file") 18 | nameNodeName, _ = avsproperty.NewNodeName("name") 19 | 20 | keyNodeName, _ = avsproperty.NewNodeName("key_idx") 21 | pathNodeName, _ = avsproperty.NewNodeName("dst_path") 22 | ) 23 | 24 | type drmError string 25 | 26 | func (err drmError) Error() string { 27 | return "eapki/drmfs: " + string(err) 28 | } 29 | 30 | func Dump(root string, ks keyring.KeySource) (chan fsdump.File, error) { 31 | state := &dumpState{ 32 | root: root, 33 | ch: make(chan fsdump.File, 2), 34 | } 35 | state.obfuscator.Init(ks.ContentsCode()) 36 | 37 | if err := state.openKeyring(ks); err != nil { 38 | return nil, err 39 | } 40 | node, err := state.openFileList() 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | go func() { 46 | defer close(state.ch) 47 | state.dump(node, "") 48 | }() 49 | return state.ch, nil 50 | } 51 | 52 | type dumpState struct { 53 | obfuscator PathObfuscator 54 | keyring *keyring.Keyring 55 | root string 56 | ch chan fsdump.File 57 | } 58 | 59 | func (state *dumpState) dump(node *avsproperty.Node, current string) { 60 | for _, child := range node.Children() { 61 | filename := child.AttributeValueNodeName(nameNodeName) 62 | if len(filename) == 0 { 63 | log.Println("name attribute not found") 64 | continue 65 | } 66 | 67 | if entry := child.Name(); entry.Equals(dirNodeName) { 68 | // recursively walk directory 69 | state.dump(child, path.Join(current, filename)) 70 | } else if entry.Equals(fileNodeName) { 71 | if err := state.dumpFile(child, path.Join(current, filename)); err != nil { 72 | log.Println(err) 73 | } 74 | } else { 75 | log.Println(filename+":", "invalid file type:", entry) 76 | } 77 | } 78 | } 79 | 80 | func (state *dumpState) dumpFile(node *avsproperty.Node, realPath string) error { 81 | var ( 82 | inPath string 83 | key uint32 84 | err error 85 | ) 86 | 87 | // is the path obfuscated? 88 | if child := node.SearchChildNodeName(pathNodeName); child != nil { 89 | if inPath, err = formatHashPath(child.BinaryValue()); err != nil { 90 | return err 91 | } 92 | } else { 93 | inPath = realPath 94 | } 95 | file, err := os.Open(path.Join(state.root, inPath)) 96 | if err != nil { 97 | // log.Println(realPath+":", err) 98 | return nil 99 | } 100 | 101 | // is the file encrypted under drmfs? 102 | if child := node.SearchChildNodeName(keyNodeName); child != nil { 103 | key = uint32(child.UintValue()) 104 | } 105 | rd := io.Reader(file) 106 | if key != 0 { 107 | if rd, err = state.keyring.MakeReader(file, key); err != nil { 108 | return err 109 | } 110 | } 111 | 112 | state.ch <- fsdump.File{ 113 | Reader: rd, 114 | Closer: file, 115 | Path: realPath, 116 | } 117 | return nil 118 | } 119 | 120 | func (state *dumpState) openKeyring(ks keyring.KeySource) error { 121 | rd, err := state.readFile("keyring.dat", -1) 122 | if err != nil { 123 | return err 124 | } 125 | kr, err := keyring.New(rd, ks) 126 | if err != nil { 127 | return err 128 | } 129 | 130 | state.keyring = kr 131 | return nil 132 | } 133 | 134 | func (state *dumpState) openFileList() (*avsproperty.Node, error) { 135 | rd, err := state.readFile("file.inf", 0) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | prop := &avsproperty.Property{} 141 | if err := prop.Read(rd); err != nil { 142 | return nil, err 143 | } 144 | root := prop.Root 145 | if root == nil || root.Name().String() != "fileinfo" { 146 | return nil, drmError("invalid root node in file list") 147 | } 148 | return root, nil 149 | } 150 | 151 | func (state *dumpState) readFile(filename string, key int64) (*bytes.Reader, error) { 152 | f, err := os.Open(path.Join(state.root, state.obfuscator.Obfuscate(filename))) 153 | if err != nil { 154 | return nil, err 155 | } 156 | defer f.Close() 157 | 158 | rd := io.Reader(f) 159 | if key >= 0 { 160 | rd, err = state.keyring.MakeReader(f, 0) 161 | if err != nil { 162 | return nil, err 163 | } 164 | } 165 | b, err := io.ReadAll(rd) 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | state.ch <- fsdump.File{ 171 | Reader: bytes.NewReader(b), 172 | Closer: io.NopCloser(nil), 173 | Path: filename, 174 | } 175 | return bytes.NewReader(b), nil 176 | } 177 | -------------------------------------------------------------------------------- /keyring/keyring.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "encoding/binary" 7 | "io" 8 | ) 9 | 10 | const ( 11 | headerSize = 168 12 | entrySize = 20 13 | masterSize = 128 14 | contentHeaderSize = 30 15 | kekSize = 32 16 | cekSize = 62 17 | ) 18 | 19 | type keyringError string 20 | 21 | func (e keyringError) Error() string { 22 | return "eapki/keyring: " + string(e) 23 | } 24 | 25 | type keyEntry struct { 26 | kekOffset uint32 27 | cekOffset uint32 28 | } 29 | 30 | type keyringString struct { 31 | Size uint32 32 | Value [64]byte 33 | } 34 | 35 | func (s keyringString) string() string { 36 | if s.Size > 64 { 37 | s.Size = 64 38 | } 39 | return string(s.Value[:s.Size]) 40 | } 41 | 42 | type Keyring struct { 43 | rd io.ReaderAt 44 | 45 | entries []keyEntry 46 | keks []byte 47 | 48 | kekOffset uint32 49 | headSize uint32 50 | 51 | master []byte 52 | code string 53 | version string 54 | } 55 | 56 | func New(rd io.ReaderAt, ks KeySource) (*Keyring, error) { 57 | var header struct { 58 | HeadSize uint32 59 | Code keyringString 60 | Version keyringString 61 | KeyCount uint32 62 | _ uint64 63 | MasterOffset uint32 64 | MasterSize uint32 65 | EakekOffset uint32 66 | EakekSize uint32 67 | } 68 | 69 | if err := binary.Read(io.NewSectionReader(rd, 0, headerSize), binary.BigEndian, &header); err != nil { 70 | return nil, err 71 | } 72 | 73 | if header.MasterSize != masterSize { 74 | return nil, keyringError("invalid master key size") 75 | } 76 | if header.EakekSize != contentHeaderSize { 77 | return nil, keyringError("invalid eakek header size") 78 | } 79 | code := header.Code.string() 80 | if code != ks.ContentsCode() { 81 | return nil, keyringError("invalid contents code") 82 | } 83 | 84 | kr := &Keyring{ 85 | rd: rd, 86 | 87 | entries: make([]keyEntry, header.KeyCount), 88 | keks: make([]byte, header.KeyCount*kekSize), 89 | 90 | headSize: header.HeadSize, 91 | 92 | code: code, 93 | version: header.Version.string(), 94 | } 95 | 96 | /* 97 | */ 98 | 99 | entries := make([]byte, entrySize*header.KeyCount) 100 | if _, err := rd.ReadAt(entries, headerSize); err != nil { 101 | return nil, err 102 | } 103 | 104 | for i := range kr.entries { 105 | entry := &kr.entries[i] 106 | entry.kekOffset = binary.BigEndian.Uint32(entries) 107 | if binary.BigEndian.Uint32(entries[4:]) != kekSize { 108 | return nil, keyringError("invalid kek size") 109 | } 110 | entry.cekOffset = binary.BigEndian.Uint32(entries[8:]) 111 | if binary.BigEndian.Uint32(entries[16:]) != cekSize { 112 | return nil, keyringError("invalid cek size") 113 | } 114 | entries = entries[entrySize:] 115 | } 116 | 117 | /* 118 | */ 119 | 120 | master := make([]byte, masterSize) 121 | if _, err := rd.ReadAt(master, int64(header.MasterOffset)+152); err != nil { 122 | return nil, err 123 | } 124 | master, err := ks.DecryptKey(master) 125 | if err != nil { 126 | return nil, err 127 | } 128 | kr.master = master 129 | 130 | /* 131 | */ 132 | 133 | eakekOffset := header.EakekOffset + 160 134 | eakek, err := kr.makeContentReader(io.NewSectionReader(rd, int64(eakekOffset), int64(contentHeaderSize+len(kr.keks))), master) 135 | if err != nil { 136 | return nil, err 137 | } 138 | if _, err := io.ReadFull(eakek, kr.keks); err != nil { 139 | return nil, err 140 | } 141 | kr.kekOffset = eakekOffset + contentHeaderSize 142 | 143 | /* 144 | */ 145 | 146 | return kr, nil 147 | } 148 | 149 | func (kr *Keyring) MakeReader(rd io.Reader, key uint32) (io.Reader, error) { 150 | if key > uint32(len(kr.entries)) { 151 | return nil, keyringError("key not found") 152 | } 153 | entry := kr.entries[key] 154 | 155 | ko := (entry.kekOffset + headerSize + entrySize*key) - kr.kekOffset 156 | if int64(ko)+kekSize > int64(len(kr.keks)) { 157 | return nil, keyringError("invalid kek offset") 158 | } 159 | kek := kr.keks[ko : ko+kekSize] 160 | 161 | crd, err := kr.makeContentReader(io.NewSectionReader(kr.rd, int64(kr.headSize+entry.cekOffset), cekSize), kek) 162 | if err != nil { 163 | return nil, err 164 | } 165 | cek := make([]byte, cekSize-contentHeaderSize) 166 | if _, err := io.ReadFull(crd, cek); err != nil { 167 | return nil, err 168 | } 169 | 170 | return kr.makeContentReader(rd, cek) 171 | } 172 | 173 | func (kr *Keyring) MasterKey() []byte { 174 | return kr.master 175 | } 176 | 177 | func (kr *Keyring) ContentsCode() string { 178 | return kr.code 179 | } 180 | 181 | func (kr *Keyring) Version() string { 182 | return kr.version 183 | } 184 | 185 | func (kr *Keyring) makeContentReader(rd io.Reader, key []byte) (io.Reader, error) { 186 | header := make([]byte, contentHeaderSize) 187 | if _, err := io.ReadFull(rd, header); err != nil { 188 | return nil, err 189 | } 190 | 191 | if header[0] != 6 || header[1] != 3 { 192 | return nil, keyringError("invalid encrypted file header") 193 | } 194 | 195 | block, err := aes.NewCipher(key) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | return &cipher.StreamReader{S: cipher.NewCTR(block, header[14:]), R: rd}, nil 201 | } 202 | -------------------------------------------------------------------------------- /dongle/dongle.go: -------------------------------------------------------------------------------- 1 | package dongle 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/binary" 8 | "io" 9 | "log" 10 | "os" 11 | "runtime" 12 | 13 | "github.com/miekg/pkcs11" 14 | ) 15 | 16 | var ( 17 | certTmpl = []*pkcs11.Attribute{ 18 | pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_CERTIFICATE), 19 | pkcs11.NewAttribute(pkcs11.CKA_CERTIFICATE_TYPE, pkcs11.CKC_X_509), 20 | } 21 | valueTmpl = []*pkcs11.Attribute{pkcs11.NewAttribute(pkcs11.CKA_VALUE, nil)} 22 | ) 23 | 24 | type dongleError string 25 | 26 | func (err dongleError) Error() string { 27 | return "eapki/dongle: " + string(err) 28 | } 29 | 30 | type KeyType int 31 | 32 | const ( 33 | LicenseKey KeyType = iota 34 | AccountKey 35 | ) 36 | 37 | // implements keyring.KeySource, crypto.Decrypter, and crypto.Signer 38 | type Dongle struct { 39 | typ KeyType 40 | 41 | ctx *pkcs11.Ctx 42 | sh pkcs11.SessionHandle 43 | 44 | cert *x509.Certificate 45 | priv pkcs11.ObjectHandle 46 | } 47 | 48 | func Find(typ KeyType) (*Dongle, error) { 49 | dongle := &Dongle{ 50 | typ: typ, 51 | } 52 | if err := dongle.initModule(); err != nil { 53 | return nil, err 54 | } 55 | if err := dongle.find(); err != nil { 56 | dongle.Close() 57 | return nil, err 58 | } 59 | return dongle, nil 60 | } 61 | 62 | func (dongle *Dongle) Type() KeyType { 63 | return dongle.typ 64 | } 65 | 66 | func (dongle *Dongle) Certificate() *x509.Certificate { 67 | return dongle.cert 68 | } 69 | 70 | func (dongle *Dongle) ContentsCode() string { 71 | if dongle.typ != LicenseKey { 72 | return "" 73 | } 74 | return dongle.cert.Subject.CommonName 75 | } 76 | 77 | func (dongle *Dongle) CommonName() string { 78 | return dongle.cert.Subject.CommonName 79 | } 80 | 81 | func (dongle *Dongle) Public() crypto.PublicKey { 82 | return dongle.cert.PublicKey 83 | } 84 | 85 | func (dongle *Dongle) DecryptKey(b []byte) ([]byte, error) { 86 | return dongle.Decrypt(nil, b, nil) 87 | } 88 | 89 | func (dongle *Dongle) Decrypt(rand io.Reader, msg []byte, opts crypto.DecrypterOpts) ([]byte, error) { 90 | if opts != nil { 91 | return nil, dongleError("invalid options for Decrypt") 92 | } 93 | 94 | if err := dongle.ctx.DecryptInit(dongle.sh, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS, nil)}, dongle.priv); err != nil { 95 | return nil, err 96 | } 97 | return dongle.ctx.Decrypt(dongle.sh, msg) 98 | } 99 | 100 | func (dongle *Dongle) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { 101 | var saltLen uint32 102 | 103 | pss, ok := opts.(*rsa.PSSOptions) 104 | if !ok { 105 | return nil, dongleError("invalid options for Sign") 106 | } 107 | 108 | // adapted from https://github.com/ThalesGroup/crypto11/blob/a81014c7c41025fb5533c0c6b1b14bec016be695/rsa.go#L247 109 | 110 | switch pss.SaltLength { 111 | case rsa.PSSSaltLengthAuto: 112 | return nil, dongleError("unsupported PSS salt length") 113 | case rsa.PSSSaltLengthEqualsHash: 114 | saltLen = uint32(pss.Hash.Size()) 115 | default: 116 | saltLen = uint32(pss.SaltLength) 117 | } 118 | 119 | mech, mgf, err := hashToPKCS11(pss.Hash) 120 | if err != nil { 121 | return nil, err 122 | } 123 | param := binary.LittleEndian.AppendUint32(nil, uint32(mech)) 124 | param = binary.LittleEndian.AppendUint32(param, uint32(mgf)) 125 | param = binary.LittleEndian.AppendUint32(param, saltLen) 126 | if err := dongle.ctx.SignInit(dongle.sh, []*pkcs11.Mechanism{pkcs11.NewMechanism(pkcs11.CKM_RSA_PKCS_PSS, param)}, dongle.priv); err != nil { 127 | return nil, err 128 | } 129 | return dongle.ctx.Sign(dongle.sh, digest) 130 | } 131 | 132 | func (dongle *Dongle) Close() { 133 | if dongle.sh != 0 { 134 | dongle.ctx.CloseSession(dongle.sh) 135 | } 136 | dongle.ctx.Finalize() 137 | dongle.ctx.Destroy() 138 | } 139 | 140 | func (dongle *Dongle) find() error { 141 | slots, err := dongle.ctx.GetSlotList(true) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | for _, slot := range slots { 147 | log.Println("opening session on slot", slot) 148 | 149 | if dongle.sh, err = dongle.ctx.OpenSession(slot, pkcs11.CKF_SERIAL_SESSION); err != nil { 150 | return err 151 | } 152 | if err := dongle.initSession(slot); err != nil { 153 | dongle.ctx.CloseSession(dongle.sh) 154 | log.Println(err) 155 | continue 156 | } 157 | 158 | log.Println("initialized dongle:", dongle.cert.Subject.CommonName) 159 | return nil 160 | } 161 | return dongleError("dongle not found") 162 | } 163 | 164 | func (dongle *Dongle) initSession(slot uint) error { 165 | if err := dongle.findCert(); err != nil { 166 | return err 167 | } 168 | if err := dongle.login(slot); err != nil { 169 | return err 170 | } 171 | return dongle.findPriv() 172 | } 173 | 174 | func (dongle *Dongle) login(slot uint) error { 175 | info, err := dongle.ctx.GetTokenInfo(slot) 176 | if err != nil { 177 | return err 178 | } 179 | if info.Flags&pkcs11.CKF_USER_PIN_COUNT_LOW != 0 { 180 | return dongleError("number of remaining login attempts is low") 181 | } 182 | 183 | pg, err := NewPinGenerator([]byte(info.SerialNumber)) 184 | if err != nil { 185 | return err 186 | } 187 | for i := 0; i < NumberOfPins; i++ { 188 | if err := dongle.ctx.Login(dongle.sh, pkcs11.CKU_USER, string(pg.Generate())); err == nil { 189 | return nil 190 | } else { 191 | log.Printf("login attempt failed (%d/%d)\n", i+1, NumberOfPins) 192 | } 193 | } 194 | return dongleError("all login attempts failed") 195 | } 196 | 197 | func (dongle *Dongle) findPriv() error { 198 | pub, ok := dongle.cert.PublicKey.(*rsa.PublicKey) 199 | if !ok { 200 | return dongleError("certificate does not contain an RSA public key") 201 | } 202 | 203 | // search for a private key with the same modulus as the public key 204 | priv, err := dongle.findObjects([]*pkcs11.Attribute{ 205 | pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), 206 | pkcs11.NewAttribute(pkcs11.CKA_MODULUS, pub.N.Bytes()), 207 | }) 208 | if err != nil { 209 | return err 210 | } 211 | 212 | dongle.priv = priv[0] 213 | return nil 214 | } 215 | 216 | func (dongle *Dongle) findCert() error { 217 | var newest *x509.Certificate 218 | 219 | objs, err := dongle.findObjects(certTmpl) 220 | if err != nil { 221 | return err 222 | } 223 | 224 | // find the newest certificate 225 | for _, obj := range objs { 226 | attribs, err := dongle.ctx.GetAttributeValue(dongle.sh, obj, valueTmpl) 227 | if err != nil { 228 | return err 229 | } 230 | 231 | cert, err := x509.ParseCertificate(attribs[0].Value) 232 | if err != nil { 233 | return err 234 | } 235 | 236 | if newest == nil || cert.NotAfter.After(newest.NotAfter) { 237 | newest = cert 238 | } 239 | } 240 | 241 | ou := newest.Subject.OrganizationalUnit 242 | if len(ou) == 0 { 243 | return dongleError("certificate missing OU in subject") 244 | } 245 | if (dongle.typ == LicenseKey && ou[0] == "e-AMUSEMENT/License") || 246 | (dongle.typ == AccountKey && (ou[0] == "e-AMUSEMENT/Game" || ou[0] == "e-AMUSEMENT/Charge")) { 247 | dongle.cert = newest 248 | return nil 249 | } 250 | return dongleError("invalid certificate for dongle type: " + ou[0]) 251 | } 252 | 253 | func (dongle *Dongle) findObjects(tmpl []*pkcs11.Attribute) ([]pkcs11.ObjectHandle, error) { 254 | if err := dongle.ctx.FindObjectsInit(dongle.sh, tmpl); err != nil { 255 | return nil, err 256 | } 257 | defer dongle.ctx.FindObjectsFinal(dongle.sh) 258 | 259 | objs, _, err := dongle.ctx.FindObjects(dongle.sh, 30) 260 | if err != nil { 261 | return nil, err 262 | } 263 | if len(objs) == 0 { 264 | return nil, dongleError("no objects found") 265 | } 266 | 267 | return objs, nil 268 | } 269 | 270 | func (dongle *Dongle) initModule() error { 271 | name := getModuleName() 272 | if dongle.ctx = pkcs11.New(name); dongle.ctx == nil { 273 | return dongleError(name + ": " + " failed to open module") 274 | } 275 | if err := dongle.ctx.Initialize(); err != nil { 276 | dongle.ctx.Destroy() 277 | return err 278 | } 279 | return nil 280 | } 281 | 282 | func getModuleName() (module string) { 283 | module = os.Getenv("PKCS11_MODULE") 284 | if module == "" { 285 | if runtime.GOOS == "windows" { 286 | module = "eTPkcs11.dll" 287 | } else if runtime.GOOS == "linux" { 288 | module = "libeTPkcs11.so" 289 | } else { 290 | panic("automatic module detection not supported on this platform") 291 | } 292 | } 293 | return 294 | } 295 | 296 | // adapted from https://github.com/ThalesGroup/crypto11/blob/a81014c7c41025fb5533c0c6b1b14bec016be695/rsa.go#L230 297 | func hashToPKCS11(hash crypto.Hash) (mech uint, mgf uint, err error) { 298 | switch hash { 299 | case crypto.SHA1: 300 | return pkcs11.CKM_SHA_1, pkcs11.CKG_MGF1_SHA1, nil 301 | case crypto.SHA224: 302 | return pkcs11.CKM_SHA224, pkcs11.CKG_MGF1_SHA224, nil 303 | case crypto.SHA256: 304 | return pkcs11.CKM_SHA256, pkcs11.CKG_MGF1_SHA256, nil 305 | case crypto.SHA384: 306 | return pkcs11.CKM_SHA384, pkcs11.CKG_MGF1_SHA384, nil 307 | case crypto.SHA512: 308 | return pkcs11.CKM_SHA512, pkcs11.CKG_MGF1_SHA512, nil 309 | default: 310 | return 0, 0, dongleError("invalid hash function") 311 | } 312 | } 313 | --------------------------------------------------------------------------------