Skip to content

Commit fe49929

Browse files
committed
Switch secure cookie implementation
Previously, secure cookies in web.go were only cryptographically signed. This prevented them from being tampered with. However, the contents of the cookies were still transmitted in plain text to the client. Instead of only signing the contents of the cookie, encrypt the contents as well. This prevents any kind of information leakage. Secure cookies are now encrypted with AES counter mode with a 32 bit key. The contents are still signed using HMAC. Both the encryption key and the signature key are generated using pbkdf2 using the CookieSecret config option as the password source. The ciphertext, initialization vector, and signature are now transmitted to the client. Although the API is the same, cookies previously stored will not be readable. Unfortunately there is no smooth upgrade process. An example of using secure cookies has been added as well. Fixes hoisie#160
1 parent 1bed69c commit fe49929

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)