Skip to content

An encrypted, authenticated, transport agnostic rpc protocol.

License

Notifications You must be signed in to change notification settings

OpenAssetStandards/safe-rpc

Repository files navigation

safe-rpc

npm version npm downloads bundle Codecov License JSDocs

An encrypted, authenticated, transport agnostic rpc protocol.

Create RPC services that communicate over atypical transport layers like the postMessage API(see safe-rpc-iframe), usb flash drive, homing pigeon, snail mail, audio, QR codes and more.

🚀 Quick Start

Install:

# npm
npm i safe-rpc

# yarn
yarn add safe-rpc

Example Usage: First define your channel and peer classes:

import {
  EncryptedRPCSession,
  EncryptedSession,
  EncryptedSessionMessageHub,
  IEncryptedMessage,
  ISyncRawMessageChannel,
  deserializePeerSessionConfig,
  serializePeerSessionConfig,
} from "safe-rpc";
import {
  SimpleSyncTransport,
  ISimpleSyncTransportChannelHandler,
} from "safe-rpc/simpleTransport";

function parseConfigString(cfgString: string) {
  const splitParts = cfgString.split("~");
  if (splitParts.length !== 3) {
    throw new Error("invalid cfg string");
  }
  const [serializedPeerSessionConfig, id, peerId] = splitParts;

  return {
    serializedPeerSessionConfig,
    id,
    peerId,
  };
}

function encodeConfigString(
  serializedPeerSessionConfig: string,
  id: string,
  peerId: string
) {
  if(serializedPeerSessionConfig.indexOf("~") !== -1){
    throw new Error("serializedPeerSessionConfig must not contain the character '~'");
  }
  if(id.indexOf("~") !== -1){
    throw new Error("id must not contain the character '~'");
  }
  if(peerId.indexOf("~") !== -1){
    throw new Error("peerId must not contain the character '~'");
  }
  return [serializedPeerSessionConfig, id, peerId].join("~");
}

class SimpleSyncChannel
  implements ISyncRawMessageChannel, ISimpleSyncTransportChannelHandler
{
  id: string;
  peerId: string;
  transport: SimpleSyncTransport;
  disposed = false;
  callbacks: ((message: IEncryptedMessage) => void)[] = [];

  constructor(id: string, peerId: string, transport: SimpleSyncTransport) {
    this.id = id;
    this.peerId = peerId;
    this.transport = transport;
    transport.registerPeer(id, this);
  }
  sendMessageRaw(message: IEncryptedMessage) {
    this.transport.sendMessage(this.peerId, message);
  }
  addMessageListener(callback: (message: IEncryptedMessage) => void) {
    if (this.callbacks.indexOf(callback) !== -1) {
      return;
    }
    this.callbacks.push(callback);
  }

  notifyMessage(message: any): any {
    for (const cb of this.callbacks) {
      try {
        cb(message);
      } catch (err) {
        // comment the line below to ignore callback errors
        throw err;
      }
    }
  }
  removeMessageListener(callback: (message: IEncryptedMessage) => void) {
    const index = this.callbacks.indexOf(callback);
    if (index !== -1) {
      this.callbacks = this.callbacks
        .slice(0, index)
        .concat(this.callbacks.slice(index + 1));
    }
  }
  connect(config?: any) {
    return true;
  }
  canSendMessage(): boolean {
    return !this.disposed;
  }
  dispose() {
    if (this.disposed) {
      return;
    }
    this.transport.unregisterPeer(this.id);
    this.disposed = true;
    this.callbacks = [];
  }
}

class SimplePeer extends EncryptedSessionMessageHub {
  //@ts-ignore
  channel: SimpleSyncChannel;
  id: string;
  peerId: string;
  constructor(
    session: EncryptedSession,
    channel: SimpleSyncChannel,
    id: string,
    peerId: string,
    onMessageHandlerError: (error: Error) => any | null = (error: Error) =>
      console.error(error),
    onDecryptionError?: (error: Error) => any
  ) {
    super(session, channel, onMessageHandlerError, onDecryptionError);
    this.channel = channel;
    this.id = id;
    this.peerId = peerId;
  }
  async getConfigStringForPeer(): Promise<string> {
    const peerSessionConfig = await this.session.generatePeerSessionConfig();
    return encodeConfigString(
      serializePeerSessionConfig(peerSessionConfig),
      this.peerId,
      this.id
    );
  }

  static async createWithPeerFromConfigString(
    transport: SimpleSyncTransport,
    configString: string,
    onMessageHandlerError: (error: Error) => any | null = (error: Error) =>
      console.error(error),
    onDecryptionError?: (error: Error) => any
  ): Promise<SimplePeer> {
    const { id, peerId, serializedPeerSessionConfig } =
      parseConfigString(configString);
    const deserializedPeerSessionInfo = deserializePeerSessionConfig(
      serializedPeerSessionConfig
    );
    const session = await EncryptedSession.fromPeerSessionInfo(
      deserializedPeerSessionInfo
    );
    const channel = new SimpleSyncChannel(id, peerId, transport);
    const hub = new SimplePeer(
      session,
      channel,
      id,
      peerId,
      onMessageHandlerError,
      onDecryptionError
    );
    return hub;
  }

  static async newPeerSession(
    transport: SimpleSyncTransport,
    id: string,
    peerId: string,
    onMessageHandlerError: (error: Error) => any | null = (error: Error) =>
      console.error(error),
    onDecryptionError?: (error: Error) => any
  ): Promise<SimplePeer> {
    const session = await EncryptedSession.newRandomSession();

    const channel = new SimpleSyncChannel(id, peerId, transport);
    const peer = new SimplePeer(
      session,
      channel,
      id,
      peerId,
      onMessageHandlerError,
      onDecryptionError
    );
    return peer;
  }
  dispose(): void {
    super.dispose();
    this.channel.dispose();
  }
}

Then create your sessions, register RPC handlers and start making calls 🔥

const transportHub = new SimpleSyncTransport();
const peerA = await SimplePeer.newPeerSession(
  transportHub,
  "peer_a",
  "peer_b"
);
const peerBConfigString = await peerA.getConfigStringForPeer();
const peerB = await SimplePeer.createWithPeerFromConfigString(
  transportHub,
  peerBConfigString
);
const rpcSessionPeerA = new EncryptedRPCSession(peerA);
const rpcSessionPeerB = new EncryptedRPCSession(peerB);
rpcSessionPeerA.registerRPCFunction("ping", async () => {
  const result = await rpcSessionPeerA.callRPC("reverse", "gnop".split(""));
  return result.join("");
});
rpcSessionPeerB.registerRPCFunction("reverse", (array: any[]) =>
  array.concat([]).reverse()
);
const result = await rpcSessionPeerB.callRPC("ping");
// should print "pong"
console.log(result);

✔️ Works with Node.js

We use conditional exports to detect Node.js

📦 Bundler Notes

  • All targets are exported with Module and CommonJS format and named exports
  • No export is transpiled for sake of modern syntax
  • You probably need to transpile safe-rpc with babel for ES5 support

❓ FAQ

Why do HMAC if GCM already does authentication with GMAC? GMAC is known to have some problems (Authentication Failures in NIST version of GCM , Cycling Attacks on GCM, GHASH and Other Polynomial MACs and Hashes), and we don't want to take any chances.

Why not transpiled?

safe-rpc is only compatible with modern Node/Deno (v10.0.0+) and modern browsers with support for SubtleCrypto.

License

MIT. Copyright 2023 Zero Knowledge Labs Limited

About

An encrypted, authenticated, transport agnostic rpc protocol.

Resources

License

Stars

Watchers

Forks

Packages

No packages published