Skip to content

Commit

Permalink
Add support for automatic forwarding (#54)
Browse files Browse the repository at this point in the history
  • Loading branch information
larabr authored and wussler committed Jan 17, 2023
1 parent 0027208 commit 4bd29e5
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 22 deletions.
67 changes: 52 additions & 15 deletions openpgp/ecdh/ecdh.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,38 @@ import (
)

type KDF struct {
Hash algorithm.Hash
Cipher algorithm.Cipher
Version int // Defaults to v1; non-standard v2 allows forwarding
Hash algorithm.Hash
Cipher algorithm.Cipher
Flags byte // (v2 only)
ReplacementFingerprint []byte // (v2 only) fingerprint to use instead of recipient's (for v5 keys, the 20 leftmost bytes only)
ReplacementKDFParams []byte // (v2 only) serialized KDF params to use in KDF digest computation
}

func (kdf *KDF) serialize(w io.Writer) (err error) {
if kdf.Version != 2 {
// Default version is 1
// Length || Version || Hash || Cipher
if _, err := w.Write([]byte{3, 1, kdf.Hash.Id(), kdf.Cipher.Id()}); err != nil {
return err
}

return nil
}

// Length || Version || Hash || Cipher || Flags || (Optional) v2 Fields...
v2Length := byte(4 + len(kdf.ReplacementFingerprint) + len(kdf.ReplacementKDFParams))
if _, err := w.Write([]byte{v2Length, 2, kdf.Hash.Id(), kdf.Cipher.Id(), kdf.Flags}); err != nil {
return err
}
if _, err := w.Write(kdf.ReplacementFingerprint); err != nil {
return err
}
if _, err := w.Write(kdf.ReplacementKDFParams); err != nil {
return err
}

return nil
}

type PublicKey struct {
Expand All @@ -32,13 +62,10 @@ type PrivateKey struct {
D []byte
}

func NewPublicKey(curve ecc.ECDHCurve, kdfHash algorithm.Hash, kdfCipher algorithm.Cipher) *PublicKey {
func NewPublicKey(curve ecc.ECDHCurve, kdf KDF) *PublicKey {
return &PublicKey{
curve: curve,
KDF: KDF{
Hash: kdfHash,
Cipher: kdfCipher,
},
KDF: kdf,
}
}

Expand Down Expand Up @@ -149,27 +176,37 @@ func Decrypt(priv *PrivateKey, vsG, c, curveOID, fingerprint []byte) (msg []byte
}

func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLeading, stripTrailing bool) ([]byte, error) {
// Param = curve_OID_len || curve_OID || public_key_alg_ID || 03
// || 01 || KDF_hash_ID || KEK_alg_ID for AESKeyWrap
// Param = curve_OID_len || curve_OID || public_key_alg_ID
// || KDF_params for AESKeyWrap
// || "Anonymous Sender " || recipient_fingerprint;
param := new(bytes.Buffer)
if _, err := param.Write(curveOID); err != nil {
return nil, err
}
algKDF := []byte{18, 3, 1, pub.KDF.Hash.Id(), pub.KDF.Cipher.Id()}
if _, err := param.Write(algKDF); err != nil {
algo := []byte{18}
if _, err := param.Write(algo); err != nil {
return nil, err
}
if pub.KDF.ReplacementKDFParams != nil {
kdf := pub.KDF.ReplacementKDFParams
if _, err := param.Write(kdf); err != nil {
return nil, err
}
} else {
if err := pub.KDF.serialize(param); err != nil {
return nil, err
}
}
if _, err := param.Write([]byte("Anonymous Sender ")); err != nil {
return nil, err
}
if pub.KDF.ReplacementFingerprint != nil {
fingerprint = pub.KDF.ReplacementFingerprint
}
// For v5 keys, the 20 leftmost octets of the fingerprint are used.
if _, err := param.Write(fingerprint[:20]); err != nil {
return nil, err
}
if param.Len() - len(curveOID) != 45 {
return nil, errors.New("ecdh: malformed KDF Param")
}

// MB = Hash ( 00 || 00 || 00 || 01 || ZB || Param );
h := pub.KDF.Hash.New()
Expand All @@ -189,7 +226,7 @@ func buildKey(pub *PublicKey, zb []byte, curveOID, fingerprint []byte, stripLead
// (See https://github.com/openpgpjs/openpgpjs/pull/853.)
for ; j >= 0 && zb[j] == 0; j-- {}
}
if _, err := h.Write(zb[i:j+1]); err != nil {
if _, err := h.Write(zb[i : j+1]); err != nil {
return nil, err
}
if _, err := h.Write(param.Bytes()); err != nil {
Expand Down
74 changes: 72 additions & 2 deletions openpgp/ecdh/ecdh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ func testEncryptDecrypt(t *testing.T, priv *PrivateKey, oid, fingerprint []byte)
}
}


func testValidation(t *testing.T, priv *PrivateKey) {
if err := Validate(priv); err != nil {
t.Fatalf("valid key marked as invalid: %s", err)
Expand All @@ -89,7 +88,7 @@ func testMarshalUnmarshal(t *testing.T, priv *PrivateKey) {
p := priv.MarshalPoint()
d := priv.MarshalByteSecret()

parsed := NewPrivateKey(*NewPublicKey(priv.GetCurve(), priv.KDF.Hash, priv.KDF.Cipher))
parsed := NewPrivateKey(*NewPublicKey(priv.GetCurve(), priv.KDF))

if err := parsed.UnmarshalPoint(p); err != nil {
t.Fatalf("unable to unmarshal point: %s", err)
Expand All @@ -113,3 +112,74 @@ func testMarshalUnmarshal(t *testing.T, priv *PrivateKey) {
t.Fatal("failed to marshal/unmarshal correctly")
}
}

func TestKDFParamsWrite(t *testing.T) {
kdf := KDF{
Hash: algorithm.SHA512,
Cipher: algorithm.AES256,
}
byteBuffer := new(bytes.Buffer)

testFingerprint := make([]byte, 20)

expectBytesV1 := []byte{3, 1, kdf.Hash.Id(), kdf.Cipher.Id()}
kdf.serialize(byteBuffer)
gotBytes := byteBuffer.Bytes()
if !bytes.Equal(gotBytes, expectBytesV1) {
t.Errorf("error serializing KDF params, got %x, want: %x", gotBytes, expectBytesV1)
}
byteBuffer.Reset()

kdfV2Flags0x01 := KDF{
Version: 2,
Hash: algorithm.SHA512,
Cipher: algorithm.AES256,
Flags: 0x01,
ReplacementFingerprint: testFingerprint,
}
expectBytesV2Flags0x01 := []byte{24, 2, kdfV2Flags0x01.Hash.Id(), kdfV2Flags0x01.Cipher.Id(), 0x01}
expectBytesV2Flags0x01 = append(expectBytesV2Flags0x01, testFingerprint...)

kdfV2Flags0x01.serialize(byteBuffer)
gotBytes = byteBuffer.Bytes()
if !bytes.Equal(gotBytes, expectBytesV2Flags0x01) {
t.Errorf("error serializing KDF params v2 (flags 0x01), got %x, want: %x", gotBytes, expectBytesV2Flags0x01)
}
byteBuffer.Reset()

kdfV2Flags0x02 := KDF{
Version: 2,
Hash: algorithm.SHA512,
Cipher: algorithm.AES256,
Flags: 0x02,
ReplacementKDFParams: expectBytesV1,
}
expectBytesV2Flags0x02 := []byte{8, 2, kdfV2Flags0x02.Hash.Id(), kdfV2Flags0x01.Cipher.Id(), 0x02}
expectBytesV2Flags0x02 = append(expectBytesV2Flags0x02, expectBytesV1...)

kdfV2Flags0x02.serialize(byteBuffer)
gotBytes = byteBuffer.Bytes()
if !bytes.Equal(gotBytes, expectBytesV2Flags0x02) {
t.Errorf("error serializing KDF params v2 (flags 0x02), got %x, want: %x", gotBytes, expectBytesV2Flags0x02)
}
byteBuffer.Reset()

kdfV2Flags0x03 := KDF{
Version: 2,
Hash: algorithm.SHA512,
Cipher: algorithm.AES256,
Flags: 0x03,
ReplacementFingerprint: testFingerprint,
ReplacementKDFParams: expectBytesV1,
}
expectBytesV2Flags0x03 := []byte{28, 2, kdfV2Flags0x03.Hash.Id(), kdfV2Flags0x03.Cipher.Id(), 0x03}
expectBytesV2Flags0x03 = append(expectBytesV2Flags0x03, testFingerprint...)
expectBytesV2Flags0x03 = append(expectBytesV2Flags0x03, expectBytesV1...)

kdfV2Flags0x03.serialize(byteBuffer)
gotBytes = byteBuffer.Bytes()
if !bytes.Equal(gotBytes, expectBytesV2Flags0x03) {
t.Errorf("error serializing KDF params v2 (flags 0x03), got %x, want: %x", gotBytes, expectBytesV2Flags0x03)
}
byteBuffer.Reset()
}
70 changes: 70 additions & 0 deletions openpgp/forwarding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package openpgp

import (
"bytes"
"io/ioutil"
"strings"
"testing"

"golang.org/x/crypto/openpgp/armor"
)

var (
charlieKeyArmored = `-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: OpenPGP.js v4.10.4
Comment: https://openpgpjs.org
xVgEXqG7KRYJKwYBBAHaRw8BAQdA/q4cs9Pwms3R4trjUd7YyrsRYdQHC9wI
MqLdefob4KUAAQDfy9e8qleM+a1EnPCjDpm69FIY769mo/dpwYlkuI2T/RQt
zSlCb2IgKEZvcndhcmRlZCB0byBDaGFybGllKSA8aW5mb0Bib2IuY29tPsJ4
BBAWCgAgBQJeobspBgsJBwgDAgQVCAoCBBYCAQACGQECGwMCHgEACgkQN2cz
+W7U/RnS8AEArtRly8vW6uUSng9EJ0iuIwJpwgZfykSLl/t4u3HTBZ4BALzY
3XsnvKtZZVvaKvFvCUu/2NvC/1yw2wJk9wGbCwEOx3YEXqG7KRIKKwYBBAGX
VQEFAQEHQCGxSJahhDUdTKnlqT3UIn3rXn5i47I4MsG4kSWfTwcOHAIIBwPe
7fJ+kOrMea9aIUeYtGpUzABa9gMBCAcAAP95QjbjU7kyugp39vhi60YW5T8p
Me0kKFCWzmSYzstgGBBbwmEEGBYIAAkFAl6huykCGwwACgkQN2cz+W7U/RkP
WQD+KcU1HKn6PkVJKxg6RS0Q7RcCZwaQ1DyEyjUoneMCRAgA/jUl9uvPAoCS
3+4Wqg9Q//zOwXNImimIPIdpWNXYZJID
=FVvG
-----END PGP PRIVATE KEY BLOCK-----`

fwdCiphertextArmored = `-----BEGIN PGP MESSAGE-----
Version: OpenPGP.js v4.10.4
Comment: https://openpgpjs.org
wV4Dog8LAQLriGUSAQdA/I6k0IvGxyNG2SdSDHrv3bZQDWH18OhTWkcmSF0M
Bxcw3w8KMjr2v69ro5cyZztymEXi5RemRx+oPZGKIZ9N5T+26TaOltH7h8eR
Mu4H03Lp0k4BRsjpFNUBL3HsAuMIemNf4369g+szlpuzjNE1KQhQzZbh87AU
T7KAKygwz0EpOWpx2RHtshDy/bZ1EC8Ia4qDAebameIqCU929OmY1uI=
=3iIr
-----END PGP MESSAGE-----`
)

func TestForwardingDecryption(t *testing.T) {
charlieKey, err := ReadArmoredKeyRing(bytes.NewBufferString(charlieKeyArmored))
if err != nil {
t.Error(err)
return
}
ciphertext, err := armor.Decode(strings.NewReader(string(fwdCiphertextArmored)))
if err != nil {
t.Error(err)
return
}
// Decrypt message
md, err := ReadMessage(ciphertext.Body, charlieKey, nil, nil)
if err != nil {
t.Error(err)
return
}
body, err := ioutil.ReadAll(md.UnverifiedBody)
if err != nil {
t.Fatal(err)
}

expectedBody := "Hello Bob, hello world"
gotBody := string(body)
if gotBody != expectedBody {
t.Fatal("Decrypted body did not match expected body")
}
}
47 changes: 42 additions & 5 deletions openpgp/packet/public_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,11 +373,13 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) {
return errors.UnsupportedError(fmt.Sprintf("unsupported oid: %x", pk.oid))
}

if kdfLen := len(pk.kdf.Bytes()); kdfLen < 3 {
kdfLen := len(pk.kdf.Bytes())
if kdfLen < 3 {
return errors.UnsupportedError("unsupported ECDH KDF length: " + strconv.Itoa(kdfLen))
}
if reserved := pk.kdf.Bytes()[0]; reserved != 0x01 {
return errors.UnsupportedError("unsupported KDF reserved field: " + strconv.Itoa(int(reserved)))
kdfVersion := int(pk.kdf.Bytes()[0])
if kdfVersion != 1 && kdfVersion != 2 {
return errors.UnsupportedError("unsupported ECDH KDF version: " + strconv.Itoa(int(kdfVersion)))
}
kdfHash, ok := algorithm.HashById[pk.kdf.Bytes()[1]]
if !ok {
Expand All @@ -388,10 +390,45 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) {
return errors.UnsupportedError("unsupported ECDH KDF cipher: " + strconv.Itoa(int(pk.kdf.Bytes()[2])))
}

ecdhKey := ecdh.NewPublicKey(c, kdfHash, kdfCipher)
kdf := ecdh.KDF{
Version: kdfVersion,
Hash: kdfHash,
Cipher: kdfCipher,
}

if kdfVersion == 2 {
if kdfLen < 4 {
return errors.UnsupportedError("unsupported ECDH KDF v2 length: " + strconv.Itoa(kdfLen))
}

kdf.Flags = pk.kdf.Bytes()[3]
readBytes := 4
if kdf.Flags&0x01 != 0x0 {
// Expect 20-byte fingerprint
if kdfLen < readBytes+20 {
return errors.UnsupportedError("malformed ECDH KDF params")
}
kdf.ReplacementFingerprint = pk.kdf.Bytes()[readBytes : readBytes+20]
readBytes += 20
}

if kdf.Flags&0x02 != 0x0 {
// Expect replacement params
// Read length field
if kdfLen < readBytes+1 {
return errors.UnsupportedError("malformed ECDH KDF params")
}
fieldLen := int(pk.kdf.Bytes()[readBytes]) + 1 // Account for length field
if kdfLen < readBytes+fieldLen {
return errors.UnsupportedError("malformed ECDH KDF params")
}
kdf.ReplacementKDFParams = pk.kdf.Bytes()[readBytes : readBytes+fieldLen]
}
}

ecdhKey := ecdh.NewPublicKey(c, kdf)
err = ecdhKey.UnmarshalPoint(pk.p.Bytes())
pk.PublicKey = ecdhKey

return
}

Expand Down

0 comments on commit 4bd29e5

Please sign in to comment.