Skip to content

Commit 06d920f

Browse files
authored
Merge pull request hoisie#204 from hoisie/encrypted-cookie
Switch secure cookie implementation
2 parents 1bed69c + fe49929 commit 06d920f

File tree

5 files changed

+176
-63
lines changed

5 files changed

+176
-63
lines changed

examples/secure_cookie.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"github.com/hoisie/web"
6+
"html"
7+
)
8+
9+
var cookieName = "cookie"
10+
11+
var notice = `
12+
<div>%v</div>
13+
`
14+
var form = `
15+
<form method="POST" action="update">
16+
<div class="field">
17+
<label for="cookie"> Set a cookie: </label>
18+
<input id="cookie" name="cookie"> </input>
19+
</div>
20+
21+
<input type="submit" value="Submit"></input>
22+
<input type="submit" name="submit" value="Delete"></input>
23+
</form>
24+
`
25+
26+
func index(ctx *web.Context) string {
27+
cookie, ok := ctx.GetSecureCookie(cookieName)
28+
var top string
29+
if !ok {
30+
top = fmt.Sprintf(notice, "The cookie has not been set")
31+
} else {
32+
var val = html.EscapeString(cookie)
33+
top = fmt.Sprintf(notice, "The value of the cookie is '"+val+"'.")
34+
}
35+
return top + form
36+
}
37+
38+
func update(ctx *web.Context) {
39+
if ctx.Params["submit"] == "Delete" {
40+
ctx.SetCookie(web.NewCookie(cookieName, "", -1))
41+
} else {
42+
ctx.SetSecureCookie(cookieName, ctx.Params["cookie"], 0)
43+
}
44+
ctx.Redirect(301, "/")
45+
}
46+
47+
func main() {
48+
web.Config.CookieSecret = "a long secure cookie secret"
49+
web.Get("/", index)
50+
web.Post("/update", update)
51+
web.Run("0.0.0.0:9999")
52+
}

secure_cookie.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package web
2+
3+
import (
4+
"bytes"
5+
"crypto/aes"
6+
"crypto/cipher"
7+
"crypto/hmac"
8+
"crypto/rand"
9+
"crypto/sha512"
10+
"encoding/base64"
11+
"errors"
12+
"golang.org/x/crypto/pbkdf2"
13+
"io"
14+
"strings"
15+
)
16+
17+
const (
18+
pbkdf2Iterations = 64000
19+
keySize = 32
20+
)
21+
22+
var (
23+
ErrMissingCookieSecret = errors.New("Secret Key for secure cookies has not been set. Assign one to web.Config.CookieSecret.")
24+
ErrInvalidKey = errors.New("The keys for secure cookies have not been initialized. Ensure that a Run* method is being called")
25+
)
26+
27+
func (ctx *Context) SetSecureCookie(name string, val string, age int64) error {
28+
serverConfig := ctx.Server.Config
29+
if len(serverConfig.CookieSecret) == 0 {
30+
return ErrMissingCookieSecret
31+
}
32+
33+
if len(serverConfig.encKey) == 0 || len(serverConfig.signKey) == 0 {
34+
return ErrInvalidKey
35+
}
36+
ciphertext, err := encrypt([]byte(val), serverConfig.encKey)
37+
if err != nil {
38+
return err
39+
}
40+
sig := sign(ciphertext, serverConfig.signKey)
41+
data := base64.StdEncoding.EncodeToString(ciphertext) + "|" + base64.StdEncoding.EncodeToString(sig)
42+
ctx.SetCookie(NewCookie(name, data, age))
43+
return nil
44+
}
45+
46+
func (ctx *Context) GetSecureCookie(name string) (string, bool) {
47+
for _, cookie := range ctx.Request.Cookies() {
48+
if cookie.Name != name {
49+
continue
50+
}
51+
52+
parts := strings.SplitN(cookie.Value, "|", 2)
53+
if len(parts) != 2 {
54+
return "", false
55+
}
56+
57+
ciphertext, err := base64.StdEncoding.DecodeString(parts[0])
58+
if err != nil {
59+
return "", false
60+
}
61+
sig, err := base64.StdEncoding.DecodeString(parts[1])
62+
if err != nil {
63+
return "", false
64+
}
65+
expectedSig := sign([]byte(ciphertext), ctx.Server.Config.signKey)
66+
if !bytes.Equal(expectedSig, sig) {
67+
return "", false
68+
}
69+
plaintext, err := decrypt(ciphertext, ctx.Server.Config.encKey)
70+
if err != nil {
71+
return "", false
72+
}
73+
return string(plaintext), true
74+
}
75+
return "", false
76+
}
77+
78+
func genKey(password string, salt string) []byte {
79+
return pbkdf2.Key([]byte(password), []byte(salt), pbkdf2Iterations, keySize, sha512.New)
80+
}
81+
82+
func encrypt(plaintext []byte, key []byte) ([]byte, error) {
83+
aesCipher, err := aes.NewCipher(key)
84+
if err != nil {
85+
return nil, err
86+
}
87+
ciphertext := make([]byte, aes.BlockSize+len(plaintext))
88+
iv := ciphertext[:aes.BlockSize]
89+
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
90+
return nil, err
91+
}
92+
stream := cipher.NewCTR(aesCipher, iv)
93+
stream.XORKeyStream(ciphertext[aes.BlockSize:], plaintext)
94+
return ciphertext, nil
95+
}
96+
97+
func decrypt(ciphertext []byte, key []byte) ([]byte, error) {
98+
if len(ciphertext) <= aes.BlockSize {
99+
return nil, errors.New("Invalid cipher text")
100+
}
101+
aesCipher, err := aes.NewCipher(key)
102+
if err != nil {
103+
return nil, err
104+
}
105+
plaintext := make([]byte, len(ciphertext)-aes.BlockSize)
106+
stream := cipher.NewCTR(aesCipher, ciphertext[:aes.BlockSize])
107+
stream.XORKeyStream(plaintext, ciphertext[aes.BlockSize:])
108+
return plaintext, nil
109+
}
110+
111+
func sign(data []byte, key []byte) []byte {
112+
mac := hmac.New(sha512.New, key)
113+
mac.Write(data)
114+
return mac.Sum(nil)
115+
}

