-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
HMS-2695 feat: JWK encryption with AES-GCM
Implement helpers to encrypt and decrypt a JWK with AES-GCM AEAD. `EncryptJWK()` takes a raw AES encryption key plus `jwk.Key` and returns the encrypted key. The key is encrypted and protected with AES-GCM. The random nonce is pre-pended to the output stream. `DecryptJWK()` is the reverse operation. It takes a raw AES key and cipher text with nonce pre-pended. On success, it returns a `jwk.Key`. Invalid key and tampering are detected by AEAD. Also pulls the latest `lestrrat-go/jwx@v2` go module. Signed-off-by: Christian Heimes <[email protected]>
- Loading branch information
Showing
4 changed files
with
153 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package token | ||
|
||
import ( | ||
"crypto/aes" | ||
"crypto/cipher" | ||
"crypto/rand" | ||
"encoding/json" | ||
"errors" | ||
"io" | ||
|
||
"github.com/lestrrat-go/jwx/v2/jwk" | ||
) | ||
|
||
// Encrypt a JWK Key with encryption key | ||
// Uses AES-GCM with a random nonce. The nonce is pre-pended to ciphertext. | ||
func EncryptJWK(aeskey []byte, jwkey jwk.Key) (enc []byte, err error) { | ||
// create AES-GCM AEAD and nonce | ||
block, err := aes.NewCipher(aeskey) | ||
if err != nil { | ||
return nil, err | ||
} | ||
aesgcm, err := cipher.NewGCM(block) | ||
if err != nil { | ||
return nil, err | ||
} | ||
nonce := make([]byte, aesgcm.NonceSize()) | ||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil { | ||
return nil, err | ||
} | ||
|
||
// serialize JWK to JSON bytes | ||
jwkbuf, err := json.Marshal(jwkey) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// encrypt and seal | ||
ciphertext := aesgcm.Seal(nil, nonce, jwkbuf, nil) | ||
// combine nonce, ciphertext + tag | ||
return append(nonce, ciphertext...), nil | ||
} | ||
|
||
// Decrypt an AES-GCM encrypted JWK | ||
func DecryptJWK(aeskey []byte, encrypted []byte) (jwkey jwk.Key, err error) { | ||
// create AES-GCM AEAD and get nonce | ||
block, err := aes.NewCipher(aeskey) | ||
if err != nil { | ||
return nil, err | ||
} | ||
aesgcm, err := cipher.NewGCM(block) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if len(encrypted) < (aesgcm.NonceSize() + aesgcm.Overhead()) { | ||
return nil, errors.New("encrypted blob is too short") | ||
} | ||
nonce := encrypted[:aesgcm.NonceSize()] | ||
ciphertext := encrypted[aesgcm.NonceSize():] | ||
|
||
// unseal and decrypt | ||
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return jwk.ParseKey(plaintext) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package token | ||
|
||
import ( | ||
"crypto/ecdsa" | ||
"crypto/elliptic" | ||
"crypto/rand" | ||
"testing" | ||
|
||
"github.com/lestrrat-go/jwx/v2/jwk" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestJWKEncryption(t *testing.T) { | ||
raw, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
assert.NoError(t, err) | ||
jwkey, err := jwk.FromRaw(raw) | ||
assert.NoError(t, err) | ||
|
||
aeskey1 := make([]byte, 16) | ||
_, err = rand.Reader.Read(aeskey1) | ||
assert.NoError(t, err) | ||
|
||
encrypted, err := EncryptJWK(aeskey1, jwkey) | ||
assert.NoError(t, err) | ||
|
||
decrypted, err := DecryptJWK(aeskey1, encrypted) | ||
assert.NoError(t, err) | ||
assert.Equal(t, decrypted, jwkey) | ||
|
||
aeskey2 := make([]byte, 16) | ||
decrypted, err = DecryptJWK(aeskey2, encrypted) | ||
assert.Nil(t, decrypted) | ||
assert.EqualError(t, err, "cipher: message authentication failed") | ||
} |