mirror of
https://mau.dev/mautrix/go.git
synced 2026-03-14 14:25:53 +01:00
verificationhelper/qrcode: begin implementing flow
Signed-off-by: Sumner Evans <sumner@beeper.com>
This commit is contained in:
parent
f46d2d349a
commit
582ce5de49
5 changed files with 974 additions and 0 deletions
14
client.go
14
client.go
|
|
@ -35,6 +35,19 @@ type CryptoHelper interface {
|
|||
Init(context.Context) error
|
||||
}
|
||||
|
||||
type VerificationHelper interface {
|
||||
Init(context.Context) error
|
||||
StartVerification(ctx context.Context, to id.UserID) (id.VerificationTransactionID, error)
|
||||
StartInRoomVerification(ctx context.Context, roomID id.RoomID, to id.UserID) (id.VerificationTransactionID, error)
|
||||
AcceptVerification(ctx context.Context, txnID id.VerificationTransactionID) error
|
||||
|
||||
HandleScannedQRData(ctx context.Context, data []byte) error
|
||||
ConfirmQRCodeScanned(ctx context.Context, txnID id.VerificationTransactionID) error
|
||||
|
||||
StartSAS(ctx context.Context, txnID id.VerificationTransactionID) error
|
||||
ConfirmSAS(ctx context.Context, txnID id.VerificationTransactionID) error
|
||||
}
|
||||
|
||||
// Deprecated: switch to zerolog
|
||||
type Logger interface {
|
||||
Debugfln(message string, args ...interface{})
|
||||
|
|
@ -58,6 +71,7 @@ type Client struct {
|
|||
Store SyncStore // The thing which can store tokens/ids
|
||||
StateStore StateStore
|
||||
Crypto CryptoHelper
|
||||
Verification VerificationHelper
|
||||
|
||||
Log zerolog.Logger
|
||||
// Deprecated: switch to the zerolog instance in Log
|
||||
|
|
|
|||
257
crypto/verificationhelper/reciprocate.go
Normal file
257
crypto/verificationhelper/reciprocate.go
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
// 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"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// HandleScannedQRData verifies the keys from a scanned QR code and if
|
||||
// successful, sends the m.key.verification.start event and
|
||||
// m.key.verification.done event.
|
||||
func (vh *VerificationHelper) HandleScannedQRData(ctx context.Context, data []byte) error {
|
||||
qrCode, err := NewQRCodeFromBytes(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log := vh.getLog(ctx).With().
|
||||
Str("verification_action", "handle scanned QR data").
|
||||
Stringer("transaction_id", qrCode.TransactionID).
|
||||
Int("mode", int(qrCode.Mode)).
|
||||
Logger()
|
||||
|
||||
txn, ok := vh.activeTransactions[qrCode.TransactionID]
|
||||
if !ok {
|
||||
log.Warn().Msg("Ignoring QR code scan for an unknown transaction")
|
||||
return nil
|
||||
} else if txn.VerificationStep != verificationStepReady {
|
||||
log.Warn().Msg("Ignoring QR code scan for a transaction that is not in the ready state")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Verify the keys
|
||||
log.Info().Msg("Verifying keys from QR code")
|
||||
|
||||
switch qrCode.Mode {
|
||||
case QRCodeModeCrossSigning:
|
||||
// TODO
|
||||
panic("unimplemented")
|
||||
// TODO sign their master key
|
||||
case QRCodeModeSelfVerifyingMasterKeyTrusted:
|
||||
// The QR was created by a device that trusts the master key, which
|
||||
// means that we don't trust the key. Key1 is the master key public
|
||||
// key, and Key2 is what the other device thinks our device key is.
|
||||
|
||||
if vh.client.UserID != txn.TheirUser {
|
||||
return fmt.Errorf("mode %d is only allowed when the other user is the same as the current user", qrCode.Mode)
|
||||
}
|
||||
|
||||
// Verify the master key is correct
|
||||
crossSigningPubkeys := vh.mach.GetOwnCrossSigningPublicKeys(ctx)
|
||||
crossSigningMasterKeyBytes, err := base64.RawStdEncoding.DecodeString(crossSigningPubkeys.MasterKey.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Equal(crossSigningMasterKeyBytes, qrCode.Key1[:]) {
|
||||
log.Info().Msg("Verified that the other device has the same master key")
|
||||
} else {
|
||||
return fmt.Errorf("the master key does not match")
|
||||
}
|
||||
|
||||
// Verify that the device key that the other device things we have is
|
||||
// correct.
|
||||
myDevice := vh.mach.OwnIdentity()
|
||||
myDeviceKeyBytes, err := base64.RawStdEncoding.DecodeString(myDevice.IdentityKey.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Equal(myDeviceKeyBytes, qrCode.Key2[:]) {
|
||||
log.Info().Msg("Verified that the other device has the correct key for this device")
|
||||
} else {
|
||||
return fmt.Errorf("the other device has the wrong key for this device")
|
||||
}
|
||||
|
||||
case QRCodeModeSelfVerifyingMasterKeyUntrusted:
|
||||
// The QR was created by a device that does not trust the master key,
|
||||
// which means that we do trust the master key. Key1 is the other
|
||||
// device's device key, and Key2 is what the other device thinks the
|
||||
// master key is.
|
||||
|
||||
if vh.client.UserID != txn.TheirUser {
|
||||
return fmt.Errorf("mode %d is only allowed when the other user is the same as the current user", qrCode.Mode)
|
||||
}
|
||||
|
||||
// Get their device
|
||||
theirDevice, err := vh.mach.GetOrFetchDevice(ctx, txn.TheirUser, txn.TheirDevice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify that the other device's key is what we expect.
|
||||
myDeviceKeyBytes, err := base64.RawStdEncoding.DecodeString(theirDevice.IdentityKey.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Equal(myDeviceKeyBytes, qrCode.Key1[:]) {
|
||||
log.Info().Msg("Verified that the other device key is what we expected")
|
||||
} else {
|
||||
return fmt.Errorf("the other device's key is not what we expected")
|
||||
}
|
||||
|
||||
// Verify that what they think the master key is is correct.
|
||||
crossSigningPubkeys := vh.mach.GetOwnCrossSigningPublicKeys(ctx)
|
||||
crossSigningMasterKeyBytes, err := base64.RawStdEncoding.DecodeString(crossSigningPubkeys.MasterKey.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Equal(crossSigningMasterKeyBytes, qrCode.Key2[:]) {
|
||||
log.Info().Msg("Verified that the other device has the correct master key")
|
||||
} else {
|
||||
return fmt.Errorf("the master key does not match")
|
||||
}
|
||||
|
||||
// Trust their device
|
||||
theirDevice.Trust = id.TrustStateVerified
|
||||
err = vh.mach.CryptoStore.PutDevice(ctx, txn.TheirUser, theirDevice)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update device trust state after verifying: %w", err)
|
||||
}
|
||||
|
||||
// TODO Cross-sign their device with the cross-signing key
|
||||
default:
|
||||
return fmt.Errorf("unknown QR code mode %d", qrCode.Mode)
|
||||
}
|
||||
|
||||
// Send a m.key.verification.start event with the secret
|
||||
startEvt := &event.VerificationStartEventContent{
|
||||
FromDevice: vh.client.DeviceID,
|
||||
Method: event.VerificationMethodReciprocate,
|
||||
Secret: qrCode.SharedSecret,
|
||||
}
|
||||
err = vh.sendVerificationEvent(ctx, txn, event.InRoomVerificationStart, startEvt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Immediately send the m.key.verification.done event, as our side of the
|
||||
// transaction is done.
|
||||
err = vh.sendVerificationEvent(ctx, txn, event.InRoomVerificationDone, &event.VerificationDoneEventContent{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vh.activeTransactionsLock.Lock()
|
||||
defer vh.activeTransactionsLock.Unlock()
|
||||
delete(vh.activeTransactions, txn.TransactionID)
|
||||
|
||||
// Broadcast that the verification is complete.
|
||||
vh.verificationDone(ctx, txn.TransactionID)
|
||||
// TODO do we need to also somehow broadcast that we are now a trusted
|
||||
// device?
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vh *VerificationHelper) ConfirmQRCodeScanned(ctx context.Context, txnID id.VerificationTransactionID) error {
|
||||
log := vh.getLog(ctx).With().
|
||||
Str("verification_action", "confirm QR code scanned").
|
||||
Stringer("transaction_id", txnID).
|
||||
Logger()
|
||||
|
||||
txn, ok := vh.activeTransactions[txnID]
|
||||
if !ok {
|
||||
log.Warn().Msg("Ignoring QR code scan confirmation for an unknown transaction")
|
||||
return nil
|
||||
} else if txn.VerificationStep != verificationStepStarted {
|
||||
log.Warn().Msg("Ignoring QR code scan confirmation for a transaction that is not in the started state")
|
||||
return nil
|
||||
}
|
||||
log.Info().Msg("Confirming QR code scanned")
|
||||
|
||||
// TODO trust the keys somehow
|
||||
|
||||
err := vh.sendVerificationEvent(ctx, txn, event.InRoomVerificationDone, &event.VerificationDoneEventContent{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
vh.activeTransactionsLock.Lock()
|
||||
defer vh.activeTransactionsLock.Unlock()
|
||||
delete(vh.activeTransactions, txn.TransactionID)
|
||||
|
||||
// Broadcast that the verification is complete.
|
||||
vh.verificationDone(ctx, txn.TransactionID)
|
||||
// TODO do we need to also somehow broadcast that we are now a trusted
|
||||
// device?
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vh *VerificationHelper) generateAndShowQRCode(ctx context.Context, txn *verificationTransaction) error {
|
||||
log := vh.getLog(ctx).With().
|
||||
Str("verification_action", "generate and show QR code").
|
||||
Stringer("transaction_id", txn.TransactionID).
|
||||
Logger()
|
||||
if vh.showQRCode == nil || !slices.Contains(txn.SupportedMethods, event.VerificationMethodQRCodeShow) {
|
||||
log.Warn().Msg("Ignoring QR code generation request as showing a QR code is not enabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
ownCrossSigningPublicKeys := vh.mach.GetOwnCrossSigningPublicKeys(ctx)
|
||||
|
||||
mode := QRCodeModeCrossSigning
|
||||
if vh.client.UserID == txn.TheirUser {
|
||||
// This is a self-signing situation.
|
||||
// TODO determine if it's trusted or not.
|
||||
mode = QRCodeModeSelfVerifyingMasterKeyUntrusted
|
||||
}
|
||||
|
||||
var key1, key2 []byte
|
||||
switch mode {
|
||||
case QRCodeModeCrossSigning:
|
||||
// Key 1 is the current user's master signing key.
|
||||
key1 = ownCrossSigningPublicKeys.MasterKey.Bytes()
|
||||
|
||||
// Key 2 is the other user's master signing key.
|
||||
theirSigningKeys, err := vh.mach.GetCrossSigningPublicKeys(ctx, txn.TheirUser)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key2 = theirSigningKeys.MasterKey.Bytes()
|
||||
case QRCodeModeSelfVerifyingMasterKeyTrusted:
|
||||
// Key 1 is the current user's master signing key.
|
||||
key1 = ownCrossSigningPublicKeys.MasterKey.Bytes()
|
||||
|
||||
// Key 2 is the other device's key.
|
||||
theirDevice, err := vh.mach.GetOrFetchDevice(ctx, txn.TheirUser, txn.TheirDevice)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key2 = theirDevice.IdentityKey.Bytes()
|
||||
case QRCodeModeSelfVerifyingMasterKeyUntrusted:
|
||||
// Key 1 is the current device's key
|
||||
key1 = vh.mach.OwnIdentity().IdentityKey.Bytes()
|
||||
|
||||
// Key 2 is the master signing key.
|
||||
key2 = ownCrossSigningPublicKeys.MasterKey.Bytes()
|
||||
default:
|
||||
log.Fatal().Str("mode", string(mode)).Msg("Unknown QR code mode")
|
||||
}
|
||||
|
||||
qrCode := NewQRCode(mode, txn.TransactionID, [32]byte(key1), [32]byte(key2))
|
||||
txn.QRCodeSharedSecret = qrCode.SharedSecret
|
||||
vh.showQRCode(ctx, txn.TransactionID, qrCode)
|
||||
return nil
|
||||
}
|
||||
655
crypto/verificationhelper/verificationhelper.go
Normal file
655
crypto/verificationhelper/verificationhelper.go
Normal file
|
|
@ -0,0 +1,655 @@
|
|||
// 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"
|
||||
"context"
|
||||
"crypto/ecdh"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/jsontime"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/crypto"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
type verificationStep int
|
||||
|
||||
const (
|
||||
verificationStepRequested verificationStep = iota
|
||||
verificationStepReady
|
||||
verificationStepStarted
|
||||
)
|
||||
|
||||
func (step verificationStep) String() string {
|
||||
switch step {
|
||||
case verificationStepRequested:
|
||||
return "requested"
|
||||
case verificationStepReady:
|
||||
return "ready"
|
||||
case verificationStepStarted:
|
||||
return "started"
|
||||
default:
|
||||
return fmt.Sprintf("verificationStep(%d)", step)
|
||||
}
|
||||
}
|
||||
|
||||
type verificationTransaction struct {
|
||||
// RoomID is the room ID if the verification is happening in a room or
|
||||
// empty if it is a to-device verification.
|
||||
RoomID id.RoomID
|
||||
|
||||
// VerificationStep is the current step of the verification flow.
|
||||
VerificationStep verificationStep
|
||||
// TransactionID is the ID of the verification transaction.
|
||||
TransactionID id.VerificationTransactionID
|
||||
|
||||
// TheirDevice is the device ID of the device that either made the initial
|
||||
// request or accepted our request.
|
||||
TheirDevice id.DeviceID
|
||||
|
||||
// TheirUser is the user ID of the other user.
|
||||
TheirUser id.UserID
|
||||
|
||||
// SentToDeviceIDs is a list of devices which the initial request was sent
|
||||
// to. This is only used for to-device verification requests, and is meant
|
||||
// to be used to send cancellation requests to all other devices when a
|
||||
// verification request is accepted via a m.key.verification.ready event.
|
||||
SentToDeviceIDs []id.DeviceID
|
||||
|
||||
// SupportedMethods is a list of verification methods that the other device
|
||||
// supports.
|
||||
SupportedMethods []event.VerificationMethod
|
||||
|
||||
// QRCodeSharedSecret is the shared secret that was encoded in the QR code
|
||||
// that we showed.
|
||||
QRCodeSharedSecret []byte
|
||||
|
||||
StartedByUs bool // Whether the verification was started by us
|
||||
StartEventContent *event.VerificationStartEventContent // The m.key.verification.start event content
|
||||
Commitment []byte // The commitment from the m.key.verification.accept event
|
||||
EphemeralKey *ecdh.PrivateKey // The ephemeral key
|
||||
EphemeralPublicKeyShared bool // Whether this device's ephemeral public key has been shared
|
||||
OtherPublicKey *ecdh.PublicKey // The other device's ephemeral public key
|
||||
}
|
||||
|
||||
// RequiredCallbacks is an interface representing the callbacks required for
|
||||
// the [VerificationHelper].
|
||||
type RequiredCallbacks interface {
|
||||
// VerificationRequested is called when a verification request is received
|
||||
// from another device.
|
||||
VerificationRequested(ctx context.Context, txnID id.VerificationTransactionID, from id.UserID)
|
||||
|
||||
// VerificationError is called when an error occurs during the verification
|
||||
// process.
|
||||
VerificationError(ctx context.Context, txnID id.VerificationTransactionID, err error)
|
||||
|
||||
// VerificationCancelled is called when the verification is cancelled.
|
||||
VerificationCancelled(ctx context.Context, txnID id.VerificationTransactionID, code event.VerificationCancelCode, reason string)
|
||||
|
||||
// VerificationDone is called when the verification is done.
|
||||
VerificationDone(ctx context.Context, txnID id.VerificationTransactionID)
|
||||
}
|
||||
|
||||
type showSASCallbacks interface {
|
||||
// ShowSAS is called when the SAS verification has generated a short
|
||||
// authentication string to show. It is guaranteed that either the emojis
|
||||
// list, or the decimals list, or both will be present.
|
||||
ShowSAS(ctx context.Context, txnID id.VerificationTransactionID, emojis []rune, decimals []int)
|
||||
}
|
||||
|
||||
type showQRCodeCallbacks interface {
|
||||
// ShowQRCode is called when the verification has been accepted and a QR
|
||||
// code should be shown to the user.
|
||||
ShowQRCode(ctx context.Context, txnID id.VerificationTransactionID, qrCode *QRCode)
|
||||
|
||||
// QRCodeScanned is called when the other user has scanned the QR code and
|
||||
// sent the m.key.verification.start event.
|
||||
QRCodeScanned(ctx context.Context, txnID id.VerificationTransactionID)
|
||||
}
|
||||
|
||||
type VerificationHelper struct {
|
||||
client *mautrix.Client
|
||||
mach *crypto.OlmMachine
|
||||
|
||||
activeTransactions map[id.VerificationTransactionID]*verificationTransaction
|
||||
activeTransactionsLock sync.Mutex
|
||||
|
||||
supportedMethods []event.VerificationMethod
|
||||
verificationRequested func(ctx context.Context, txnID id.VerificationTransactionID, from id.UserID)
|
||||
verificationError func(ctx context.Context, txnID id.VerificationTransactionID, err error)
|
||||
verificationCancelled func(ctx context.Context, txnID id.VerificationTransactionID, code event.VerificationCancelCode, reason string)
|
||||
verificationDone func(ctx context.Context, txnID id.VerificationTransactionID)
|
||||
|
||||
showSAS func(ctx context.Context, txnID id.VerificationTransactionID, emojis []rune, decimals []int)
|
||||
|
||||
showQRCode func(ctx context.Context, txnID id.VerificationTransactionID, qrCode *QRCode)
|
||||
qrCodeScaned func(ctx context.Context, txnID id.VerificationTransactionID)
|
||||
}
|
||||
|
||||
var _ mautrix.VerificationHelper = (*VerificationHelper)(nil)
|
||||
|
||||
func NewVerificationHelper(client *mautrix.Client, mach *crypto.OlmMachine, callbacks any, supportsScan bool) *VerificationHelper {
|
||||
if client.Crypto == nil {
|
||||
panic("client.Crypto is nil")
|
||||
}
|
||||
|
||||
helper := VerificationHelper{
|
||||
client: client,
|
||||
mach: mach,
|
||||
activeTransactions: map[id.VerificationTransactionID]*verificationTransaction{},
|
||||
}
|
||||
|
||||
if c, ok := callbacks.(RequiredCallbacks); !ok {
|
||||
panic("callbacks must implement VerificationRequested")
|
||||
} else {
|
||||
helper.verificationRequested = c.VerificationRequested
|
||||
helper.verificationError = func(ctx context.Context, txnID id.VerificationTransactionID, err error) {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Verification error")
|
||||
c.VerificationError(ctx, txnID, err)
|
||||
}
|
||||
helper.verificationCancelled = c.VerificationCancelled
|
||||
helper.verificationDone = c.VerificationDone
|
||||
}
|
||||
|
||||
if c, ok := callbacks.(showEmojiCallbacks); ok {
|
||||
helper.supportedMethods = append(helper.supportedMethods, event.VerificationMethodSAS)
|
||||
helper.showEmojis = c.ShowEmojis
|
||||
}
|
||||
if c, ok := callbacks.(showDecimalCallbacks); ok {
|
||||
helper.supportedMethods = append(helper.supportedMethods, event.VerificationMethodSAS)
|
||||
helper.showDecimal = c.ShowDecimal
|
||||
}
|
||||
if c, ok := callbacks.(showQRCodeCallbacks); ok {
|
||||
helper.supportedMethods = append(helper.supportedMethods,
|
||||
event.VerificationMethodQRCodeShow, event.VerificationMethodReciprocate)
|
||||
helper.showQRCode = c.ShowQRCode
|
||||
helper.qrCodeScaned = c.QRCodeScanned
|
||||
}
|
||||
if supportsScan {
|
||||
helper.supportedMethods = append(helper.supportedMethods,
|
||||
event.VerificationMethodQRCodeScan, event.VerificationMethodReciprocate)
|
||||
}
|
||||
|
||||
slices.Sort(helper.supportedMethods)
|
||||
helper.supportedMethods = slices.Compact(helper.supportedMethods)
|
||||
return &helper
|
||||
}
|
||||
|
||||
func (vh *VerificationHelper) getLog(ctx context.Context) *zerolog.Logger {
|
||||
logger := vh.client.Log.With().
|
||||
Any("supported_methods", vh.supportedMethods).
|
||||
Str("component", "verification").
|
||||
Logger()
|
||||
return &logger
|
||||
}
|
||||
|
||||
// Init initializes the verification helper by adding the necessary event
|
||||
// handlers to the syncer.
|
||||
func (vh *VerificationHelper) Init(ctx context.Context) error {
|
||||
if vh == nil {
|
||||
return fmt.Errorf("verification helper is nil")
|
||||
}
|
||||
syncer, ok := vh.client.Syncer.(mautrix.ExtensibleSyncer)
|
||||
if !ok {
|
||||
return fmt.Errorf("the client syncer must implement ExtensibleSyncer")
|
||||
}
|
||||
|
||||
// Event handlers for verification requests. These are special since we do
|
||||
// not need to check that the transaction ID is known.
|
||||
syncer.OnEventType(event.ToDeviceVerificationRequest, vh.onVerificationRequest)
|
||||
syncer.OnEventType(event.EventMessage, func(ctx context.Context, evt *event.Event) {
|
||||
if evt.Content.AsMessage().MsgType == event.MsgVerificationRequest {
|
||||
vh.onVerificationRequest(ctx, evt)
|
||||
}
|
||||
})
|
||||
|
||||
// Wrapper for the event handlers to check that the transaction ID is known
|
||||
// and ignore the event if it isn't.
|
||||
wrapHandler := func(callback func(context.Context, *verificationTransaction, *event.Event)) func(context.Context, *event.Event) {
|
||||
return func(ctx context.Context, evt *event.Event) {
|
||||
log := vh.getLog(ctx).With().
|
||||
Str("verification_action", "check transaction ID").
|
||||
Stringer("sender", evt.Sender).
|
||||
Stringer("room_id", evt.RoomID).
|
||||
Stringer("event_id", evt.ID).
|
||||
Logger()
|
||||
|
||||
var transactionID id.VerificationTransactionID
|
||||
if evt.ID != "" {
|
||||
transactionID = id.VerificationTransactionID(evt.ID)
|
||||
} else {
|
||||
txnID, ok := evt.Content.Raw["transaction_id"].(string)
|
||||
if !ok {
|
||||
log.Warn().Msg("Ignoring verification event without a transaction ID")
|
||||
return
|
||||
}
|
||||
transactionID = id.VerificationTransactionID(txnID)
|
||||
}
|
||||
vh.activeTransactionsLock.Lock()
|
||||
txn, ok := vh.activeTransactions[transactionID]
|
||||
vh.activeTransactionsLock.Unlock()
|
||||
if !ok {
|
||||
log.Warn().
|
||||
Stringer("transaction_id", transactionID).
|
||||
Msg("Ignoring verification event for an unknown transaction")
|
||||
|
||||
txn = &verificationTransaction{
|
||||
RoomID: evt.RoomID,
|
||||
TheirUser: evt.Sender,
|
||||
}
|
||||
txn.TransactionID = evt.Content.Parsed.(event.VerificationTransactionable).GetTransactionID()
|
||||
if txn.TransactionID == "" {
|
||||
txn.TransactionID = id.VerificationTransactionID(evt.ID)
|
||||
}
|
||||
if fromDevice, ok := evt.Content.Raw["from_device"]; ok {
|
||||
txn.TheirDevice = id.DeviceID(fromDevice.(string))
|
||||
}
|
||||
cancelEvt := event.VerificationCancelEventContent{
|
||||
Code: event.VerificationCancelCodeUnknownTransaction,
|
||||
Reason: "The transaction ID was not recognized.",
|
||||
}
|
||||
err := vh.sendVerificationEvent(ctx, txn, event.InRoomVerificationCancel, &cancelEvt)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to send cancellation event")
|
||||
}
|
||||
vh.verificationCancelled(ctx, txn.TransactionID, cancelEvt.Code, cancelEvt.Reason)
|
||||
return
|
||||
}
|
||||
|
||||
logCtx := vh.getLog(ctx).With().
|
||||
Stringer("transaction_id", transactionID).
|
||||
Stringer("transaction_step", txn.VerificationStep).
|
||||
Stringer("sender", evt.Sender)
|
||||
if evt.RoomID != "" {
|
||||
logCtx = logCtx.
|
||||
Stringer("room_id", evt.RoomID).
|
||||
Stringer("event_id", evt.ID)
|
||||
}
|
||||
callback(logCtx.Logger().WithContext(ctx), txn, evt)
|
||||
}
|
||||
}
|
||||
|
||||
// Event handlers for the to-device verification events.
|
||||
syncer.OnEventType(event.ToDeviceVerificationReady, wrapHandler(vh.onVerificationReady))
|
||||
syncer.OnEventType(event.ToDeviceVerificationStart, wrapHandler(vh.onVerificationStart))
|
||||
syncer.OnEventType(event.ToDeviceVerificationDone, wrapHandler(vh.onVerificationDone))
|
||||
syncer.OnEventType(event.ToDeviceVerificationCancel, wrapHandler(vh.onVerificationCancel))
|
||||
syncer.OnEventType(event.ToDeviceVerificationAccept, wrapHandler(vh.onVerificationAccept)) // SAS
|
||||
syncer.OnEventType(event.ToDeviceVerificationKey, wrapHandler(vh.onVerificationKey)) // SAS
|
||||
syncer.OnEventType(event.ToDeviceVerificationMAC, wrapHandler(vh.onVerificationMAC)) // SAS
|
||||
|
||||
// Event handlers for the in-room verification events.
|
||||
syncer.OnEventType(event.InRoomVerificationReady, wrapHandler(vh.onVerificationReady))
|
||||
syncer.OnEventType(event.InRoomVerificationStart, wrapHandler(vh.onVerificationStart))
|
||||
syncer.OnEventType(event.InRoomVerificationDone, wrapHandler(vh.onVerificationDone))
|
||||
syncer.OnEventType(event.InRoomVerificationCancel, wrapHandler(vh.onVerificationCancel))
|
||||
syncer.OnEventType(event.InRoomVerificationAccept, wrapHandler(vh.onVerificationAccept)) // SAS
|
||||
syncer.OnEventType(event.InRoomVerificationKey, wrapHandler(vh.onVerificationKey)) // SAS
|
||||
syncer.OnEventType(event.InRoomVerificationMAC, wrapHandler(vh.onVerificationMAC)) // SAS
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartVerification starts an interactive verification flow with the given
|
||||
// user via a to-device event.
|
||||
func (vh *VerificationHelper) StartVerification(ctx context.Context, to id.UserID) (id.VerificationTransactionID, error) {
|
||||
txnID := id.NewVerificationTransactionID()
|
||||
|
||||
devices, err := vh.mach.CryptoStore.GetDevices(ctx, to)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get devices for user: %w", err)
|
||||
}
|
||||
|
||||
vh.getLog(ctx).Info().
|
||||
Str("verification_action", "start verification").
|
||||
Stringer("transaction_id", txnID).
|
||||
Stringer("to", to).
|
||||
Any("device_ids", maps.Keys(devices)).
|
||||
Msg("Sending verification request")
|
||||
|
||||
content := &event.Content{
|
||||
Parsed: &event.VerificationRequestEventContent{
|
||||
ToDeviceVerificationEvent: event.ToDeviceVerificationEvent{TransactionID: txnID},
|
||||
FromDevice: vh.client.DeviceID,
|
||||
Methods: vh.supportedMethods,
|
||||
Timestamp: jsontime.UnixMilliNow(),
|
||||
},
|
||||
}
|
||||
|
||||
req := mautrix.ReqSendToDevice{Messages: map[id.UserID]map[id.DeviceID]*event.Content{to: {}}}
|
||||
for deviceID := range devices {
|
||||
if deviceID == vh.client.DeviceID {
|
||||
// Don't ever send the event to the current device. We are likely
|
||||
// trying to send a verification request to our other devices.
|
||||
continue
|
||||
}
|
||||
|
||||
req.Messages[to][deviceID] = content
|
||||
}
|
||||
_, err = vh.client.SendToDevice(ctx, event.ToDeviceVerificationRequest, &req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to send verification request: %w", err)
|
||||
}
|
||||
|
||||
vh.activeTransactionsLock.Lock()
|
||||
defer vh.activeTransactionsLock.Unlock()
|
||||
vh.activeTransactions[txnID] = &verificationTransaction{
|
||||
VerificationStep: verificationStepRequested,
|
||||
TransactionID: txnID,
|
||||
TheirUser: to,
|
||||
SentToDeviceIDs: maps.Keys(devices),
|
||||
}
|
||||
return txnID, nil
|
||||
}
|
||||
|
||||
// StartVerification starts an interactive verification flow with the given
|
||||
// user in the given room.
|
||||
func (vh *VerificationHelper) StartInRoomVerification(ctx context.Context, roomID id.RoomID, to id.UserID) (id.VerificationTransactionID, error) {
|
||||
log := vh.getLog(ctx).With().
|
||||
Str("verification_action", "start in-room verification").
|
||||
Stringer("room_id", roomID).
|
||||
Stringer("to", to).
|
||||
Logger()
|
||||
|
||||
log.Info().Msg("Sending verification request")
|
||||
content := event.MessageEventContent{
|
||||
MsgType: event.MsgVerificationRequest,
|
||||
Body: "Alice is requesting to verify your device, but your client does not support verification, so you may need to use a different verification method.",
|
||||
FromDevice: vh.client.DeviceID,
|
||||
Methods: vh.supportedMethods,
|
||||
To: to,
|
||||
}
|
||||
encryptedContent, err := vh.client.Crypto.Encrypt(ctx, roomID, event.EventMessage, &content)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encrypt verification request: %w", err)
|
||||
}
|
||||
resp, err := vh.client.SendMessageEvent(ctx, roomID, event.EventMessage, encryptedContent)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to send verification request: %w", err)
|
||||
}
|
||||
|
||||
txnID := id.VerificationTransactionID(resp.EventID)
|
||||
log.Info().Stringer("transaction_id", txnID).Msg("Got a transaction ID for the verification request")
|
||||
|
||||
vh.activeTransactionsLock.Lock()
|
||||
defer vh.activeTransactionsLock.Unlock()
|
||||
vh.activeTransactions[txnID] = &verificationTransaction{
|
||||
RoomID: roomID,
|
||||
VerificationStep: verificationStepRequested,
|
||||
TransactionID: txnID,
|
||||
TheirUser: to,
|
||||
}
|
||||
return txnID, nil
|
||||
}
|
||||
|
||||
// AcceptVerification accepts a verification request. The transaction ID should
|
||||
// be the transaction ID of a verification request that was received via the
|
||||
// [RequiredCallbacks.VerificationRequested] callback.
|
||||
func (vh *VerificationHelper) AcceptVerification(ctx context.Context, txnID id.VerificationTransactionID) error {
|
||||
log := vh.getLog(ctx).With().
|
||||
Str("verification_action", "accept verification").
|
||||
Stringer("transaction_id", txnID).
|
||||
Logger()
|
||||
|
||||
txn, ok := vh.activeTransactions[txnID]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown transaction ID")
|
||||
}
|
||||
|
||||
log.Info().Msg("Sending ready event")
|
||||
readyEvt := &event.VerificationReadyEventContent{
|
||||
FromDevice: vh.client.DeviceID,
|
||||
Methods: vh.supportedMethods,
|
||||
}
|
||||
err := vh.sendVerificationEvent(ctx, txn, event.InRoomVerificationReady, readyEvt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
txn.VerificationStep = verificationStepReady
|
||||
|
||||
return vh.generateAndShowQRCode(ctx, txn)
|
||||
}
|
||||
|
||||
// sendVerificationEvent sends a verification event to the other user's device
|
||||
// setting the m.relates_to or transaction ID as necessary.
|
||||
//
|
||||
// Notes:
|
||||
//
|
||||
// - "content" must implement [event.Relatable] and
|
||||
// [event.VerificationTransactionable].
|
||||
// - evtType can be either the to-device or in-room version of the event type
|
||||
// as it is always stringified.
|
||||
func (vh *VerificationHelper) sendVerificationEvent(ctx context.Context, txn *verificationTransaction, evtType event.Type, content any) error {
|
||||
if txn.RoomID != "" {
|
||||
content.(event.Relatable).SetRelatesTo(&event.RelatesTo{Type: event.RelReference, EventID: id.EventID(txn.TransactionID)})
|
||||
_, err := vh.client.SendMessageEvent(ctx, txn.RoomID, evtType, &event.Content{
|
||||
Parsed: content,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send start event: %w", err)
|
||||
}
|
||||
} else {
|
||||
content.(event.VerificationTransactionable).SetTransactionID(txn.TransactionID)
|
||||
req := mautrix.ReqSendToDevice{Messages: map[id.UserID]map[id.DeviceID]*event.Content{
|
||||
txn.TheirUser: {
|
||||
txn.TheirDevice: &event.Content{Parsed: content},
|
||||
},
|
||||
}}
|
||||
_, err := vh.client.SendToDevice(ctx, evtType, &req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send start event: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (vh *VerificationHelper) onVerificationRequest(ctx context.Context, evt *event.Event) {
|
||||
logCtx := vh.getLog(ctx).With().
|
||||
Str("verification_action", "verification request").
|
||||
Stringer("sender", evt.Sender)
|
||||
if evt.RoomID != "" {
|
||||
logCtx = logCtx.
|
||||
Stringer("room_id", evt.RoomID).
|
||||
Stringer("event_id", evt.ID)
|
||||
}
|
||||
log := logCtx.Logger()
|
||||
|
||||
var verificationRequest *event.VerificationRequestEventContent
|
||||
switch evt.Type {
|
||||
case event.EventMessage:
|
||||
to := evt.Content.AsMessage().To
|
||||
if to != vh.client.UserID {
|
||||
log.Info().Stringer("to", to).Msg("Ignoring verification request for another user")
|
||||
return
|
||||
}
|
||||
|
||||
verificationRequest = event.VerificationRequestEventContentFromMessage(evt)
|
||||
case event.ToDeviceVerificationRequest:
|
||||
verificationRequest = evt.Content.AsVerificationRequest()
|
||||
default:
|
||||
log.Warn().Str("type", evt.Type.Type).Msg("Ignoring verification request of unknown type")
|
||||
return
|
||||
}
|
||||
|
||||
if verificationRequest.FromDevice == vh.client.DeviceID {
|
||||
log.Warn().Msg("Ignoring verification request from our own device. Why did it even get sent to us?")
|
||||
return
|
||||
}
|
||||
|
||||
if verificationRequest.TransactionID == "" {
|
||||
log.Warn().Msg("Ignoring verification request without a transaction ID")
|
||||
return
|
||||
}
|
||||
|
||||
log = log.With().Any("requested_methods", verificationRequest.Methods).Logger()
|
||||
ctx = log.WithContext(ctx)
|
||||
log.Info().Msg("Received verification request")
|
||||
|
||||
vh.activeTransactionsLock.Lock()
|
||||
_, ok := vh.activeTransactions[verificationRequest.TransactionID]
|
||||
if ok {
|
||||
vh.activeTransactionsLock.Unlock()
|
||||
log.Info().Msg("Ignoring verification request for an already active transaction")
|
||||
return
|
||||
}
|
||||
vh.activeTransactions[verificationRequest.TransactionID] = &verificationTransaction{
|
||||
RoomID: evt.RoomID,
|
||||
VerificationStep: verificationStepRequested,
|
||||
TransactionID: verificationRequest.TransactionID,
|
||||
TheirDevice: verificationRequest.FromDevice,
|
||||
TheirUser: evt.Sender,
|
||||
SupportedMethods: verificationRequest.Methods,
|
||||
}
|
||||
vh.activeTransactionsLock.Unlock()
|
||||
|
||||
vh.verificationRequested(ctx, verificationRequest.TransactionID, evt.Sender)
|
||||
}
|
||||
|
||||
func (vh *VerificationHelper) onVerificationReady(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
|
||||
log := vh.getLog(ctx).With().
|
||||
Str("verification_action", "verification ready").
|
||||
Logger()
|
||||
|
||||
if txn.VerificationStep != verificationStepRequested {
|
||||
log.Warn().Msg("Ignoring verification ready event for a transaction that is not in the requested state")
|
||||
return
|
||||
}
|
||||
|
||||
vh.activeTransactionsLock.Lock()
|
||||
defer vh.activeTransactionsLock.Unlock()
|
||||
|
||||
readyEvt := evt.Content.AsVerificationReady()
|
||||
|
||||
// Update the transaction state.
|
||||
txn.VerificationStep = verificationStepReady
|
||||
txn.TheirDevice = readyEvt.FromDevice
|
||||
txn.SupportedMethods = readyEvt.Methods
|
||||
|
||||
// If we sent this verification request, send cancellations to all of the
|
||||
// other devices.
|
||||
if len(txn.SentToDeviceIDs) > 0 {
|
||||
content := &event.Content{
|
||||
Parsed: &event.VerificationCancelEventContent{
|
||||
ToDeviceVerificationEvent: event.ToDeviceVerificationEvent{TransactionID: txn.TransactionID},
|
||||
Code: event.VerificationCancelCodeAccepted,
|
||||
Reason: "The verification was accepted on another device.",
|
||||
},
|
||||
}
|
||||
devices, err := vh.mach.CryptoStore.GetDevices(ctx, txn.TheirUser)
|
||||
if err != nil {
|
||||
vh.verificationError(ctx, txn.TransactionID, fmt.Errorf("failed to get devices for %s: %w", txn.TheirUser, err))
|
||||
return
|
||||
}
|
||||
req := mautrix.ReqSendToDevice{Messages: map[id.UserID]map[id.DeviceID]*event.Content{txn.TheirUser: {}}}
|
||||
for deviceID := range devices {
|
||||
if deviceID == txn.TheirDevice {
|
||||
// Don't ever send a cancellation to the device that accepted
|
||||
// the request.
|
||||
continue
|
||||
}
|
||||
|
||||
req.Messages[txn.TheirUser][deviceID] = content
|
||||
}
|
||||
_, err = vh.client.SendToDevice(ctx, event.ToDeviceVerificationRequest, &req)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to send cancellation requests")
|
||||
}
|
||||
}
|
||||
err := vh.generateAndShowQRCode(ctx, txn)
|
||||
if err != nil {
|
||||
vh.verificationError(ctx, txn.TransactionID, fmt.Errorf("failed to generate and show QR code: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
func (vh *VerificationHelper) onVerificationStart(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
|
||||
startEvt := evt.Content.AsVerificationStart()
|
||||
log := vh.getLog(ctx).With().
|
||||
Str("verification_action", "verification start").
|
||||
Str("method", string(startEvt.Method)).
|
||||
Logger()
|
||||
|
||||
if txn.VerificationStep != verificationStepReady {
|
||||
log.Warn().Msg("Ignoring verification start event for a transaction that is not in the ready state")
|
||||
return
|
||||
}
|
||||
vh.activeTransactionsLock.Lock()
|
||||
defer vh.activeTransactionsLock.Unlock()
|
||||
txn.VerificationStep = verificationStepStarted
|
||||
|
||||
switch startEvt.Method {
|
||||
case event.VerificationMethodSAS:
|
||||
// TODO
|
||||
log.Info().Msg("Received SAS verification start event")
|
||||
err := vh.onVerificationStartSAS(ctx, txn, evt)
|
||||
if err != nil {
|
||||
// TODO should we cancel on all errors?
|
||||
vh.verificationError(ctx, txn.TransactionID, fmt.Errorf("failed to handle SAS verification start: %w", err))
|
||||
}
|
||||
case event.VerificationMethodReciprocate:
|
||||
log.Info().Msg("Received reciprocate start event")
|
||||
|
||||
if !bytes.Equal(txn.QRCodeSharedSecret, startEvt.Secret) {
|
||||
vh.verificationError(ctx, txn.TransactionID, errors.New("reciprocated shared secret does not match"))
|
||||
return
|
||||
}
|
||||
|
||||
vh.qrCodeScaned(ctx, txn.TransactionID)
|
||||
default:
|
||||
// Note that we should never get m.qr_code.show.v1 or m.qr_code.scan.v1
|
||||
// here, since the start command for scanning and showing QR codes
|
||||
// should be of type m.reciprocate.v1.
|
||||
log.Error().Str("method", string(startEvt.Method)).Msg("Unsupported verification method in start event")
|
||||
}
|
||||
}
|
||||
|
||||
func (vh *VerificationHelper) onVerificationDone(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
|
||||
vh.getLog(ctx).Info().
|
||||
Str("verification_action", "done").
|
||||
Stringer("transaction_id", txn.TransactionID).
|
||||
Msg("Verification done")
|
||||
vh.activeTransactionsLock.Lock()
|
||||
defer vh.activeTransactionsLock.Unlock()
|
||||
delete(vh.activeTransactions, txn.TransactionID)
|
||||
}
|
||||
|
||||
func (vh *VerificationHelper) onVerificationCancel(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
|
||||
cancelEvt := evt.Content.AsVerificationCancel()
|
||||
vh.getLog(ctx).Info().
|
||||
Str("verification_action", "cancel").
|
||||
Stringer("transaction_id", txn.TransactionID).
|
||||
Str("cancel_code", string(cancelEvt.Code)).
|
||||
Str("reason", cancelEvt.Reason).
|
||||
Msg("Verification was cancelled")
|
||||
vh.activeTransactionsLock.Lock()
|
||||
defer vh.activeTransactionsLock.Unlock()
|
||||
delete(vh.activeTransactions, txn.TransactionID)
|
||||
vh.verificationCancelled(ctx, txn.TransactionID, cancelEvt.Code, cancelEvt.Reason)
|
||||
}
|
||||
|
||||
// SAS verification events
|
||||
func (vh *VerificationHelper) onVerificationAccept(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
|
||||
// TODO
|
||||
vh.getLog(ctx).Error().Any("evt", evt).Msg("ACCEPT UNIMPLEMENTED")
|
||||
}
|
||||
|
||||
func (vh *VerificationHelper) onVerificationKey(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
|
||||
// TODO
|
||||
vh.getLog(ctx).Error().Any("evt", evt).Msg("KEY UNIMPLEMENTED")
|
||||
}
|
||||
|
||||
func (vh *VerificationHelper) onVerificationMAC(ctx context.Context, txn *verificationTransaction, evt *event.Event) {
|
||||
// TODO
|
||||
vh.getLog(ctx).Error().Any("evt", evt).Msg("MAC UNIMPLEMENTED")
|
||||
}
|
||||
|
|
@ -513,3 +513,38 @@ func (content *Content) AsModPolicy() *ModPolicyContent {
|
|||
}
|
||||
return casted
|
||||
}
|
||||
func (content *Content) AsVerificationRequest() *VerificationRequestEventContent {
|
||||
casted, ok := content.Parsed.(*VerificationRequestEventContent)
|
||||
if !ok {
|
||||
return &VerificationRequestEventContent{}
|
||||
}
|
||||
return casted
|
||||
}
|
||||
func (content *Content) AsVerificationReady() *VerificationReadyEventContent {
|
||||
casted, ok := content.Parsed.(*VerificationReadyEventContent)
|
||||
if !ok {
|
||||
return &VerificationReadyEventContent{}
|
||||
}
|
||||
return casted
|
||||
}
|
||||
func (content *Content) AsVerificationStart() *VerificationStartEventContent {
|
||||
casted, ok := content.Parsed.(*VerificationStartEventContent)
|
||||
if !ok {
|
||||
return &VerificationStartEventContent{}
|
||||
}
|
||||
return casted
|
||||
}
|
||||
func (content *Content) AsVerificationDone() *VerificationDoneEventContent {
|
||||
casted, ok := content.Parsed.(*VerificationDoneEventContent)
|
||||
if !ok {
|
||||
return &VerificationDoneEventContent{}
|
||||
}
|
||||
return casted
|
||||
}
|
||||
func (content *Content) AsVerificationCancel() *VerificationCancelEventContent {
|
||||
casted, ok := content.Parsed.(*VerificationCancelEventContent)
|
||||
if !ok {
|
||||
return &VerificationCancelEventContent{}
|
||||
}
|
||||
return casted
|
||||
}
|
||||
|
|
|
|||
13
id/crypto.go
13
id/crypto.go
|
|
@ -7,6 +7,7 @@
|
|||
package id
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
|
|
@ -74,6 +75,12 @@ func (ed25519 Ed25519) String() string {
|
|||
return string(ed25519)
|
||||
}
|
||||
|
||||
func (ed25519 Ed25519) Bytes() []byte {
|
||||
val, _ := base64.RawStdEncoding.DecodeString(string(ed25519))
|
||||
// TODO handle errors
|
||||
return val
|
||||
}
|
||||
|
||||
func (ed25519 Ed25519) Fingerprint() string {
|
||||
spacedSigningKey := make([]byte, len(ed25519)+(len(ed25519)-1)/4)
|
||||
var ptr = 0
|
||||
|
|
@ -97,6 +104,12 @@ func (curve25519 Curve25519) String() string {
|
|||
return string(curve25519)
|
||||
}
|
||||
|
||||
func (curve25519 Curve25519) Bytes() []byte {
|
||||
val, _ := base64.RawStdEncoding.DecodeString(string(curve25519))
|
||||
// TODO handle errors
|
||||
return val
|
||||
}
|
||||
|
||||
// A DeviceID is an arbitrary string that references a specific device.
|
||||
type DeviceID string
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue