diff --git a/Gopkg.lock b/Gopkg.lock index 6a85d80..5736959 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -1,6 +1,12 @@ # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. +[[projects]] + name = "github.com/SermoDigital/jose" + packages = [".","crypto","jws","jwt"] + revision = "f6df55f235c24f236d11dbcf665249a59ac2021f" + version = "1.1" + [[projects]] name = "github.com/assembla/cony" packages = ["."] @@ -55,6 +61,12 @@ packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"] revision = "8f6b1344a92ff8877cf24a5de9177bf7d0a2a187" +[[projects]] + branch = "master" + name = "github.com/howeyc/gopass" + packages = ["."] + revision = "bf9dde6d0d2c004a008c27aaee91170c786f6db8" + [[projects]] name = "github.com/inconshreveable/mousetrap" packages = ["."] @@ -73,6 +85,12 @@ packages = ["cluster","pool","redis","util"] revision = "d234cfb904a91daafa4e1f92599a893b349cc0c2" +[[projects]] + branch = "master" + name = "github.com/mitchellh/go-homedir" + packages = ["."] + revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" + [[projects]] branch = "master" name = "github.com/mitchellh/mapstructure" @@ -109,6 +127,12 @@ revision = "792786c7400a136282c1664665ae0a8db921c6c2" version = "v1.0.0" +[[projects]] + branch = "master" + name = "github.com/segmentio/go-prompt" + packages = ["."] + revision = "f0d19b6901ade831d5a3204edc0d6a7d6457fbb2" + [[projects]] branch = "master" name = "github.com/spf13/afero" @@ -157,10 +181,16 @@ revision = "69483b4bd14f5845b5a1e55bca19e954e827f1d0" version = "v1.1.4" +[[projects]] + branch = "master" + name = "golang.org/x/crypto" + packages = ["ssh/terminal"] + revision = "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8" + [[projects]] branch = "master" name = "golang.org/x/sys" - packages = ["unix"] + packages = ["unix","windows"] revision = "7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce" [[projects]] @@ -184,6 +214,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "11938f85225b2839e4ed7cd4345bed8f44510b6eb50c003b89c8e14e0fd6b6e7" + inputs-digest = "a12e681ec671ce8a93256cd754d4e70797476b2d2ce4379c3860df09c4b6a552" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index da94f32..a1ba591 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -23,6 +23,17 @@ ignored = ["elyby/minecraft-skinsystem"] name = "github.com/assembla/cony" version = "^0.3.2" +[[constraint]] + name = "github.com/SermoDigital/jose" + version = "~1.1.0" + +[[constraint]] + name = "github.com/mitchellh/go-homedir" + +[[constraint]] + name = "github.com/segmentio/go-prompt" + branch = "master" + # Testing dependencies [[constraint]] diff --git a/auth/jwt.go b/auth/jwt.go new file mode 100644 index 0000000..8ed9f97 --- /dev/null +++ b/auth/jwt.go @@ -0,0 +1,124 @@ +package auth + +import ( + "encoding/base64" + "io/ioutil" + "math" + "math/rand" + "os" + "time" + + "github.com/SermoDigital/jose/crypto" + "github.com/SermoDigital/jose/jws" + "github.com/mitchellh/go-homedir" +) + +var hashAlg = crypto.SigningMethodHS256 + +const appHomeDirName = ".minecraft-skinsystem" +const scopesClaim = "scopes" + +type Scope string + +var ( + SkinScope = Scope("skin") +) + +type JwtAuth struct { + signingKey []byte +} + +func (t *JwtAuth) NewToken(scopes ...Scope) ([]byte, error) { + key, err := t.getSigningKey() + if err != nil { + return nil, err + } + + claims := jws.Claims{} + claims.Set(scopesClaim, scopes) + claims.SetIssuedAt(time.Now()) + encoder := jws.NewJWT(claims, hashAlg) + token, err := encoder.Serialize(key) + if err != nil { + return nil, err + } + + return token, nil +} + +func (t *JwtAuth) GenerateSigningKey() error { + if err := createAppHomeDir(); err != nil { + return err + } + + key := generateRandomBytes(64) + if err := ioutil.WriteFile(getKeyPath(), key, 0600); err != nil { + return err + } + + return nil +} + +func (t *JwtAuth) getSigningKey() ([]byte, error) { + if t.signingKey != nil { + return t.signingKey, nil + } + + path := getKeyPath() + if _, err := os.Stat(path); err != nil { + if os.IsNotExist(err) { + return nil, &SigningKeyNotAvailable{} + } + + return nil, err + } + + key, err := ioutil.ReadFile(path) + if err != nil { + return nil, err + } + + return key, nil +} + +func createAppHomeDir() error { + path := getAppHomeDirPath() + if _, err := os.Stat(path); os.IsNotExist(err) { + err := os.Mkdir(path, 0755) // rwx r-x r-x + if err != nil { + return err + } + } + + return nil +} + +func getAppHomeDirPath() string { + path, err := homedir.Expand("~/" + appHomeDirName) + if err != nil { + panic(err) + } + + return path +} + +func getKeyPath() string { + return getAppHomeDirPath() + "/jwt-key" +} + +func generateRandomBytes(n int) []byte { + randLen := int(math.Ceil(float64(n) / 1.37)) // base64 will increase length in 1.37 times + randBytes := make([]byte, randLen) + rand.Read(randBytes) + resBytes := make([]byte, n) + base64.URLEncoding.Encode(resBytes, randBytes) + + return resBytes +} + +type SigningKeyNotAvailable struct { +} + +func (*SigningKeyNotAvailable) Error() string { + return "Signing key not available" +} diff --git a/cmd/root.go b/cmd/root.go index 7c32742..d4b0df2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -41,6 +41,7 @@ func initConfig() { viper.AutomaticEnv() if err := viper.ReadInConfig(); err == nil { + // TODO: show only on verbose mode fmt.Println("Using config file:", viper.ConfigFileUsed()) } } diff --git a/cmd/token.go b/cmd/token.go new file mode 100644 index 0000000..a26f681 --- /dev/null +++ b/cmd/token.go @@ -0,0 +1,65 @@ +package cmd + +import ( + "fmt" + "log" + + "elyby/minecraft-skinsystem/auth" + + "github.com/segmentio/go-prompt" + "github.com/spf13/cobra" +) + +var tokenCmd = &cobra.Command{ + Use: "token", + Short: "API tokens operations", +} + +var createCmd = &cobra.Command{ + Use: "create", + Short: "Create the new token, that allows interacting with Ely.by Skinsystem API", + Run: func(cmd *cobra.Command, args []string) { + jwtAuth := &auth.JwtAuth{} + for { + token, err := jwtAuth.NewToken(auth.SkinScope) + if err != nil { + if _, ok := err.(*auth.SigningKeyNotAvailable); !ok { + log.Fatalf("Unable to create new token. The error is %v\n", err) + } + + log.Println("Signing key not available. Creating...") + err := jwtAuth.GenerateSigningKey() + if err != nil { + log.Fatalf("Unable to generate new signing key. The error is %v\n", err) + } + + continue + } + + fmt.Printf("%s\n", token) + } + }, +} + +var resetCmd = &cobra.Command{ + Use: "reset", + Short: "Regenerate the secret key, that invalidate all tokens", + Run: func(cmd *cobra.Command, args []string) { + if !prompt.Confirm("Do you really want to invalidate all exists tokens?") { + fmt.Println("Aboart.") + return + } + + jwtAuth := &auth.JwtAuth{} + if err := jwtAuth.GenerateSigningKey(); err != nil { + log.Fatalf("Unable to generate new signing key. The error is %v\n", err) + } + + fmt.Println("Token successfully regenerated.") + }, +} + +func init() { + tokenCmd.AddCommand(createCmd, resetCmd) + RootCmd.AddCommand(tokenCmd) +}