server.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type ServerConfig struct {
2828
RecoverPanic bool
2929
Profiler bool
3030
ColorOutput bool
31+
encKey []byte
32+
signKey []byte
3133
}
3234

3335
// Server represents a web.go server.
@@ -56,6 +58,12 @@ func (s *Server) initServer() {
5658
if s.Logger == nil {
5759
s.Logger = log.New(os.Stdout, "", log.Ldate|log.Ltime)
5860
}
61+
62+
if len(s.Config.CookieSecret) > 0 {
63+
s.Logger.Println("Generating cookie encryption keys")
64+
s.Config.encKey = genKey(s.Config.CookieSecret, "encryption key salt")
65+
s.Config.signKey = genKey(s.Config.CookieSecret, "signature key salt")
66+
}
5967
}
6068

6169
type route struct {

web.go

Lines changed: 0 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,15 @@
33
package web
44

55
import (
6-
"bytes"
7-
"crypto/hmac"
8-
"crypto/sha1"
96
"crypto/tls"
10-
"encoding/base64"
11-
"fmt"
127
"golang.org/x/net/websocket"
13-
"io/ioutil"
148
"log"
159
"mime"
1610
"net/http"
1711
"os"
1812
"path"
1913
"reflect"
20-
"strconv"
2114
"strings"
22-
"time"
2315
)
2416

2517
// A Context object is created for every incoming HTTP request, and is
@@ -117,61 +109,6 @@ func (ctx *Context) SetCookie(cookie *http.Cookie) {
117109
ctx.SetHeader("Set-Cookie", cookie.String(), false)
118110
}
119111

120-
func getCookieSig(key string, val []byte, timestamp string) string {
121-
hm := hmac.New(sha1.New, []byte(key))
122-
123-
hm.Write(val)
124-
hm.Write([]byte(timestamp))
125-
126-
return fmt.Sprintf("%02x", hm.Sum(nil))
127-
}
128-
129-
func (ctx *Context) SetSecureCookie(name string, val string, age int64) {
130-
if len(ctx.Server.Config.CookieSecret) == 0 {
131-
ctx.Server.Logger.Println("Secret Key for secure cookies has not been set. Please assign a cookie secret to web.Config.CookieSecret.")
132-
return
133-
}
134-
encoded := base64.StdEncoding.EncodeToString([]byte(val))
135-
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
136-
sig := getCookieSig(ctx.Server.Config.CookieSecret, []byte(encoded), timestamp)
137-
cookie := strings.Join([]string{encoded, timestamp, sig}, "|")
138-
ctx.SetCookie(NewCookie(name, cookie, age))
139-
}
140-
141-
func (ctx *Context) GetSecureCookie(name string) (string, bool) {
142-
for _, cookie := range ctx.Request.Cookies() {
143-
if cookie.Name != name {
144-
continue
145-
}
146-
147-
parts := strings.SplitN(cookie.Value, "|", 3)
148-
if len(parts) != 3 {
149-
return "", false
150-
}
151-
152-
val := parts[0]
153-
timestamp := parts[1]
154-
sig := parts[2]
155-
156-
if getCookieSig(ctx.Server.Config.CookieSecret, []byte(val), timestamp) != sig {
157-
return "", false
158-
}
159-
160-
ts, _ := strconv.ParseInt(timestamp, 0, 64)
161-
162-
if time.Now().Unix()-31*86400 > ts {
163-
return "", false
164-
}
165-
166-
buf := bytes.NewBufferString(val)
167-
encoder := base64.NewDecoder(base64.StdEncoding, buf)
168-
169-
res, _ := ioutil.ReadAll(encoder)
170-
return string(res), true
171-
}
172-
return "", false
173-
}
174-
175112
// small optimization: cache the context type instead of repeteadly calling reflect.Typeof
176113
var contextType reflect.Type
177114

web_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@ func makeCookie(vals map[string]string) []*http.Cookie {
487487

488488
func TestSecureCookie(t *testing.T) {
489489
mainServer.Config.CookieSecret = "7C19QRmwf3mHZ9CPAaPQ0hsWeufKd"
490+
mainServer.initServer()
490491
resp1 := getTestResponse("POST", "/securecookie/set/a/1", "", nil, nil)
491492
sval, ok := resp1.cookies["a"]
492493
if !ok {

0 commit comments

Comments
 (0)