Secure your cookies of Hunchentoot in Common Lisp.
Hunchentoot - The Common Lisp web server formerly known as TBNL
Hunchentoot is a web server written in Common Lisp and at the same time a toolkit for building dynamic websites. As a stand-alone web server, Hunchentoot is capable of HTTP/1.1 chunking (both directions), persistent connections (keep-alive), and SSL.
By default, hunchentoot supports cookies, but all cookies will be exposed once a request was done from the client. And there is not a solution to secure the cookies transform by now in hunchentoot. So the temporary choice is using the hunchentoot session:
Hunchentoot supports sessions: Once a request handler has called START-SESSION, Hunchentoot uses either cookies or (if the client doesn't send the cookies back) rewrites URLs to keep track of this client, i.e. to provide a kind of 'state' for the stateless http protocol. The session associated with the client is a CLOS object which can be used to store arbitrary data between requests.
Hunchentoot makes some reasonable effort to prevent eavesdroppers from hijacking sessions (see below), but this should not be considered really secure. Don't store sensitive data in sessions and rely solely on the session mechanism as a safeguard against malicious users who want to get at this data!
For each request there's one SESSION object which is accessible to the handler via the special variable *SESSION*. This object holds all the information available about the session and can be accessed with the functions described in this chapter. Note that the internal structure of SESSION objects should be considered opaque and may change in future releases of Hunchentoot.
Sessions are automatically verified for validity and age when the REQUEST object is instantiated, i.e. if *SESSION* is not NIL then this session is valid (as far as Hunchentoot is concerned) and not too old. Old sessions are automatically removed.
A memory stored session is not always enough in many cases and it's not so secured with the simple SESSION ID implementation. There are many other web frameworks using encrypted cookies. How easy to do that in Common Lisp?
Cryptography in Common Lisp
Encryption and decryption in CL using Ironclad.
Ironclad is a cryptography library written entirely in Common Lisp. It includes support for several popular ciphers, digests, and MACs. Rudimentary support for public-key cryptography is included. For several implementations that support Gray Streams, support is included for convenient stream wrappers.
Here is an example: encrypt and decrypt a message using a passphrase
;; passphrase (defparameter pass "1234567890123456") ;; generate encrypt key by the passphrase (defparameter key (ironclad:ascii-string-to-byte-array pass)) ;; the message content (defparameter message "this is a secret") ;; convert message to bytes to encrypt (defparameter message-bytes (ironclad:ascii-string-to-byte-array message)) ;; message-bytes => CL-USER> message-bytes #(116 104 105 115 32 105 115 32 97 32 115 101 99 114 101 116) CL-USER> ;; encrypt message-bytes in place (ironclad:encrypt-in-place ;; generate a cipher(AES-ECB) use for the encryption (ironclad:make-cipher 'ironclad:aes :key key :mode 'ironclad:ecb) ;; the bytes to encrypt with and also the place to store the encrypted bytes. message-bytes) ;; now what is in message-bytes? CL-USER> message-bytes #(125 201 221 179 71 36 194 30 40 206 19 13 252 191 141 46) CL-USER> ;; now decrypt (ironclad:decrypt-in-place ;; generate a cipher use for the encryption (ironclad:make-cipher 'ironclad:aes :key key :mode 'ironclad:ecb) ;; the bytes to decrypt with and also the place to store the decrypted bytes. message-bytes) ;; see the decrypted data CL-USER> message-bytes #(116 104 105 115 32 105 115 32 97 32 115 101 99 114 101 116) CL-USER> ;; is as the same as the original one. ;; get the orignal text: CL-USER> (map 'string #'code-char message-bytes) "this is a secret" CL-USER>
Ok, how to encrypt a COOKIE? Exactly a COOKIE value.
Set Cookie in hunchentoot
(defun set-cookie (name &key (value "") expires max-age path domain secure http-only (reply *reply*)) "Creates a cookie object from the parameters provided and adds it to the outgoing cookies of the REPLY object REPLY. If a cookie with the name NAME \(case-sensitive) already exists, it is replaced." (set-cookie* (make-instance 'cookie :name name :value value :expires expires :max-age max-age :path path :domain domain :secure secure :http-only http-only) reply))
Now it's set-secure-cookie:
;; set http-only to true in SECURE COOKIE (defun set-secure-cookie (name &key (value "") expires max-age path domain secure (http-only t) (reply *reply*)) "set the secure cookie, works like set-cookie in hunchentoot." (when (secure-cookie-p) (set-cookie* (make-instance 'cookie :name name :value (encrypt-and-encode name value) :expires expires :max-age max-age :path path :domain domain :secure secure :http-only http-only) reply)))
Encode and encrypt the cookie value
- encode (convert the string of cookie value to bytes array)
- encrypt (like the above example). In the hunchentoot-secure-cookie library, I use AES-CBC-256 cipher to encrypt data, so the encrypt data will contains the IV value that used by the cipher.
- pack: add identity to generate output string. for example: concatnate the name of cookie, datetime of now, and the encrypted value
- sign: generate HMAC digest from the pack data.
- enocde the encrypted value plus HMAC digest to BASE64.
Then the client will receive the base64 string as a cookie.
Decode and decrypt the cookie value
It's a reverse flow of above. a cookie was sent back to the server when a request was called. and hunchent will extract the cookie value from the request.
- decode from BASE64 to normal string
- unpack: extract the datatime, name, encrypted values from the string.
- verify HMAC digest.
- then decrypt the data.
- decode from bytes to normal string.
return the noraml string which is the one before encrypted and transformed.
Learn more from the source code, and welcome to point out any bugs.
The following the README of package:
About package hunchentoot-secure-cookie
Package hunchentoot-secure-cookie encodes and decodes authenticated and optionally encrypted cookie values.
Secure cookies can't be forged, because their values are validated using HMAC. When encrypted, the content is also inaccessible to malicious eyes.
if using ASDF-2, you can install it to to the ASDF-2 load dir:
cd ~/.local/share/common-lisp/source/ git clone firstname.lastname@example.org:gihnius/hunchentoot-secure-cookie.git LISP> (asdf:load-system :hunchentoot-secure-cookie)
hunchentoot-secure-cookie:set-cookie-secret-key-base hunchentoot-secure-cookie:set-secure-cookie hunchentoot-secure-cookie:get-secure-cookie hunchentoot-secure-cookie:delete-secure-cookie ;; init the secret key, it is recommended to use a key with 32 or 64 bytes. (set-cookie-secret-key-base "................") ;; set a cookie value (set-secure-cookie "cookie-name" :value "something secret...") ;; with more options (set-secure-cookie "cookie-name" :value "something secret..." :path "/" :max-age 86400) ; set the max-age in seconds. ;; get a cookie value (get-secure-cookie "cookie-name") ;; return => decrypted string ;; delete a cookie (delete-secure-cookie "cookie-name")
;; your app define (asdf:defsystem #:my-web-app :serial t :depends-on (#:cl-ppcre #:hunchentoot #:hunchentoot-secure-cookie ...)) ;; set the secret token some where (hunchentoot-secure-cookie:set-cookie-secret-key-base "passphrase...") ;; start hunchentoot server (hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 4242)) ;; define your handlers (hunchentoot:define-easy-handler (set-cookie-val :uri "/set") (value) (setf (hunchentoot:content-type*) "text/plain") (hunchentoot-secure-cookie:set-secure-cookie "secure-cookie" :value (if value value "")) (format nil "You set cookie: ~A" value)) (hunchentoot:define-easy-handler (get-cookie-val :uri "/get") () (setf (hunchentoot:content-type*) "text/plain") (format nil "secure-cookie: ~A~&original encoded cookie: ~A" (hunchentoot-secure-cookie::get-secure-cookie "secure-cookie") (Hunchentoot:cookie-in "secure-cookie"))) ;; test set cookie: ;; visit http://localhost:4242/set?value=this ;; test get cookie ;; visit http://localhost:4242/get