Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
324 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,166 @@ | ||
// Package hpke contains functions for working with Hybrid Public Key Encryption. | ||
package hpke | ||
|
||
import ( | ||
"crypto/rand" | ||
"encoding/base64" | ||
"fmt" | ||
|
||
"github.com/cloudflare/circl/hpke" | ||
"github.com/cloudflare/circl/kem" | ||
) | ||
|
||
var ( | ||
kemID = hpke.KEM_X25519_HKDF_SHA256 | ||
kdfID = hpke.KDF_HKDF_SHA256 | ||
aeadID = hpke.AEAD_ChaCha20Poly1305 | ||
suite = hpke.NewSuite(kemID, kdfID, aeadID) | ||
|
||
kdfExpandInfo = []byte("pomerium/hpke") | ||
) | ||
|
||
// PrivateKey is an HPKE private key. | ||
type PrivateKey struct { | ||
key kem.PrivateKey | ||
} | ||
|
||
// DerivePrivateKey derives a private key from a seed. The same seed will always result in the same private key. | ||
func DerivePrivateKey(seed []byte) PrivateKey { | ||
pk := kdfID.Extract(seed, nil) | ||
data := kdfID.Expand(pk, kdfExpandInfo, uint(kemID.Scheme().SeedSize())) | ||
_, key := kemID.Scheme().DeriveKeyPair(data) | ||
return PrivateKey{key: key} | ||
} | ||
|
||
// GeneratePrivateKey generates an HPKE private key. | ||
func GeneratePrivateKey() (PrivateKey, error) { | ||
_, privateKey, err := kemID.Scheme().GenerateKeyPair() | ||
if err != nil { | ||
return PrivateKey{}, err | ||
} | ||
return PrivateKey{key: privateKey}, nil | ||
} | ||
|
||
// PrivateKeyFromString takes a string and returns a PrivateKey. | ||
func PrivateKeyFromString(raw string) (PrivateKey, error) { | ||
bs, err := decode(raw) | ||
if err != nil { | ||
return PrivateKey{}, err | ||
} | ||
|
||
key, err := kemID.Scheme().UnmarshalBinaryPrivateKey(bs) | ||
if err != nil { | ||
return PrivateKey{}, err | ||
} | ||
|
||
return PrivateKey{key: key}, nil | ||
} | ||
|
||
// PublicKey returns the public key for the private key. | ||
func (key PrivateKey) PublicKey() PublicKey { | ||
return PublicKey{key: key.key.Public()} | ||
} | ||
|
||
// String converts the private key into a string. | ||
func (key PrivateKey) String() string { | ||
bs, err := key.key.MarshalBinary() | ||
if err != nil { | ||
// this should not happen | ||
panic(fmt.Sprintf("failed to marshal private HPKE key: %v", err)) | ||
} | ||
|
||
return base64.RawStdEncoding.EncodeToString(bs) | ||
} | ||
|
||
// PublicKey is an HPKE public key. | ||
type PublicKey struct { | ||
key kem.PublicKey | ||
} | ||
|
||
// PublicKeyFromString converts a string into a public key. | ||
func PublicKeyFromString(raw string) (PublicKey, error) { | ||
bs, err := decode(raw) | ||
if err != nil { | ||
return PublicKey{}, err | ||
} | ||
|
||
key, err := kemID.Scheme().UnmarshalBinaryPublicKey(bs) | ||
if err != nil { | ||
return PublicKey{}, err | ||
} | ||
|
||
return PublicKey{key: key}, nil | ||
} | ||
|
||
// String converts a public key into a string. | ||
func (key PublicKey) String() string { | ||
bs, err := key.key.MarshalBinary() | ||
if err != nil { | ||
// this should not happen | ||
panic(fmt.Sprintf("failed to marshal public HPKE key: %v", err)) | ||
} | ||
|
||
return encode(bs) | ||
} | ||
|
||
// Seal seales a message using HPKE. | ||
func Seal( | ||
senderPrivateKey PrivateKey, | ||
receiverPublicKey PublicKey, | ||
message []byte, | ||
) (sealed []byte, err error) { | ||
sender, err := suite.NewSender(receiverPublicKey.key, nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("hpke: error creating sender: %w", err) | ||
} | ||
|
||
enc, sealer, err := sender.SetupAuth(rand.Reader, senderPrivateKey.key) | ||
if err != nil { | ||
return nil, fmt.Errorf("hpke: error creating sealer: %w", err) | ||
} | ||
|
||
ct, err := sealer.Seal(message, nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("hpke: error sealing message: %w", err) | ||
} | ||
|
||
return append(enc, ct...), nil | ||
} | ||
|
||
// Open opens a message using HPKE. | ||
func Open( | ||
receiverPrivateKey PrivateKey, | ||
senderPublicKey PublicKey, | ||
sealed []byte, | ||
) (message []byte, err error) { | ||
encSize := kemID.Scheme().SharedKeySize() | ||
if len(sealed) < encSize { | ||
return nil, fmt.Errorf("hpke: invalid sealed message") | ||
} | ||
enc, sealed := sealed[:encSize], sealed[encSize:] | ||
|
||
receiver, err := suite.NewReceiver(receiverPrivateKey.key, nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("hpke: error creating receiver: %w", err) | ||
} | ||
|
||
opener, err := receiver.SetupAuth(enc, senderPublicKey.key) | ||
if err != nil { | ||
return nil, fmt.Errorf("hpke: error creating opener: %w", err) | ||
} | ||
|
||
message, err = opener.Open(sealed, nil) | ||
if err != nil { | ||
return nil, fmt.Errorf("hpke: error opening sealed message: %w", err) | ||
} | ||
|
||
return message, nil | ||
} | ||
|
||
func decode(raw string) ([]byte, error) { | ||
return base64.RawURLEncoding.DecodeString(raw) | ||
} | ||
|
||
func encode(data []byte) string { | ||
return base64.RawURLEncoding.EncodeToString(data) | ||
} |
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,32 @@ | ||
package hpke | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestSeal(t *testing.T) { | ||
k1, err := GeneratePrivateKey() | ||
require.NoError(t, err) | ||
k2, err := GeneratePrivateKey() | ||
require.NoError(t, err) | ||
|
||
sealed, err := Seal(k1, k2.PublicKey(), []byte("HELLO WORLD")) | ||
assert.NoError(t, err) | ||
assert.NotEmpty(t, sealed) | ||
|
||
message, err := Open(k2, k1.PublicKey(), sealed) | ||
assert.NoError(t, err) | ||
assert.Equal(t, []byte("HELLO WORLD"), message) | ||
} | ||
|
||
func TestDerivePrivateKey(t *testing.T) { | ||
k1a := DerivePrivateKey([]byte("KEY 1")) | ||
k1b := DerivePrivateKey([]byte("KEY 1")) | ||
k2 := DerivePrivateKey([]byte("KEY 2")) | ||
|
||
assert.Equal(t, k1a.String(), k1b.String()) | ||
assert.NotEqual(t, k1a.String(), k2.String()) | ||
} |
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,86 @@ | ||
package hpke | ||
|
||
import ( | ||
"fmt" | ||
"net/url" | ||
) | ||
|
||
// URL Parameters | ||
const ( | ||
ParamSenderPublicKey = "pomerium_hpke_sender_pub" | ||
ParamQuery = "pomerium_hpke_query" | ||
) | ||
|
||
// IsEncryptedURL returns true if the url.Values contain an HPKE encrypted query. | ||
func IsEncryptedURL(values url.Values) bool { | ||
return values.Has(ParamSenderPublicKey) && values.Has(ParamQuery) | ||
} | ||
|
||
// EncryptURLValues encrypts URL values using the Seal method. | ||
func EncryptURLValues( | ||
senderPrivateKey PrivateKey, | ||
receiverPublicKey PublicKey, | ||
values url.Values, | ||
) (encrypted url.Values, err error) { | ||
values = withoutHPKEParams(values) | ||
|
||
sealed, err := Seal(senderPrivateKey, receiverPublicKey, []byte(values.Encode())) | ||
if err != nil { | ||
return nil, fmt.Errorf("hpke: failed to seal URL values %w", err) | ||
} | ||
|
||
return url.Values{ | ||
ParamSenderPublicKey: {senderPrivateKey.PublicKey().String()}, | ||
ParamQuery: {encode(sealed)}, | ||
}, nil | ||
} | ||
|
||
// DecryptURLValues decrypts URL values using the Open method. | ||
func DecryptURLValues( | ||
receiverPrivateKey PrivateKey, | ||
encrypted url.Values, | ||
) (senderPublicKey PublicKey, values url.Values, err error) { | ||
if !encrypted.Has(ParamSenderPublicKey) { | ||
return senderPublicKey, nil, fmt.Errorf("hpke: missing sender public key in query parameters") | ||
} | ||
if !encrypted.Has(ParamQuery) { | ||
return senderPublicKey, nil, fmt.Errorf("hpke: missing encrypted query in query parameters") | ||
} | ||
|
||
senderPublicKey, err = PublicKeyFromString(encrypted.Get(ParamSenderPublicKey)) | ||
if err != nil { | ||
return senderPublicKey, nil, fmt.Errorf("hpke: invalid sender public key parameter: %w", err) | ||
} | ||
|
||
sealed, err := decode(encrypted.Get(ParamQuery)) | ||
if err != nil { | ||
return senderPublicKey, nil, fmt.Errorf("hpke: failed decoding query parameter: %w", err) | ||
} | ||
|
||
message, err := Open(receiverPrivateKey, senderPublicKey, sealed) | ||
if err != nil { | ||
return senderPublicKey, nil, fmt.Errorf("hpke: failed to open sealed message: %w", err) | ||
} | ||
|
||
decrypted, err := url.ParseQuery(string(message)) | ||
if err != nil { | ||
return senderPublicKey, nil, fmt.Errorf("hpke: invalid query parameter: %w", err) | ||
} | ||
|
||
values = withoutHPKEParams(encrypted) | ||
for k, vs := range decrypted { | ||
values[k] = vs | ||
} | ||
|
||
return senderPublicKey, values, err | ||
} | ||
|
||
func withoutHPKEParams(values url.Values) url.Values { | ||
filtered := make(url.Values) | ||
for k, vs := range values { | ||
if k != ParamSenderPublicKey && k != ParamQuery { | ||
filtered[k] = vs | ||
} | ||
} | ||
return filtered | ||
} |
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,37 @@ | ||
package hpke | ||
|
||
import ( | ||
"net/url" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestEncryptURLValues(t *testing.T) { | ||
k1, err := GeneratePrivateKey() | ||
require.NoError(t, err) | ||
k2, err := GeneratePrivateKey() | ||
require.NoError(t, err) | ||
|
||
encrypted, err := EncryptURLValues(k1, k2.PublicKey(), url.Values{ | ||
"a": {"b", "c"}, | ||
"x": {"y", "z"}, | ||
}) | ||
assert.NoError(t, err) | ||
assert.True(t, encrypted.Has(ParamSenderPublicKey)) | ||
assert.True(t, encrypted.Has(ParamQuery)) | ||
|
||
assert.True(t, IsEncryptedURL(encrypted)) | ||
|
||
encrypted.Set("extra", "value") | ||
encrypted.Set("a", "notb") | ||
senderPublicKey, decrypted, err := DecryptURLValues(k2, encrypted) | ||
assert.NoError(t, err) | ||
assert.Equal(t, url.Values{ | ||
"a": {"b", "c"}, | ||
"x": {"y", "z"}, | ||
"extra": {"value"}, | ||
}, decrypted) | ||
assert.Equal(t, k1.PublicKey().String(), senderPublicKey.String()) | ||
} |