From 7e1abcd47897d32695997a95e4d230873586bcf7 Mon Sep 17 00:00:00 2001 From: Robin Neatherway Date: Tue, 17 May 2022 21:58:34 +0100 Subject: [PATCH] Add support for windows --- .goreleaser.yml | 7 +- go.mod | 4 +- go.sum | 8 +- internal/slackclient/cookie_password_linux.go | 15 ---- internal/slackclient/cookie_password_macos.go | 15 ---- .../slackclient/cookie_password_windows.go | 51 +++++++++++++ internal/slackclient/slack_configuration.go | 31 ++++++++ internal/slackclient/token.go | 73 +++++++++---------- internal/slackclient/unix_decryptor.go | 35 +++++++++ internal/slackclient/windows_decryptor.go | 28 +++++++ 10 files changed, 193 insertions(+), 74 deletions(-) create mode 100644 internal/slackclient/cookie_password_windows.go create mode 100644 internal/slackclient/slack_configuration.go create mode 100644 internal/slackclient/unix_decryptor.go create mode 100644 internal/slackclient/windows_decryptor.go diff --git a/.goreleaser.yml b/.goreleaser.yml index 3aad830..fc50bad 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -2,8 +2,13 @@ project_name: slack builds: - main: ./cmd/gh-slack - goos: [linux, darwin] + goos: [linux, darwin, windows] goarch: [amd64, arm64] + ignore: + - goos: linux + goarch: arm64 + - goos: windows + goarch: arm64 ldflags: - -X github.com/rneatherway/gh-slack/internal/version.version={{.Version}} - -X github.com/rneatherway/gh-slack/internal/version.commit={{.Commit}} diff --git a/go.mod b/go.mod index 9cd9be7..8afca7b 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,11 @@ module github.com/rneatherway/gh-slack go 1.18 require ( + github.com/billgraziano/dpapi v0.4.0 github.com/cli/go-gh v0.0.3 github.com/jessevdk/go-flags v1.5.0 github.com/zalando/go-keyring v0.2.1 - golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 modernc.org/sqlite v1.15.3 r00t2.io/gosecret v1.1.5 ) @@ -21,6 +22,7 @@ require ( github.com/henvic/httpretty v0.0.6 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/mattn/go-isatty v0.0.12 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect golang.org/x/mod v0.3.0 // indirect golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect diff --git a/go.sum b/go.sum index e06fb60..19b08ef 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/billgraziano/dpapi v0.4.0 h1:t39THI1Ld1hkkLVrhkOX6u5TUxwzRddOffq4jcwh2AE= +github.com/billgraziano/dpapi v0.4.0/go.mod h1:gi1Lin0jvovT53j0EXITkY6UPb3hTfI92POaZgj9JBA= github.com/cli/go-gh v0.0.3 h1:GcVgUa7q0SeauIRbch3VSUXVij6+c49jtAHv7WuWj5c= github.com/cli/go-gh v0.0.3/go.mod h1:J1eNgrPJYAUy7TwPKj7GW1ibqI+WCiMndtyzrCyZIiQ= github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= @@ -36,6 +38,8 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.14.10 h1:MLn+5bFRlWMGoSRmJour3CL1w/qL96mvipqpwQW/Sfk= github.com/mattn/go-sqlite3 v1.14.10/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= @@ -50,9 +54,8 @@ github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2 github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= -golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -65,6 +68,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200828161417-c663848e9a16/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/slackclient/cookie_password_linux.go b/internal/slackclient/cookie_password_linux.go index 0f87ebe..77ce7d2 100644 --- a/internal/slackclient/cookie_password_linux.go +++ b/internal/slackclient/cookie_password_linux.go @@ -5,21 +5,10 @@ package slackclient import ( "errors" - "os" - "path" "r00t2.io/gosecret" ) -func slackConfigDirs() []string { - if xdgConfigDir, found := os.LookupEnv("XDG_CONFIG_DIR"); found { - return []string{xdgConfigDir} - } - - home := os.Getenv("HOME") - return []string{path.Join(home, ".config")} -} - func cookiePassword() ([]byte, error) { service, err := gosecret.NewService() if err != nil { @@ -46,7 +35,3 @@ func cookiePassword() ([]byte, error) { return nil, errors.New("multiple items found") } } - -func iterations() int { - return 1 -} diff --git a/internal/slackclient/cookie_password_macos.go b/internal/slackclient/cookie_password_macos.go index 005da01..d5412ae 100644 --- a/internal/slackclient/cookie_password_macos.go +++ b/internal/slackclient/cookie_password_macos.go @@ -4,20 +4,9 @@ package slackclient import ( - "os" - "path" - "github.com/zalando/go-keyring" ) -func slackConfigDirs() []string { - home := os.Getenv("HOME") - return []string{ - path.Join(home, "Library", "Application Support"), - path.Join(home, "Library", "Containers", "com.tinyspeck.slackmacgap", "Data", "Library", "Application Support"), - } -} - func cookiePassword() ([]byte, error) { secret, err := keyring.Get("Slack Safe Storage", "Slack") if err != nil { @@ -26,7 +15,3 @@ func cookiePassword() ([]byte, error) { return []byte(secret), nil } - -func iterations() int { - return 1003 -} diff --git a/internal/slackclient/cookie_password_windows.go b/internal/slackclient/cookie_password_windows.go new file mode 100644 index 0000000..7527473 --- /dev/null +++ b/internal/slackclient/cookie_password_windows.go @@ -0,0 +1,51 @@ +//go:build windows +// +build windows + +package slackclient + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "os" + "path" + + "github.com/billgraziano/dpapi" +) + +type EncryptedKey struct { + EncryptedKey string `json:"encrypted_key"` +} +type LocalState struct { + OSCrypt EncryptedKey `json:"os_crypt"` +} + +func cookiePassword() ([]byte, error) { + bs, err := os.ReadFile(path.Join(slackConfigDir(), "Local State")) + if err != nil { + return nil, err + } + + var localState LocalState + err = json.Unmarshal(bs, &localState) + if err != nil { + return nil, err + } + + encryptedKey, err := base64.StdEncoding.DecodeString(localState.OSCrypt.EncryptedKey) + if err != nil { + return nil, err + } + + encryptionMethod := encryptedKey[:5] + if string(encryptionMethod) != "DPAPI" { + return nil, fmt.Errorf("encryption method %q is not supported", encryptionMethod) + } + + encryptedKey = encryptedKey[5:] + decryptedKey, err := dpapi.DecryptBytes(encryptedKey) + if err != nil { + return nil, err + } + return decryptedKey, nil +} diff --git a/internal/slackclient/slack_configuration.go b/internal/slackclient/slack_configuration.go new file mode 100644 index 0000000..94451e5 --- /dev/null +++ b/internal/slackclient/slack_configuration.go @@ -0,0 +1,31 @@ +package slackclient + +import ( + "fmt" + "os" + "path" + "runtime" +) + +func slackConfigDir() string { + switch runtime.GOOS { + case "windows": + return path.Join(os.Getenv("APPDATA"), "Slack") + case "darwin": + home := os.Getenv("HOME") + first := path.Join(home, "Library", "Application Support", "Slack") + second := path.Join(home, "Library", "Containers", "com.tinyspeck.slackmacgap", "Data", "Library", "Application Support", "Slack") + + if _, err := os.Stat(first); err == nil { + return first + } + return second + case "linux": + if xdgConfigDir, found := os.LookupEnv("XDG_CONFIG_DIR"); found { + return path.Join(xdgConfigDir, "Slack") + } + return path.Join(os.Getenv("HOME"), ".config", "Slack") + default: + panic(fmt.Sprintf("Platform %q not supported", runtime.GOOS)) + } +} diff --git a/internal/slackclient/token.go b/internal/slackclient/token.go index 7d0785e..e615605 100644 --- a/internal/slackclient/token.go +++ b/internal/slackclient/token.go @@ -1,23 +1,18 @@ package slackclient import ( - "crypto/aes" - "crypto/cipher" - "crypto/sha1" "database/sql" "errors" "fmt" "io" - "io/fs" "net/http" "net/url" "os" "path" "regexp" + "runtime" "strings" - "golang.org/x/crypto/pbkdf2" - _ "modernc.org/sqlite" ) @@ -29,25 +24,34 @@ type SlackAuth struct { var stmt = "SELECT value, encrypted_value FROM cookies WHERE host_key=\".slack.com\" AND name=\"d\"" type CookieDecryptor interface { - Password() string + Decrypt(value, key []byte) ([]byte, error) +} + +func decrypt(encryptedValue, key []byte) ([]byte, error) { + switch runtime.GOOS { + case "windows": + return WindowsDecryptor{}.Decrypt(encryptedValue, key) + case "darwin": + return UnixCookieDecryptor{rounds: 1003}.Decrypt(encryptedValue, key) + case "linux": + return UnixCookieDecryptor{rounds: 1}.Decrypt(encryptedValue, key) + default: + panic(fmt.Sprintf("platform %q not supported", runtime.GOOS)) + } } func getCookie() (string, error) { - var cookieDBFile string - - for _, config := range slackConfigDirs() { - cookieDBFile = path.Join(config, "Slack", "Cookies") - stat, err := os.Stat(cookieDBFile) - if errors.Is(err, fs.ErrNotExist) { - continue - } - if err != nil { - return "", fmt.Errorf("could not access Slack cookie database: %w", err) - } - if stat.IsDir() { - return "", fmt.Errorf("directory found at expected Slack cookie database location %q", cookieDBFile) - } - break + cookieDBFile := path.Join(slackConfigDir(), "Cookies") + if runtime.GOOS == "windows" { + cookieDBFile = path.Join(slackConfigDir(), "Network", "Cookies") + } + + stat, err := os.Stat(cookieDBFile) + if err != nil { + return "", fmt.Errorf("could not access Slack cookie database: %w", err) + } + if stat.IsDir() { + return "", fmt.Errorf("directory found at expected Slack cookie database location %q", cookieDBFile) } if cookieDBFile == "" { @@ -60,8 +64,8 @@ func getCookie() (string, error) { } var cookie string - var encrypted_value []byte - err = db.QueryRow(stmt).Scan(&cookie, &encrypted_value) + var encryptedValue []byte + err = db.QueryRow(stmt).Scan(&cookie, &encryptedValue) if err != nil { return "", err } @@ -70,32 +74,21 @@ func getCookie() (string, error) { return cookie, nil } - // We need to decrypt the cookie. + // Remove the version number e.g. v11 + encryptedValue = encryptedValue[3:] + // We need to decrypt the cookie. key, err := cookiePassword() if err != nil { return "", fmt.Errorf("failed to get cookie password: %w", err) } - dk := pbkdf2.Key(key, []byte("saltysalt"), iterations(), 16, sha1.New) - block, err := aes.NewCipher(dk) + decryptedValue, err := decrypt(encryptedValue, key) if err != nil { return "", err } - iv := make([]byte, 16) - for i := range iv { - iv[i] = ' ' - } - - mode := cipher.NewCBCDecrypter(block, iv) - - encrypted_value = encrypted_value[3:] - mode.CryptBlocks(encrypted_value, encrypted_value) - - bytesToStrip := int(encrypted_value[len(encrypted_value)-1]) - - return string(encrypted_value[:len(encrypted_value)-bytesToStrip]), nil + return string(decryptedValue), err } var apiTokenRE = regexp.MustCompile("\"api_token\":\"([^\"]+)\"") diff --git a/internal/slackclient/unix_decryptor.go b/internal/slackclient/unix_decryptor.go new file mode 100644 index 0000000..f7c6501 --- /dev/null +++ b/internal/slackclient/unix_decryptor.go @@ -0,0 +1,35 @@ +package slackclient + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha1" + + "golang.org/x/crypto/pbkdf2" +) + +type UnixCookieDecryptor struct { + rounds int +} + +func (d UnixCookieDecryptor) Decrypt(value, key []byte) ([]byte, error) { + dk := pbkdf2.Key(key, []byte("saltysalt"), d.rounds, 16, sha1.New) + + block, err := aes.NewCipher(dk) + if err != nil { + return nil, err + } + + iv := make([]byte, 16) + for i := range iv { + iv[i] = ' ' + } + + mode := cipher.NewCBCDecrypter(block, iv) + + mode.CryptBlocks(value, value) + + bytesToStrip := int(value[len(value)-1]) + + return value[:len(value)-bytesToStrip], nil +} diff --git a/internal/slackclient/windows_decryptor.go b/internal/slackclient/windows_decryptor.go new file mode 100644 index 0000000..a0d42a5 --- /dev/null +++ b/internal/slackclient/windows_decryptor.go @@ -0,0 +1,28 @@ +package slackclient + +import ( + "crypto/aes" + "crypto/cipher" + "fmt" +) + +type WindowsDecryptor struct{} + +func (WindowsDecryptor) Decrypt(value, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, fmt.Errorf("failed to create NewGCM: %w", err) + } + + decryptedValue := make([]byte, 0) + decryptedValue, err = gcm.Open(decryptedValue, value[:12], value[12:], nil) + if err != nil { + return nil, fmt.Errorf("failed to decrypt: %w", err) + } + return decryptedValue, nil +}