Skip to content

Commit

Permalink
Merge pull request #625 from dfinity/unity-ii-improvement
Browse files Browse the repository at this point in the history
Improve Unity ii integration samples.
  • Loading branch information
vincent-dfinity authored Nov 29, 2023
2 parents abf2568 + 5ce9af2 commit b47a971
Show file tree
Hide file tree
Showing 60 changed files with 446 additions and 385 deletions.
13 changes: 10 additions & 3 deletions native-apps/unity_ii_applink/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,22 @@ This is a Unity project with [ICP.NET](https://github.com/BoomDAO/ICP.NET) embed
## Workflow
Before continuing, please read through the [Android App Links](https://developer.android.com/studio/write/app-link-indexing) to understand how Android App Links works.

Here is the basic workflow that how to integrate with Internet Identity from a Unity Android game. The basic idea is to open the Web Browser from the game, login in with II in the browser, and pass the DelegationIdentity back to the game.
Here is the basic workflow that how to integrate with Internet Identity from a Unity Android game. The basic idea is to open the Web Browser from the game, login in with II in the browser, and pass the `DelegationChain` back to the game.

The steps in detail are described below:

1. Set up an [Internet Identity integration dapp](#ii_integration_dapp) which supports logging in with II, with an `assetlinks.json` file associated.
Please refer to [ii_integration_dapp](./ii_integration_dapp/README.md) to set up the dapp.

2. Run a Unity game on Android, which is built from [android_integration sample](#unity_project).
Please refer to [unity_project](./unity_project/README.md) to build the Unity Android game.

3. Launch the Web Browser from the game to open the dapp frontend deployed in #1, with the public key of `Ed25519Identity` as a parameter.

4. Login with your Internet Identity in the Web Browser.
5. Launch the application via App Links, and pass the `DelegationIdentity` back to the game as the URL parameter.
6. Call the backend canister with the `DelegationIdentity` to greet.

5. Launch the application via App Links, and pass the `DelegationChain` back to the game as the URL parameter.

6. Composite the `DelegationIdentity` with `DelegationChain` and the `Ed25519Identity`.

7. Call the backend canister with the `DelegationIdentity` to greet.
8 changes: 5 additions & 3 deletions native-apps/unity_ii_applink/ii_integration_dapp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ This example derives from the [internet_identity_integration](https://github.com

## Overview

This example shows a special case to support login with the `IncompleteEd25519KeyIdentity` which only contains the public key. The reason why we support this is all for security.
This example shows a use case to support login with the two delegations on the `DelegationChain`.

As we described in [Internet Identity Integration](../README.md#workflow), users can log in with II from the game. Usually what they do is

1. Generate the `Ed25519KeyIdentity` supported by [ICP.NET](https://github.com/BoomDAO/ICP.NET) in the Unity game.
2. For security purposes, only pass the public key of the `Ed25519KeyIdentity` to the Web browser for login, this is where `IncompleteEd25519KeyIdentity` can be used for.
3. In [index.js](./src/greet_frontend/src/index.js), we describe how to retrieve the public key of the `Ed25519Identity` from the URL parameter, use it to instantiate an `IncompleteEd25519KeyIdentity`, and log in with Internet Identity.
2. For security purposes, only pass the public key of the `Ed25519KeyIdentity` to the Web browser for login. And only the public key is necessary when creating a `DelegationChain`.
3. In [index.js](./src/greet_frontend/src/index.js), we describe how to
- log in with Internet Identity with the frontend generated session key
- retrieve the public key of the `Ed25519Identity` from the URL parameter and create another delegation with it.

With this, users don't need to pass the private key around, also they don't need to store the private key outside of the game as they can regenerate the key pairs for every session.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"greet_backend": {
"ic": "72rj2-biaaa-aaaan-qdatq-cai"
},
"greet_frontend": {
"ic": "6x7nu-oaaaa-aaaan-qdaua-cai"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.DefaultCompany.II_AppLink_Integration",
"package_name": "com.dfinity.ii_applink_integration",
"sha256_cert_fingerprints":
["86:C9:CA:6F:5A:53:7E:75:9C:D7:29:1E:8A:94:90:BE:90:0B:02:12:40:45:19:B6:65:84:3C:02:AB:B5:97:14"]
["A3:E2:36:BC:E9:04:3F:8F:A9:C5:9B:B5:FE:89:95:C8:08:BA:35:2D:07:D8:76:13:65:A9:27:D6:33:6B:44:6E"]
}
}]
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
<button id="open">Launch Application by AppLink</button>
</form>
<br />
<form>
<button id="greet">Greet</button>
</form>
<section id="greeting"></section>
</main>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,47 +1,30 @@
import {AuthClient} from "@dfinity/auth-client"
import {SignIdentity} from "@dfinity/agent";
import {DelegationIdentity, Ed25519PublicKey } from "@dfinity/identity";

// An incomplete Ed25519KeyIdentity with only the public key provided.
class IncompleteEd25519KeyIdentity extends SignIdentity {
constructor(publicKey) {
super();
this._publicKey = publicKey;
}

getPublicKey () {
return this._publicKey;
}
}

function fromHexString(hexString) {
return new Uint8Array((hexString.match(/.{1,2}/g) ?? []).map(byte => parseInt(byte, 16))).buffer;
}
import {createActor, greet_backend} from "../../declarations/greet_backend";
import {AuthClient} from "@dfinity/auth-client";
import {HttpAgent} from "@dfinity/agent";
import {DelegationIdentity, Ed25519PublicKey, ECDSAKeyIdentity, DelegationChain} from "@dfinity/identity";
import {fromHexString} from "@dfinity/identity/lib/cjs/buffer";

let myKeyIdentity;
let sessionKeyIndex = -1;
let appPublicKey;

var url = window.location.href;
sessionKeyIndex = url.indexOf("sessionkey=");
if (sessionKeyIndex !== -1) {
// Parse the public session key and instantiate an IncompleteEd25519KeyIdentity.
var sessionkey = url.substring(sessionKeyIndex + "sessionkey=".length);

var publicKey = Ed25519PublicKey.fromDer(fromHexString(sessionkey));
myKeyIdentity = new IncompleteEd25519KeyIdentity(publicKey);
} else {
// TODO: initialize an Ed25519KeyIdentity();
var publicKeyIndex = url.indexOf("sessionkey=");
if (publicKeyIndex !== -1) {
// Parse the public key.
var publicKeyString = url.substring(publicKeyIndex + "sessionkey=".length);
appPublicKey = Ed25519PublicKey.fromDer(fromHexString(publicKeyString));
}

let delegationIdentity;
let actor = greet_backend;
let delegationChain;

const loginButton = document.getElementById("login");
loginButton.onclick = async (e) => {
e.preventDefault();

// Create an auth client.
var middleKeyIdentity = await ECDSAKeyIdentity.generate();
let authClient = await AuthClient.create({
identity: myKeyIdentity,
identity: middleKeyIdentity,
});

// Start the login process and wait for it to finish.
Expand All @@ -53,9 +36,24 @@ loginButton.onclick = async (e) => {
});

// At this point we're authenticated, and we can get the identity from the auth client.
const identity = authClient.getIdentity();
if (identity instanceof DelegationIdentity) {
delegationIdentity = identity;
const middleIdentity = authClient.getIdentity();

// Using the identity obtained from the auth client to create an agent to interact with the IC.
const agent = new HttpAgent({identity: middleIdentity});
actor = createActor(process.env.GREET_BACKEND_CANISTER_ID, {
agent,
});

// Create another delegation with the app public key, then we have two delegations on the chain.
if (appPublicKey != null && middleIdentity instanceof DelegationIdentity ) {
let middleToApp = await DelegationChain.create(
middleKeyIdentity,
appPublicKey,
new Date(Date.now() + 15 * 60 * 1000),
{ previous: middleIdentity.getDelegation() },
);

delegationChain = middleToApp;
}

return false;
Expand All @@ -65,19 +63,33 @@ const openButton = document.getElementById("open");
openButton.onclick = async (e) => {
e.preventDefault();

// if (sessionKeyIndex === -1) {
// // TODO: warning for not login from a game.
// return false;
// }
if (delegationChain == null){
console.log("Invalid delegation chain.");
return false;
}

var url = "https://6x7nu-oaaaa-aaaan-qdaua-cai.icp0.io/authorize?";
if (delegationIdentity != null) {
var delegationString = JSON.stringify(delegationIdentity.getDelegation().toJSON());
console.log(delegationString);
url = url + "delegation=" + encodeURIComponent(delegationString);
}
var delegationString = JSON.stringify(delegationChain.toJSON());
url = url + "delegation=" + encodeURIComponent(delegationString);
//console.log(url);

window.open(url, "_self");

return false;
};

const greetButton = document.getElementById("greet");
greetButton.onclick = async (e) => {
e.preventDefault();

greetButton.setAttribute("disabled", true);

// Interact with backend actor, calling the greet method
const greeting = await actor.greet();

greetButton.removeAttribute("disabled");

document.getElementById("greeting").innerText = greeting;

return false;
};
Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file not shown.
Binary file not shown.

This file was deleted.

Binary file not shown.
Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# ICP.NET

The libraries in ICP.NET are built from https://github.com/BoomDAO/ICP.NET, with

1. Version `4.0.0`, hashtag `7b51d78b4f4356d767bb86074918a41973c41214`.
2. `ByteUtil` class is changed from `internal` to `public` in `\ICP.NET\src\Candid\Utilities\ByteUtil.cs` file.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.

This file was deleted.

Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file not shown.
Binary file not shown.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 5c68e31d1a87467468d62c1cd739b0a0, type: 3}
m_Name:
m_EditorClassIdentifier:
greetFrontend: https://6x7nu-oaaaa-aaaan-qdaua-cai.icp0.io/
greetBackendCanister: 72rj2-biaaa-aaaan-qdatq-cai
--- !u!114 &272906662
MonoBehaviour:
Expand All @@ -181,7 +182,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 17feafab2ee1b6b4684003f4b0edc397, type: 3}
m_Name:
m_EditorClassIdentifier:
greetFrontend: https://6x7nu-oaaaa-aaaan-qdaua-cai.icp0.io
--- !u!1 &671559154
GameObject:
m_ObjectHideFlags: 0
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
using UnityEngine;
using EdjCase.ICP.Agent;
using EdjCase.ICP.Agent.Identities;
using EdjCase.ICP.Agent.Models;
using EdjCase.ICP.Candid.Models;
using EdjCase.ICP.Candid.Utilities;
using Newtonsoft.Json;
using System;
using System.Web;
using System.Collections.Generic;

namespace IC.GameKit
{
Expand All @@ -22,7 +28,7 @@ public void Start()

public void OpenBrowser()
{
var target = mTestICPAgent.greetFrontend + "?sessionkey=" + ByteUtil.ToHexString(mTestICPAgent.TestIdentity.PublicKey.Value);
var target = mTestICPAgent.greetFrontend + "?sessionkey=" + ByteUtil.ToHexString(mTestICPAgent.TestIdentity.PublicKey.ToDerEncoding());
Application.OpenURL(target);
}

Expand All @@ -40,8 +46,36 @@ public void OnDeepLinkActivated(string url)
}

var delegationString = HttpUtility.UrlDecode(url.Substring(indexOfDelegation + kDelegationParam.Length));
var delegation = JsonConvert.DeserializeObject<DelegationChainModel>(delegationString);
mTestICPAgent.Delegation = delegation;
mTestICPAgent.DelegationIdentity = ConvertJsonToDelegationIdentity(delegationString);
}

internal DelegationIdentity ConvertJsonToDelegationIdentity(string jsonDelegation)
{
var delegationChainModel = JsonConvert.DeserializeObject<DelegationChainModel>(jsonDelegation);
if (delegationChainModel == null && delegationChainModel.delegations.Length == 0)
{
Debug.LogError("Invalid delegation chain.");
return null;
}

// Initialize DelegationIdentity.
var delegations = new List<SignedDelegation>();
foreach (var signedDelegationModel in delegationChainModel.delegations)
{
var pubKey = SubjectPublicKeyInfo.FromDerEncoding(ByteUtil.FromHexString(signedDelegationModel.delegation.pubkey));
var expiration = ICTimestamp.FromNanoSeconds(Convert.ToUInt64(signedDelegationModel.delegation.expiration, 16));
var delegation = new Delegation(pubKey, expiration);

var signature = ByteUtil.FromHexString(signedDelegationModel.signature);
var signedDelegation = new SignedDelegation(delegation, signature);
delegations.Add(signedDelegation);
}

var chainPublicKey = SubjectPublicKeyInfo.FromDerEncoding(ByteUtil.FromHexString(delegationChainModel.publicKey));
var delegationChain = new DelegationChain(chainPublicKey, delegations);
var delegationIdentity = new DelegationIdentity(mTestICPAgent.TestIdentity, delegationChain);

return delegationIdentity;
}
}
}
Loading

0 comments on commit b47a971

Please sign in to comment.