-
-
Notifications
You must be signed in to change notification settings - Fork 119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Overhaul CookieJar
: more efficient, more secure, more docs.
#76
Conversation
src/jar.rs
Outdated
/// jar.add(Cookie::new("name", "value")); | ||
/// assert_eq!(jar.find("name").map(|c| c.value()), Some("value")); | ||
/// ``` | ||
pub fn find<'a>(&'a self, name: &str) -> Option<&'a Cookie<'static>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that the 'a
here can be elided, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh also, for symmetry with HashMap
we may want to call this method get
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the same time, perhaps this could implement Index
with &str
to allow doing:
println!("{}", jar["foo"].value());
This may prefer to avoid IndexMut
as changes need to be tracked for delta
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, the 'a
can be elided. Yeah, get
sounds good to me. I'm against implementing Index
since the contents of the CookieJar
are usually determined by some opaque actor (i.e, a web framework), so you don't know what's in the jar. As a result, the index is likely to panic often, thus being error prone. We could return an Option
from the Index
impl
, but then it'll stray from convention.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure yeah, seems ok to not implement Index
for now (certainly a conservative approach)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks fantastic, thanks so much! Minor comments here and there but nothing major.
Some other random thoughts:
- I wonder if we could drop
rustc-serialize
as a dep? If it's only used for base64 we don't really need it (could use a perhaps more targeted crate) - Maybe
chrono
should be investigated instead oftime
? Or perhaps migrating tostd::time
if possible?
src/jar.rs
Outdated
/// jar.add(Cookie::new("name", "value")); | ||
/// assert_eq!(jar.find("name").map(|c| c.value()), Some("value")); | ||
/// ``` | ||
pub fn find<'a>(&'a self, name: &str) -> Option<&'a Cookie<'static>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh also, for symmetry with HashMap
we may want to call this method get
src/jar.rs
Outdated
for cookie in root.new_cookies.borrow().iter() { | ||
ret.push(map.get(cookie).unwrap().clone()); | ||
/// Removes all cookies from this cookie jar. | ||
#[deprecated(since = "0.7.0", note = "calling this method may not remove \ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh if the crate is going to 0.7.0 I think it's fine to just delete this outright and mention it in the tag's changelog
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to deprecate it to smooth transition because I feel that this method may actually be used in the wild. I don't know if that's true or not, however.
src/secure/private.rs
Outdated
const BASE64_NONCE_LEN: usize = 16; | ||
|
||
/// Extends `CookieJar` with a `private` method to retrieve a private child jar. | ||
pub trait Private<'a, 'k> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To me it seems somewhat odd to have this as a trait because I don't think it's implementable by downstream clients, right? (no constructors for PrivateJar
)
Is there a reason to have this not just as an inherent method on CookieJar
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't...think so. Let me think about that for a bit and change it if I can't think of any reason.
src/secure/private.rs
Outdated
/// | ||
/// # Panics | ||
/// | ||
/// Panics if `key` is not exactly 32 bytes long. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For flexibility could we accept keys of different lengths? I could imagine us switching to different encryption algorithms in the future which may have different key length restrictions.
Although I'm not privvy to the security implications here, I'd naively expect this to be able to take any length key and then hash that to 32-bytes and use that as a key, but does that weaken security?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could accept keys of length greater than 32 bytes, but accepting and padding anything below that weakens security if the attacker knows the size of the key the user is using.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem with taking a variable-length key and hashing it is that many people will use a password instead of a key. And a hash of a password isn't a strong key. Taking a fixed length of bytes as a key encourages people to read the docs and not pass a password instead of a real key.
If you are worried about the implications of changing algorithms in the future, then I recommend that you provide a function that returns the currently-required key length.
And/or, you could design the API such that the user only has to supply one master (256-bit) key, and then different keys for HMAC-signing and sealing are derived (e.g. using ring::hkdf
) from that master key. That's what I recommend, because then it is less likely that the user will use related keys for HMAC signing and for sealing.
And/or, you could replace the use of HMAC for sign-only operations with the use of seal_in_place()
, where the data to be signed-but-not-encrypted is the ad
and the plaintext is empty. This will sign with GHASH (GCM) instead of HMAC in the case of AES-GCM. Then it is OK to use the same key for both signing-only and sign-and-encrypt operation.
src/jar.rs
Outdated
/// jar.add(Cookie::new("name", "value")); | ||
/// assert_eq!(jar.find("name").map(|c| c.value()), Some("value")); | ||
/// ``` | ||
pub fn find<'a>(&'a self, name: &str) -> Option<&'a Cookie<'static>> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the same time, perhaps this could implement Index
with &str
to allow doing:
println!("{}", jar["foo"].value());
This may prefer to avoid IndexMut
as changes need to be tracked for delta
src/secure/private.rs
Outdated
// Keep these in sync, and keep the key len synced with the `private` docs. | ||
static ALGO: &'static Algorithm = &AES_256_GCM; | ||
const KEY_LEN: usize = 32; | ||
const NONCE_LEN: usize = 12; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there perhaps a reference to add for why 12 was chosen?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nonce size is determined by the AEAD algorithm, here, 12
for a 96-bit nonce.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's ALGO.nonce_len()
.
src/secure/signed.rs
Outdated
/// | ||
/// # Panics | ||
/// | ||
/// Panics if `key` is not exactly 64 bytes long. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(similar concerns about usability with the 32-byte restriction above, just makign a note)
src/secure/signed.rs
Outdated
/// assert_eq!(jar.signed(&key).find("name").unwrap().value(), "value"); | ||
/// ``` | ||
pub fn add(&mut self, mut cookie: Cookie<'static>) { | ||
let key = SigningKey::new(DIGEST, self.key); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is constant, is there a downside to creating this then the Signed
jar was created? That'd help avoid the 'k
lifetime parameter I think as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I think this'd apply to Private
as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only downside is that we might have to include the lifetime in the future if SigningKey
changes or if we use a different library that doesn't store the key directly. In other words, we lose flexibility to swap out ring
easily.
Do you think the tradeoff is worth it?
src/secure/signed.rs
Outdated
/// signed_jar.remove(Cookie::named("name")); | ||
/// assert!(signed_jar.find("name").is_none()); | ||
/// ``` | ||
pub fn remove(&mut self, cookie: Cookie<'static>) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this (and other remove methods) perhaps return Option<Cookie<'static>>
? (the value that was actually removed)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, we reuse the cookie removed from original_cookies
, so we'd have to clone there. If the cookie was previously added via add
, we could just return.
I'd rather not clone
unnecessary. We could return a bool
though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh I thought we already had the cookie on hand to return and we just dropped it somewhere. Reading again though it looks like the returned value is squirreled away somewhere else, so yeah let's just return a bool.
src/secure/signed.rs
Outdated
|
||
// Keep these three in sync, and keep the key len synced with the `signed` docs. | ||
static DIGEST: &'static Algorithm = &SHA256; | ||
const BASE64_DIGEST_LEN: usize = 44; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be a function of KEY_LEN
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, this is a function of DIGEST_LEN
, which isn't declared but would be 256 / 8 = 32
. The function would the same as for BASE64_NONCE_LEN
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's approximately DIGEST.output_len
* 5 / 4`, or whatever the overhead calculation for base64 is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See my comment on BASE64_NONCE_LEN
for the true calculation.
Also cc @briansmith (usage of ring here), although to me at least it all looked great! (and ergonomic to use) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't review this carefully. Also, I was in a hurry so forgive the terseness or any perceived curtness.
src/secure/private.rs
Outdated
/// This type is only available when the `secure` feature is enabled. | ||
pub struct PrivateJar<'a, 'k> { | ||
parent: &'a mut CookieJar, | ||
key: &'k [u8] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is better for key
to be replaced with opening_key : OpeningKey, sealing_key: SealingKey
. This way, you minimize the chance of misuse of the key bytes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ties us to ring
pretty heavily. See my other comment about whether this tradeoff is worth it or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ties us to ring pretty heavily. See my other comment about whether this tradeoff is worth it or not.
I don't think it more tightly couples the library to ring in any significant way because these fields are private and so can't be accessed outside the module/crate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also like @briansmith's suggestion, but @SergioBenitez to leave the door open to having a slice of bytes in the future perhaps this could add a PhantomData<&'k [u8]>
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
slice of bytes in the future perhaps this could add a PhantomData<&'k [u8]>?
Is it really a good idea to reference the key as a slice anyway? In typical use, the user would need to create a wrapper around the PrivateJar
and key storage just to accomodate the key being a slice. IMO, even if my suggestion isn't taken, the key should be copied into a known value in the PrivateJar
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@briansmith That's exactly what I was thinking: in any case, we could just copy the key into the structure. I've removed the lifetime.
src/secure/private.rs
Outdated
fn unseal(&self, value: &str) -> Result<String, &'static str> { | ||
let (nonce_s, sealed_s) = value.split_at(BASE64_NONCE_LEN); | ||
let nonce = nonce_s.from_base64().map_err(|_| "bad nonce base64")?; | ||
let mut sealed = sealed_s.from_base64().map_err(|_| "bad sealed base64")?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All else being equal, IMO it is better to do the base64 decoding once and then split the decoded bytes, e.g. using split_at_mut(ALGO.nonce_len()).
src/secure/private.rs
Outdated
pub fn find(&self, name: &str) -> Option<Cookie<'static>> { | ||
if let Some(cookie_ref) = self.parent.find(name) { | ||
let mut cookie = cookie_ref.clone(); | ||
if cookie.value().len() <= BASE64_NONCE_LEN { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this check be in unseal()
to avoid a panic?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docstring for unseal
clarifies its intentions, and since it's used a single time, it doesn't really matter where this is.
src/secure/private.rs
Outdated
pub fn add(&mut self, mut cookie: Cookie<'static>) { | ||
// Generate the nonce. | ||
let mut nonce = [0; NONCE_LEN]; | ||
SystemRandom::new().fill(&mut nonce).expect("couldn't randomly fill nonce"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At a minimum, please document the fact that using a random nonce this short is not the best thing.
The nonce is only 96 bits. That means there is a 248 chance of collision even assuming the RNG is perfect. That's usually too low for nonce generation, which is why I don't recommend using regular AEAD algorithms for this kind of application, and also why there is no SealingKey::open_in_place_with_own_key()
. In briansmith/ring#411 and other issues, we're tracking adding better algorithms to ring to support this kind of use case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is something to be concerned about. AES-256-GCM is the industry standard AEAD, and while 96 bits of nonce isn't the best-possible-thing, I don't think it's reasonable to be concerned about a nonce collision occurring after 281,474,976,710,656 cookies have been encrypted.
In any case, I designed the code such that replacing the AEAD algorithm with something better is simply changing 3 trivial lines. Once there's something better, we should certainly move to that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AES-256-GCM is the industry standard AEAD, and while 96 bits of nonce isn't the best-possible-thing, I don't think it's reasonable to be concerned about a nonce collision occurring after 281,474,976,710,656 cookies have been encrypted.
Here is what the actual AES-GCM standard, from NIST, says:
Unless an implementation only uses 96-bit IVs that are generated by the deterministic construction: The total number of invocations of the authenticated encryption function shall not exceed 232, including all IV lengths and all instances of the authenticated encryption function with the given key.
(The emphasis is NIST's, not mine.)
In other words, if you use random nonces with AES-GCM then you must not encrypt more than ~4 billion cookies with the same key before replacing the key, according to the standard.
src/secure/private.rs
Outdated
let key = SealingKey::new(ALGO, self.key).expect("sealing key creation"); | ||
|
||
// Setup the input and output for the sealing operation. | ||
let overhead = ALGO.max_overhead_len(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not make in_out
contain nonce+ciphertext+tag (tag == overhead), and then base64-encode the whole thing in one shot? It seems like the resulting value would be slightly more efficiently encoded, and you'd save some mallocs in this function and in unseal()
, I think.
Then...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome suggestion. Thank you!
src/secure/signed.rs
Outdated
|
||
// Keep these three in sync, and keep the key len synced with the `signed` docs. | ||
static DIGEST: &'static Algorithm = &SHA256; | ||
const BASE64_DIGEST_LEN: usize = 44; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's approximately DIGEST.output_len
* 5 / 4`, or whatever the overhead calculation for base64 is.
src/secure/signed.rs
Outdated
use {Cookie, CookieJar}; | ||
|
||
// Keep these three in sync, and keep the key len synced with the `signed` docs. | ||
static DIGEST: &'static Algorithm = &SHA256; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, SIGNING_HMAC_DIGEST_ALG
or similar is a better name for this. In particular, it is strange that we have DIGEST
here and then ALGO
for the AEAD algorithm.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I toyed with the name a bit here. SIGNING_HMAC_DIGEST_ALG
is just too long for no real gain. Maybe just ALGO
, or HMAC_DIGEST
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think HMAC_DIGEST
is better than ALGO
.
src/secure/private.rs
Outdated
// Keep these in sync, and keep the key len synced with the `private` docs. | ||
static ALGO: &'static Algorithm = &AES_256_GCM; | ||
const KEY_LEN: usize = 32; | ||
const NONCE_LEN: usize = 12; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's ALGO.nonce_len()
.
src/secure/private.rs
Outdated
|
||
// Keep these in sync, and keep the key len synced with the `private` docs. | ||
static ALGO: &'static Algorithm = &AES_256_GCM; | ||
const KEY_LEN: usize = 32; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe document that this is ALGO.key_len()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the "keep in sync comment" implies this pretty directly.
src/secure/signed.rs
Outdated
// Keep these three in sync, and keep the key len synced with the `signed` docs. | ||
static DIGEST: &'static Algorithm = &SHA256; | ||
const BASE64_DIGEST_LEN: usize = 44; | ||
const KEY_LEN: usize = 64; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why did you choose the digest algorithm block size instead of the digest algorithm output size?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, in the upcoming, not-yet-released version of ring, I've added a new function to generate a serializable HMAC key; see briansmith/ring@f0d4760, based on this review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on this review, in the upcoming version of ring, I added a new API to help people generate serializable HMAC keys of a good length. See
briansmith/ring@f0d4760. Using the block size isn't wrong though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No great reason except that it makes brute-force-key searching harder. So while the security of any leaked, decrypted cookies via some collision doesn't increase, the probability that they've found the real key is greatly reduced. Likely doesn't matter in practice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No great reason except that it makes brute-force-key searching harder. So while the security of any leaked, decrypted cookies via some collision doesn't increase, the probability that they've found the real key is greatly reduced. Likely doesn't matter in practice.
It's not a big deal. I just want to point out that if the difference between a 256-bit key and a 512-bit key matters here then SHA-256 probably isn't the right thing to use. In general I recommend people use 256-bit keys for SHA-256 and 512-bit keys for SHA-384 (and don't use SHA-512).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I'll just use 32-byte keys.
I wish we could migrate to On the |
src/secure/private.rs
Outdated
// Keep these in sync, and keep the key len synced with the `private` docs. | ||
static ALGO: &'static Algorithm = &AES_256_GCM; | ||
const KEY_LEN: usize = 32; | ||
const NONCE_LEN: usize = 12; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reviewed the whole PR and I think you don't need the KEY_LEN
or NONCE_LEN
constants at all. You can now always use ALGO.key_len()
and ALGO.nonce_len()
instead, AFAICT, since there's no place where you need them to be compile-time constants.
return Err("length of value is <= BASE64_DIGEST_LEN"); | ||
} | ||
|
||
let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you base64-decode first and then split, you don't need to define BASE64_DIGEST_LEN
, since you can use HMAC_ALG.output_len
for the split index instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you base64-decode first and then split, you don't need to define BASE64_DIGEST_LEN, since you can use HMAC_ALG.output_len for the split index instead.
Sorry, I just realized that it doesn't make sense to base64-encode the entire value since, presumably, the initial value was already header-safe and base64-encoding would be unnecessarily wasteful.
FWIW, I released ring 0.7 with a new API for the HMAC key length recommendation; see https://briansmith.org/rustdoc/ring/hmac/fn.recommended_key_len.html. Your choice is also correct, but if you use |
Thanks so much for the review @briansmith! |
Yeah that's true, all we'd get is offsets from
Oh just kinda a random thought, I'd love to burn rustc-serialize to the ground but it's a private dep so we can change this at any time :) |
src/secure/signed.rs
Outdated
/// # Panics | ||
/// | ||
/// Panics if `key` is not exactly 64 bytes long. | ||
#[doc(hidden)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be a submodule of jar
so the constructor doesn't need to be public to the world?
In #78 I suggest using Z85 encoding instead of Base64 encoding. It's more space-efficient and just as compatible as Base64-encoding, AFAICT. Also there are two crates that implement Z85 already. |
@briansmith oh oops sorry! I was away all weekend and been digging backwards through email so I ended up seeing that later :). In any case, sounds like a great idea to me! |
src/jar.rs
Outdated
fn sign(root: &Root, cookie: Cookie<'static>) -> Cookie<'static> { | ||
secure::sign(&root._key, cookie) | ||
} | ||
pub fn private<'a, 'k>(&'a mut self, key: &[u8]) -> PrivateJar<'a> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This 'k
is now unused I think
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(and I think the 'a
can be elided)
let mut data; | ||
let output_len = { | ||
// Create the `SealingKey` structure. | ||
let key = SealingKey::new(ALGO, &self.key).expect("sealing key creation"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this be stored in the PrivateJar
struct itself?
Ok so API-wise I think we're all set here modulo the key length question. I'd like to not have a restriction that the keys passed in are a precise length because that seems (a) difficult to change over time and (b) surprising in terms of trying to match it. It sounds like hashing the input is a bad idea, but could we perhaps say that you need to pass in at least X bytes of data and we use slices within that for various keys? That way we can also provide a sample command for generating the keys to pass in perhaps. |
Keep in mind that if/when the algorithms change, the keys need to change too. For example, let's say you switch AES-256-GCM to ChaCha20-Poly1305. Even though the key size is the same, it isn't safe to use the same key (or, rather, we assume it is unsafe and nobody has proven otherwise, nor will anybody ever do so). Thus, if you want to have some flexibility then you need to specify, at least, a minimum key length, e.g. 256 bits. Then you can use HKDF, (see Then whenever you change anything related to the crypto in cookie-rs, you need to change the those labels. Also note that with this scheme, different origins will always have different keys. For this latter reason alone, it may be a good idea to move to the derived-key scheme. However, still, I recommend that instead of specifying a minimum key length, you instead specify a fixed key length. In general we (crypto people) don't like variables, especially regarding key lengths. (We now generally regard the way HMAC's key was specified as a mistake.) |
@alexcrichton I'm not sure I understand what you mean by "sample command for generating the keys to pass in". Do you mean like a shell command? If so, how would that be any different if we had a fixed-key length? And if you mean some example Rust code, then you should consider that most of these keys will be fixed. That is, they won't be generated at runtime. This is because (especially in production) you generally want to accept cookies that were signed the last time you started your application or by other instances of that application. As a result, you need one, fixed key. In general, I'm in agreement with @briansmith here, as evidenced by my choice of a fixed-key length to begin with. |
@briansmith that sounds like a great idea! (sorry I'm pretty unfamiliar with hkdf). I'd be totally fine though requiring a fixed key length if we can use that to derive further keys (even if we change underlying algorithms) @SergioBenitez eh I basically just mean as a new users I see this key restriction or these random constants pass in and I'm not sure how to do so myself. Just some docs for "here's how to create your own key then pass it in". |
@alexcrichton I think allowing variable sized keys or extracting keys of different lengths from one key is something that should be built on-top of As far as how to get these random constants...the safest way (and the way Rocket will do this) is to encode the key in a text-safe format (base64 in Rocket) and then get the byte representation of the key. It's trivial to generate a safe 32-byte random key in this way: |
But, you shouldn't use the same key for different algorithms. So if there's any possibility that the algorithm will change (and I think there is), it is better to derive the keys from the master key.
Yes, and this is why variable-length inputs are bad, because some callers are likely to pass in the base64-encoded key bytes, instead of decoding the base64 first. Using base64-encoded bytes as a key, without decoding them, is bad. |
How do we defend against this with Running
Indeed. |
Yes, I explained in #76 (comment). I don't want to get too hung up in providing advice here, so I won't debate it further. |
@SergioBenitez lets use hkdf. "This probably won't change soon" sounds like "when this changes it'll be too difficult to change when we really need to". Let's cover our bases and start out conservatively? If this really has a perf impact or something like that we can reevaluate. |
This commit completely rewrites `CookieJar` so that it is more efficient while being simpler and more correct overall. At a high-level, a `CookieJar` now returns borrows to `Cookie` structures whenever possible. `CookieJar` and children jars (now `PrivateJar` and `SignedJar`) are distinct types. The "encrypted" child jar has been replaced by `PrivateJar` which uses authenticated encryption. All crypto is done via `ring`. A detailed list of changes is below: * The "permanent" child jar has been removed. * A `make_permanent` method has been added to `Cookie`. * A `permanent` method has been added to `CookieBuilder`. * A `named` constructor has been added to `Cookie`. * `CookieJar` no longer has a generic lifetime parameter. * A `CookieJar` no longer contains `RefCell`s. * `CookieJar::{add_original, add, remove, clear}` take `&mut self`. * `CookieJar::new` does not take in a `key` parameter. * `CookieJar::find` returns a borrow to a `Cookie`. * `CookieJar::remove` takes in a `Cookie`. * `CookieJar::clear` has been deprecated. * A `CookieJar` tracks its modifications more robustly, passing a new suite of unit tests. * Computing the `delta` is zero-cost operation. * Cookie iteration is significantly simplified. * The `signed` method has been removed from `CookieJar`. * A new `Signed` trait was added with a `signed(&key)` method. * The `Signed` trait is implemented for `CookieJar`. * `SignedJar` cookies sign via ring's HMAC-SHA256 implementation. * The `encrypted` method has been removed from `CookieJar`. * A new `Private` trait was added with a `private(&key)` method. * The `Private` trait is implemented for `CookieJar`. * `PrivateJar` cookies use AEAD (AES-256-GCM) from ring. * The `openssl` dependency was dropped. * A `ring` dependency was introduced. * All items are extensively documented. Fixes #35. Fixes #50. Fixes #64. Closes #68. Fixes #75.
Okay. Now using HKDF via a This should be all! |
👍 |
@SergioBenitez do you want to do the release on this one? |
Released! :) |
This commit completely rewrites
CookieJar
so that it is more efficient while being simpler and more correct overall. At a high-level, aCookieJar
now returns borrows toCookie
structures whenever possible.CookieJar
and children jars (nowPrivateJar
andSignedJar
) are distinct types. The "encrypted" child jar has been replaced byPrivateJar
which uses authenticated encryption. All crypto is done viaring
.A detailed list of changes is below:
make_permanent
method has been added toCookie
.permanent
method has been added toCookieBuilder
.named
constructor has been added toCookie
.CookieJar
no longer has a generic lifetime parameter.CookieJar
no longer containsRefCell
s.CookieJar::{add_original, add, remove, clear}
take&mut self
.CookieJar::new
does not take in akey
parameter.CookieJar::find
returns a borrow to aCookie
.CookieJar::remove
takes in aCookie
.CookieJar::clear
has been deprecated.CookieJar
tracks its modifications more robustly, passing a new suite of unit tests.delta
is zero-cost operation.signed
method has been removed fromCookieJar
.Signed
trait was added with asigned(&key)
method.Signed
trait is implemented forCookieJar
.SignedJar
cookies sign via ring's HMAC-SHA256 implementation.encrypted
method has been removed fromCookieJar
.Private
trait was added with aprivate(&key)
method.Private
trait is implemented forCookieJar
.PrivateJar
cookies use AEAD (AES-256-GCM) from ring.openssl
dependency was dropped.ring
dependency was introduced.Fixes #35.
Fixes #50.
Fixes #64.
Closes #68.
Fixes #75.
Finally, I'd like to, once again, request that I be given maintainer access to the repository/crate. I'd really like to continue improving and maintaining this crate in the future.