/
compact.clj
149 lines (133 loc) · 6.16 KB
/
compact.clj
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
;; Copyright (c) 2014-2016 Andrey Antukh <niwi@niwi.nz>
;;
;; Licensed under the Apache License, Version 2.0 (the "License")
;; you may not use this file except in compliance with the License.
;; You may obtain a copy of the License at
;;
;; http://www.apache.org/licenses/LICENSE-2.0
;;
;; Unless required by applicable law or agreed to in writing, software
;; distributed under the License is distributed on an "AS IS" BASIS,
;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
;; See the License for the specific language governing permissions and
;; limitations under the License.
(ns buddy.sign.compact
"Compact high level message signing implementation.
It has high influence by django's cryptographic library
and json web signature/encryption but with focus on have
a compact representation. It's build on top of fantastic
ptaoussanis/nippy serialization library.
This singing implementation is not very efficient with
small messages, but is very space efficient with big
messages.
The purpose of this implementation is for secure message
transfer, it is not really good candidate for auth token
because of not good space efficiency for small messages."
(:require [buddy.core.codecs :as codecs]
[buddy.core.codecs.base64 :as b64]
[buddy.core.bytes :as bytes]
[buddy.core.keys :as keys]
[buddy.core.mac :as mac]
[buddy.core.dsa :as dsa]
[buddy.core.nonce :as nonce]
[buddy.sign.util :as util]
[clojure.string :as str]
[taoensso.nippy :as nippy]
[taoensso.nippy.compression :as nippycompress])
(:import clojure.lang.Keyword))
(defn- sign-poly
[input options]
(let [iv (or (:iv options)
(nonce/random-bytes 16))]
(-> (mac/hash input (assoc options :iv iv))
(bytes/concat iv))))
(defn- verify-poly
[input signature options]
(let [iv (bytes/slice signature 16 (count signature))
signature' (bytes/slice signature 0 16)]
(mac/verify input signature' (assoc options :iv iv))))
(def ^{:doc "List of supported signing algorithms"
:dynamic true}
*signers-map*
{:hs256 {:signer #(mac/hash %1 {:alg :hmac+sha256 :key %2})
:verifier #(mac/verify %1 %2 {:alg :hmac+sha256 :key %3})}
:hs512 {:signer #(mac/hash %1 {:alg :hmac+sha512 :key %2})
:verifier #(mac/verify %1 %2 {:alg :hmac+sha512 :key %3})}
:rs256 {:signer #(dsa/sign %1 {:alg :rsassa-pkcs15+sha256 :key %2})
:verifier #(dsa/verify %1 %2 {:alg :rsassa-pkcs15+sha256 :key %3})}
:rs512 {:signer #(dsa/sign %1 {:alg :rsassa-pkcs15+sha512 :key %2})
:verifier #(dsa/verify %1 %2 {:alg :rsassa-pkcs15+sha512 :key %3})}
:ps256 {:signer #(dsa/sign %1 {:alg :rsassa-pss+sha256 :key %2})
:verifier #(dsa/verify %1 %2 {:alg :rsassa-pss+sha256 :key %3})}
:ps512 {:signer #(dsa/sign %1 {:alg :rsassa-pss+sha512 :key %2})
:verifier #(dsa/verify %1 %2 {:alg :rsassa-pss+sha512 :key %3})}
:es256 {:signer #(dsa/sign %1 {:alg :ecdsa+sha256 :key %2})
:verifier #(dsa/verify %1 %2 {:alg :ecdsa+sha256 :key %3 })}
:es512 {:signer #(dsa/sign %1 {:alg :ecdsa+sha512 :key %2})
:verifier #(dsa/verify %1 %2 {:alg :ecdsa+sha512 :key %3})}
:poly1305-aes {:signer #(sign-poly %1 {:alg :poly1305+aes :key %2})
:verifier #(verify-poly %1 %2 {:alg :poly1305+aes :key %3})}
:poly1305-serpent {:signer #(sign-poly %1 {:alg :poly1305+serpent :key %2})
:verifier #(verify-poly %1 %2 {:alg :poly1305+serpent :key %3})}
:poly1305-twofish {:signer #(sign-poly %1 {:alg :poly1305+twofish :key %2})
:verifier #(verify-poly %1 %2 {:alg :poly1305+twofish :key %3})}})
(defn- calculate-signature
"Given the bunch of bytes, a private key and algorithm,
return a calculated signature as byte array."
[^bytes input ^bytes key ^Keyword alg]
(let [signer (get-in *signers-map* [alg :signer])]
(signer input key)))
(defn- verify-signature
"Given a bunch of bytes, a previously generated
signature, the private key and algorithm, return
signature matches or not."
[^bytes input ^bytes signature ^bytes key ^Keyword alg]
(let [verifier (get-in *signers-map* [alg :verifier])]
(verifier input signature key)))
(defn- serialize
[data compress]
(cond
(true? compress)
(nippy/freeze data {:compressor nippy/snappy-compressor})
(satisfies? nippycompress/ICompressor compress)
(nippy/freeze data {:compressor compress})
:else
(nippy/freeze data)))
(def ^:private bytes->base64str
(comp codecs/bytes->str #(b64/encode % true)))
(defn sign
"Sign arbitrary length string/byte array using
compact sigining method."
[data key & [{:keys [alg compress]
:or {alg :hs256 compress true}}]]
(let [input (serialize data compress)
salt (nonce/random-bytes 12)
stamp (codecs/long->bytes (util/now))
signature (-> (bytes/concat input salt stamp)
(calculate-signature key alg))]
(str/join "." [(bytes->base64str input)
(bytes->base64str signature)
(bytes->base64str salt)
(bytes->base64str stamp)])))
(defn unsign
"Given a signed message, verify it and return
the decoded data."
[data key & [{:keys [alg compress max-age]
:or {alg :hs256 compress true}}]]
(let [[input signature salt stamp] (str/split data #"\." 4)
input (b64/decode input)
signature (b64/decode signature)
salt (b64/decode salt)
stamp (b64/decode stamp)
candidate (bytes/concat input salt stamp)]
(when-not (verify-signature candidate signature key alg)
(throw (ex-info "Message seems corrupt or manipulated."
{:type :validation :cause :signature})))
(let [now (util/now)
stamp (codecs/bytes->long stamp)]
(when (and (number? max-age) (> (- now stamp) max-age))
(throw (ex-info (format "Token is older than %s" max-age)
{:type :validation :cause :max-age})))
(nippy/thaw input {:v1-compatibility? false}))))
(util/defalias encode sign)
(util/defalias decode unsign)