Fully encrypted & authenticated communication between an iframe and a parent window using safe-rpc.
Install:
# npm
npm i safe-rpc safe-rpc-iframe
# yarn
yarn add safe-rpc safe-rpc-iframe
Note: You can skip this if you are not using TypeScript
interface IParentFrameRPCInterface {
stringLength(str: string): Promise<number>;
// note that you can only pass in one parameter.
// so if you need to send multiple parameters, put it in an array or an object like the line below
splitString(params: {str: string, delimiter: string}): Promise<string[]>;
}
interface IChildFrameRPCInterface {
sumNumbers(numbers: number[]): Promise<number>;
reverseArray(arr: any[]): Promise<any[]>;
}
interface IParentFrameRPCInterface {
stringLength(str: string): Promise<number>;
// note that you can only pass in one parameter.
// so if you need to send multiple parameters, put it in an array or an object like the line below
splitString(params: {str: string, delimiter: string}): Promise<string[]>;
}
interface IChildFrameRPCInterface {
sumNumbers(numbers: number[]): Promise<number>;
reverseArray(arr: any[]): Promise<any[]>;
setName(name: string): Promise<void>;
getName(): Promise<string>;
}
abstract class InternalLogic {
peer: IParentFrameRPCInterface;
name: string = "";
constructor(peer: IParentFrameRPCInterface){
this.peer = peer;
}
// this function will NOT be exposed over RPC
// only functions implemented in the class instance passed to registerHandlerClass will be exposed
// the functions in classes inherited by the instance will NOT be exposed via RPC
hidden(){
return "i am hidden";
}
}
class ChildRPCHandlerClass extends InternalLogic implements IChildFrameRPCInterface{
constructor(peer: IParentFrameRPCInterface){
super(peer);
}
// this function will be exposed over RPC
async sumNumbers({numbers}: { numbers: number[]; }): Promise<number> {
return numbers.reduce((a,b)=>a+b,0)+splitTest;
}
// this function will be exposed over RPC
async reverseArray(arr: any[]): Promise<any[]> {
// you can call a function exposed by the parent frame inside an RPC function
const splitTest = await this.peer.stringLength({str:"test string"});
console.log("splitTest: ",splitTest);
return arr.concat([]).reverse();
}
async setName(name: string): Promise<void> {
this.name = name;
}
async getName(): Promise<string> {
if(!this.name){
throw new Error("my name has not yet been set");
}
return this.name;
}
}
// boilerplate initialization code
async function demoChild(){
// create an RPC session using the information in the "#" of the URL
const rpcSession = await createChildIFrameRPCSession();
// expose class ChildRPCHandlerClass to a parent which exposes an RPC interface 'IParentFrameRPCInterface'
const handler = rpcSession.registerHandlerClass<ChildRPCHandlerClass, IParentFrameRPCInterface>(
(peer)=>new ChildRPCHandlerClass(peer)
);
}
demoChild()
.then(()=>{
console.log("[child] RPC API listening for calls")
})
.catch(err=>{
console.error("[child] Error starting RPC API listener: ",err);
});
For this tutorial, we will assume that the iframe was uploaded to https://example.com/iframe.html
interface IParentFrameRPCInterface {
stringLength(str: string): Promise<number>;
// note that you can only pass in one parameter.
// so if you need to send multiple parameters, put it in an array or an object like the line below
splitString(params: {str: string, delimiter: string}): Promise<string[]>;
}
interface IChildFrameRPCInterface {
sumNumbers(numbers: number[]): Promise<number>;
reverseArray(arr: any[]): Promise<any[]>;
setName(name: string): Promise<void>;
getName(): Promise<string>;
}
abstract class InternalLogic {
peer: IChildFrameRPCInterface;
rpcSession: EncryptedRPCSession;
constructor(peer: IChildFrameRPCInterface, rpcSession: EncryptedRPCSession){
this.peer = peer;
this.rpcSession = rpcSession;
}
hidden(){
return "i am hidden (parent)";
}
dispose(){
this.rpcSession.dispose();
}
}
class ParentRPCHandlerClass extends InternalLogic implements IParentFrameRPCInterface{
constructor(peer: IChildFrameRPCInterface, rpcSession: EncryptedRPCSession){
super(peer, rpcSession);
}
async stringLength({str}: { str: string; }): Promise<number> {
return str.length;
}
async splitString({str, delimiter}: { str: string; delimiter: string; }): Promise<string[]> {
return str.split(delimiter);
}
}
async function createChildFrame(childFrameSrc: string): Promise<ParentRPCHandlerClass>{
const rpcSession = await createParentIFrameRPCSession(childFrameSrc);
const handler = rpcSession.registerHandlerClass<ParentRPCHandlerClass, IChildFrameRPCInterface>(
(peer, rpcSession)=>new ParentRPCHandlerClass(peer, rpcSession)
);
return handler;
}
async function demoParent() {
// replace the line below with your iframe's URL
const childFrameSrc = "https://example.com/iframe.html";
// create a child iframe instance (this will add an invisible iframe to document.body)
const childFrame1 = await createChildFrame(childFrameSrc);
// you can call functions on childFrame.peer now
const reversedArrayExample = await childFrame1.peer.reverseArray([1, 3, 5, 7]);
console.log("your reversed array: ", reversedArrayExample);
// the child iframe can throw exceptions and messages will be forwarded to the parent caller
try {
const childFrame1OldName = await childFrame1.peer.getName();
// the code on the line below will not run because an exception is thrown
console.log("the first iframe's name is: "+childFrame1OldName)
}catch(err){
// this will show the error 'my name has not yet been set'
console.error("Child iframe threw an error: ",err);
}
await childFrame1.peer.setName("Mike");
const childFrame1Name = await childFrame1.peer.getName();
// the code on the line below will print "the iframe's name is: Mike"
console.log("the first iframe's name is: "+childFrame1Name);
// you can have multiple iframe instances at once
const childFrame2 = await createChildFrame(childFrameSrc);
await childFrame2.peer.setName("Sally");
const childFrame2Name = await childFrame2.peer.getName();
// the code on the line below will print "the iframe's name is: Mike"
console.log("the second iframe's name is: "+childFrame2Name);
// you can dispose of iframes (after this line, the first iframe will be removed from document.body)
childFrame1.dispose();
await childFrame2.peer.setName("I can still call functions on the second iframe because it has not been disposed");
childFrame2.dispose();
}
demoParent()
.catch(err=>{
console.error("[parent] Error: ",err);
});
Why do we need to encrypt and sign messages between parent and child? Cross-domain iframe communication works using the postMessage API, and can leak information/messages sent from other windows/iframe instances from the same origin.
MIT. Copyright 2023 Zero Knowledge Labs Limited