From f46d2d349a80d31218f305c9094e26e27fbe1b04 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Tue, 23 Jan 2024 14:49:00 -0700 Subject: [PATCH] verificationhelper/qrcode: add (en|de)coder Signed-off-by: Sumner Evans --- crypto/verificationhelper/qrcode.go | 98 ++++++++++++++++++++++++ crypto/verificationhelper/qrcode_test.go | 58 ++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 crypto/verificationhelper/qrcode.go create mode 100644 crypto/verificationhelper/qrcode_test.go diff --git a/crypto/verificationhelper/qrcode.go b/crypto/verificationhelper/qrcode.go new file mode 100644 index 00000000..a28d8fc3 --- /dev/null +++ b/crypto/verificationhelper/qrcode.go @@ -0,0 +1,98 @@ +// Copyright (c) 2024 Sumner Evans +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package verificationhelper + +import ( + "bytes" + "encoding/binary" + "errors" + + "go.mau.fi/util/random" + + "maunium.net/go/mautrix/id" +) + +var ( + ErrInvalidQRCodeHeader = errors.New("invalid QR code header") + ErrUnknownQRCodeVersion = errors.New("invalid QR code version") + ErrInvalidQRCodeMode = errors.New("invalid QR code mode") +) + +type QRCodeMode byte + +const ( + QRCodeModeCrossSigning QRCodeMode = 0x00 + QRCodeModeSelfVerifyingMasterKeyTrusted QRCodeMode = 0x01 + QRCodeModeSelfVerifyingMasterKeyUntrusted QRCodeMode = 0x02 +) + +type QRCode struct { + Mode QRCodeMode + TransactionID id.VerificationTransactionID + Key1, Key2 [32]byte + SharedSecret []byte +} + +func NewQRCode(mode QRCodeMode, txnID id.VerificationTransactionID, key1, key2 [32]byte) *QRCode { + return &QRCode{ + Mode: mode, + TransactionID: txnID, + Key1: key1, + Key2: key2, + SharedSecret: random.Bytes(16), + } +} + +// NewQRCodeFromBytes parses the bytes from a QR code scan as defined in +// [Section 11.12.2.4.1] of the Spec. +// +// [Section 11.12.2.4.1]: https://spec.matrix.org/v1.9/client-server-api/#qr-code-format +func NewQRCodeFromBytes(data []byte) (*QRCode, error) { + if !bytes.HasPrefix(data, []byte("MATRIX")) { + return nil, ErrInvalidQRCodeHeader + } + if data[6] != 0x02 { + return nil, ErrUnknownQRCodeVersion + } + if data[7] != 0x00 && data[7] != 0x01 && data[7] != 0x02 { + return nil, ErrInvalidQRCodeMode + } + transactionIDLength := binary.BigEndian.Uint16(data[8:10]) + transactionID := data[10 : 10+transactionIDLength] + + var key1, key2 [32]byte + copy(key1[:], data[10+transactionIDLength:10+transactionIDLength+32]) + copy(key2[:], data[10+transactionIDLength+32:10+transactionIDLength+64]) + + return &QRCode{ + Mode: QRCodeMode(data[7]), + TransactionID: id.VerificationTransactionID(transactionID), + Key1: key1, + Key2: key2, + SharedSecret: data[10+transactionIDLength+64:], + }, nil +} + +// Bytes returns the bytes that need to be encoded in the QR code as defined in +// [Section 11.12.2.4.1] of the Spec. +// +// [Section 11.12.2.4.1]: https://spec.matrix.org/v1.9/client-server-api/#qr-code-format +func (q *QRCode) Bytes() []byte { + var buf bytes.Buffer + buf.WriteString("MATRIX") // Header + buf.WriteByte(0x02) // Version + buf.WriteByte(byte(q.Mode)) // Mode + + // Transaction ID length + Transaction ID + buf.Write(binary.BigEndian.AppendUint16(nil, uint16(len(q.TransactionID.String())))) + buf.WriteString(q.TransactionID.String()) + + buf.Write(q.Key1[:]) // Key 1 + buf.Write(q.Key2[:]) // Key 2 + buf.Write(q.SharedSecret) // Shared secret + return buf.Bytes() +} diff --git a/crypto/verificationhelper/qrcode_test.go b/crypto/verificationhelper/qrcode_test.go new file mode 100644 index 00000000..d2767734 --- /dev/null +++ b/crypto/verificationhelper/qrcode_test.go @@ -0,0 +1,58 @@ +// Copyright (c) 2024 Sumner Evans +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package verificationhelper_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "maunium.net/go/mautrix/crypto/verificationhelper" +) + +func TestQRCode_Roundtrip(t *testing.T) { + var key1, key2 [32]byte + copy(key1[:], bytes.Repeat([]byte{0x01}, 32)) + copy(key2[:], bytes.Repeat([]byte{0x02}, 32)) + qrCode := verificationhelper.NewQRCode(verificationhelper.QRCodeModeCrossSigning, "test", key1, key2) + + encoded := qrCode.Bytes() + decoded, err := verificationhelper.NewQRCodeFromBytes(encoded) + require.NoError(t, err) + + assert.Equal(t, verificationhelper.QRCodeModeCrossSigning, decoded.Mode) + assert.EqualValues(t, "test", decoded.TransactionID) + assert.Equal(t, key1, decoded.Key1) + assert.Equal(t, key2, decoded.Key2) +} + +func TestQRCodeDecode(t *testing.T) { + qrcodeData := []byte{ + 0x4d, 0x41, 0x54, 0x52, 0x49, 0x58, 0x02, 0x01, 0x00, 0x20, 0x47, 0x6e, 0x41, 0x65, 0x43, 0x76, + 0x74, 0x57, 0x6a, 0x7a, 0x4d, 0x4f, 0x56, 0x57, 0x51, 0x54, 0x6b, 0x74, 0x33, 0x35, 0x59, 0x52, + 0x55, 0x72, 0x75, 0x6a, 0x6d, 0x52, 0x50, 0x63, 0x38, 0x61, 0x18, 0x32, 0x7c, 0xc3, 0x8c, 0xc2, + 0xa6, 0xc2, 0xb5, 0xc2, 0xa7, 0x50, 0x57, 0x67, 0x19, 0x5e, 0xc3, 0xaf, 0xc2, 0xa0, 0xc2, 0x98, + 0xc2, 0x9d, 0x36, 0xc3, 0xad, 0x7a, 0x10, 0x2e, 0x18, 0x3e, 0x4e, 0xc3, 0x84, 0xc3, 0x81, 0x45, + 0x0c, 0xc2, 0xae, 0x19, 0x78, 0xc2, 0x99, 0x06, 0xc2, 0x92, 0xc2, 0x94, 0xc2, 0x8e, 0xc2, 0xb7, + 0x59, 0xc2, 0x96, 0xc2, 0xad, 0xc3, 0xbd, 0x70, 0x6a, 0x11, 0xc2, 0xba, 0xc2, 0xa9, 0x29, 0xc3, + 0x8f, 0x0d, 0xc2, 0xb8, 0xc2, 0x88, 0x67, 0x5b, 0xc3, 0xb3, 0x01, 0xc2, 0xb0, 0x63, 0x2e, 0xc2, + 0xa5, 0xc3, 0xb3, 0x60, 0xc3, 0x82, 0x04, 0xc3, 0xa3, 0x72, 0x7d, 0x7c, 0x1d, 0xc2, 0xb6, 0xc2, + 0xba, 0xc2, 0x81, 0x1e, 0xc2, 0x99, 0xc2, 0xb8, 0x7f, 0x0a, + } + decoded, err := verificationhelper.NewQRCodeFromBytes(qrcodeData) + require.NoError(t, err) + assert.Equal(t, verificationhelper.QRCodeModeSelfVerifyingMasterKeyTrusted, decoded.Mode) + assert.EqualValues(t, "GnAeCvtWjzMOVWQTkt35YRUrujmRPc8a", decoded.TransactionID) + assert.Equal(t, + [32]byte{0x18, 0x32, 0x7c, 0xc3, 0x8c, 0xc2, 0xa6, 0xc2, 0xb5, 0xc2, 0xa7, 0x50, 0x57, 0x67, 0x19, 0x5e, 0xc3, 0xaf, 0xc2, 0xa0, 0xc2, 0x98, 0xc2, 0x9d, 0x36, 0xc3, 0xad, 0x7a, 0x10, 0x2e, 0x18, 0x3e}, + decoded.Key1) + assert.Equal(t, + [32]byte{0x4e, 0xc3, 0x84, 0xc3, 0x81, 0x45, 0xc, 0xc2, 0xae, 0x19, 0x78, 0xc2, 0x99, 0x6, 0xc2, 0x92, 0xc2, 0x94, 0xc2, 0x8e, 0xc2, 0xb7, 0x59, 0xc2, 0x96, 0xc2, 0xad, 0xc3, 0xbd, 0x70, 0x6a, 0x11}, + decoded.Key2) +}