From a2b7fa3d562489d76f146a8e3396ffe0e1ef00c1 Mon Sep 17 00:00:00 2001 From: Hamish Peebles Date: Mon, 15 Jul 2024 16:42:38 +0100 Subject: [PATCH] Wire up `initiate_identity_link` and `approve_identity_link` in the FE (#6035) --- .../src/services/identity/candid/idl.d.ts | 10 +++- .../src/services/identity/candid/idl.js | 51 +++++++++++++++---- .../src/services/identity/candid/types.d.ts | 27 ++++++++++ .../src/services/identity/identity.client.ts | 47 ++++++++++++++++- .../src/services/identity/mappers.ts | 51 ++++++++++++++++++- .../src/services/identityAgent.ts | 21 +++++--- .../openchat-shared/src/domain/identity.ts | 13 +++++ 7 files changed, 200 insertions(+), 20 deletions(-) diff --git a/frontend/openchat-agent/src/services/identity/candid/idl.d.ts b/frontend/openchat-agent/src/services/identity/candid/idl.d.ts index a1a6ff330f..da74519f7e 100644 --- a/frontend/openchat-agent/src/services/identity/candid/idl.d.ts +++ b/frontend/openchat-agent/src/services/identity/candid/idl.d.ts @@ -1,18 +1,24 @@ import type { IDL } from "@dfinity/candid"; import { + ApproveIdentityLinkResponse, CheckAuthPrincipalResponse, CreateIdentityResponse, + GenerateChallengeResponse, GetDelegationResponse, + InitiateIdentityLinkResponse, PrepareDelegationResponse, - GenerateChallengeResponse, + SignedDelegation, _SERVICE, } from "./types"; export { + ApproveIdentityLinkResponse as ApiApproveIdentityLinkResponse, CheckAuthPrincipalResponse as ApiCheckAuthPrincipalResponse, CreateIdentityResponse as ApiCreateIdentityResponse, + GenerateChallengeResponse as ApiGenerateChallengeResponse, GetDelegationResponse as ApiGetDelegationResponse, + InitiateIdentityLinkResponse as ApiInitiateIdentityLinkResponse, PrepareDelegationResponse as ApiPrepareDelegationResponse, - GenerateChallengeResponse as ApiGenerateChallengeResponse, + SignedDelegation as ApiSignedDelegation, _SERVICE as IdentityService, }; diff --git a/frontend/openchat-agent/src/services/identity/candid/idl.js b/frontend/openchat-agent/src/services/identity/candid/idl.js index 358425be8a..7379ac39a3 100644 --- a/frontend/openchat-agent/src/services/identity/candid/idl.js +++ b/frontend/openchat-agent/src/services/identity/candid/idl.js @@ -1,9 +1,30 @@ export const idlFactory = ({ IDL }) => { + const PublicKey = IDL.Vec(IDL.Nat8); + const TimestampNanoseconds = IDL.Nat64; + const SignedDelegation = IDL.Record({ + 'signature' : IDL.Vec(IDL.Nat8), + 'delegation' : IDL.Record({ + 'pubkey' : PublicKey, + 'expiration' : TimestampNanoseconds, + }), + }); + const ApproveIdentityLinkArgs = IDL.Record({ + 'link_initiated_by' : IDL.Principal, + 'public_key' : IDL.Vec(IDL.Nat8), + 'delegation' : SignedDelegation, + }); + const ApproveIdentityLinkResponse = IDL.Variant({ + 'LinkRequestNotFound' : IDL.Null, + 'InvalidSignature' : IDL.Null, + 'Success' : IDL.Null, + 'MalformedSignature' : IDL.Text, + 'DelegationTooOld' : IDL.Null, + 'CallerNotRecognised' : IDL.Null, + }); const CheckAuthPrincipalResponse = IDL.Variant({ 'NotFound' : IDL.Null, 'Success' : IDL.Null, }); - const PublicKey = IDL.Vec(IDL.Nat8); const Nanoseconds = IDL.Nat64; const CreateIdentityArgs = IDL.Record({ 'public_key' : PublicKey, @@ -13,7 +34,6 @@ export const idlFactory = ({ IDL }) => { IDL.Record({ 'key' : IDL.Nat32, 'chars' : IDL.Text }) ), }); - const TimestampNanoseconds = IDL.Nat64; const PrepareDelegationSuccess = IDL.Record({ 'user_key' : PublicKey, 'expiration' : TimestampNanoseconds, @@ -34,17 +54,20 @@ export const idlFactory = ({ IDL }) => { 'session_key' : PublicKey, 'expiration' : TimestampNanoseconds, }); - const SignedDelegation = IDL.Record({ - 'signature' : IDL.Vec(IDL.Nat8), - 'delegation' : IDL.Record({ - 'pubkey' : PublicKey, - 'expiration' : TimestampNanoseconds, - }), - }); const GetDelegationResponse = IDL.Variant({ 'NotFound' : IDL.Null, 'Success' : SignedDelegation, }); + const InitiateIdentityLinkArgs = IDL.Record({ + 'public_key' : IDL.Vec(IDL.Nat8), + 'link_to_principal' : IDL.Principal, + }); + const InitiateIdentityLinkResponse = IDL.Variant({ + 'AlreadyRegistered' : IDL.Null, + 'Success' : IDL.Null, + 'TargetUserNotFound' : IDL.Null, + 'PublicKeyInvalid' : IDL.Text, + }); const PrepareDelegationArgs = IDL.Record({ 'session_key' : PublicKey, 'max_time_to_live' : IDL.Opt(Nanoseconds), @@ -54,6 +77,11 @@ export const idlFactory = ({ IDL }) => { 'Success' : PrepareDelegationSuccess, }); return IDL.Service({ + 'approve_identity_link' : IDL.Func( + [ApproveIdentityLinkArgs], + [ApproveIdentityLinkResponse], + [], + ), 'check_auth_principal' : IDL.Func( [IDL.Record({})], [CheckAuthPrincipalResponse], @@ -74,6 +102,11 @@ export const idlFactory = ({ IDL }) => { [GetDelegationResponse], ['query'], ), + 'initiate_identity_link' : IDL.Func( + [InitiateIdentityLinkArgs], + [InitiateIdentityLinkResponse], + [], + ), 'prepare_delegation' : IDL.Func( [PrepareDelegationArgs], [PrepareDelegationResponse], diff --git a/frontend/openchat-agent/src/services/identity/candid/types.d.ts b/frontend/openchat-agent/src/services/identity/candid/types.d.ts index df9e84bcc1..1f569d053a 100644 --- a/frontend/openchat-agent/src/services/identity/candid/types.d.ts +++ b/frontend/openchat-agent/src/services/identity/candid/types.d.ts @@ -2,6 +2,17 @@ import type { Principal } from '@dfinity/principal'; import type { ActorMethod } from '@dfinity/agent'; import type { IDL } from '@dfinity/candid'; +export interface ApproveIdentityLinkArgs { + 'link_initiated_by' : Principal, + 'public_key' : Uint8Array | number[], + 'delegation' : SignedDelegation, +} +export type ApproveIdentityLinkResponse = { 'LinkRequestNotFound' : null } | + { 'InvalidSignature' : null } | + { 'Success' : null } | + { 'MalformedSignature' : string } | + { 'DelegationTooOld' : null } | + { 'CallerNotRecognised' : null }; export type CheckAuthPrincipalResponse = { 'NotFound' : null } | { 'Success' : null }; export interface CreateIdentityArgs { @@ -24,6 +35,14 @@ export interface GetDelegationArgs { } export type GetDelegationResponse = { 'NotFound' : null } | { 'Success' : SignedDelegation }; +export interface InitiateIdentityLinkArgs { + 'public_key' : Uint8Array | number[], + 'link_to_principal' : Principal, +} +export type InitiateIdentityLinkResponse = { 'AlreadyRegistered' : null } | + { 'Success' : null } | + { 'TargetUserNotFound' : null } | + { 'PublicKeyInvalid' : string }; export type Nanoseconds = bigint; export interface PrepareDelegationArgs { 'session_key' : PublicKey, @@ -42,10 +61,18 @@ export interface SignedDelegation { } export type TimestampNanoseconds = bigint; export interface _SERVICE { + 'approve_identity_link' : ActorMethod< + [ApproveIdentityLinkArgs], + ApproveIdentityLinkResponse + >, 'check_auth_principal' : ActorMethod<[{}], CheckAuthPrincipalResponse>, 'create_identity' : ActorMethod<[CreateIdentityArgs], CreateIdentityResponse>, 'generate_challenge' : ActorMethod<[{}], GenerateChallengeResponse>, 'get_delegation' : ActorMethod<[GetDelegationArgs], GetDelegationResponse>, + 'initiate_identity_link' : ActorMethod< + [InitiateIdentityLinkArgs], + InitiateIdentityLinkResponse + >, 'prepare_delegation' : ActorMethod< [PrepareDelegationArgs], PrepareDelegationResponse diff --git a/frontend/openchat-agent/src/services/identity/identity.client.ts b/frontend/openchat-agent/src/services/identity/identity.client.ts index e5ca17fe3c..530bac8ec5 100644 --- a/frontend/openchat-agent/src/services/identity/identity.client.ts +++ b/frontend/openchat-agent/src/services/identity/identity.client.ts @@ -2,23 +2,29 @@ import type { Identity, SignIdentity } from "@dfinity/agent"; import { idlFactory, type IdentityService } from "./candid/idl"; import { CandidService } from "../candidService"; import type { + ApproveIdentityLinkResponse, ChallengeAttempt, CheckAuthPrincipalResponse, CreateIdentityResponse, GenerateChallengeResponse, GetDelegationResponse, + InitiateIdentityLinkResponse, PrepareDelegationResponse, } from "openchat-shared"; import { + approveIdentityLinkResponse, checkAuthPrincipalResponse, createIdentityResponse, generateChallengeResponse, getDelegationResponse, + initiateIdentityLinkResponse, prepareDelegationResponse, } from "./mappers"; -import type { CreateIdentityArgs } from "./candid/types"; +import type { CreateIdentityArgs, SignedDelegation } from "./candid/types"; import { apiOptional } from "../common/chatMappers"; import { identity } from "../../utils/mapping"; +import { Principal } from "@dfinity/principal"; +import type { DelegationIdentity } from "@dfinity/identity"; export class IdentityClient extends CandidService { private service: IdentityService; @@ -40,7 +46,7 @@ export class IdentityClient extends CandidService { challengeAttempt: ChallengeAttempt | undefined, ): Promise { const args: CreateIdentityArgs = { - public_key: new Uint8Array((this.identity as SignIdentity).getPublicKey().toDer()), + public_key: this.publicKey(), session_key: sessionKey, max_time_to_live: [] as [] | [bigint], challenge_attempt: apiOptional(identity, challengeAttempt), @@ -87,4 +93,41 @@ export class IdentityClient extends CandidService { generateChallenge(): Promise { return this.handleResponse(this.service.generate_challenge({}), generateChallengeResponse); } + + initiateIdentityLink(linkToPrincipal: string): Promise { + return this.handleResponse( + this.service.initiate_identity_link({ + link_to_principal: Principal.fromText(linkToPrincipal), + public_key: this.publicKey(), + }), + initiateIdentityLinkResponse, + ); + } + + approveIdentityLink(linkInitiatedBy: string): Promise { + return this.handleResponse( + this.service.approve_identity_link({ + link_initiated_by: Principal.fromText(linkInitiatedBy), + public_key: this.publicKey(), + delegation: this.delegation(), + }), + approveIdentityLinkResponse, + ); + } + + private publicKey(): Uint8Array { + return new Uint8Array((this.identity as SignIdentity).getPublicKey().toDer()); + } + + private delegation(): SignedDelegation { + const delegation = (this.identity as DelegationIdentity).getDelegation().delegations[0]; + + return { + signature: new Uint8Array(delegation.signature), + delegation: { + pubkey: new Uint8Array(delegation.delegation.pubkey), + expiration: delegation.delegation.expiration, + }, + }; + } } diff --git a/frontend/openchat-agent/src/services/identity/mappers.ts b/frontend/openchat-agent/src/services/identity/mappers.ts index c34d6fb5b9..fb8dadae94 100644 --- a/frontend/openchat-agent/src/services/identity/mappers.ts +++ b/frontend/openchat-agent/src/services/identity/mappers.ts @@ -1,18 +1,22 @@ import type { + ApiApproveIdentityLinkResponse, ApiCheckAuthPrincipalResponse, ApiCreateIdentityResponse, ApiGenerateChallengeResponse, ApiGetDelegationResponse, + ApiInitiateIdentityLinkResponse, ApiPrepareDelegationResponse, } from "./candid/idl"; import { + type ApproveIdentityLinkResponse, type CheckAuthPrincipalResponse, type CreateIdentityResponse, + type GenerateChallengeResponse, type GetDelegationResponse, + type InitiateIdentityLinkResponse, type PrepareDelegationResponse, type PrepareDelegationSuccess, UnsupportedValueError, - type GenerateChallengeResponse, } from "openchat-shared"; import { consolidateBytes } from "../../utils/mapping"; import type { Signature } from "@dfinity/agent"; @@ -121,3 +125,48 @@ export function generateChallengeResponse( candid, ); } + +export function initiateIdentityLinkResponse( + candid: ApiInitiateIdentityLinkResponse, +): InitiateIdentityLinkResponse { + if ("Success" in candid) { + return "success"; + } + if ("AlreadyRegistered" in candid) { + return "already_registered"; + } + if ("TargetUserNotFound" in candid) { + return "target_user_not_found"; + } + if ("PublicKeyInvalid" in candid) { + return "public_key_invalid"; + } + throw new UnsupportedValueError( + "Unexpected ApiInitiateIdentityLinkResponse type received", + candid, + ); +} + +export function approveIdentityLinkResponse( + candid: ApiApproveIdentityLinkResponse, +): ApproveIdentityLinkResponse { + if ("Success" in candid) { + return "success"; + } + if ("CallerNotRecognised" in candid) { + return "caller_not_recognised"; + } + if ("LinkRequestNotFound" in candid) { + return "link_request_not_found"; + } + if ("MalformedSignature" in candid || "InvalidSignature" in candid) { + return "invalid_signature"; + } + if ("DelegationTooOld" in candid) { + return "delegation_too_old"; + } + throw new UnsupportedValueError( + "Unexpected ApiApproveIdentityLinkResponse type received", + candid, + ); +} diff --git a/frontend/openchat-agent/src/services/identityAgent.ts b/frontend/openchat-agent/src/services/identityAgent.ts index d9292363f1..fef6fd13d8 100644 --- a/frontend/openchat-agent/src/services/identityAgent.ts +++ b/frontend/openchat-agent/src/services/identityAgent.ts @@ -1,13 +1,14 @@ import { IdentityClient } from "./identity/identity.client"; import type { Identity, SignIdentity } from "@dfinity/agent"; import { DelegationIdentity } from "@dfinity/identity"; -import type { GenerateChallengeResponse } from "openchat-shared"; -import { - buildDelegationIdentity, - type ChallengeAttempt, - type CreateOpenChatIdentityError, - toDer, +import type { + ApproveIdentityLinkResponse, + ChallengeAttempt, + CreateOpenChatIdentityError, + GenerateChallengeResponse, + InitiateIdentityLinkResponse, } from "openchat-shared"; +import { buildDelegationIdentity, toDer } from "openchat-shared"; export class IdentityAgent { private _identityClient: IdentityClient; @@ -64,6 +65,14 @@ export class IdentityAgent { return this._identityClient.generateChallenge(); } + initiateIdentityLink(linkToPrincipal: string): Promise { + return this._identityClient.initiateIdentityLink(linkToPrincipal); + } + + approveIdentityLink(linkInitiatedBy: string): Promise { + return this._identityClient.approveIdentityLink(linkInitiatedBy); + } + private async getDelegation( userKey: Uint8Array, sessionKey: SignIdentity, diff --git a/frontend/openchat-shared/src/domain/identity.ts b/frontend/openchat-shared/src/domain/identity.ts index e06728ebf6..3397695df3 100644 --- a/frontend/openchat-shared/src/domain/identity.ts +++ b/frontend/openchat-shared/src/domain/identity.ts @@ -81,3 +81,16 @@ export type Challenge = { }; export type CreateOpenChatIdentityResponse = "success" | CreateOpenChatIdentityError; + +export type InitiateIdentityLinkResponse = + | "success" + | "already_registered" + | "target_user_not_found" + | "public_key_invalid"; + +export type ApproveIdentityLinkResponse = + | "success" + | "caller_not_recognised" + | "link_request_not_found" + | "invalid_signature" + | "delegation_too_old";