From 06ef899c26cd27c61ceb37b184c415da6bd011a8 Mon Sep 17 00:00:00 2001 From: Brad Rydzewski Date: Tue, 18 Aug 2015 23:59:30 -0700 Subject: [PATCH] initial implementation and endpoint for #1145 --- cmd/drone-server/drone.go | 1 + pkg/server/commits.go | 5 ++ pkg/server/hooks.go | 5 ++ pkg/server/repos.go | 21 ++++++ pkg/yaml/secure/secure.go | 115 +++++++++++++++++++++++++++++++++ pkg/yaml/secure/secure_test.go | 75 +++++++++++++++++++++ 6 files changed, 222 insertions(+) create mode 100644 pkg/yaml/secure/secure.go create mode 100644 pkg/yaml/secure/secure_test.go diff --git a/cmd/drone-server/drone.go b/cmd/drone-server/drone.go index 45163a719..5c3606a8d 100644 --- a/cmd/drone-server/drone.go +++ b/cmd/drone-server/drone.go @@ -174,6 +174,7 @@ func main() { repo.GET("", server.GetRepo) repo.PATCH("", server.PutRepo) repo.DELETE("", server.DeleteRepo) + repo.POST("/encrypt", server.Encrypt) repo.POST("/watch", server.Subscribe) repo.DELETE("/unwatch", server.Unsubscribe) diff --git a/pkg/server/commits.go b/pkg/server/commits.go index 04c9e4848..6694fd4c3 100644 --- a/pkg/server/commits.go +++ b/pkg/server/commits.go @@ -10,6 +10,7 @@ import ( "github.com/drone/drone/pkg/queue" common "github.com/drone/drone/pkg/types" "github.com/drone/drone/pkg/yaml/inject" + "github.com/drone/drone/pkg/yaml/secure" // "github.com/gin-gonic/gin/binding" ) @@ -182,6 +183,10 @@ func RunBuild(c *gin.Context) { if repo.Params != nil && len(repo.Params) != 0 { raw = []byte(inject.InjectSafe(string(raw), repo.Params)) } + encrypted, _ := secure.Parse(repo.Hash, string(raw)) + if encrypted != nil && len(encrypted) != 0 { + raw = []byte(inject.InjectSafe(string(raw), encrypted)) + } c.JSON(202, build) diff --git a/pkg/server/hooks.go b/pkg/server/hooks.go index 3263ac6c6..a5bb99489 100644 --- a/pkg/server/hooks.go +++ b/pkg/server/hooks.go @@ -10,6 +10,7 @@ import ( "github.com/drone/drone/pkg/yaml" "github.com/drone/drone/pkg/yaml/inject" "github.com/drone/drone/pkg/yaml/matrix" + "github.com/drone/drone/pkg/yaml/secure" ) // PostHook accepts a post-commit hook and parses the payload @@ -100,6 +101,10 @@ func PostHook(c *gin.Context) { if repo.Params != nil && len(repo.Params) != 0 { raw = []byte(inject.InjectSafe(string(raw), repo.Params)) } + encrypted, _ := secure.Parse(repo.Hash, string(raw)) + if encrypted != nil && len(encrypted) != 0 { + raw = []byte(inject.InjectSafe(string(raw), encrypted)) + } axes, err := matrix.Parse(string(raw)) if err != nil { log.Errorf("failure to calculate matrix for %s. %s", repo.FullName, err) diff --git a/pkg/server/repos.go b/pkg/server/repos.go index a27c342dc..c3931660d 100644 --- a/pkg/server/repos.go +++ b/pkg/server/repos.go @@ -3,6 +3,7 @@ package server import ( "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "github.com/drone/drone/Godeps/_workspace/src/github.com/gin-gonic/gin" @@ -12,6 +13,7 @@ import ( common "github.com/drone/drone/pkg/types" "github.com/drone/drone/pkg/utils/httputil" "github.com/drone/drone/pkg/utils/sshutil" + "github.com/drone/drone/pkg/yaml/secure" ) // repoResp is a data structure used for sending @@ -240,6 +242,25 @@ func PostRepo(c *gin.Context) { c.JSON(200, r) } +// Encrypt accapets a request to encrypt the +// body of the request using the repository secret +// key. +// +// POST /api/repos/:owner/:name/encrypt +// +func Encrypt(c *gin.Context) { + repo := ToRepo(c) + + in := map[string]string{} + json.NewDecoder(c.Request.Body).Decode(&in) + err := secure.EncryptMap(repo.Hash, in) + if err != nil { + c.Fail(500, err) + return + } + c.JSON(200, &in) +} + // Unubscribe accapets a request to unsubscribe the // currently authenticated user to the repository. // diff --git a/pkg/yaml/secure/secure.go b/pkg/yaml/secure/secure.go new file mode 100644 index 000000000..3ad71c206 --- /dev/null +++ b/pkg/yaml/secure/secure.go @@ -0,0 +1,115 @@ +package secure + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + + "github.com/drone/drone/Godeps/_workspace/src/gopkg.in/yaml.v2" +) + +// Parse parses and returns the secure section of the +// yaml file as plaintext parameters. +func Parse(key, raw string) (map[string]string, error) { + params, err := parseSecure(raw) + if err != nil { + return nil, err + } + err = DecryptMap(key, params) + return params, err +} + +// Encrypt encrypts a string to base64 crypto using AES. +func Encrypt(key, text string) (_ string, err error) { + plaintext := []byte(text) + + block, err := aes.NewCipher(trimKey(key)) + if err != nil { + return + } + + ciphertext := make([]byte, aes.BlockSize+len(plaintext)) + iv := ciphertext[:aes.BlockSize] + if _, err = io.ReadFull(rand.Reader, iv); err != nil { + return + } + + stream := cipher.NewCFBEncrypter(block, iv) + stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext) + + return base64.URLEncoding.EncodeToString(ciphertext), nil +} + +// Decrypt decrtyps from base64 to decrypted string. +func Decrypt(key, text string) (_ string, err error) { + ciphertext, err := base64.URLEncoding.DecodeString(text) + if err != nil { + return + } + + block, err := aes.NewCipher(trimKey(key)) + if err != nil { + return + } + + if len(ciphertext) < aes.BlockSize { + err = fmt.Errorf("ciphertext too short") + return + } + iv := ciphertext[:aes.BlockSize] + ciphertext = ciphertext[aes.BlockSize:] + + stream := cipher.NewCFBDecrypter(block, iv) + stream.XORKeyStream(ciphertext, ciphertext) + + return fmt.Sprintf("%s", ciphertext), nil +} + +// DecryptMap decrypts a map of named parameters +// from base64 to decrypted string. +func DecryptMap(key string, params map[string]string) error { + var err error + for name, value := range params { + params[name], err = Decrypt(key, value) + if err != nil { + return err + } + } + return nil +} + +// EncryptMap encrypts encrypts a map of string parameters +// to base64 crypto using AES. +func EncryptMap(key string, params map[string]string) error { + var err error + for name, value := range params { + params[name], err = Encrypt(key, value) + if err != nil { + return err + } + } + return nil +} + +// helper function that trims a key to a maximum +// of 32 bytes to match the expected AES block size. +func trimKey(key string) []byte { + b := []byte(key) + if len(b) > 32 { + b = b[:32] + } + return b +} + +// helper function to parse the Secure data from +// the raw yaml file. +func parseSecure(raw string) (map[string]string, error) { + data := struct { + Secure map[string]string + }{} + err := yaml.Unmarshal([]byte(raw), &data) + return data.Secure, err +} diff --git a/pkg/yaml/secure/secure_test.go b/pkg/yaml/secure/secure_test.go new file mode 100644 index 000000000..8fed79f1f --- /dev/null +++ b/pkg/yaml/secure/secure_test.go @@ -0,0 +1,75 @@ +package secure + +import ( + "testing" + + "github.com/drone/drone/Godeps/_workspace/src/github.com/franela/goblin" +) + +func Test_Secure(t *testing.T) { + + g := goblin.Goblin(t) + g.Describe("Encrypt params", func() { + + key := "9T2tH3qZ8FSPr9uxrhzV4mn2VdVgA56xPVtYvCh0" + text := "super_duper_secret" + long := "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC,32495A90F3FF199D\nlrMAsSjjkKiRxGdgR8p5kZJj0AFgdWYa3OT2snIXnN5+/p7j13PSkseUcrAFyokc\nV9pgeDfitAhb9lpdjxjjuxRcuQjBfmNVLPF9MFyNOvhrprGNukUh/12oSKO9dFEt\ns39F/2h6Ld5IQrGt3gZaBB1aGO+tw3ill1VBy2zGPIDeuSz6DS3GG/oQ2gLSSMP4\nOVfQ32Oajo496iHRkdIh/7Hho7BNzMYr1GxrYTcE9/Znr6xgeSdNT37CCeCH8cmP\naEAUgSMTeIMVSpILwkKeNvBURic1EWaqXRgPRIWK0vNyOCs/+jNoFISnV4pu1ROF\n92vayHDNSVw9wHcdSQ75XSE4Msawqv5U1iI7e2lD64uo1qhmJdrPcXDJQCiDbh+F\nhQhF+wAoLRvMNwwhg+LttL8vXqMDQl3olsWSvWPs6b/MZpB0qwd1bklzA6P+PeAU\nsfOvTqi9edIOfKqvXqTXEhBP8qC7ZtOKLGnryZb7W04SSVrNtuJUFRcLiqu+w/F/\nMSxGSGalYpzIZ1B5HLQqISgWMXdbt39uMeeooeZjkuI3VIllFjtybecjPR9ZYQPt\nFFEP1XqNXjLFmGh84TXtvGLWretWM1OZmN8UKKUeATqrr7zuh5AYGAIbXd8BvweL\nPigl9ei0hTculPqohvkoc5x1srPBvzHrirGlxOYjW3fc4kDgZpy+6ik5k5g7JWQD\nlbXCRz3HGazgUPeiwUr06a52vhgT7QuNIUZqdHb4IfCYs2pQTLHzQjAqvVk1mm2D\nkh4myIcTtf69BFcu/Wuptm3NaKd1nwk1squR6psvcTXOWII81pstnxNYkrokx4r2\n7YVllNruOD+cMDNZbIG2CwT6V9ukIS8tl9EJp8eyb0a1uAEc22BNOjYHPF50beWF\nukf3uc0SA+G3zhmXCM5sMf5OxVjKr5jgcir7kySY5KbmG71omYhczgr4H0qgxYo9\nZyj2wMKrTHLfFOpd4OOEun9Gi3srqlKZep7Hj7gNyUwZu1qiBvElmBVmp0HJxT0N\nmktuaVbaFgBsTS0/us1EqWvCA4REh1Ut/NoA9oG3JFt0lGDstTw1j+orDmIHOmSu\n7FKYzr0uCz14AkLMSOixdPD1F0YyED1NMVnRVXw77HiAFGmb0CDi2KEg70pEKpn3\nksa8oe0MQi6oEwlMsAxVTXOB1wblTBuSBeaECzTzWE+/DHF+QQfQi8kAjjSdmmMJ\nyN+shdBWHYRGYnxRkTatONhcDBIY7sZV7wolYHz/rf7dpYUZf37vdQnYV8FpO1um\nYa0GslyRJ5GqMBfDS1cQKne+FvVHxEE2YqEGBcOYhx/JI2soE8aA8W4XffN+DoEy\nZkinJ/+BOwJ/zUI9GZtwB4JXqbNEE+j7r7/fJO9KxfPp4MPK4YWu0H0EUWONpVwe\nTWtbRhQUCOe4PVSC/Vv1pstvMD/D+E/0L4GQNHxr+xyFxuvILty5lvFTxoAVYpqD\nu8gNhk3NWefTrlSkhY4N+tPP6o7E4t3y40nOA/d9qaqiid+lYcIDB0cJTpZvgeeQ\nijohxY3PHruU4vVZa37ITQnco9az6lsy18vbU0bOyK2fEZ2R9XVO8fH11jiV8oGH\n-----END RSA PRIVATE KEY-----" + + g.It("Should encrypt a string", func() { + encrypted, err := Encrypt(key, text) + g.Assert(err == nil).IsTrue() + decrypted, err := Decrypt(key, encrypted) + g.Assert(err == nil).IsTrue() + g.Assert(text).Equal(decrypted) + }) + + g.It("Should encrypt a long string", func() { + encrypted, err := Encrypt(key, long) + g.Assert(err == nil).IsTrue() + decrypted, err := Decrypt(key, encrypted) + g.Assert(err == nil).IsTrue() + g.Assert(long).Equal(decrypted) + }) + + g.It("Should decrypt a map", func() { + params := map[string]string{ + "foo": "2NQPoQfxPERVi42OEYzuVTjQrEQSrcN2-Pwk4kTlIVN5HA==", + } + err := DecryptMap(key, params) + g.Assert(err == nil).IsTrue() + g.Assert(params["foo"]).Equal("super_duper_secret") + }) + + g.It("Should trim a key with blocksize greater than 32 bytes", func() { + trimmed := trimKey("9T2tH3qZ8FSPr9uxrhzV4mn2VdVgA56x") + g.Assert(len(key) > 32).IsTrue() + g.Assert(len(trimmed)).Equal(32) + }) + + g.It("Should decrypt a yaml", func() { + yaml := `secure: {"foo": "2NQPoQfxPERVi42OEYzuVTjQrEQSrcN2-Pwk4kTlIVN5HA=="}` + decrypted, err := Parse(key, yaml) + g.Assert(err == nil).IsTrue() + g.Assert(decrypted["foo"]).Equal("super_duper_secret") + }) + + g.It("Should decrypt a yaml with no secure section", func() { + yaml := `foo: bar` + decrypted, err := Parse(key, yaml) + g.Assert(err == nil).IsTrue() + g.Assert(len(decrypted)).Equal(0) + }) + + g.It("Should encrypt a map", func() { + params := map[string]string{ + "foo": text, + } + err := EncryptMap(key, params) + g.Assert(err == nil).IsTrue() + g.Assert(params["foo"] == "super_duper_secret").IsFalse() + err = DecryptMap(key, params) + g.Assert(err == nil).IsTrue() + g.Assert(params["foo"] == "super_duper_secret").IsTrue() + }) + }) +}