Repository: mike-engel/jwt-cli (Rust)

Issue:
@ prefix to read a binary file cannot be used with HMAC algorithms
### Summary

The help text claims that a value starting with `@` can be provided to the `-S`/`--secret` parameter in order to read the secret from a binary file. However, this is not implemented for the HMAC algorithms (like HS256) - instead, the string starting with `@` provided on the command line is used verbatim as the secret, leading to confusing signature validation issues.

This unfortunately leads this tool to encourage the exclusive use of human-readable strings as JWT secrets when using the HMAC modes, which considerably reduces the size of the keyspace.

### Steps to reproduce

```
jwt encode -S @secret.key -a fake -i fake -p fake -s fake
```

### Expected behavior

The bytes contained in the file `secret.key` are used as the JWT secret.

### Actual behavior

The bytes of the string `@secret.key` are used as the JWT secret.


Discussion:

Patch:
diff --git a/src/main.rs b/src/main.rs
index ad5409c..250c353 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -345,7 +345,12 @@ fn slurp_file(file_name: &str) -> Vec<u8> {
 fn encoding_key_from_secret(alg: &Algorithm, secret_string: &str) -> JWTResult<EncodingKey> {
     match alg {
         Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
-            Ok(EncodingKey::from_secret(secret_string.as_bytes()))
+            if secret_string.starts_with("@") {
+                let secret = slurp_file(&secret_string.chars().skip(1).collect::<String>());
+                Ok(EncodingKey::from_secret(&secret))
+            } else {
+                Ok(EncodingKey::from_secret(secret_string.as_bytes()))
+            }
         }
         Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => {
             let secret = slurp_file(&secret_string.chars().skip(1).collect::<String>());
@@ -373,7 +378,12 @@ fn decoding_key_from_secret(
 ) -> JWTResult<DecodingKey<'static>> {
     match alg {
         Algorithm::HS256 | Algorithm::HS384 | Algorithm::HS512 => {
-            Ok(DecodingKey::from_secret(secret_string.as_bytes()).into_static())
+            if secret_string.starts_with("@") {
+                let secret = slurp_file(&secret_string.chars().skip(1).collect::<String>());
+                Ok(DecodingKey::from_secret(&secret).into_static())
+            } else {
+                Ok(DecodingKey::from_secret(secret_string.as_bytes()).into_static())
+            }
         }
         Algorithm::RS256 | Algorithm::RS384 | Algorithm::RS512 => {
             let secret = slurp_file(&secret_string.chars().skip(1).collect::<String>());


Test patch:
diff --git a/tests/hmac-key.bin b/tests/hmac-key.bin
new file mode 100644
index 0000000..405571c
--- /dev/null
+++ b/tests/hmac-key.bin
@@ -0,0 +1,1 @@
+��/���Goɂ��Pؿ>V�%����N�p⹠p
\ No newline at end of file
diff --git a/tests/jwt-cli.rs b/tests/jwt-cli.rs
index 8f9cf88..993b6c1 100644
--- a/tests/jwt-cli.rs
+++ b/tests/jwt-cli.rs
@@ -3,12 +3,12 @@ include!("../src/main.rs");
 #[cfg(test)]
 mod tests {
     use super::{
-        config_options, create_header, decode_token, encode_token, is_payload_item,
-        is_timestamp_or_duration, translate_algorithm, OutputFormat, Payload, PayloadItem,
-        SupportedAlgorithms,
+        config_options, create_header, decode_token, decoding_key_from_secret, encode_token,
+        encoding_key_from_secret, is_payload_item, is_timestamp_or_duration, translate_algorithm,
+        OutputFormat, Payload, PayloadItem, SupportedAlgorithms,
     };
     use chrono::{Duration, TimeZone, Utc};
-    use jsonwebtoken::{Algorithm, Header, TokenData};
+    use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, TokenData};
     use serde_json::{from_value, json};
 
     fn empty_args() -> impl IntoIterator<Item = String> {
@@ -592,6 +592,20 @@ mod tests {
         assert!(result.is_ok());
     }
 
+    #[test]
+    fn encoding_key_from_secret_handles_at() {
+        let expected = EncodingKey::from_secret(include_bytes!("hmac-key.bin"));
+        let key = encoding_key_from_secret(&Algorithm::HS256, "@./tests/hmac-key.bin").unwrap();
+        assert_eq!(expected, key);
+    }
+
+    #[test]
+    fn decoding_key_from_secret_handles_at() {
+        let expected = DecodingKey::from_secret(include_bytes!("hmac-key.bin"));
+        let key = decoding_key_from_secret(&Algorithm::HS256, "@./tests/hmac-key.bin").unwrap();
+        assert_eq!(expected, key);
+    }
+
     #[test]
     fn encodes_and_decodes_an_ecdsa_token_using_key_from_file() {
         let body: String = "{\"field\":\"value\"}".to_string();