Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Forwarding #54

Merged
merged 4 commits into from
Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 50 additions & 10 deletions openpgp/ecdh/ecdh.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,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 Down Expand Up @@ -112,27 +142,37 @@ func Decrypt(priv *PrivateKey, vsG, m, 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 @@ -152,7 +192,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
70 changes: 69 additions & 1 deletion openpgp/ecdh/ecdh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ var (
testFingerprint = make([]byte, 20)
)


// TODO: Improve this.
func TestEncryptDecrypt(t *testing.T) {
kdf := KDF{
Expand All @@ -45,3 +44,72 @@ func TestEncryptDecrypt(t *testing.T) {
t.Errorf("decryption failed, got: %x, want: %x", message2, message)
}
}

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

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: 40 additions & 7 deletions openpgp/packet/public_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,11 +389,13 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) {
return errors.UnsupportedError("failed to parse EC point")
}

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])
larabr marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -404,15 +406,46 @@ func (pk *PublicKey) parseECDH(r io.Reader) (err error) {
return errors.UnsupportedError("unsupported ECDH KDF cipher: " + strconv.Itoa(int(pk.kdf.Bytes()[2])))
}

kdf := ecdh.KDF{
larabr marked this conversation as resolved.
Show resolved Hide resolved
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]
}
}

pk.PublicKey = &ecdh.PublicKey{
CurveType: cType,
Curve: c,
X: x,
Y: y,
KDF: ecdh.KDF{
Hash: kdfHash,
Cipher: kdfCipher,
},
KDF: kdf,
}
return
}
Expand Down