diff --git a/openpgp/ecdh/ecdh.go b/openpgp/ecdh/ecdh.go index b09e2a735..75c482b76 100644 --- a/openpgp/ecdh/ecdh.go +++ b/openpgp/ecdh/ecdh.go @@ -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 { @@ -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, } } @@ -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() @@ -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 { diff --git a/openpgp/ecdh/ecdh_test.go b/openpgp/ecdh/ecdh_test.go index 18dca8aca..6f4dffb43 100644 --- a/openpgp/ecdh/ecdh_test.go +++ b/openpgp/ecdh/ecdh_test.go @@ -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) @@ -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) @@ -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() +} diff --git a/openpgp/forwarding_test.go b/openpgp/forwarding_test.go new file mode 100644 index 000000000..9b286a8e4 --- /dev/null +++ b/openpgp/forwarding_test.go @@ -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") + } +} diff --git a/openpgp/packet/public_key.go b/openpgp/packet/public_key.go index e0f5f74a9..d208bce47 100644 --- a/openpgp/packet/public_key.go +++ b/openpgp/packet/public_key.go @@ -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 { @@ -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 }