Repository: lestrrat-go/jwx
Pull request #213 (2020-10-08 03:57:45)

Implement direct key encoding
This key encryption mode simply bypasses a key provided by the user to
the content encryptor.

Here is an example of usage:

  key := []byte{...}
  jwe.Decrypt(data, jwa.DIRECT, key)

Signed-off-by: Anatol Pomozov <anatol.pomozov@gmail.com>


Patch:
diff --git a/jwe/message.go b/jwe/message.go
index 5ad91fa0..5a9e64f7 100644
--- a/jwe/message.go
+++ b/jwe/message.go
@@ -453,23 +453,32 @@ func (m *Message) Decrypt(alg jwa.KeyEncryptionAlgorithm, key interface{}) ([]by
 			continue
 		}
 
-		k, err := buildKeyDecrypter(h2.Algorithm(), h2, key, keysize)
-		if err != nil {
-			lastError = errors.Wrap(err, `failed to build key decrypter`)
-			if pdebug.Enabled {
-				pdebug.Printf(`%s`, lastError)
+		var cek []byte
+		if h2.Algorithm() == jwa.DIRECT {
+			var ok bool
+			cek, ok = key.([]byte)
+			if !ok {
+				return nil, errors.Errorf("[]byte is required as the key to build %s key decrypter", alg)
+			}
+		} else {
+			k, err := buildKeyDecrypter(h2.Algorithm(), h2, key, keysize)
+			if err != nil {
+				lastError = errors.Wrap(err, `failed to build key decrypter`)
+				if pdebug.Enabled {
+					pdebug.Printf(`%s`, lastError)
+				}
+				continue
 			}
-			continue
-		}
 
-		cek, err := k.Decrypt(recipient.EncryptedKey().Bytes())
-		if err != nil {
-			lastError = errors.Wrap(err, `failed to decrypt key`)
-			if pdebug.Enabled {
-				pdebug.Printf(`%s`, lastError)
+			cek, err = k.Decrypt(recipient.EncryptedKey().Bytes())
+			if err != nil {
+				lastError = errors.Wrap(err, `failed to decrypt key`)
+				if pdebug.Enabled {
+					pdebug.Printf(`%s`, lastError)
+				}
+				return nil, lastError
+				//			continue
 			}
-			return nil, lastError
-			//			continue
 		}
 
 		plaintext, err = cipher.Decrypt(cek, iv, ciphertext, tag, computedAad)


Test patch:
diff --git a/jwe/jwe_test.go b/jwe/jwe_test.go
index 5748ea55..22f611d0 100644
--- a/jwe/jwe_test.go
+++ b/jwe/jwe_test.go
@@ -490,6 +490,138 @@ func Test_GHIssue207(t *testing.T) {
 	}
 }
 
+// tests direct key encryption by encrypting-decrypting a plaintext
+func TestEncode_Direct(t *testing.T) {
+	// TODO: uncomment once jwe.Encrypt() supports jwa.DIRECT
+	/*	var testcases = []struct {
+			Algorithm jwa.ContentEncryptionAlgorithm
+			KeySize   int // in bytes
+		}{
+			{jwa.A128CBC_HS256, 16},
+			{jwa.A128GCM, 16},
+			{jwa.A192CBC_HS384, 24},
+			{jwa.A192GCM, 24},
+			{jwa.A256CBC_HS512, 32},
+			{jwa.A256GCM, 32},
+		}
+		plaintext := []byte("Lorem ipsum")
+
+		for _, tc := range testcases {
+			tc := tc
+			t.Run(tc.Algorithm.String(), func(t *testing.T) {
+				key := make([]byte, tc.KeySize)
+				_, err := rand.Read(key)
+				if !assert.NoError(t, err, "Key generation succeeds") {
+					return
+				}
+
+				encrypted, err := jwe.Encrypt(plaintext, jwa.DIRECT, key, tc.Algorithm, jwa.NoCompress)
+				if !assert.NoError(t, err, "Encrypt succeeds") {
+					return
+				}
+
+				decrypted, err := jwe.Decrypt(encrypted, jwa.DIRECT, key)
+				if !assert.NoError(t, err, `jwe.Decrypt should succeed`) {
+					return
+				}
+
+				assert.Equal(t, plaintext, decrypted, `jwe.Decrypt should match input plaintext`)
+			})
+		}
+	*/
+}
+
+// Decrypts messages generated by `jose` tool. It helps check compatibility with other jwx implementations.
+func TestDecodePredefined_Direct(t *testing.T) {
+	var testcases = []struct {
+		Algorithm  jwa.ContentEncryptionAlgorithm
+		Key        string // generated with 'jose jwk gen -i '{"alg":"A128GCM"}' -o key.jwk'
+		Thumbprint string // generated with 'jose jwk thp -i key.jwk`
+		Data       string // generated with 'jose jwe enc -I msg.txt -k key.jwk -o msg.jwe'
+	}{
+		{
+			jwa.A128CBC_HS256,
+			`{"alg":"A128GCM","k":"9hexZKVSV9pZhPNzgXiD8g","key_ops":["encrypt","decrypt"],"kty":"oct"}`,
+			`RwW22IemrIJLFwlqZ-OQUe_Lnbo`,
+			`{"ciphertext":"FX_px9cuyO_hZfo","encrypted_key":"","header":{"alg":"dir"},"iv":"Z9CRJCFPtpEI5Pwq","protected":"eyJlbmMiOiJBMTI4R0NNIn0","tag":"1iq0MNDX40XVtqGYinhUtQ"}`,
+		},
+		{
+			jwa.A128GCM,
+			`{"alg":"A128GCM","k":"9hexZKVSV9pZhPNzgXiD8g","key_ops":["encrypt","decrypt"],"kty":"oct"}`,
+			`RwW22IemrIJLFwlqZ-OQUe_Lnbo`,
+			`{"ciphertext":"FX_px9cuyO_hZfo","encrypted_key":"","header":{"alg":"dir"},"iv":"Z9CRJCFPtpEI5Pwq","protected":"eyJlbmMiOiJBMTI4R0NNIn0","tag":"1iq0MNDX40XVtqGYinhUtQ"}`,
+		},
+		{
+			jwa.A192CBC_HS384,
+			`{"alg":"A128GCM","k":"9hexZKVSV9pZhPNzgXiD8g","key_ops":["encrypt","decrypt"],"kty":"oct"}`,
+			`RwW22IemrIJLFwlqZ-OQUe_Lnbo`,
+			`{"ciphertext":"FX_px9cuyO_hZfo","encrypted_key":"","header":{"alg":"dir"},"iv":"Z9CRJCFPtpEI5Pwq","protected":"eyJlbmMiOiJBMTI4R0NNIn0","tag":"1iq0MNDX40XVtqGYinhUtQ"}`,
+		},
+		{
+			jwa.A192GCM,
+			`{"alg":"A128GCM","k":"9hexZKVSV9pZhPNzgXiD8g","key_ops":["encrypt","decrypt"],"kty":"oct"}`,
+			`RwW22IemrIJLFwlqZ-OQUe_Lnbo`,
+			`{"ciphertext":"FX_px9cuyO_hZfo","encrypted_key":"","header":{"alg":"dir"},"iv":"Z9CRJCFPtpEI5Pwq","protected":"eyJlbmMiOiJBMTI4R0NNIn0","tag":"1iq0MNDX40XVtqGYinhUtQ"}`,
+		},
+		{
+			jwa.A256CBC_HS512,
+			`{"alg":"A128GCM","k":"9hexZKVSV9pZhPNzgXiD8g","key_ops":["encrypt","decrypt"],"kty":"oct"}`,
+			`RwW22IemrIJLFwlqZ-OQUe_Lnbo`,
+			`{"ciphertext":"FX_px9cuyO_hZfo","encrypted_key":"","header":{"alg":"dir"},"iv":"Z9CRJCFPtpEI5Pwq","protected":"eyJlbmMiOiJBMTI4R0NNIn0","tag":"1iq0MNDX40XVtqGYinhUtQ"}`,
+		},
+		{
+			jwa.A256GCM,
+			`{"alg":"A128GCM","k":"9hexZKVSV9pZhPNzgXiD8g","key_ops":["encrypt","decrypt"],"kty":"oct"}`,
+			`RwW22IemrIJLFwlqZ-OQUe_Lnbo`,
+			`{"ciphertext":"FX_px9cuyO_hZfo","encrypted_key":"","header":{"alg":"dir"},"iv":"Z9CRJCFPtpEI5Pwq","protected":"eyJlbmMiOiJBMTI4R0NNIn0","tag":"1iq0MNDX40XVtqGYinhUtQ"}`,
+		},
+	}
+	plaintext := "Lorem ipsum"
+
+	for _, tc := range testcases {
+		tc := tc
+		t.Run(tc.Algorithm.String(), func(t *testing.T) {
+			webKeys, err := jwk.ParseString(tc.Key)
+			if !assert.NoError(t, err, `jwk.ParseString should succeed`) {
+				return
+			}
+			webKey := webKeys.Keys[0]
+			thumbprint, err := webKey.Thumbprint(crypto.SHA1)
+			if !assert.NoError(t, err, `jwk.Thumbprint should succeed`) {
+				return
+			}
+
+			if !assert.Equal(t, base64.RawURLEncoding.EncodeToString(thumbprint), tc.Thumbprint, `thumbprints should match`) {
+				return
+			}
+
+			var key []byte
+			if !assert.NoError(t, webKey.Raw(&key), `jwk.Raw should succeed`) {
+				return
+			}
+
+			msg, err := jwe.ParseString(tc.Data)
+			if !assert.NoError(t, err, `jwe.ParseString should succeed`) {
+				return
+			}
+
+			{
+				buf, _ := json.MarshalIndent(msg, "", "  ")
+				t.Logf("%s", buf)
+			}
+
+			decrypted, err := msg.Decrypt(jwa.DIRECT, key)
+			if !assert.NoError(t, err, `jwe.Decrypt should succeed`) {
+				return
+			}
+
+			if !assert.Equal(t, plaintext, string(decrypted), `plaintext should match`) {
+				return
+			}
+		})
+	}
+}
+
 /*
 XXX If I'm reading this right, when a message is encrypted, the exact output
 depends on the JSON serialization format of the data, including things like