platform/packages/security/lib/src/cookie_signer.dart

133 lines
4.6 KiB
Dart
Raw Normal View History

2019-08-16 13:00:56 +00:00
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:crypto/crypto.dart';
2019-08-16 13:16:46 +00:00
/// A utility that signs, and verifies, cookies using an [Hmac].
///
/// It aims to mitigate so-called "cookie poisoning" attacks by
/// ensuring that clients cannot tamper with the cookies they have been
/// sent.
2019-08-16 13:00:56 +00:00
class CookieSigner {
2019-08-16 13:16:46 +00:00
/// The [Hmac] used to sign and verify cookies.
2019-08-16 13:00:56 +00:00
final Hmac hmac;
2019-08-16 13:16:46 +00:00
/// Creates an [hmac] from an array of [keyBytes] and a
/// [hash] (defaults to [sha256]).
2019-08-16 13:00:56 +00:00
CookieSigner(List<int> keyBytes, {Hash hash})
: hmac = Hmac(hash ?? sha256, keyBytes);
2019-08-16 13:02:59 +00:00
CookieSigner.fromHmac(this.hmac);
2019-08-16 13:16:46 +00:00
/// Creates an [hmac] from a string [key] and a
/// [hash] (defaults to [sha256]).
2019-08-16 13:00:56 +00:00
factory CookieSigner.fromStringKey(String key, {Hash hash}) {
return CookieSigner(utf8.encode(key), hash: hash);
}
2019-08-16 13:16:46 +00:00
/// Returns a set of all the incoming cookies that had a
/// valid signature attached. Any cookies without a
/// signature, or with a signature that does not match the
/// provided data, are not included in the output.
2019-08-16 13:32:50 +00:00
///
/// If an [onInvalidCookie] callback is passed, then it will
/// be invoked for each unsigned or improperly-signed cookie.
List<Cookie> readCookies(RequestContext req,
{void Function(Cookie) onInvalidCookie}) {
return req.cookies.fold([], (out, cookie) {
var data = getCookiePayloadAndSignature(cookie.value);
if (data == null || (data[1] != computeCookieSignature(data[0]))) {
if (onInvalidCookie != null) {
onInvalidCookie(cookie);
}
return out;
} else {
return out..add(cookieWithNewValue(cookie, data[0]));
}
});
}
/// Determines whether a cookie is properly signed,
/// if it is signed at all.
///
/// If there is no signature, returns `false`.
/// If the provided signature does not match the payload
/// provided, returns `false`.
/// Otherwise, returns true.
bool verify(Cookie cookie) {
var data = getCookiePayloadAndSignature(cookie.value);
return (data != null && (data[1] == computeCookieSignature(data[0])));
}
/// Gets the payload and signature of a given [cookie], WITHOUT
/// verifying its integrity.
///
/// Returns `null` if no payload can be found.
/// Otherwise, returns a list with a length of 2, where
/// the item at index `0` is the payload, and the item at
/// index `1` is the signature.
List<String> getCookiePayloadAndSignature(String cookieValue) {
var dot = cookieValue.indexOf('.');
if (dot <= 0) {
return null;
} else if (dot >= cookieValue.length - 1) {
return null;
} else {
var payload = cookieValue.substring(0, dot);
var sig = cookieValue.substring(dot + 1);
return [payload, sig];
}
}
2019-08-16 13:02:59 +00:00
2019-08-16 13:53:15 +00:00
/// Signs a single [cookie], and adds it to an outgoing
/// [res]ponse. The input [cookie] is not modified.
///
/// See [createSignedCookie].
void writeCookie(ResponseContext res, Cookie cookie) {
res.cookies.add(createSignedCookie(cookie));
}
2019-08-16 13:16:46 +00:00
/// Signs a set of [cookies], and adds them to an outgoing
2019-08-16 13:32:50 +00:00
/// [res]ponse. The input [cookies] are not modified.
2019-08-16 13:16:46 +00:00
///
2019-08-16 13:32:50 +00:00
/// See [createSignedCookie].
2019-08-16 13:02:59 +00:00
void writeCookies(ResponseContext res, Iterable<Cookie> cookies) {
2019-08-16 13:53:15 +00:00
cookies.forEach((c) => writeCookie(res, c));
2019-08-16 13:02:59 +00:00
}
2019-08-16 13:19:48 +00:00
/// Returns a new cookie, replacing the value of an input
/// [cookie] with one that is signed with the [hmac].
2019-08-16 13:09:11 +00:00
///
2019-08-16 13:50:50 +00:00
/// The new value is:
/// `cookie.value + "." + base64Url(sig)`
2019-08-16 13:09:11 +00:00
///
/// Where `sig` is the cookie's value, signed with the [hmac].
2019-08-16 13:32:50 +00:00
Cookie createSignedCookie(Cookie cookie) {
2019-08-16 13:50:50 +00:00
return cookieWithNewValue(
cookie, cookie.value + '.' + computeCookieSignature(cookie.value));
2019-08-16 13:32:50 +00:00
}
/// Returns a new [Cookie] that is the same as the input
/// [cookie], but with a [newValue].
Cookie cookieWithNewValue(Cookie cookie, String newValue) {
return Cookie(cookie.name, newValue)
2019-08-16 13:19:48 +00:00
..domain = cookie.domain
..expires = cookie.expires
..httpOnly = cookie.httpOnly
..maxAge = cookie.maxAge
..path = cookie.path
..secure = cookie.secure;
}
2019-08-16 13:50:50 +00:00
/// Computes the *signature* of a [cookieValue], either for
2019-08-16 13:19:48 +00:00
/// signing an outgoing cookie, or verifying an incoming cookie.
String computeCookieSignature(String cookieValue) {
2019-08-16 13:09:11 +00:00
// base64Url(cookie) + "." + base64Url(sig)
2019-08-16 13:50:50 +00:00
// var encodedCookie = base64Url.encode(cookieValue.codeUnits);
2019-08-16 13:19:48 +00:00
var sigBytes = hmac.convert(cookieValue.codeUnits).bytes;
2019-08-16 13:50:50 +00:00
return base64Url.encode(sigBytes);
// var sig = base64Url.encode(sigBytes);
// return encodedCookie + '.' + sig;
2019-08-16 13:09:11 +00:00
}
2019-08-16 13:00:56 +00:00
}