github.com/hyperledger/aries-framework-go@v0.3.2/pkg/client/legacyconnection/client.go (about)

     1  /*
     2  Copyright Avast Software. All Rights Reserved.
     3  
     4  SPDX-License-Identifier: Apache-2.0
     5  */
     6  
     7  package legacyconnection
     8  
     9  import (
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  
    14  	"github.com/btcsuite/btcutil/base58"
    15  	"github.com/google/uuid"
    16  
    17  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/common/service"
    18  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/legacyconnection"
    19  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/protocol/mediator"
    20  	"github.com/hyperledger/aries-framework-go/pkg/didcomm/transport"
    21  	"github.com/hyperledger/aries-framework-go/pkg/doc/did"
    22  	"github.com/hyperledger/aries-framework-go/pkg/kms"
    23  	"github.com/hyperledger/aries-framework-go/pkg/store/connection"
    24  	"github.com/hyperledger/aries-framework-go/spi/storage"
    25  )
    26  
    27  // InvitationMsgType defines the connection invite message type.
    28  const InvitationMsgType = legacyconnection.InvitationMsgType
    29  
    30  // ErrConnectionNotFound is returned when connection not found.
    31  var ErrConnectionNotFound = errors.New("connection not found")
    32  
    33  type options struct {
    34  	routerConnections  []string
    35  	routerConnectionID string
    36  }
    37  
    38  func applyOptions(args ...Opt) *options {
    39  	opts := &options{}
    40  
    41  	for i := range args {
    42  		args[i](opts)
    43  	}
    44  
    45  	return opts
    46  }
    47  
    48  // Opt represents option function.
    49  type Opt func(*options)
    50  
    51  // InvOpt represents option for the CreateInvitation function.
    52  type InvOpt Opt
    53  
    54  // WithRouterConnectionID allows you to specify the router connection ID.
    55  func WithRouterConnectionID(conn string) InvOpt {
    56  	return func(opts *options) {
    57  		opts.routerConnectionID = conn
    58  	}
    59  }
    60  
    61  // WithRouterConnections allows you to specify the router connections.
    62  func WithRouterConnections(conns ...string) Opt {
    63  	return func(opts *options) {
    64  		for _, conn := range conns {
    65  			// filters out empty connections
    66  			if conn != "" {
    67  				opts.routerConnections = append(opts.routerConnections, conn)
    68  			}
    69  		}
    70  	}
    71  }
    72  
    73  // provider contains dependencies for the legacy-connection protocol and is typically created by using aries.Context().
    74  type provider interface {
    75  	Service(id string) (interface{}, error)
    76  	KMS() kms.KeyManager
    77  	ServiceEndpoint() string
    78  	StorageProvider() storage.Provider
    79  	ProtocolStateStorageProvider() storage.Provider
    80  	MediaTypeProfiles() []string
    81  }
    82  
    83  // Client enable access to legacyconnection api.
    84  type Client struct {
    85  	service.Event
    86  	legacyconnectionSvc protocolService
    87  	routeSvc            mediator.ProtocolService
    88  	kms                 kms.KeyManager
    89  	serviceEndpoint     string
    90  	connectionStore     *connection.Recorder
    91  	mediaTypeProfiles   []string
    92  }
    93  
    94  // protocolService defines legacy-connection service.
    95  type protocolService interface {
    96  	// DIDComm service
    97  	service.DIDComm
    98  
    99  	// Accepts/Approves connection request
   100  	AcceptConnectionRequest(connectionID, publicDID, label string, routerConnections []string) error
   101  
   102  	// Accepts/Approves connection invitation
   103  	AcceptInvitation(connectionID, publicDID, label string, routerConnections []string) error
   104  
   105  	// CreateImplicitInvitation creates implicit invitation. Inviter DID is required, invitee DID is optional.
   106  	// If invitee DID is not provided new peer DID will be created for implicit invitation connection request.
   107  	CreateImplicitInvitation(inviterLabel, inviterDID, inviteeLabel,
   108  		inviteeDID string, routerConnections []string) (string, error)
   109  
   110  	// CreateConnection saves the connection record.
   111  	CreateConnection(*connection.Record, *did.Doc) error
   112  }
   113  
   114  // New return new instance of legacyconnection client.
   115  func New(ctx provider) (*Client, error) {
   116  	svc, err := ctx.Service(legacyconnection.LegacyConnection)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	legacyconnectionSvc, ok := svc.(protocolService)
   122  	if !ok {
   123  		return nil, errors.New("cast service to legacyconnection Service failed")
   124  	}
   125  
   126  	s, err := ctx.Service(mediator.Coordination)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	routeSvc, ok := s.(mediator.ProtocolService)
   132  	if !ok {
   133  		return nil, errors.New("cast service to Route Service failed")
   134  	}
   135  
   136  	connectionStore, err := connection.NewRecorder(ctx)
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  
   141  	mtp := ctx.MediaTypeProfiles()
   142  	if len(mtp) == 0 {
   143  		mtp = []string{transport.MediaTypeRFC0019EncryptedEnvelope}
   144  	}
   145  
   146  	return &Client{
   147  		Event:               legacyconnectionSvc,
   148  		legacyconnectionSvc: legacyconnectionSvc,
   149  		routeSvc:            routeSvc,
   150  		kms:                 ctx.KMS(),
   151  		serviceEndpoint:     ctx.ServiceEndpoint(),
   152  		connectionStore:     connectionStore,
   153  		mediaTypeProfiles:   mtp,
   154  	}, nil
   155  }
   156  
   157  // CreateInvitation creates an invitation. New key pair will be generated and did:key encoded public key will be
   158  // used as basis for invitation. This invitation will be stored so client can cross-reference this invitation during
   159  // connection protocol.
   160  func (c *Client) CreateInvitation(label string, args ...InvOpt) (*Invitation, error) {
   161  	opts := &options{}
   162  
   163  	for i := range args {
   164  		args[i](opts)
   165  	}
   166  
   167  	_, pubKey, err := c.kms.CreateAndExportPubKeyBytes(kms.ED25519Type)
   168  	if err != nil {
   169  		return nil, fmt.Errorf("createInvitation: failed to extract public SigningKey bytes from handle: %w", err)
   170  	}
   171  
   172  	recKey := base58.Encode(pubKey)
   173  
   174  	var (
   175  		serviceEndpoint = c.serviceEndpoint
   176  		routingKeys     []string
   177  	)
   178  
   179  	if opts.routerConnectionID != "" {
   180  		// get the route configs
   181  		serviceEndpoint, routingKeys, err = mediator.GetRouterConfig(c.routeSvc,
   182  			opts.routerConnectionID, c.serviceEndpoint)
   183  		if err != nil {
   184  			return nil, fmt.Errorf("createInvitation: getRouterConfig: %w", err)
   185  		}
   186  
   187  		if err = mediator.AddKeyToRouter(c.routeSvc, opts.routerConnectionID, recKey); err != nil {
   188  			return nil, fmt.Errorf("createInvitation: AddKeyToRouter: %w", err)
   189  		}
   190  	}
   191  
   192  	invitation := &legacyconnection.Invitation{
   193  		ID:              uuid.New().String(),
   194  		Label:           label,
   195  		RecipientKeys:   []string{recKey},
   196  		ServiceEndpoint: serviceEndpoint,
   197  		Type:            legacyconnection.InvitationMsgType,
   198  		RoutingKeys:     routingKeys,
   199  	}
   200  
   201  	err = c.connectionStore.SaveInvitation(invitation.ID, invitation)
   202  	if err != nil {
   203  		return nil, fmt.Errorf("createInvitation: failed to save invitation: %w", err)
   204  	}
   205  
   206  	return &Invitation{invitation}, nil
   207  }
   208  
   209  // CreateInvitationWithDID creates an invitation with specified public DID. This invitation will be stored
   210  // so client can cross reference this invitation during connection protocol.
   211  func (c *Client) CreateInvitationWithDID(label, publicDID string) (*Invitation, error) {
   212  	invitation := &legacyconnection.Invitation{
   213  		ID:    uuid.New().String(),
   214  		Label: label,
   215  		DID:   publicDID,
   216  		Type:  legacyconnection.InvitationMsgType,
   217  	}
   218  
   219  	err := c.connectionStore.SaveInvitation(invitation.ID, invitation)
   220  	if err != nil {
   221  		return nil, fmt.Errorf("createInvitationWithDID: failed to save invitation with DID: %w", err)
   222  	}
   223  
   224  	return &Invitation{invitation}, nil
   225  }
   226  
   227  // HandleInvitation handle incoming invitation and returns the connectionID that can be used to query the state
   228  // of legacy connection protocol. Upon successful completion of protocol, connection details will be used
   229  // for securing communication between agents.
   230  func (c *Client) HandleInvitation(invitation *Invitation) (string, error) {
   231  	payload, err := json.Marshal(invitation)
   232  	if err != nil {
   233  		return "", fmt.Errorf("handleInvitation: failed marshal invitation: %w", err)
   234  	}
   235  
   236  	msg, err := service.ParseDIDCommMsgMap(payload)
   237  	if err != nil {
   238  		return "", fmt.Errorf("handleInvitation: failed to create DIDCommMsg: %w", err)
   239  	}
   240  
   241  	connectionID, err := c.legacyconnectionSvc.HandleInbound(msg, service.EmptyDIDCommContext())
   242  	if err != nil {
   243  		return "", fmt.Errorf("handleInvitation: failed from legacyconnection service handle: %w", err)
   244  	}
   245  
   246  	return connectionID, nil
   247  }
   248  
   249  // AcceptInvitation accepts/approves connection invitation. This call is not used if auto execute is setup
   250  // for this client (see package example for more details about how to setup auto execute).
   251  func (c *Client) AcceptInvitation(connectionID, publicDID, label string, args ...Opt) error {
   252  	opts := applyOptions(args...)
   253  
   254  	if err := c.legacyconnectionSvc.AcceptInvitation(connectionID, publicDID, label, opts.routerConnections); err != nil {
   255  		return fmt.Errorf("legacyconnection client - accept connection invitation: %w", err)
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  // AcceptConnectionRequest accepts/approves connection request. This call is not used if auto execute is setup
   262  // for this client (see package example for more details about how to setup auto execute).
   263  func (c *Client) AcceptConnectionRequest(connectionID, publicDID, label string, args ...Opt) error {
   264  	err := c.legacyconnectionSvc.AcceptConnectionRequest(connectionID, publicDID, label,
   265  		applyOptions(args...).routerConnections)
   266  	if err != nil {
   267  		return fmt.Errorf("legacyconnection client - accept connection request: %w", err)
   268  	}
   269  
   270  	return nil
   271  }
   272  
   273  // CreateImplicitInvitation enables invitee to create and send connection request using inviter public DID.
   274  func (c *Client) CreateImplicitInvitation(inviterLabel, inviterDID string, args ...Opt) (string, error) {
   275  	return c.legacyconnectionSvc.CreateImplicitInvitation(inviterLabel, inviterDID,
   276  		"", "", applyOptions(args...).routerConnections)
   277  }
   278  
   279  // CreateImplicitInvitationWithDID enables invitee to create implicit invitation using inviter and invitee public DID.
   280  func (c *Client) CreateImplicitInvitationWithDID(inviter, invitee *DIDInfo) (string, error) {
   281  	if inviter == nil || invitee == nil {
   282  		return "", errors.New("missing inviter and/or invitee public DID(s)")
   283  	}
   284  
   285  	return c.legacyconnectionSvc.CreateImplicitInvitation(inviter.Label, inviter.DID, invitee.Label, invitee.DID, nil)
   286  }
   287  
   288  // QueryConnections queries connections matching given criteria(parameters).
   289  func (c *Client) QueryConnections(request *QueryConnectionsParams) ([]*Connection, error) { //nolint: gocyclo
   290  	// TODO https://github.com/hyperledger/aries-framework-go/issues/655 - query all connections from all criteria and
   291  	//  also results needs to be paged.
   292  	records, err := c.connectionStore.QueryConnectionRecords()
   293  	if err != nil {
   294  		return nil, fmt.Errorf("failed query connections: %w", err)
   295  	}
   296  
   297  	var result []*Connection
   298  
   299  	for _, record := range records {
   300  		if request.State != "" && request.State != record.State {
   301  			continue
   302  		}
   303  
   304  		if request.InvitationID != "" && request.InvitationID != record.InvitationID {
   305  			continue
   306  		}
   307  
   308  		if request.ParentThreadID != "" && request.ParentThreadID != record.ParentThreadID {
   309  			continue
   310  		}
   311  
   312  		if request.MyDID != "" && request.MyDID != record.MyDID {
   313  			continue
   314  		}
   315  
   316  		if request.TheirDID != "" && request.TheirDID != record.TheirDID {
   317  			continue
   318  		}
   319  
   320  		result = append(result, &Connection{Record: record})
   321  	}
   322  
   323  	return result, nil
   324  }
   325  
   326  // GetConnection fetches single connection record for given id.
   327  func (c *Client) GetConnection(connectionID string) (*Connection, error) {
   328  	conn, err := c.connectionStore.GetConnectionRecord(connectionID)
   329  	if err != nil {
   330  		if errors.Is(err, storage.ErrDataNotFound) {
   331  			return nil, ErrConnectionNotFound
   332  		}
   333  
   334  		return nil, fmt.Errorf("cannot fetch state from store: connectionid=%s err=%w", connectionID, err)
   335  	}
   336  
   337  	return &Connection{
   338  		conn,
   339  	}, nil
   340  }
   341  
   342  // GetConnectionAtState fetches connection record for connection id at particular state.
   343  func (c *Client) GetConnectionAtState(connectionID, stateID string) (*Connection, error) {
   344  	conn, err := c.connectionStore.GetConnectionRecordAtState(connectionID, stateID)
   345  	if err != nil {
   346  		if errors.Is(err, storage.ErrDataNotFound) {
   347  			return nil, ErrConnectionNotFound
   348  		}
   349  
   350  		return nil, fmt.Errorf("cannot fetch state from store: connectionid=%s err=%w", connectionID, err)
   351  	}
   352  
   353  	return &Connection{
   354  		conn,
   355  	}, nil
   356  }
   357  
   358  // CreateConnection creates a new connection between myDID and theirDID and returns the connectionID.
   359  func (c *Client) CreateConnection(myDID string, theirDID *did.Doc, options ...ConnectionOption) (string, error) {
   360  	conn := &Connection{&connection.Record{
   361  		ConnectionID: uuid.New().String(),
   362  		State:        connection.StateNameCompleted,
   363  		TheirDID:     theirDID.ID,
   364  		MyDID:        myDID,
   365  		Namespace:    connection.MyNSPrefix,
   366  	}}
   367  
   368  	for i := range options {
   369  		options[i](conn)
   370  	}
   371  
   372  	destination, err := service.CreateDestination(theirDID)
   373  	if err != nil {
   374  		return "", fmt.Errorf("createConnection: failed to create destination: %w", err)
   375  	}
   376  
   377  	conn.ServiceEndPoint = destination.ServiceEndpoint
   378  	conn.RecipientKeys = destination.RecipientKeys
   379  	conn.RoutingKeys = destination.RoutingKeys
   380  	conn.MediaTypeProfiles = destination.MediaTypeProfiles
   381  
   382  	err = c.legacyconnectionSvc.CreateConnection(conn.Record, theirDID)
   383  	if err != nil {
   384  		return "", fmt.Errorf("createConnection: err: %w", err)
   385  	}
   386  
   387  	return conn.ConnectionID, nil
   388  }
   389  
   390  // RemoveConnection removes connection record for given id.
   391  func (c *Client) RemoveConnection(connectionID string) error {
   392  	err := c.connectionStore.RemoveConnection(connectionID)
   393  	if err != nil {
   394  		return fmt.Errorf("cannot remove connection from the store: err=%w", err)
   395  	}
   396  
   397  	return nil
   398  }
   399  
   400  // ConnectionOption allows you to customize details of the connection record.
   401  type ConnectionOption func(*Connection)
   402  
   403  // WithTheirLabel sets TheirLabel on the connection record.
   404  func WithTheirLabel(l string) ConnectionOption {
   405  	return func(c *Connection) {
   406  		c.TheirLabel = l
   407  	}
   408  }
   409  
   410  // WithThreadID sets ThreadID on the connection record.
   411  func WithThreadID(thid string) ConnectionOption {
   412  	return func(c *Connection) {
   413  		c.ThreadID = thid
   414  	}
   415  }
   416  
   417  // WithParentThreadID sets ParentThreadID on the connection record.
   418  func WithParentThreadID(pthid string) ConnectionOption {
   419  	return func(c *Connection) {
   420  		c.ParentThreadID = pthid
   421  	}
   422  }
   423  
   424  // WithInvitationID sets InvitationID on the connection record.
   425  func WithInvitationID(id string) ConnectionOption {
   426  	return func(c *Connection) {
   427  		c.InvitationID = id
   428  	}
   429  }
   430  
   431  // WithInvitationDID sets InvitationDID on the connection record.
   432  func WithInvitationDID(didID string) ConnectionOption {
   433  	return func(c *Connection) {
   434  		c.InvitationDID = didID
   435  	}
   436  }
   437  
   438  // WithImplicit sets Implicit on the connection record.
   439  func WithImplicit(i bool) ConnectionOption {
   440  	return func(c *Connection) {
   441  		c.Implicit = i
   442  	}
   443  }