Skip to content

Commit

Permalink
Merge pull request square#190 from maraino/v2
Browse files Browse the repository at this point in the history
Add support for PBES2 Key algorithms
  • Loading branch information
csstaub authored Aug 1, 2018
2 parents 349dc03 + 3a85db9 commit 8254d6c
Show file tree
Hide file tree
Showing 5 changed files with 431 additions and 7 deletions.
28 changes: 25 additions & 3 deletions crypter.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,18 @@ func (eo *EncrypterOptions) WithType(typ ContentType) *EncrypterOptions {
}

// Recipient represents an algorithm/key to encrypt messages to.
//
// PBES2Count and PBES2Salt correspond with the "p2c" and "p2s" headers used
// on the password-based encryption algorithms PBES2-HS256+A128KW,
// PBES2-HS384+A192KW, and PBES2-HS512+A256KW. If they are not provided a safe
// default of 100000 will be used for the count and a 128-bit random salt will
// be generated.
type Recipient struct {
Algorithm KeyAlgorithm
Key interface{}
KeyID string
Algorithm KeyAlgorithm
Key interface{}
KeyID string
PBES2Count int
PBES2Salt []byte
}

// NewEncrypter creates an appropriate encrypter based on the key type
Expand Down Expand Up @@ -228,6 +236,14 @@ func (ctx *genericEncrypter) addRecipient(recipient Recipient) (err error) {
recipientInfo.keyID = recipient.KeyID
}

switch recipient.Algorithm {
case PBES2_HS256_A128KW, PBES2_HS384_A192KW, PBES2_HS512_A256KW:
if sr, ok := recipientInfo.keyEncrypter.(*symmetricKeyCipher); ok {
sr.p2c = recipient.PBES2Count
sr.p2s = recipient.PBES2Salt
}
}

if err == nil {
ctx.recipients = append(ctx.recipients, recipientInfo)
}
Expand All @@ -242,6 +258,8 @@ func makeJWERecipient(alg KeyAlgorithm, encryptionKey interface{}) (recipientKey
return newECDHRecipient(alg, encryptionKey)
case []byte:
return newSymmetricRecipient(alg, encryptionKey)
case string:
return newSymmetricRecipient(alg, []byte(encryptionKey))
case *JSONWebKey:
recipient, err := makeJWERecipient(alg, encryptionKey.Key)
recipient.keyID = encryptionKey.KeyID
Expand All @@ -266,6 +284,10 @@ func newDecrypter(decryptionKey interface{}) (keyDecrypter, error) {
return &symmetricKeyCipher{
key: decryptionKey,
}, nil
case string:
return &symmetricKeyCipher{
key: []byte(decryptionKey),
}, nil
case JSONWebKey:
return newDecrypter(decryptionKey.Key)
case *JSONWebKey:
Expand Down
218 changes: 214 additions & 4 deletions crypter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ import (
"crypto/rsa"
"fmt"
"io"
"math/big"
"reflect"
"regexp"
"testing"

"golang.org/x/crypto/ed25519"
Expand All @@ -40,7 +42,15 @@ var ecTestKey521, _ = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
var ed25519PublicKey, ed25519PrivateKey, _ = ed25519.GenerateKey(rand.Reader)

func RoundtripJWE(keyAlg KeyAlgorithm, encAlg ContentEncryption, compressionAlg CompressionAlgorithm, serializer func(*JSONWebEncryption) (string, error), corrupter func(*JSONWebEncryption) bool, aad []byte, encryptionKey interface{}, decryptionKey interface{}) error {
enc, err := NewEncrypter(encAlg, Recipient{Algorithm: keyAlg, Key: encryptionKey}, &EncrypterOptions{Compression: compressionAlg})
var rcpt Recipient
switch keyAlg {
case PBES2_HS256_A128KW, PBES2_HS384_A192KW, PBES2_HS512_A256KW:
// use 1k iterations instead of 100k to reduce computational cost
rcpt = Recipient{Algorithm: keyAlg, Key: encryptionKey, PBES2Count: 1000}
default:
rcpt = Recipient{Algorithm: keyAlg, Key: encryptionKey}
}
enc, err := NewEncrypter(encAlg, rcpt, &EncrypterOptions{Compression: compressionAlg})
if err != nil {
return fmt.Errorf("error on new encrypter: %s", err)
}
Expand Down Expand Up @@ -87,7 +97,9 @@ func TestRoundtripsJWE(t *testing.T) {
// Test matrix
keyAlgs := []KeyAlgorithm{
DIRECT, ECDH_ES, ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW, A128KW, A192KW, A256KW,
RSA1_5, RSA_OAEP, RSA_OAEP_256, A128GCMKW, A192GCMKW, A256GCMKW}
RSA1_5, RSA_OAEP, RSA_OAEP_256, A128GCMKW, A192GCMKW, A256GCMKW,
PBES2_HS256_A128KW, PBES2_HS384_A192KW, PBES2_HS512_A256KW,
}
encAlgs := []ContentEncryption{A128GCM, A192GCM, A256GCM, A128CBC_HS256, A192CBC_HS384, A256CBC_HS512}
zipAlgs := []CompressionAlgorithm{NONE, DEFLATE}

Expand Down Expand Up @@ -123,7 +135,7 @@ func TestRoundtripsJWE(t *testing.T) {

func TestRoundtripsJWECorrupted(t *testing.T) {
// Test matrix
keyAlgs := []KeyAlgorithm{DIRECT, ECDH_ES, ECDH_ES_A128KW, A128KW, RSA1_5, RSA_OAEP, RSA_OAEP_256, A128GCMKW}
keyAlgs := []KeyAlgorithm{DIRECT, ECDH_ES, ECDH_ES_A128KW, A128KW, RSA1_5, RSA_OAEP, RSA_OAEP_256, A128GCMKW, PBES2_HS256_A128KW}
encAlgs := []ContentEncryption{A128GCM, A192GCM, A256GCM, A128CBC_HS256, A192CBC_HS384, A256CBC_HS512}
zipAlgs := []CompressionAlgorithm{NONE, DEFLATE}

Expand Down Expand Up @@ -220,7 +232,7 @@ func TestEncrypterWithJWKAndKeyID(t *testing.T) {
}

func TestEncrypterWithBrokenRand(t *testing.T) {
keyAlgs := []KeyAlgorithm{ECDH_ES_A128KW, A128KW, RSA1_5, RSA_OAEP, RSA_OAEP_256, A128GCMKW}
keyAlgs := []KeyAlgorithm{ECDH_ES_A128KW, A128KW, RSA1_5, RSA_OAEP, RSA_OAEP_256, A128GCMKW, PBES2_HS256_A128KW}
encAlgs := []ContentEncryption{A128GCM, A192GCM, A256GCM, A128CBC_HS256, A192CBC_HS384, A256CBC_HS512}

serializer := func(obj *JSONWebEncryption) (string, error) { return obj.CompactSerialize() }
Expand Down Expand Up @@ -406,6 +418,197 @@ func TestEncrypterExtraHeaderInclusion(t *testing.T) {
}
}

// TestPBES2JWKEncryption uses the plaintext and serialization reference of
// JWK RFC https://tools.ietf.org/html/rfc7517#appendix-C.4
func TestPBES2JWKEncryption(t *testing.T) {
passphrase := []byte("Thus from my lips, by yours, my sin is purged.")

plaintext := []byte(`{
"kty":"RSA",
"kid":"[email protected]",
"use":"enc",
"n":"t6Q8PWSi1dkJj9hTP8hNYFlvadM7DflW9mWepOJhJ66w7nyoK1gPNqFMSQRy
O125Gp-TEkodhWr0iujjHVx7BcV0llS4w5ACGgPrcAd6ZcSR0-Iqom-QFcNP
8Sjg086MwoqQU_LYywlAGZ21WSdS_PERyGFiNnj3QQlO8Yns5jCtLCRwLHL0
Pb1fEv45AuRIuUfVcPySBWYnDyGxvjYGDSM-AqWS9zIQ2ZilgT-GqUmipg0X
OC0Cc20rgLe2ymLHjpHciCKVAbY5-L32-lSeZO-Os6U15_aXrk9Gw8cPUaX1
_I8sLGuSiVdt3C_Fn2PZ3Z8i744FPFGGcG1qs2Wz-Q",
"e":"AQAB",
"d":"GRtbIQmhOZtyszfgKdg4u_N-R_mZGU_9k7JQ_jn1DnfTuMdSNprTeaSTyWfS
NkuaAwnOEbIQVy1IQbWVV25NY3ybc_IhUJtfri7bAXYEReWaCl3hdlPKXy9U
vqPYGR0kIXTQRqns-dVJ7jahlI7LyckrpTmrM8dWBo4_PMaenNnPiQgO0xnu
ToxutRZJfJvG4Ox4ka3GORQd9CsCZ2vsUDmsXOfUENOyMqADC6p1M3h33tsu
rY15k9qMSpG9OX_IJAXmxzAh_tWiZOwk2K4yxH9tS3Lq1yX8C1EWmeRDkK2a
hecG85-oLKQt5VEpWHKmjOi_gJSdSgqcN96X52esAQ",
"p":"2rnSOV4hKSN8sS4CgcQHFbs08XboFDqKum3sc4h3GRxrTmQdl1ZK9uw-PIHf
QP0FkxXVrx-WE-ZEbrqivH_2iCLUS7wAl6XvARt1KkIaUxPPSYB9yk31s0Q8
UK96E3_OrADAYtAJs-M3JxCLfNgqh56HDnETTQhH3rCT5T3yJws",
"q":"1u_RiFDP7LBYh3N4GXLT9OpSKYP0uQZyiaZwBtOCBNJgQxaj10RWjsZu0c6I
edis4S7B_coSKB0Kj9PaPaBzg-IySRvvcQuPamQu66riMhjVtG6TlV8CLCYK
rYl52ziqK0E_ym2QnkwsUX7eYTB7LbAHRK9GqocDE5B0f808I4s",
"dp":"KkMTWqBUefVwZ2_Dbj1pPQqyHSHjj90L5x_MOzqYAJMcLMZtbUtwKqvVDq3
tbEo3ZIcohbDtt6SbfmWzggabpQxNxuBpoOOf_a_HgMXK_lhqigI4y_kqS1w
Y52IwjUn5rgRrJ-yYo1h41KR-vz2pYhEAeYrhttWtxVqLCRViD6c",
"dq":"AvfS0-gRxvn0bwJoMSnFxYcK1WnuEjQFluMGfwGitQBWtfZ1Er7t1xDkbN9
GQTB9yqpDoYaN06H7CFtrkxhJIBQaj6nkF5KKS3TQtQ5qCzkOkmxIe3KRbBy
mXxkb5qwUpX5ELD5xFc6FeiafWYY63TmmEAu_lRFCOJ3xDea-ots",
"qi":"lSQi-w9CpyUReMErP1RsBLk7wNtOvs5EQpPqmuMvqW57NBUczScEoPwmUqq
abu9V0-Py4dQ57_bapoKRu1R90bvuFnU63SHWEFglZQvJDMeAvmj4sm-Fp0o
Yu_neotgQ0hzbI5gry7ajdYy9-2lNx_76aBZoOUu9HCJ-UsfSOI8"
}`)

serializationReference := `
eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJwMnMiOiIyV0NUY0paMVJ2ZF9DSn
VKcmlwUTF3IiwicDJjIjo0MDk2LCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5Ijoi
andrK2pzb24ifQ.
TrqXOwuNUfDV9VPTNbyGvEJ9JMjefAVn-TR1uIxR9p6hsRQh9Tk7BA.
Ye9j1qs22DmRSAddIh-VnA.
AwhB8lxrlKjFn02LGWEqg27H4Tg9fyZAbFv3p5ZicHpj64QyHC44qqlZ3JEmnZTgQo
wIqZJ13jbyHB8LgePiqUJ1hf6M2HPLgzw8L-mEeQ0jvDUTrE07NtOerBk8bwBQyZ6g
0kQ3DEOIglfYxV8-FJvNBYwbqN1Bck6d_i7OtjSHV-8DIrp-3JcRIe05YKy3Oi34Z_
GOiAc1EK21B11c_AE11PII_wvvtRiUiG8YofQXakWd1_O98Kap-UgmyWPfreUJ3lJP
nbD4Ve95owEfMGLOPflo2MnjaTDCwQokoJ_xplQ2vNPz8iguLcHBoKllyQFJL2mOWB
wqhBo9Oj-O800as5mmLsvQMTflIrIEbbTMzHMBZ8EFW9fWwwFu0DWQJGkMNhmBZQ-3
lvqTc-M6-gWA6D8PDhONfP2Oib2HGizwG1iEaX8GRyUpfLuljCLIe1DkGOewhKuKkZ
h04DKNM5Nbugf2atmU9OP0Ldx5peCUtRG1gMVl7Qup5ZXHTjgPDr5b2N731UooCGAU
qHdgGhg0JVJ_ObCTdjsH4CF1SJsdUhrXvYx3HJh2Xd7CwJRzU_3Y1GxYU6-s3GFPbi
rfqqEipJDBTHpcoCmyrwYjYHFgnlqBZRotRrS95g8F95bRXqsaDY7UgQGwBQBwy665
d0zpvTasvfXf_c0MWAl-neFaKOW_Px6g4EUDjG1GWSXV9cLStLw_0ovdApDIFLHYHe
PyagyHjouQUuGiq7BsYwYrwaF06tgB8hV8omLNfMEmDPJaZUzMuHw6tBDwGkzD-tS_
ub9hxrpJ4UsOWnt5rGUyoN2N_c1-TQlXxm5oto14MxnoAyBQBpwIEgSH3Y4ZhwKBhH
PjSo0cdwuNdYbGPpb-YUvF-2NZzODiQ1OvWQBRHSbPWYz_xbGkgD504LRtqRwCO7CC
_CyyURi1sEssPVsMJRX_U4LFEOc82TiDdqjKOjRUfKK5rqLi8nBE9soQ0DSaOoFQZi
GrBrqxDsNYiAYAmxxkos-i3nX4qtByVx85sCE5U_0MqG7COxZWMOPEFrDaepUV-cOy
rvoUIng8i8ljKBKxETY2BgPegKBYCxsAUcAkKamSCC9AiBxA0UOHyhTqtlvMksO7AE
hNC2-YzPyx1FkhMoS4LLe6E_pFsMlmjA6P1NSge9C5G5tETYXGAn6b1xZbHtmwrPSc
ro9LWhVmAaA7_bxYObnFUxgWtK4vzzQBjZJ36UTk4OTB-JvKWgfVWCFsaw5WCHj6Oo
4jpO7d2yN7WMfAj2hTEabz9wumQ0TMhBduZ-QON3pYObSy7TSC1vVme0NJrwF_cJRe
hKTFmdlXGVldPxZCplr7ZQqRQhF8JP-l4mEQVnCaWGn9ONHlemczGOS-A-wwtnmwjI
B1V_vgJRf4FdpV-4hUk4-QLpu3-1lWFxrtZKcggq3tWTduRo5_QebQbUUT_VSCgsFc
OmyWKoj56lbxthN19hq1XGWbLGfrrR6MWh23vk01zn8FVwi7uFwEnRYSafsnWLa1Z5
TpBj9GvAdl2H9NHwzpB5NqHpZNkQ3NMDj13Fn8fzO0JB83Etbm_tnFQfcb13X3bJ15
Cz-Ww1MGhvIpGGnMBT_ADp9xSIyAM9dQ1yeVXk-AIgWBUlN5uyWSGyCxp0cJwx7HxM
38z0UIeBu-MytL-eqndM7LxytsVzCbjOTSVRmhYEMIzUAnS1gs7uMQAGRdgRIElTJE
SGMjb_4bZq9s6Ve1LKkSi0_QDsrABaLe55UY0zF4ZSfOV5PMyPtocwV_dcNPlxLgNA
D1BFX_Z9kAdMZQW6fAmsfFle0zAoMe4l9pMESH0JB4sJGdCKtQXj1cXNydDYozF7l8
H00BV_Er7zd6VtIw0MxwkFCTatsv_R-GsBCH218RgVPsfYhwVuT8R4HarpzsDBufC4
r8_c8fc9Z278sQ081jFjOja6L2x0N_ImzFNXU6xwO-Ska-QeuvYZ3X_L31ZOX4Llp-
7QSfgDoHnOxFv1Xws-D5mDHD3zxOup2b2TppdKTZb9eW2vxUVviM8OI9atBfPKMGAO
v9omA-6vv5IxUH0-lWMiHLQ_g8vnswp-Jav0c4t6URVUzujNOoNd_CBGGVnHiJTCHl
88LQxsqLHHIu4Fz-U2SGnlxGTj0-ihit2ELGRv4vO8E1BosTmf0cx3qgG0Pq0eOLBD
IHsrdZ_CCAiTc0HVkMbyq1M6qEhM-q5P6y1QCIrwg.
0HFmhOzsQ98nNWJjIHkR7A`

// remove white spaces and line breaks
r := regexp.MustCompile(`\s`)
plaintext = r.ReplaceAll(plaintext, []byte(""))
serializationReference = r.ReplaceAllString(serializationReference, "")

rcpt := Recipient{
Algorithm: PBES2_HS256_A128KW,
Key: passphrase,
PBES2Count: 4096,
PBES2Salt: []byte{
217, 96, 147, 112, 150, 117, 70,
247, 127, 8, 155, 137, 174, 42, 80, 215,
},
}

enc, err := NewEncrypter(A128CBC_HS256, rcpt, nil)
if err != nil {
t.Fatal("error on NewEncrypter:", err)
}

obj, err := enc.Encrypt(plaintext)
if err != nil {
t.Fatal("error on new Encrypt:", err)
}

serialized, err := obj.CompactSerialize()
if err != nil {
t.Fatal("error on CompactSerialize")
}

jwe1, err := ParseEncrypted(serialized)
if err != nil {
t.Fatal("error in ParseEncrypted")
}

jwe2, err := ParseEncrypted(serializationReference)
if err != nil {
t.Fatal("error in ParseEncrypted")
}

original1, err := jwe1.Decrypt(passphrase)
if err != nil {
t.Fatal("error in Decrypt:", err)
}

original2, err := jwe2.Decrypt(passphrase)
if err != nil {
t.Fatal("error in Decrypt reference:", err)
}

if bytes.Compare(original1, original2) != 0 {
t.Error("decryption does not match reference decryption")
}

if bytes.Compare(plaintext, original1) != 0 {
t.Error("decryption does not match plaintext")
}

if bytes.Compare(plaintext, original2) != 0 {
t.Error("reference decryption does not match plaintext")
}
}

func TestEncrypterWithPBES2(t *testing.T) {
expected := []byte("Lorem ipsum dolor sit amet")
algs := []KeyAlgorithm{
PBES2_HS256_A128KW, PBES2_HS384_A192KW, PBES2_HS512_A256KW,
}

// Check with both strings and []byte
recipientKeys := []interface{}{"password", []byte("password")}
for _, key := range recipientKeys {
for _, alg := range algs {
enc, err := NewEncrypter(A128GCM, Recipient{Algorithm: alg, Key: &JSONWebKey{
KeyID: "test-id",
Key: key,
}}, nil)
if err != nil {
t.Error(err)
}

ciphertext, _ := enc.Encrypt(expected)

serialized1, _ := ciphertext.CompactSerialize()
serialized2 := ciphertext.FullSerialize()

parsed1, _ := ParseEncrypted(serialized1)
parsed2, _ := ParseEncrypted(serialized2)

actual1, err := parsed1.Decrypt("password")
if err != nil {
t.Fatal("error on Decrypt:", err)
}

actual2, err := parsed2.Decrypt([]byte("password"))
if err != nil {
t.Fatal("error on Decrypt:", err)
}

if bytes.Compare(actual1, expected) != 0 {
t.Errorf("error comparing decrypted message (%s) and expected (%s)", actual1, expected)
}

if bytes.Compare(actual2, expected) != 0 {
t.Errorf("error comparing decrypted message (%s) and expected (%s)", actual2, expected)
}
}
}
}

type testKey struct {
enc, dec interface{}
}
Expand Down Expand Up @@ -459,6 +662,13 @@ func generateTestKeys(keyAlg KeyAlgorithm, encAlg ContentEncryption) []testKey {
dec: rsaTestKey,
enc: &rsaTestKey.PublicKey,
}}
case PBES2_HS256_A128KW, PBES2_HS384_A192KW, PBES2_HS512_A256KW:
// size does not matter, use random integer
i, err := rand.Int(rand.Reader, big.NewInt(64))
if err != nil {
panic(err)
}
return symmetricTestKey(int(i.Int64()))
}

panic("Must update test case")
Expand Down
24 changes: 24 additions & 0 deletions shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,10 @@ const (
headerJWK = "jwk" // *JSONWebKey
headerKeyID = "kid" // string
headerNonce = "nonce" // string

headerP2C = "p2c" // *byteBuffer (int)
headerP2S = "p2s" // *byteBuffer ([]byte)

)

// rawHeader represents the JOSE header for JWE/JWS objects (used for parsing).
Expand Down Expand Up @@ -320,6 +324,26 @@ func (parsed rawHeader) getCritical() ([]string, error) {
return q, nil
}

// getS2C extracts parsed "p2c" from the raw JSON.
func (parsed rawHeader) getP2C() (int, error) {
v := parsed[headerP2C]
if v == nil {
return 0, nil
}

var p2c int
err := json.Unmarshal(*v, &p2c)
if err != nil {
return 0, err
}
return p2c, nil
}

// getS2S extracts parsed "p2s" from the raw JSON.
func (parsed rawHeader) getP2S() (*byteBuffer, error) {
return parsed.getByteBuffer(headerP2S)
}

// sanitized produces a cleaned-up header object from the raw JSON.
func (parsed rawHeader) sanitized() (h Header, err error) {
for k, v := range parsed {
Expand Down
Loading

0 comments on commit 8254d6c

Please sign in to comment.