diff --git a/bindings/README.md b/bindings/README.md index fa8554e5ed47c313d89c50d100de7ca9e5eb684e..6aebd389abc87365d4a9c003c8e4fbdfea0f3658 100644 --- a/bindings/README.md +++ b/bindings/README.md @@ -1,185 +1,8 @@ -# Supporting Elixxir payments - -### Protocol buffer crash course for Elixxir - -#### Why use protocol buffers? - -Protocol buffer definitions (written in a .proto file) can generate code for serializing and deserializing byte sequences in a huge variety of languages. This means that, if you decide to standardize your message format with a protocol buffer declaration, you can decode and encode the messages in any language without having to interact with the Go client library at all. Just as important is the ability to define enumerations in protocol buffers. In particular, the message types that are currently used for the CUI and the command-line client are defined in the Types enum in `client/cmixproto/types.proto`. If you have any questions about the way that the data are serialized for any message type, you should read this file and its comments. - -#### Generating protocol buffer code - -To generate the code, use the `protoc` tool, requesting output in the target language of your choice. For Go: - -`protoc --go_out=. types.proto` - -For Java: - -`protoc --java_out=/path/to/android/project/source/code types.proto` - -You can download and install the protocol buffer compiler for your preferred language from [its downloads page](https://developers.google.com/protocol-buffers/docs/downloads). - -#### Message types used to implement payments UI - -The payments portion of the user interface should only need to register its listeners with the wallet. To get the wallet (currently there's only one), call `Bindings.getActiveWallet()`. You can listen to the wallet with `Bindings.getActiveWallet().Listen(...)`. When the client has received a message that the UI needs to respond to, it should listen to the wallet for messages of these types: `PAYMENT_INVOICE_UI`, `PAYMENT_RESPONSE`, and `PAYMENT_RECEIPT_UI`. See cmixproto/types.proto for some documentation about these file formats. - -### What must client implementers do for the minimal payment implementation? - -There are three parties that must participate to complete a payment: the payer, the payee, and the payment bots. Payer and payee are both normal, human users using some Elixxir client program, and the payment bots are automatic users that arbitrate exchanges of which tokens have value. -A payment happens like this: payee sends invoice with some payee-owned tokens that don't yet have value, payer receives invoice and decides to pay it, payer sends payment message to bots with payee-owned tokens and some tokens of his own that are worth the same and have value, bots locally see that the payer's tokens have value, bots store value in the payee's new tokens and destroy the payer's old tokens, bots reply to the payer with a message saying that the payment was successful, payer responds to the payee with a message asserting that they made the payment. - -Under the hood, the client library updates a lot of state to perform the necessary cryptography to update the wallet to its correct state. At the time, though, the mechanism for rolling back failed transactions is relatively untested and needs some ironing out. In the meantime, if a transaction fails and further transactions are messed up, you should restart the whole server infrastructure--server, gateway, and payment bot--to reset the tokens that are available on the payment bot, and wipe the stored sessions of the clients to register with a fresh set of tokens. - -In short, if you're implementing a client, your client must be able to do the following things to deliver the baseline payments experience: - -- send an invoice to another user on the payee's client -- receive and display an incoming invoice on the payer's client -- pay the received invoice on the payer's client -- receive and display the payment bots' response on the payer's client -- receive and display the payer's receipt on the payee's client - -How to do each of these things with the current payments API follows. Assume that `w` is a reference or pointer to the active wallet. Assume that `CmixProto` is an imported package with proto buffer code generated in Java. The code is written in Java-like pseudocode. Of course, you should structure your own code in the best way for your own application. - -#### 0. Actually having tokens to spend - -To actually have tokens to spend, you must mint the same tokens as are on the payment bot. Currently, the tokens are hard-coded. To do this, pass `true` to the last parameter of `Bindings.Register()`. Then, there will be tokens that are stored in the wallet that happen to be the same as the tokens that are stored on the payment bot (when the payment bot is run with `--mint`), and the client will be able to spend them. - -#### 1. Send an invoice to another user on the payee's client - -First, generate the invoice, then send it. - -```java -public static void sendInvoice() throws Throwable { - // Generate the invoice message: Request 500 tokens from the payer - Message invoiceMessage = w.Invoice(payerId.bytes(), 500, - "for creating a completely new flavor of ice cream"); - - // Send the invoice message to the payer - Bindings.send(invoiceMessage); -} -``` - -#### 2. Receive and display an incoming invoice on the payer's client - -During client startup, register a listener with the wallet. The wallet has a separate listener matching structure from the main switchboard, and you can use it to receive messages from the wallet rather than from the network. - -```java -public static void setup() throws Throwable { - Bindings.InitClient(...); - Bindings.Register(...); - - // The wallet and listener data structure (switchboard) are both ready after Register. - // On the other hand, the client begins receiving messages after Login. So, if - // you want to receive all messages that the client receives, it's best to register - // listeners between Register and Login. - registerListeners(); - - Bindings.Login(...); -} - -public static void registerListeners() { - // Listen for messages of type PAYMENT_INVOICE_UI originating from all users - w.Listen(zeroId.bytes(), CmixProto.Type.PAYMENT_INVOICE_UI, new PaymentInvoiceListener()); - // and so on... -} -``` - -The message you'll get contains an invoice ID. You can use it to query the invoice's transaction for display. You should also store the invoice ID as it's the parameter for the Pay method, the next phase. - -```java -public class PaymentInvoiceListener implements Bindings.Listener { - @Override - public void Hear(Bindings.Message msg, bool isHeardElsewhere) { - // Keep this ID around somewhere for the next step - byte[] invoiceID = msg.GetPayload(); - bindings.Transaction invoice = w.GetInboundRequests.Get(invoiceID); - - // Display the transaction somehow - invoiceDisplay.setTime(invoice.timestamp); - invoiceDisplay.setValue(invoice.value); - invoiceDisplay.setMemo(invoice.memo); - invoiceDisplay.show(); - } -} -``` - -#### 3. Pay the received invoice on the payer's client - -When the payer approves the invoice's payment, call the Pay() method with the invoice ID. This will generate a message that the payer can send to the payment bot. The message contains the payee's tokens, which the payment bot will vest, and the payer's proof of ownership of tokens of equal value, which the payment will destroy to facilitate the exchange. - -```java -public static void sendPayment() throws Throwable { - Message msg = Bindings.pay(invoiceID); - Bindings.send(msg); -} -``` - -#### 4. Receive and display the payment bots' response on the payer's client - -When you register listeners, listen to the wallet for the type `PAYMENT_RESPONSE`. - -```java -public static void registerListeners() { - // ... - w.Listen(zeroId.bytes(), CmixProto.Type.PAYMENT_RESPONSE, new PaymentResponseListener()); - // ... -} -``` - -The payment response is a serialized protocol buffer with a few fields. You should deserialize it using the protocol buffer code you generated. - -```java -public class PaymentResponseListener implements Binding.Listener { - @Override - public void Hear(Bindings.Message msg, bool isHeardElsewhere) { - // Parse the payment bot's response - PaymentResponse response = PaymentResponse.parseFrom(msg.GetPayload()); - - // Then, show it to the user somehow - responseDisplay.setSuccess(response.getSuccess()); - responseDisplay.setText(response.getResponse()); - responseDisplay.show(); - } -} -``` - -The client automatically converts the payment bot's response to a receipt and sends it on to the payee. - -#### 5. Receive and display the payer's receipt on the payee's client - -When you register listeners, listen to the wallet for the type `PAYMENT_RECEIPT_UI`. - -```java -public static void registerListeners() { - // ... - w.Listen(zeroId.bytes(), CmixProto.Type.PAYMENT_RECEIPT_UI, new PaymentReceiptListener()); - // ... -} -``` - -The payment receipt UI message is the ID of the original invoice that was paid. You can get the transaction itself from the CompletedOutboundPayments transaction list, then display the transaction information of the completed payment. - -```java -public class PaymentReceiptListener implements Bindings.Listener { - @Override - public void Hear(bindings.Message msg, bool isHeardElsewhere) { - // Get the relevant transaction - bindings.Transaction completedTransaction = w.getCompletedOutboundTransaction(msg.GetPayload()); - - // Show the receipt, including the time that the original invoice was sent. - receiptDisplay.setTime(completedTransaction.time); - receiptDisplay.setMemo(completedTransaction.memo); - receiptDisplay.setValue(completedTransaction.value); - receiptDisplay.show(); - } -} -``` - -### Cyclic group for registration JSON format: - -```json -{ - "gen": "5c7ff6b06f8f143fe8288433493e4769c4d988ace5be25a0e24809670716c613d7b0cee6932f8faa7c44d2cb24523da53fbe4f6ec3595892d1aa58c4328a06c46a15662e7eaa703a1decf8bbb2d05dbe2eb956c142a338661d10461c0d135472085057f3494309ffa73c611f78b32adbb5740c361c9f35be90997db2014e2ef5aa61782f52abeb8bd6432c4dd097bc5423b285dafb60dc364e8161f4a2a35aca3a10b1c4d203cc76a470a33afdcbdd92959859abd8b56e1725252d78eac66e71ba9ae3f1dd2487199874393cd4d832186800654760e1e34c09e4d155179f9ec0dc4473f996bdce6eed1cabed8b6f116f7ad9cf505df0f998e34ab27514b0ffe7", - "prime": "9db6fb5951b66bb6fe1e140f1d2ce5502374161fd6538df1648218642f0b5c48c8f7a41aadfa187324b87674fa1822b00f1ecf8136943d7c55757264e5a1a44ffe012e9936e00c1d3e9310b01c7d179805d3058b2a9f4bb6f9716bfe6117c6b5b3cc4d9be341104ad4a80ad6c94e005f4b993e14f091eb51743bf33050c38de235567e1b34c3d6a5c0ceaa1a0f368213c3d19843d0b4b09dcb9fc72d39c8de41f1bf14d4bb4563ca28371621cad3324b6a2d392145bebfac748805236f5ca2fe92b871cd8f9c36d3292b5509ca8caa77a2adfc7bfd77dda6f71125a7456fea153e433256a2261c6a06ed3693797e7995fad5aabbcfbe3eda2741e375404ae25b", - "primeQ": "f2c3119374ce76c9356990b465374a17f23f9ed35089bd969f61c6dde9998c1f" -} -``` +# Client Bindings + +"bindings" is the client bindings which can be used to generate Android +and iOS client libraries for apps using gomobile. Gomobile is +limited to int, string, []byte, interfaces, and only a couple other types, so +it is necessary to define several interfaces to support passing more complex +data across the boundary (see `interfaces.go`). The rest of the logic +is located in `api.go` diff --git a/bindings/api.go b/bindings/api.go new file mode 100644 index 0000000000000000000000000000000000000000..d3d673fa09d8f1619761ebfca1058ee4bc3c49ad --- /dev/null +++ b/bindings/api.go @@ -0,0 +1,43 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2019 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + "gitlab.com/elixxir/client/api" +) + +// NewClient connects and registers to the network using a json encoded +// network information string and then creates a new client at the specified +// storageDir using the specified password. This function will fail +// when: +// - network information cannot be read or the client cannot connect +// to the network and register within the defined timeout. +// - storageDir does not exist and cannot be created +// - It cannot create, read, or write files inside storageDir +// - Client files already exist inside storageDir. +// - cryptographic functionality is unavailable (e.g. random number +// generation) +// The password is passed as a byte array so that it can be cleared from +// memory and stored as securely as possible using the memguard library. +// NewClient will block until the client has completed registration with +// the network permissioning server. +func NewClient(network, storageDir string, password []byte) (Client, error) { + // TODO: This should wrap the bindings ClientImpl, when available. + return api.NewClient(network, storageDir, password) +} + +// LoadClient will load an existing client from the storageDir +// using the password. This will fail if the client doesn't exist or +// the password is incorrect. +// The password is passed as a byte array so that it can be cleared from +// memory and stored as securely as possible using the memguard library. +// LoadClient does not block on network connection, and instead loads and +// starts subprocesses to perform network operations. +func LoadClient(storageDir string, password []byte) (Client, error) { + // TODO: This should wrap the bindings ClientImpl, when available. + return api.LoadClient(storageDir, password) +} diff --git a/bindings/interfaces.go b/bindings/interfaces.go index a41a1c6df3c79f0a1b5b145ab0b422e3de929bf3..77b4d6134afcb075c0df64dfbbc0475e6f19f13e 100644 --- a/bindings/interfaces.go +++ b/bindings/interfaces.go @@ -11,14 +11,133 @@ import ( "gitlab.com/elixxir/primitives/switchboard" ) -// Message used for binding +// Client is defined inside the api package. At minimum, it implements all of +// functionality defined here. A Client handles all network connectivity, key +// generation, and storage for a given cryptographic identity on the cmix +// network. +type Client interface { + + // ----- Reception ----- + + // RegisterListener records and installs a listener for messages + // matching specific uid, msgType, and/or username + RegisterListener(uid []byte, msgType int, username string, + listener Listener) + + // ----- Transmission ----- + + // SendE2E sends an end-to-end payload to the provided recipient with + // the provided msgType. Returns the list of rounds in which parts of + // the message were sent or an error if it fails. + SendE2E(payload, recipient []byte, msgType int) (RoundList, error) + // SendUnsafe sends an unencrypted payload to the provided recipient + // with the provided msgType. Returns the list of rounds in which parts + // of the message were sent or an error if it fails. + // NOTE: Do not use this function unless you know what you are doing. + // This function always produces an error message in client logging. + SendUnsafe(payload, recipient []byte, msgType int) (RoundList, error) + // SendCMIX sends a "raw" CMIX message payload to the provided + // recipient. Note that both SendE2E and SendUnsafe call SendCMIX. + // Returns the round ID of the round the payload was sent or an error + // if it fails. + SendCMIX(payload, recipient []byte) (int, error) + + // ----- Notifications ----- + + // RegisterForNotifications allows a client to register for push + // notifications. + // Note that clients are not required to register for push notifications + // especially as these rely on third parties (i.e., Firebase *cough* + // *cough* google's palantir *cough*) that may represent a security + // risk to the user. + RegisterForNotifications(token []byte) error + // UnregisterForNotifications turns of notifications for this client + UnregisterForNotifications() error + + // ----- Registration ----- + + // Returns true if the cryptographic identity has been registered with + // the CMIX user discovery agent. + // Note that clients do not need to perform this step if they use + // out of band methods to exchange cryptographic identities + // (e.g., QR codes), but failing to be registered precludes usage + // of the user discovery mechanism (this may be preferred by user). + IsRegistered() bool + + // RegisterIdentity registers an arbitrary username with the user + // discovery protocol. Returns an error when it cannot connect or + // the username is already registered. + RegisterIdentity(username string) error + // RegisterEmail makes the users email searchable after confirmation. + // It returns a registration confirmation token to be used with + // ConfirmRegistration or an error on failure. + RegisterEmail(email string) ([]byte, error) + // RegisterPhone makes the users phone searchable after confirmation. + // It returns a registration confirmation token to be used with + // ConfirmRegistration or an error on failure. + RegisterPhone(phone string) ([]byte, error) + // ConfirmRegistration sends the user discovery agent a confirmation + // token (from Register Email/Phone) and code (string sent via Email + // or SMS to confirm ownership) to confirm ownership. + ConfirmRegistration(token, code []byte) error + + // ----- Contacts ----- + + // GetUser returns the current user Identity for this client. This + // can be serialized into a byte stream for out-of-band sharing. + GetUser() (Contact, error) + // MakeContact creates a contact from a byte stream (i.e., unmarshal's a + // Contact object), allowing out-of-band import of identities. + MakeContact(contactBytes []byte) (Contact, error) + // GetContact returns a Contact object for the given user id, or + // an error + GetContact(uid []byte) (Contact, error) + + // ----- User Discovery ----- + + // Search accepts a "separator" separated list of search elements with + // an associated list of searchTypes. It returns a ContactList which + // allows you to iterate over the found contact objects. + Search(data, separator string, searchTypes []byte) ContactList + // SearchWithHandler is a non-blocking search that also registers + // a callback interface for user disovery events. + SearchWithHandler(data, separator string, searchTypes []byte, + hdlr UserDiscoveryHandler) + + // ----- Key Exchange ----- + + // CreateAuthenticatedChannel creates a 1-way authenticated channel + // so this user can send messages to the desired recipient Contact. + // To receive confirmation from the remote user, clients must + // register a listener to do that. + CreateAuthenticatedChannel(recipient Contact, payload []byte) error + // RegierAuthEventsHandler registers a callback interface for channel + // authentication events. + RegisterAuthEventsHandler(hdlr AuthEventHandler) + + // ----- Network ----- + + // StartNetworkRunner kicks off the longrunning network client threads + // and returns an object for checking state and stopping those threads. + // Call this when returning from sleep and close when going back to + // sleep. + StartNetworkRunner() NetworkRunner + + // RegisterRoundEventsHandler registers a callback interface for round + // events. + RegisterRoundEventsHandler(hdlr RoundEventHandler) +} + +// Message is a message received from the cMix network in the clear +// or that has been decrypted using established E2E keys. type Message interface { - // Returns the message's sender ID + // Returns the message's sender ID, if available GetSender() []byte - // Returns the message payload - // Parse this with protobuf/whatever according to the type of the message + // Returns the message payload/contents + // Parse this with protobuf/whatever according to the message type GetPayload() []byte // Returns the message's recipient ID + // This is usually your userID but could be an ephemeral/group ID GetRecipient() []byte // Returns the message's type GetMessageType() int32 @@ -28,88 +147,77 @@ type Message interface { GetTimestampNano() int64 } -// Copy of the storage interface. -// It is identical to the interface used in Globals, -// and a results the types can be passed freely between the two -type Storage interface { - // Give a Location for storage. Does not need to be implemented if unused. - SetLocation(string, string) error - // Returns the Location for storage. - // Does not need to be implemented if unused. - GetLocation() string - // Stores the passed byte slice to location A - SaveA([]byte) error - // Returns the stored byte slice stored in location A - LoadA() []byte - // Stores the passed byte slice to location B - SaveB([]byte) error - // Returns the stored byte slice stored in location B - LoadB() []byte - // Returns whether the storage has even been written to. - // if something exists in A or B - IsEmpty() bool +// RoundEvent contains event information for a given round. +// TODO: This is a half-baked interface and will be filled out later. +type RoundEvent interface { + // GetID returns the round ID for this round. + GetID() int + // GetStatus returns the status of this round. + GetStatus() int } -// Translate a bindings storage to a client storage -type storageProxy struct { - boundStorage Storage +// ContactList contains a list of contacts +type ContactList interface { + // GetLen returns the number of contacts in the list + GetLen() int + // GetContact returns the contact at index i + GetContact(i int) Contact } -// Translate a bindings message to a parse message +// Contact contains the contacts information. Note that this object +// is a copy of the contact at the time it was generated, so api users +// cannot rely on this object being updated once it has been received. +type Contact interface { + // GetID returns the user ID for this user. + GetID() []byte + // GetPublicKey returns the publickey bytes for this user. + GetPublicKey() []byte + // GetSalt returns the salt used to initiate an authenticated channel + GetSalt() []byte + // IsAuthenticated returns true if an authenticated channel exists for + // this user so we can begin to send messages. + IsAuthenticated() bool + // IsConfirmed returns true if the user has confirmed the authenticated + // channel on their end. + IsConfirmed() bool + // Marshal creates a serialized representation of a contact for + // out-of-band contact exchange. + Marshal() ([]byte, error) +} + +// ----- Callback interfaces ----- + +// Listener provides a callback to hear a message // An object implementing this interface can be called back when the client // gets a message of the type that the registerer specified at registration // time. type Listener interface { - // This does not include the generic interfaces seen in the go implementation - // Those are used internally on the backend and cause errors if we try to port them - Hear(msg Message, isHeardElsewhere bool) -} - -// Translate a bindings listener to a switchboard listener -// Note to users of this package from other languages: Symbols that start with -// lowercase are unexported from the package and meant for internal use only. -type listenerProxy struct { - proxy Listener -} - -func (lp *listenerProxy) Hear(msg switchboard.Item, isHeardElsewhere bool, i ...interface{}) { - msgInterface := &parse.BindingsMessageProxy{Proxy: msg.(*parse.Message)} - lp.proxy.Hear(msgInterface, isHeardElsewhere) -} - -// Interface used to receive a callback on searching for a user -type SearchCallback interface { - Callback(userID, pubKey []byte, err error) -} - -type searchCallbackProxy struct { - proxy SearchCallback -} - -func (scp *searchCallbackProxy) Callback(userID, pubKey []byte, err error) { - scp.proxy.Callback(userID, pubKey, err) -} - -// Interface used to receive a callback on searching for a user's nickname -type NickLookupCallback interface { - Callback(nick string, err error) -} - -type nickCallbackProxy struct { - proxy NickLookupCallback + // Hear is called to receive a message in the UI + Hear(msg Message) } -// interface used to receive the result of a nickname request -func (ncp *nickCallbackProxy) Callback(nick string, err error) { - ncp.proxy.Callback(nick, err) +// AuthEventHandler handles authentication requests initiated by +// CreateAuthenticatedChannel +type AuthEventHandler interface { + // HandleConfirmation handles AuthEvents received after + // the client has called CreateAuthenticatedChannel for + // the provided contact. Payload is typically empty but + // may include a small introductory message. + HandleConfirmation(contact Contact, payload []byte) + // HandleRequest handles AuthEvents received before + // the client has called CreateAuthenticatedChannel for + // the provided contact. It should prompt the user to accept + // the channel creation "request" and, if approved, + // call CreateAuthenticatedChannel for this Contact. + HandleRequest(contact Contact, payload []byte) } -// interface used to receive a ui friendly description of the current status of -// registration -type ConnectionStatusCallback interface { - Callback(status int, TimeoutSeconds int) +// RoundEventHandler handles round events happening on the cMix network. +type RoundEventHandler interface { + HandleEvent(re RoundEvent) } -type OperationProgressCallback interface { - Callback(int) +// UserDiscoveryHandler handles search results against the user discovery agent. +type UserDiscoveryHandler interface { + HandleSearchResults(results ContactList) }