feat: add aesgcm support
This commit is contained in:
parent
ec5c996e82
commit
ae81d0cdae
|
|
@ -72,7 +72,6 @@ func (c *AutoPushClient) Unregister(channelID string) (UnregisterResponse, error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AutoPushClient) Decrypt(
|
func (c *AutoPushClient) Decrypt(
|
||||||
curve ecdh.Curve,
|
|
||||||
authSecret []byte,
|
authSecret []byte,
|
||||||
useragentPrivateKey *ecdh.PrivateKey,
|
useragentPrivateKey *ecdh.PrivateKey,
|
||||||
notification Notification,
|
notification Notification,
|
||||||
|
|
@ -82,25 +81,16 @@ func (c *AutoPushClient) Decrypt(
|
||||||
return nil, fmt.Errorf("base64 decode error: %v", err)
|
return nil, fmt.Errorf("base64 decode error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, err := rfc8291.Unmarshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("rfc8291 decode error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
appserverPublicKey, err := curve.NewPublicKey(payload.KeyId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("ecdh public key load error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
plaintext, err := c.ece.Decrypt(
|
plaintext, err := c.ece.Decrypt(
|
||||||
payload.CipherText,
|
data,
|
||||||
payload.Salt,
|
rfc8291.Encoding(notification.Headers.Encoding),
|
||||||
|
notification.Headers.Encryption,
|
||||||
|
notification.Headers.CryptoKey,
|
||||||
authSecret,
|
authSecret,
|
||||||
useragentPrivateKey,
|
useragentPrivateKey,
|
||||||
appserverPublicKey,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("rfc8291 decrypt error: %v", err)
|
return nil, fmt.Errorf("decrypt error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result webpush.WebPushPayload
|
var result webpush.WebPushPayload
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ func appserver(authSecret []byte, useragentPublicKey *ecdh.PublicKey) []byte {
|
||||||
_, salt, privateKey := rfc8291.NewSecrets(CURVE)
|
_, salt, privateKey := rfc8291.NewSecrets(CURVE)
|
||||||
ece := rfc8291.NewRFC8291(HASH)
|
ece := rfc8291.NewRFC8291(HASH)
|
||||||
|
|
||||||
encrypted, err := ece.Encrypt(
|
encrypted, err := ece.EncryptAes128gcm(
|
||||||
[]byte(PLAINTEXT),
|
[]byte(PLAINTEXT),
|
||||||
salt,
|
salt,
|
||||||
authSecret,
|
authSecret,
|
||||||
|
|
@ -53,7 +53,7 @@ func main() {
|
||||||
log.Panicln("Load PublicKey Error", err)
|
log.Panicln("Load PublicKey Error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
plaintext, err := ece.Decrypt(
|
plaintext, err := ece.DecryptAes128gcm(
|
||||||
payload.CipherText,
|
payload.CipherText,
|
||||||
payload.Salt,
|
payload.Salt,
|
||||||
authSecret,
|
authSecret,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
package rfc8291
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ecdh"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Aes128gcmScheme implements EncodingScheme for the aes128gcm encoding.
|
||||||
|
type Aes128gcmScheme struct{}
|
||||||
|
|
||||||
|
func (s Aes128gcmScheme) DeriveIKM(hash func() hash.Hash, authSecret, ecdhSecret []byte, uaKey, asKey *ecdh.PublicKey) ([]byte, error) {
|
||||||
|
prkKey := hkdf.Extract(hash, ecdhSecret, authSecret)
|
||||||
|
|
||||||
|
// aes128gcm: "WebPush: info\0" + receiver key + sender key
|
||||||
|
keyInfo := bytes.Join([][]byte{
|
||||||
|
[]byte("WebPush: info\000"),
|
||||||
|
uaKey.Bytes(),
|
||||||
|
asKey.Bytes(),
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
ikm := make([]byte, HKDF_IKM_LEN)
|
||||||
|
if _, err := io.ReadFull(hkdf.Expand(hash, prkKey, keyInfo), ikm); err != nil {
|
||||||
|
return nil, fmt.Errorf("derive IKM failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ikm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Aes128gcmScheme) DeriveCEKAndNonce(hash func() hash.Hash, ikm, salt []byte, uaKey, asKey *ecdh.PublicKey) (cek, nonce []byte, err error) {
|
||||||
|
prk := hkdf.Extract(hash, ikm, salt)
|
||||||
|
|
||||||
|
// aes128gcm: simple info strings without keys
|
||||||
|
cekInfo := []byte("Content-Encoding: aes128gcm\000")
|
||||||
|
nonceInfo := []byte("Content-Encoding: nonce\000")
|
||||||
|
|
||||||
|
cek = make([]byte, HKDF_CEK_LEN)
|
||||||
|
if _, err := io.ReadFull(hkdf.Expand(hash, prk, cekInfo), cek); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("derive CEK failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce = make([]byte, HKDF_NONCE_LEN)
|
||||||
|
if _, err := io.ReadFull(hkdf.Expand(hash, prk, nonceInfo), nonce); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("derive nonce failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cek, nonce, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Aes128gcmScheme) Pad(plaintext []byte) []byte {
|
||||||
|
// aes128gcm: append 0x02 delimiter for final record
|
||||||
|
return append(plaintext, 0x02)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Aes128gcmScheme) Unpad(data []byte) ([]byte, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, fmt.Errorf("data is empty")
|
||||||
|
}
|
||||||
|
// aes128gcm: remove trailing 0x01 or 0x02 delimiter
|
||||||
|
last := data[len(data)-1]
|
||||||
|
if last == 0x01 || last == 0x02 {
|
||||||
|
return data[:len(data)-1], nil
|
||||||
|
}
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Payload represents the aes128gcm message format with embedded crypto parameters.
|
||||||
|
type Payload struct {
|
||||||
|
RS uint32
|
||||||
|
Salt []byte
|
||||||
|
KeyId []byte
|
||||||
|
CipherText []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
baseHeaderLen = 21
|
||||||
|
)
|
||||||
|
|
||||||
|
// Marshal serializes a Payload into the aes128gcm binary format.
|
||||||
|
func Marshal(p Payload) (data []byte) {
|
||||||
|
rs := make([]byte, 4)
|
||||||
|
binary.BigEndian.PutUint32(rs, p.RS)
|
||||||
|
return bytes.Join([][]byte{
|
||||||
|
p.Salt,
|
||||||
|
rs,
|
||||||
|
{uint8(len(p.KeyId))},
|
||||||
|
p.KeyId,
|
||||||
|
p.CipherText,
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal parses the aes128gcm binary format into a Payload.
|
||||||
|
func Unmarshal(data []byte) (p Payload, err error) {
|
||||||
|
if len(data) < baseHeaderLen {
|
||||||
|
return p, errors.New("data is too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
p.Salt = data[:16]
|
||||||
|
p.RS = binary.BigEndian.Uint32(data[16:20])
|
||||||
|
|
||||||
|
idlen := int(data[20])
|
||||||
|
if len(data) < baseHeaderLen+idlen {
|
||||||
|
return p, errors.New("data is too short")
|
||||||
|
}
|
||||||
|
|
||||||
|
if idlen > 0 {
|
||||||
|
p.KeyId = data[baseHeaderLen : baseHeaderLen+idlen]
|
||||||
|
}
|
||||||
|
|
||||||
|
p.CipherText = data[baseHeaderLen+idlen:]
|
||||||
|
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptAes128gcm encrypts a message using the aes128gcm encoding scheme.
|
||||||
|
// Returns the complete payload with embedded crypto parameters.
|
||||||
|
func (c *RFC8291) EncryptAes128gcm(
|
||||||
|
plaintext []byte,
|
||||||
|
salt []byte,
|
||||||
|
authSecret []byte,
|
||||||
|
receiverPublicKey *ecdh.PublicKey,
|
||||||
|
senderPrivateKey *ecdh.PrivateKey,
|
||||||
|
) ([]byte, error) {
|
||||||
|
ciphertext, err := c.encrypt(Aes128gcmScheme{}, plaintext, salt, authSecret, receiverPublicKey, senderPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
rs := uint32(len(plaintext) + 1 + AES_GCM_OVERHEAD)
|
||||||
|
|
||||||
|
return Marshal(Payload{
|
||||||
|
RS: rs,
|
||||||
|
Salt: salt,
|
||||||
|
KeyId: senderPrivateKey.PublicKey().Bytes(),
|
||||||
|
CipherText: ciphertext,
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptAes128gcm decrypts a message encrypted with the aes128gcm encoding scheme.
|
||||||
|
func (c *RFC8291) DecryptAes128gcm(
|
||||||
|
ciphertext []byte,
|
||||||
|
salt []byte,
|
||||||
|
authSecret []byte,
|
||||||
|
receiverPrivateKey *ecdh.PrivateKey,
|
||||||
|
senderPublicKey *ecdh.PublicKey,
|
||||||
|
) ([]byte, error) {
|
||||||
|
return c.decrypt(Aes128gcmScheme{}, ciphertext, salt, authSecret, receiverPrivateKey, senderPublicKey)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,184 @@
|
||||||
|
package rfc8291
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ecdh"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"hash"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/hkdf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AesgcmScheme implements EncodingScheme for the aesgcm encoding.
|
||||||
|
type AesgcmScheme struct{}
|
||||||
|
|
||||||
|
func (s AesgcmScheme) DeriveIKM(hash func() hash.Hash, authSecret, ecdhSecret []byte, uaKey, asKey *ecdh.PublicKey) ([]byte, error) {
|
||||||
|
prkKey := hkdf.Extract(hash, ecdhSecret, authSecret)
|
||||||
|
|
||||||
|
// aesgcm: just "Content-Encoding: auth\0" without keys
|
||||||
|
keyInfo := []byte("Content-Encoding: auth\000")
|
||||||
|
|
||||||
|
ikm := make([]byte, HKDF_IKM_LEN)
|
||||||
|
if _, err := io.ReadFull(hkdf.Expand(hash, prkKey, keyInfo), ikm); err != nil {
|
||||||
|
return nil, fmt.Errorf("derive IKM failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ikm, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AesgcmScheme) DeriveCEKAndNonce(hash func() hash.Hash, ikm, salt []byte, uaKey, asKey *ecdh.PublicKey) (cek, nonce []byte, err error) {
|
||||||
|
prk := hkdf.Extract(hash, ikm, salt)
|
||||||
|
|
||||||
|
// aesgcm: info includes "P-256\0" and length-prefixed public keys
|
||||||
|
uaKeyBytes := uaKey.Bytes()
|
||||||
|
asKeyBytes := asKey.Bytes()
|
||||||
|
|
||||||
|
context := bytes.Join([][]byte{
|
||||||
|
[]byte("P-256\000"),
|
||||||
|
{0, byte(len(uaKeyBytes))},
|
||||||
|
uaKeyBytes,
|
||||||
|
{0, byte(len(asKeyBytes))},
|
||||||
|
asKeyBytes,
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
cekInfo := append([]byte("Content-Encoding: aesgcm\000"), context...)
|
||||||
|
nonceInfo := append([]byte("Content-Encoding: nonce\000"), context...)
|
||||||
|
|
||||||
|
cek = make([]byte, HKDF_CEK_LEN)
|
||||||
|
if _, err := io.ReadFull(hkdf.Expand(hash, prk, cekInfo), cek); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("derive CEK failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce = make([]byte, HKDF_NONCE_LEN)
|
||||||
|
if _, err := io.ReadFull(hkdf.Expand(hash, prk, nonceInfo), nonce); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("derive nonce failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cek, nonce, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AesgcmScheme) Pad(plaintext []byte) []byte {
|
||||||
|
// aesgcm: 2-byte big-endian padding length prefix (0 padding)
|
||||||
|
result := make([]byte, 2+len(plaintext))
|
||||||
|
// First two bytes are 0 (no padding), already zero-initialized
|
||||||
|
copy(result[2:], plaintext)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s AesgcmScheme) Unpad(data []byte) ([]byte, error) {
|
||||||
|
if len(data) < 2 {
|
||||||
|
return nil, fmt.Errorf("data too short for aesgcm padding")
|
||||||
|
}
|
||||||
|
|
||||||
|
padLen := int(data[0])<<8 | int(data[1])
|
||||||
|
|
||||||
|
if 2+padLen > len(data) {
|
||||||
|
return nil, fmt.Errorf("invalid padding length: %d (data length: %d)", padLen, len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify padding bytes are all zeros
|
||||||
|
for i := 2; i < 2+padLen; i++ {
|
||||||
|
if data[i] != 0 {
|
||||||
|
return nil, fmt.Errorf("invalid padding: non-zero byte at position %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data[2+padLen:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptResult holds the result of aesgcm encryption.
|
||||||
|
// Unlike aes128gcm which embeds crypto params in the payload,
|
||||||
|
// aesgcm requires these to be sent as HTTP headers.
|
||||||
|
type EncryptResult struct {
|
||||||
|
Ciphertext []byte // The encrypted data (for request body)
|
||||||
|
Salt []byte // For Encryption header: salt=<base64url>
|
||||||
|
SenderPublicKey []byte // For Crypto-Key header: dh=<base64url>
|
||||||
|
}
|
||||||
|
|
||||||
|
// CryptoParams holds the extracted cryptographic parameters for decryption.
|
||||||
|
type CryptoParams struct {
|
||||||
|
Salt []byte
|
||||||
|
SenderPublicKey *ecdh.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseAesgcmHeaders extracts salt and sender public key from aesgcm HTTP headers.
|
||||||
|
// encryptionHeader: e.g., "salt=FiyMDLvlVl678odI9AWL3A"
|
||||||
|
// cryptoKeyHeader: e.g., "dh=BMLYo...;p256ecdsa=BF5o..."
|
||||||
|
func ParseAesgcmHeaders(encryptionHeader, cryptoKeyHeader string, curve ecdh.Curve) (*CryptoParams, error) {
|
||||||
|
salt, err := parseHeaderParam(encryptionHeader, "salt")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse salt: %v", err)
|
||||||
|
}
|
||||||
|
if len(salt) != SALT_LEN {
|
||||||
|
return nil, fmt.Errorf("salt must be %d bytes, got %d", SALT_LEN, len(salt))
|
||||||
|
}
|
||||||
|
|
||||||
|
dhBytes, err := parseHeaderParam(cryptoKeyHeader, "dh")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse dh: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
senderPublicKey, err := curve.NewPublicKey(dhBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse sender public key: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CryptoParams{
|
||||||
|
Salt: salt,
|
||||||
|
SenderPublicKey: senderPublicKey,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHeaderParam extracts a base64url-encoded parameter value from a header string.
|
||||||
|
func parseHeaderParam(header, paramName string) ([]byte, error) {
|
||||||
|
parts := strings.FieldsFunc(header, func(r rune) bool {
|
||||||
|
return r == ';' || r == ','
|
||||||
|
})
|
||||||
|
|
||||||
|
prefix := paramName + "="
|
||||||
|
for _, part := range parts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if strings.HasPrefix(part, prefix) {
|
||||||
|
value := strings.TrimPrefix(part, prefix)
|
||||||
|
value = strings.Trim(value, "\"")
|
||||||
|
return base64.RawURLEncoding.DecodeString(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("parameter %q not found in header", paramName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptAesgcm encrypts a message using the aesgcm encoding scheme.
|
||||||
|
// Returns the ciphertext and crypto parameters needed for HTTP headers.
|
||||||
|
func (c *RFC8291) EncryptAesgcm(
|
||||||
|
plaintext []byte,
|
||||||
|
salt []byte,
|
||||||
|
authSecret []byte,
|
||||||
|
receiverPublicKey *ecdh.PublicKey,
|
||||||
|
senderPrivateKey *ecdh.PrivateKey,
|
||||||
|
) (*EncryptResult, error) {
|
||||||
|
ciphertext, err := c.encrypt(AesgcmScheme{}, plaintext, salt, authSecret, receiverPublicKey, senderPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &EncryptResult{
|
||||||
|
Ciphertext: ciphertext,
|
||||||
|
Salt: salt,
|
||||||
|
SenderPublicKey: senderPrivateKey.PublicKey().Bytes(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecryptAesgcm decrypts a message encrypted with the aesgcm encoding scheme.
|
||||||
|
func (c *RFC8291) DecryptAesgcm(
|
||||||
|
ciphertext []byte,
|
||||||
|
salt []byte,
|
||||||
|
authSecret []byte,
|
||||||
|
receiverPrivateKey *ecdh.PrivateKey,
|
||||||
|
senderPublicKey *ecdh.PublicKey,
|
||||||
|
) ([]byte, error) {
|
||||||
|
return c.decrypt(AesgcmScheme{}, ciphertext, salt, authSecret, receiverPrivateKey, senderPublicKey)
|
||||||
|
}
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
package rfc8291
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
BASE_HEADER_LEN = 21
|
|
||||||
)
|
|
||||||
|
|
||||||
type Payload struct {
|
|
||||||
RS uint32
|
|
||||||
Salt []byte
|
|
||||||
KeyId []byte
|
|
||||||
CipherText []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func Marshal(p Payload) (data []byte) {
|
|
||||||
rs := make([]byte, 4)
|
|
||||||
binary.BigEndian.PutUint32(rs, p.RS)
|
|
||||||
return bytes.Join([][]byte{
|
|
||||||
p.Salt,
|
|
||||||
rs,
|
|
||||||
{uint8(len(p.KeyId))},
|
|
||||||
p.KeyId,
|
|
||||||
p.CipherText,
|
|
||||||
}, []byte{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func Unmarshal(data []byte) (p Payload, err error) {
|
|
||||||
err = errors.New("data is too short")
|
|
||||||
|
|
||||||
if len(data) < BASE_HEADER_LEN {
|
|
||||||
return p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.Salt = data[:16]
|
|
||||||
p.RS = binary.BigEndian.Uint32(data[16:20])
|
|
||||||
|
|
||||||
idlen := int(data[20])
|
|
||||||
if len(data) < BASE_HEADER_LEN+idlen {
|
|
||||||
return p, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if idlen > 0 {
|
|
||||||
p.KeyId = data[BASE_HEADER_LEN : BASE_HEADER_LEN+idlen]
|
|
||||||
}
|
|
||||||
|
|
||||||
p.CipherText = data[BASE_HEADER_LEN+idlen:]
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package rfc8291
|
package rfc8291
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
"crypto/cipher"
|
"crypto/cipher"
|
||||||
"crypto/ecdh"
|
"crypto/ecdh"
|
||||||
|
|
@ -11,8 +10,6 @@ import (
|
||||||
"hash"
|
"hash"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"golang.org/x/crypto/hkdf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -26,11 +23,40 @@ const (
|
||||||
HKDF_NONCE_LEN = 12
|
HKDF_NONCE_LEN = 12
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Encoding represents the Content-Encoding type for WebPush messages.
|
||||||
|
type Encoding string
|
||||||
|
|
||||||
|
const (
|
||||||
|
EncodingAes128gcm Encoding = "aes128gcm"
|
||||||
|
EncodingAesgcm Encoding = "aesgcm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncodingScheme defines the encoding-specific operations for WebPush encryption.
|
||||||
|
type EncodingScheme interface {
|
||||||
|
DeriveIKM(hash func() hash.Hash, authSecret, ecdhSecret []byte, uaKey, asKey *ecdh.PublicKey) ([]byte, error)
|
||||||
|
DeriveCEKAndNonce(hash func() hash.Hash, ikm, salt []byte, uaKey, asKey *ecdh.PublicKey) (cek, nonce []byte, err error)
|
||||||
|
Pad(plaintext []byte) []byte
|
||||||
|
Unpad(data []byte) ([]byte, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scheme returns the EncodingScheme implementation for the given encoding type.
|
||||||
|
func Scheme(encoding Encoding) (EncodingScheme, error) {
|
||||||
|
switch encoding {
|
||||||
|
case EncodingAes128gcm:
|
||||||
|
return Aes128gcmScheme{}, nil
|
||||||
|
case EncodingAesgcm:
|
||||||
|
return AesgcmScheme{}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported encoding: %s", encoding)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RFC8291 implements WebPush message encryption and decryption.
|
||||||
type RFC8291 struct {
|
type RFC8291 struct {
|
||||||
hash func() hash.Hash
|
hash func() hash.Hash
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default Hash is SHA256
|
// NewRFC8291 creates a new RFC8291 instance. Default hash is SHA256.
|
||||||
func NewRFC8291(hash func() hash.Hash) *RFC8291 {
|
func NewRFC8291(hash func() hash.Hash) *RFC8291 {
|
||||||
if hash == nil {
|
if hash == nil {
|
||||||
hash = sha256.New
|
hash = sha256.New
|
||||||
|
|
@ -38,6 +64,7 @@ func NewRFC8291(hash func() hash.Hash) *RFC8291 {
|
||||||
return &RFC8291{hash: hash}
|
return &RFC8291{hash: hash}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewSecrets generates new random auth secret, salt, and ECDH private key.
|
||||||
func NewSecrets(curve ecdh.Curve) (auth, salt []byte, key *ecdh.PrivateKey) {
|
func NewSecrets(curve ecdh.Curve) (auth, salt []byte, key *ecdh.PrivateKey) {
|
||||||
auth = make([]byte, AUTH_SECRET_LEN)
|
auth = make([]byte, AUTH_SECRET_LEN)
|
||||||
salt = make([]byte, SALT_LEN)
|
salt = make([]byte, SALT_LEN)
|
||||||
|
|
@ -56,12 +83,14 @@ func NewSecrets(curve ecdh.Curve) (auth, salt []byte, key *ecdh.PrivateKey) {
|
||||||
return auth, salt, key
|
return auth, salt, key
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RFC8291) Encrypt(
|
// encrypt performs encryption using the specified encoding scheme.
|
||||||
|
func (c *RFC8291) encrypt(
|
||||||
|
scheme EncodingScheme,
|
||||||
plaintext []byte,
|
plaintext []byte,
|
||||||
salt []byte,
|
salt []byte,
|
||||||
authSecret []byte,
|
authSecret []byte,
|
||||||
useragentPublicKey *ecdh.PublicKey,
|
receiverPublicKey *ecdh.PublicKey,
|
||||||
appserverPrivateKey *ecdh.PrivateKey,
|
senderPrivateKey *ecdh.PrivateKey,
|
||||||
) ([]byte, error) {
|
) ([]byte, error) {
|
||||||
if len(authSecret) != AUTH_SECRET_LEN {
|
if len(authSecret) != AUTH_SECRET_LEN {
|
||||||
return nil, fmt.Errorf("auth_secret must be %d bytes", AUTH_SECRET_LEN)
|
return nil, fmt.Errorf("auth_secret must be %d bytes", AUTH_SECRET_LEN)
|
||||||
|
|
@ -70,17 +99,17 @@ func (c *RFC8291) Encrypt(
|
||||||
return nil, fmt.Errorf("salt must be %d bytes", SALT_LEN)
|
return nil, fmt.Errorf("salt must be %d bytes", SALT_LEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
ecdhSecret, err := appserverPrivateKey.ECDH(useragentPublicKey)
|
ecdhSecret, err := senderPrivateKey.ECDH(receiverPublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("calculate ecdh_secret failed: %v", err)
|
return nil, fmt.Errorf("calculate ecdh_secret failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ikm, err := c.ikm(authSecret, ecdhSecret, useragentPublicKey, appserverPrivateKey.PublicKey())
|
ikm, err := scheme.DeriveIKM(c.hash, authSecret, ecdhSecret, receiverPublicKey, senderPrivateKey.PublicKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cek, nonce, err := c.cekAndNonce(ikm, salt)
|
cek, nonce, err := scheme.DeriveCEKAndNonce(c.hash, ikm, salt, receiverPublicKey, senderPrivateKey.PublicKey())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -90,27 +119,20 @@ func (c *RFC8291) Encrypt(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ciphertext := gcm.Seal(nil, nonce, plaintext, nil)
|
paddedPlaintext := scheme.Pad(plaintext)
|
||||||
rs := uint32(len(plaintext) + 1 + AES_GCM_OVERHEAD)
|
ciphertext := gcm.Seal(nil, nonce, paddedPlaintext, nil)
|
||||||
|
|
||||||
// RFC8188: 0x01 or 0x02 in tail of plaintext data
|
return ciphertext, nil
|
||||||
// RFC8291: The push message plaintext has the padding delimiter octet (0x02) appended to produce
|
|
||||||
// ciphertext = bytes.Join([][]byte{ciphertext, {0x02}}, []byte{})
|
|
||||||
|
|
||||||
return Marshal(Payload{
|
|
||||||
RS: rs,
|
|
||||||
Salt: salt,
|
|
||||||
KeyId: appserverPrivateKey.PublicKey().Bytes(),
|
|
||||||
CipherText: ciphertext,
|
|
||||||
}), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RFC8291) Decrypt(
|
// decrypt performs decryption using the specified encoding scheme.
|
||||||
|
func (c *RFC8291) decrypt(
|
||||||
|
scheme EncodingScheme,
|
||||||
ciphertext []byte,
|
ciphertext []byte,
|
||||||
salt []byte,
|
salt []byte,
|
||||||
authSecret []byte,
|
authSecret []byte,
|
||||||
useragentPrivateKey *ecdh.PrivateKey,
|
receiverPrivateKey *ecdh.PrivateKey,
|
||||||
appserverPublicKey *ecdh.PublicKey,
|
senderPublicKey *ecdh.PublicKey,
|
||||||
) ([]byte, error) {
|
) ([]byte, error) {
|
||||||
if len(authSecret) != AUTH_SECRET_LEN {
|
if len(authSecret) != AUTH_SECRET_LEN {
|
||||||
return nil, fmt.Errorf("auth_secret must be %d bytes", AUTH_SECRET_LEN)
|
return nil, fmt.Errorf("auth_secret must be %d bytes", AUTH_SECRET_LEN)
|
||||||
|
|
@ -119,17 +141,17 @@ func (c *RFC8291) Decrypt(
|
||||||
return nil, fmt.Errorf("salt must be %d bytes", SALT_LEN)
|
return nil, fmt.Errorf("salt must be %d bytes", SALT_LEN)
|
||||||
}
|
}
|
||||||
|
|
||||||
ecdhSecret, err := useragentPrivateKey.ECDH(appserverPublicKey)
|
ecdhSecret, err := receiverPrivateKey.ECDH(senderPublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("calculate ecdh_secret failed: %v", err)
|
return nil, fmt.Errorf("calculate ecdh_secret failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ikm, err := c.ikm(authSecret, ecdhSecret, useragentPrivateKey.PublicKey(), appserverPublicKey)
|
ikm, err := scheme.DeriveIKM(c.hash, authSecret, ecdhSecret, receiverPrivateKey.PublicKey(), senderPublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cek, nonce, err := c.cekAndNonce(ikm, salt)
|
cek, nonce, err := scheme.DeriveCEKAndNonce(c.hash, ikm, salt, receiverPrivateKey.PublicKey(), senderPublicKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -144,13 +166,7 @@ func (c *RFC8291) Decrypt(
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RFC8188: 0x01 or 0x02 in tail of plaintext data
|
return scheme.Unpad(plaintext)
|
||||||
// RFC8291: The push message plaintext has the padding delimiter octet (0x02) appended to produce
|
|
||||||
if plaintext[len(plaintext)-1] == 0x01 || plaintext[len(plaintext)-1] == 0x02 {
|
|
||||||
plaintext = plaintext[:len(plaintext)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return plaintext, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RFC8291) gcm(cek []byte) (cipher.AEAD, error) {
|
func (c *RFC8291) gcm(cek []byte) (cipher.AEAD, error) {
|
||||||
|
|
@ -167,43 +183,42 @@ func (c *RFC8291) gcm(cek []byte) (cipher.AEAD, error) {
|
||||||
return gcm, nil
|
return gcm, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RFC8291) ikm(
|
// Decrypt decrypts a push notification, automatically selecting the correct
|
||||||
|
// encoding scheme based on the encoding parameter.
|
||||||
|
//
|
||||||
|
// For aes128gcm: crypto params are extracted from the data payload.
|
||||||
|
// For aesgcm: crypto params are extracted from the HTTP headers.
|
||||||
|
func (c *RFC8291) Decrypt(
|
||||||
|
data []byte,
|
||||||
|
encoding Encoding,
|
||||||
|
encryptionHeader string,
|
||||||
|
cryptoKeyHeader string,
|
||||||
authSecret []byte,
|
authSecret []byte,
|
||||||
ecdhSecret []byte,
|
receiverPrivateKey *ecdh.PrivateKey,
|
||||||
useragentPublicKey *ecdh.PublicKey,
|
) ([]byte, error) {
|
||||||
appserverPublicKey *ecdh.PublicKey,
|
switch encoding {
|
||||||
) (ikm []byte, err error) {
|
case EncodingAes128gcm:
|
||||||
prkKey := hkdf.Extract(c.hash, ecdhSecret, authSecret)
|
payload, err := Unmarshal(data)
|
||||||
|
if err != nil {
|
||||||
keyInfo := bytes.Join([][]byte{
|
return nil, fmt.Errorf("unmarshal aes128gcm payload: %v", err)
|
||||||
[]byte("WebPush: info\000"),
|
|
||||||
useragentPublicKey.Bytes(),
|
|
||||||
appserverPublicKey.Bytes(),
|
|
||||||
}, []byte{})
|
|
||||||
|
|
||||||
ikm = make([]byte, HKDF_IKM_LEN)
|
|
||||||
if _, err := io.ReadFull(hkdf.Expand(c.hash, prkKey, keyInfo), ikm); err != nil {
|
|
||||||
return nil, fmt.Errorf("read IKM failed: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ikm, nil
|
senderPublicKey, err := receiverPrivateKey.Curve().NewPublicKey(payload.KeyId)
|
||||||
}
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("parse sender public key: %v", err)
|
||||||
func (c *RFC8291) cekAndNonce(ikm []byte, salt []byte) (cek, nonce []byte, err error) {
|
}
|
||||||
prk := hkdf.Extract(c.hash, ikm, salt)
|
|
||||||
|
return c.DecryptAes128gcm(payload.CipherText, payload.Salt, authSecret, receiverPrivateKey, senderPublicKey)
|
||||||
cekInfo := []byte("Content-Encoding: aes128gcm\000")
|
|
||||||
nonceInfo := []byte("Content-Encoding: nonce\000")
|
case EncodingAesgcm:
|
||||||
|
params, err := ParseAesgcmHeaders(encryptionHeader, cryptoKeyHeader, receiverPrivateKey.Curve())
|
||||||
cek = make([]byte, HKDF_CEK_LEN)
|
if err != nil {
|
||||||
if _, err := io.ReadFull(hkdf.Expand(c.hash, prk, cekInfo), cek); err != nil {
|
return nil, fmt.Errorf("parse aesgcm headers: %v", err)
|
||||||
return nil, nil, fmt.Errorf("read CEK failed: %v", err)
|
}
|
||||||
}
|
|
||||||
|
return c.DecryptAesgcm(data, params.Salt, authSecret, receiverPrivateKey, params.SenderPublicKey)
|
||||||
nonce = make([]byte, HKDF_NONCE_LEN)
|
|
||||||
if _, err := io.ReadFull(hkdf.Expand(c.hash, prk, nonceInfo), nonce); err != nil {
|
default:
|
||||||
return nil, nil, fmt.Errorf("read Nonce failed: %v", err)
|
return nil, fmt.Errorf("unsupported encoding: %s", encoding)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cek, nonce, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,6 @@ func (c *NicoPushClient) Register() (channelID string, err error) {
|
||||||
|
|
||||||
func (c *NicoPushClient) Decrypt(data autopush.Notification) (*webpush.WebPushPayload, error) {
|
func (c *NicoPushClient) Decrypt(data autopush.Notification) (*webpush.WebPushPayload, error) {
|
||||||
return c.autoPushClient.Decrypt(
|
return c.autoPushClient.Decrypt(
|
||||||
ecdh.P256(),
|
|
||||||
c.authSecret,
|
c.authSecret,
|
||||||
c.privateKey,
|
c.privateKey,
|
||||||
data,
|
data,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue