1 module itsdangerous.jws;
2 
3 import std.stdio;
4 import std.conv;
5 import std.typecons;
6 import std.format;
7 import std.algorithm.searching;
8 import std.array;
9 import std.digest.sha;
10 import std.json;
11 import std.datetime;
12 import std.datetime.systime;
13 
14 import itsdangerous.serializer;
15 import itsdangerous.encoding;
16 import itsdangerous.dsigner;
17 import itsdangerous.exc;
18 
19 class JSONWebSignatureSerializer(DigestMethod, SignerType) : Serializer!SignerType {
20     /+ for DigestMethod use SHA256, SHA384, SHA512
21     SHA512 should be considered as default
22      +/
23 
24     this(string secretKey, string salt = null){
25         super(secretKey, salt);
26     }
27     
28     Tuple!(JSONValue, JSONValue) loadPayloadWithHeader(string payload){
29 
30         if(!canFind(payload, '.'))
31             throw new BadPayload("No \".\" found in value");
32         
33         auto arr = payload.split(".");
34         
35         string base64dHeader = arr[0];
36         string base64dPayload = arr[1];
37         
38         string jsonHeader, jsonPayload;
39         try{
40             jsonHeader = base64Decode(base64dHeader);
41         } catch (BadData e){
42             throw new BadHeader(format("Could not base64 decode the header because of an exception: %s", e.msg));
43         }
44 
45         try{
46             jsonPayload = base64Decode(base64dPayload);
47         } catch (BadData e){
48             throw new BadPayload(format("Could not base64 decode the payload because of an exception: %s", e.msg));
49         }
50 
51         JSONValue headerStruct;
52         try{
53             headerStruct = this.Serializer.loadPayload(jsonHeader);
54         } catch (BadData e){
55             throw new BadHeader(format("Could not unserialize header because it was malformed: %s", e.msg));
56         }
57 
58         JSONValue payloadStruct = this.Serializer.loadPayload(jsonPayload);
59         return tuple(headerStruct, payloadStruct);
60     }
61 
62     override JSONValue loadPayload(string payload){
63 
64         if(!canFind(payload, '.'))
65             throw new BadPayload("No \".\" found in value");
66         
67         auto arr = payload.split(".");
68         
69         string base64dHeader = arr[0];
70         string base64dPayload = arr[1];
71         
72         string jsonHeader, jsonPayload;
73         try{
74             jsonHeader = base64Decode(base64dHeader);
75         } catch (BadData e){
76             throw new BadHeader(format("Could not base64 decode the header because of an exception: %s", e.msg));
77         }
78 
79         try{
80             jsonPayload = base64Decode(base64dPayload);
81         } catch (BadData e){
82             throw new BadPayload(format("Could not base64 decode the payload because of an exception: %s", e.msg));
83         }
84 
85         JSONValue headerStruct;
86         try{
87             headerStruct = this.Serializer.loadPayload(jsonHeader);
88         } catch (BadData e){
89             throw new BadHeader(format("Could not unserialize header because it was malformed: %s", e.msg));
90         }
91 
92         JSONValue payloadStruct = this.Serializer.loadPayload(jsonPayload);
93         return payloadStruct;
94     }
95 
96     string dumpPayload(JSONValue header, JSONValue obj){
97         string base64dHeader = base64Encode(
98             toJSON(header)
99         );
100         string base64dPayload = base64Encode(
101             toJSON(obj)
102         );
103         return base64dHeader ~ '.' ~ base64dPayload;
104     }
105 
106     string digestName(){
107         string digname;
108         static if (is(DigestMethod == SHA!(1024u, 512u))){
109             digname = "HS512";
110         }
111         else static if (is(DigestMethod == SHA!(1024u, 384u))){
112             digname = "HS384";
113         }
114         else static if (is(DigestMethod == SHA!(512u, 256u))){
115             digname = "HS256";
116         }
117         else {
118             static assert( false, "unsupported digest type!");
119         }
120         return digname;
121     }
122 
123     auto makeSignerForTJWT(string salt = null){
124         string fsalt;
125         if (salt is null)
126             fsalt = this.salt;
127         else
128             fsalt = salt;   
129         
130         auto key_derivation = (fsalt is null)?"none":"django-concat";
131         
132         auto sigAlg = new HMACAlgorithm!SHA512();
133         
134         auto _signer = new Signer!(SHA1, SHA512)(
135             this.Serializer.secretKey,
136             fsalt,
137             '.',
138             "none"
139         );
140 
141         _signer.setAlgorithm(sigAlg);
142         return _signer;
143     }
144 
145     JSONValue makeHeader(JSONValue header){
146         header["alg"] = digestName();
147         return header;
148     }
149 
150     final string dumps(JSONValue obj, string salt = null, JSONValue headerFields = null){
151         /+Like :meth:`.Serializer.dumps` but creates a JSON Web
152         Signature. It also allows for specifying additional fields to be
153         included in the JWS header.
154         +/
155         JSONValue header = makeHeader(headerFields);
156         auto signer = makeSignerForTJWT(salt);
157         //signer.writeln;
158         //signer.getAlgorithm.writeln;
159         auto tmp = dumpPayload(header, obj);
160         return signer.sign(tmp);
161     }
162 
163     override JSONValue loads(string s, string salt = null, int maxAge = 0, int *tstamp = null){
164         /+Reverse of :meth:`dumps`. If requested via ``return_header``
165         it will return a tuple of payload and header.
166         +/
167         auto signer = makeSignerForTJWT(salt);
168         auto arr = loadPayloadWithHeader(
169             signer.unsign(s)
170         );
171         auto header = arr[0];
172         auto payload = arr[1];
173 
174         if (header["alg"].str != digestName())
175             throw new BadHeader(format("Algorithm mismatch %s != %s", header["alg"], digestName()));
176 
177         return payload;
178     }
179 
180     Tuple!(JSONValue, JSONValue) loadsWithHeader(string s, string salt = null){
181         /+Reverse of :meth:`dumps`. If requested via ``return_header``
182         it will return a tuple of payload and header.
183         +/
184         auto signer = makeSignerForTJWT(salt);
185         auto arr = loadPayloadWithHeader(signer.unsign(s));
186         auto header = arr[0];
187         auto payload = arr[1];
188 
189         if (header["alg"].str != digestName())
190             throw new BadHeader(format("Algorithm mismatch %s != %s", header["alg"], digestName()));
191 
192         return tuple(payload, header);
193     }
194 }
195 
196 enum DEFAULT_EXPIRES_IN = 3600;
197 
198 class TimedJSONWebSignatureSerializer(DigestMethod, SignerType) : 
199     JSONWebSignatureSerializer!(DigestMethod, SignerType) {
200         
201     this(string secretKey, int expiresIn = DEFAULT_EXPIRES_IN, string salt = "itsdangerous.Signer"){
202         super(secretKey, salt);
203         this.expiresIn = expiresIn;
204     }
205 
206     int expiresIn;
207 
208     int now(){
209         import std.datetime.systime;
210         return cast(int)Clock.currTime.toUnixTime();
211     }
212     
213     override JSONValue makeHeader(JSONValue header){
214         header["alg"] = digestName();
215         const iat = now();
216         const exp = iat + expiresIn;
217 
218         import std.conv;
219         header["iat"] = iat;
220         header["exp"] = exp;
221 
222         return header;
223     }
224 
225     override JSONValue loads(string s, string salt = null, int maxAge = 0, int *tstamp = null){
226         auto tup = this.JSONWebSignatureSerializer.loadsWithHeader(s, salt);
227         JSONValue payload = tup[0];
228         JSONValue header = tup[1];
229 
230         try{
231             const exp = ("exp" in header);
232         }catch(JSONException ex){
233             auto excn = new BadHeader("Missing expiry date");
234             excn.payload = payload.toJSON;
235             throw excn;
236         }
237         
238         auto int_date_error = new BadHeader("Expiry date is not an IntDate");
239         
240         try{
241             header["exp"] = header["exp"].integer;
242         } catch (Exception ex){
243             throw int_date_error;
244         }
245 
246         if (header["exp"].integer < 0)
247             throw int_date_error;
248         
249         if (header["exp"].integer < now())
250             throw new SignatureExpired(format("Signature expired: date signed: %s", getIssueDate(header)));
251         return payload;
252     }
253 
254     override Tuple!(JSONValue, JSONValue) loadsWithHeader(string s, string salt = null){
255         auto tup = this.JSONWebSignatureSerializer.loadsWithHeader(s, salt);
256         JSONValue payload = tup[0];
257         JSONValue header = tup[1];
258         
259         try{
260             const exp = ("exp" in header);
261         }catch(JSONException ex){
262             auto excn = new BadHeader("Missing expiry date!");
263             excn.payload = payload.toJSON;
264             throw excn;
265         }
266         
267         auto int_date_error = new BadHeader("Expiry date is not an IntDate!");
268         
269         try{
270             header["exp"] = header["exp"].integer;
271         } catch (JSONException ex){
272             throw int_date_error;
273         }
274 
275         if (header["exp"].integer < 0)
276             throw int_date_error;
277         
278         if (header["exp"].integer < now()){
279             auto excn = new SignatureExpired(format("Signature expired: date signed: %s", getIssueDate(header)));
280             excn.payload = payload.toJSON;
281             throw excn;
282         }
283             
284         return tuple(payload, header);
285     }
286 
287     string getIssueDate(JSONValue header){
288         int rv = header["iat"].integer.to!int;
289 
290         return SysTime.fromUnixTime(rv).toString;
291     }
292 
293 }