Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/update oob contact routing #58

Closed
wants to merge 15 commits into from
Closed
1,207 changes: 680 additions & 527 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,8 @@
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"typescript": "5.5.4"
},
"dependencies": {
"axios": "^1.7.7"
}
}
3 changes: 3 additions & 0 deletions packages/contact-exchange/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ module.exports = {
testMatch: ['**/tests/**/*.test.ts', '**/src/**/*.test.ts'],
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/libs/contact-exchange',

// Specify which node_modules to transform
transformIgnorePatterns: ['node_modules/(?!did-resolver-lib)/'],
};
8 changes: 7 additions & 1 deletion packages/contact-exchange/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,11 @@
"keywords": [],
"author": "adorsys",
"license": "ISC",
"description": ""
"description": "",
"dependencies": {
"@adorsys-gis/multiple-did-identities": "file:../packages",
"axios": "^1.7.7",
"did-resolver-lib": "https://gitpkg.vercel.app/adorsys/didcomm-messaging-clients-utilities/libs/did-resolver-lib?main",
"didcomm-node": "^0.4.1"
}
}
10 changes: 10 additions & 0 deletions packages/contact-exchange/src/services/DIDCommOOBInvitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,14 @@ interface OutOfBandInvitation {
attachments?: Array<unknown>;
}

export enum MessageType {
RoutingAccept = 'https://mediator.rootsid.cloud',
MediationRequest = 'https://didcomm.org/coordinate-mediation/2.0/mediate-request',
KeylistUpdate = 'https://didcomm.org/coordinate-mediation/2.0/keylist-update',
}

export enum MessageTyp {
Didcomm = 'application/didcomm-plain+json',
}

export { DIDCommMessage, OutOfBandInvitation };
156 changes: 156 additions & 0 deletions packages/contact-exchange/src/services/DIDCommRoutingService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { Message, SecretsResolver, Secret } from 'didcomm-node';
import { v4 as uuidv4 } from 'uuid';
import { MessageTyp, MessageType } from './DIDCommOOBInvitation';
import { DidPeerMethod } from '@adorsys-gis/multiple-did-identities/src/did-methods/DidPeerMethod';
import { PeerDIDResolver } from 'did-resolver-lib';

class DidcommSecretsResolver implements SecretsResolver {
private knownSecrets: Secret[];

constructor(knownSecrets: Secret[]) {
this.knownSecrets = knownSecrets;
}

async get_secret(secretId: string): Promise<Secret | null> {
return this.knownSecrets.find((secret) => secret.id === secretId) || null;
}

async find_secrets(secretIds: string[]): Promise<string[]> {
return secretIds.filter((id) =>
this.knownSecrets.some((secret) => secret.id === id),
);
}
}

function createSecretsResolver(knownSecrets: Secret[]): DidcommSecretsResolver {
return new DidcommSecretsResolver(knownSecrets);
}

export interface PrivateKeyJWK {
id: string;
type: 'JsonWebKey2020';
privateKeyJwk: {
crv: string;
d: string;
kty: string;
x: string;
y?: string;
};
}

function prependDidToSecretIds(
secrets: PrivateKeyJWK[],
did: string,
): PrivateKeyJWK[] {
return secrets.map((secret) => ({
...secret,
id: `${did}${secret.id}`,
}));
}

export async function processMediatorOOB(oob: string) {
try {
const oobParts = oob.split('=');
if (oobParts.length < 2) {
throw new Error('Invalid OOB format. Missing encoded payload.');
}

const oobUrl = oobParts[1];
const decodedOob = JSON.parse(
Buffer.from(oobUrl, 'base64url').toString('utf-8'),
);

if (!decodedOob.from) {
throw new Error('Invalid OOB content. Missing "from" field.');
}

const didTo = decodedOob.from;

const didPeerMethod = new DidPeerMethod();
const didPeer = await didPeerMethod.generateMethod2();

const mediationRequest = new Message({
extra_header: [{ return_route: 'all' }],
id: uuidv4(),
typ: MessageTyp.Didcomm,
type: MessageType.MediationRequest,
body: {},
});

const secrets: PrivateKeyJWK[] = [didPeer.privateKeyE, didPeer.privateKeyV];
const updatedSecrets = prependDidToSecretIds(secrets, didPeer.did);
const secretsResolver = createSecretsResolver(updatedSecrets);

const hardcodedValue =
'SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHA6Ly9leGFtcGxlLmNvbS9kaWRjb21tIiwiYWNjZXB0IjpbImRpZGNvbW0vdjIiXSwicm91dGluZ0tleXMiOlsiZGlkOmV4YW1wbGU6MTIzNDU2Nzg5YWJjZGVmZ2hpI2tleS0xIl19fQ';
const updatedDidTo = didTo
.split('.')
.slice(0, -1)
.concat(hardcodedValue)
.join('.');

const resolver = new PeerDIDResolver();

await mediationRequest.pack_encrypted(
updatedDidTo,
didPeer.did,
didPeer.did,
resolver,
secretsResolver,
{ forward: false },
);

const keylistUpdate = new Message({
id: uuidv4(),
typ: MessageTyp.Didcomm,
type: MessageType.KeylistUpdate,
body: {
updates: [
{
action: 'add',
recipient_did: didPeer.did,
},
],
},
});

await keylistUpdate.pack_encrypted(
updatedDidTo,
didPeer.did,
didPeer.did,
resolver,
secretsResolver,
{ forward: false },
);

// Send mediation request (commented out to prevent execution)
// const mediationResponse = await axios.post(oobUrl, packedMediationRequest, {
// headers: { 'Content-Type': 'application/didcomm-encrypted+json' },
// });
// const mediatorResponse = await Message.unpack(
// mediationResponse.data,
// resolver,
// secretsResolver,
// {},
// );

// Send Keylist Update (commented out to prevent execution)
// const keylistResponse = await axios.post(oobUrl, packedKeylistUpdate, {
// headers: { 'Content-Type': 'application/didcomm-encrypted+json' },
// });
// const unpackedKeylistResponse = await Message.unpack(
// keylistResponse.data,
// resolver,
// secretsResolver,
// {},
// );

// return unpackedKeylistResponse;
} catch (error: unknown) {
if (error instanceof Error) {
throw new Error(`Error processing OOB: ${error.message}`);
} else {
throw new Error('Unknown error during OOB processing');
}
}
}
4 changes: 2 additions & 2 deletions packages/contact-exchange/src/services/HandleOOBInvitation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Contact, Wallet } from './Wallet';
import { OutOfBandInvitation } from './DIDCommOOBInvitation';
import { validOutOfBandInvitation } from '../tests/OOBTestFixtures';
import { OutOfBandInvitation } from './DIDCommOOBInvitation';
import { Contact, Wallet } from './Wallet';

export function handleOOBInvitation(
wallet: Wallet,
Expand Down
111 changes: 111 additions & 0 deletions packages/contact-exchange/src/services/MessagingService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
Message,
DIDResolver,
SecretsResolver,
DIDDoc,
Secret,
} from 'didcomm-node';
import { v4 as uuidv4 } from 'uuid';
import { MessageType, MessageTyp } from './Messages.types';
import { validEncodedUrl } from '../tests/OOBTestFixtures';

class MessagingServiceDIDResolver implements DIDResolver {
knownDids: DIDDoc[];

constructor(knownDids: DIDDoc[]) {
this.knownDids = knownDids;
}

async resolve(did: string): Promise<DIDDoc | null> {
return this.knownDids.find((ddoc) => ddoc.id === did) || null;
}
}

class MessagingServiceSecretsResolver implements SecretsResolver {
knownSecrets: Secret[];

constructor(knownSecrets: Secret[]) {
this.knownSecrets = knownSecrets;
}

async get_secret(secretId: string): Promise<Secret | null> {
return this.knownSecrets.find((secret) => secret.id === secretId) || null;
}

async find_secrets(secretIds: string[]): Promise<string[]> {
return secretIds.filter((id) =>
this.knownSecrets.find((secret) => secret.id === id),
);
}
}

export async function sendContactRequest(
didResolver: DIDResolver,
secretsResolver: SecretsResolver,
senderDid: string,
receiverDid: string,
): Promise<string> {
const messageBody = {
type: validEncodedUrl,
from: senderDid,
};

const routedMessage = new Message({
id: uuidv4(),
typ: MessageTyp.Didcomm,
type: MessageType.RoutingForward,
from: senderDid,
to: [receiverDid],
created_time: Math.round(Date.now() / 1000),
body: messageBody,
});

const [encryptedMessage] = await routedMessage.pack_encrypted(
receiverDid,
senderDid,
senderDid,
didResolver,
secretsResolver,
{ forward: false },
);

return encryptedMessage;
}

export async function handleContactRequest(
encryptedMessage: string,
didResolver: DIDResolver,
secretsResolver: SecretsResolver,
): Promise<void> {
const [unpackedMessage] = await Message.unpack(
encryptedMessage,
didResolver,
secretsResolver,
{},
);

const messageBody = unpackedMessage.as_value().body;

if (messageBody.type === validEncodedUrl) {
console.log(`Received contact request from ${messageBody.from}`);
} else if (messageBody.type === MessageType.RoutingForward) {
console.log(`Received routing forward from ${messageBody.from}`);
}
}

function createDidResolver(knownDids: DIDDoc[]): MessagingServiceDIDResolver {
return new MessagingServiceDIDResolver(knownDids);
}

function createSecretsResolver(
knownSecrets: Secret[],
): MessagingServiceSecretsResolver {
return new MessagingServiceSecretsResolver(knownSecrets);
}

export {
MessagingServiceDIDResolver,
MessagingServiceSecretsResolver,
createDidResolver,
createSecretsResolver,
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// processOOBInvitation.ts
import { OutOfBandInvitation, DIDCommMessage } from './DIDCommOOBInvitation';
import { DIDCommMessage, OutOfBandInvitation } from './DIDCommOOBInvitation';
import { parseOOBInvitation } from './OOBParser';

/**
Expand Down
44 changes: 44 additions & 0 deletions packages/contact-exchange/src/tests/DIDCommRoutingService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { DidPeerMethod } from '@adorsys-gis/multiple-did-identities/src/did-methods/DidPeerMethod';
import { processMediatorOOB } from '../services/DIDCommRoutingService';

describe('DIDCommRoutingService', () => {
it('should process a valid OOB invitation for mediator coordination', async () => {
const oob =
'https://mediator.rootsid.cloud?_oob=eyJ0eXBlIjoiaHR0cHM6Ly9kaWRjb21tLm9yZy9vdXQtb2YtYmFuZC8yLjAvaW52aXRhdGlvbiIsImlkIjoiNDM3MmIxODctMDk5Zi00MjYxLWFlZTctZjQwZWM5ZTg3Zjg3IiwiZnJvbSI6ImRpZDpwZWVyOjIuRXo2TFNtczU1NVloRnRobjFXVjhjaURCcFptODZoSzl0cDgzV29qSlVteFBHazFoWi5WejZNa21kQmpNeUI0VFM1VWJiUXc1NHN6bTh5dk1NZjFmdEdWMnNRVllBeGFlV2hFLlNleUpwWkNJNkltNWxkeTFwWkNJc0luUWlPaUprYlNJc0luTWlPaUpvZEhSd2N6b3ZMMjFsWkdsaGRHOXlMbkp2YjNSemFXUXVZMnh2ZFdRaUxDSmhJanBiSW1ScFpHTnZiVzB2ZGpJaVhYMCIsImJvZHkiOnsiZ29hbF9jb2RlIjoicmVxdWVzdC1tZWRpYXRlIiwiZ29hbCI6IlJlcXVlc3RNZWRpYXRlIiwibGFiZWwiOiJNZWRpYXRvciIsImFjY2VwdCI6WyJkaWRjb21tL3YyIl19fQ';
await processMediatorOOB(oob);
});

it.each([
['Invalid OOB format', 'invalid_oob_string'],
['Missing _oob query parameter', 'https://mediator.rootsid.cloud'],
[
'Missing _oob in query string',
'https://mediator.rootsid.cloud?missing_oob_parameter',
],
[
'Improperly formatted base64',
'https://mediator.rootsid.cloud?_oob=invalid_base64',
],
[
'Invalid JSON in base64',
'https://mediator.rootsid.cloud?_oob=eyJmb28iOiJ9}',
],
[
'Empty DID from OOB payload',
'https://mediator.rootsid.cloud?_oob=eyJmcm9tIjoiIn0',
],
])('should throw an error for %s', async (_, oob) => {
await expect(processMediatorOOB(oob)).rejects.toThrow();
});

it('should throw an error for invalid DID generation', async () => {
jest
.spyOn(DidPeerMethod.prototype, 'generateMethod2')
.mockRejectedValueOnce(new Error('Invalid DID generation'));
const oob =
'https://mediator.rootsid.cloud?_oob=eyJ0eXBlIjoiaHR0cHM6Ly9kaWRjb21tLm9yZy9vdXQtb2YtYmFuZC8yLjAvaW52aXRhdGlvbiIsImlkIjoiNDM3MmIxODctMDk5Zi00MjYxLWFlZTctZjQwZWM5ZTg3Zjg3IiwiZnJvbSI6ImRpZDpwZWVyOjIuRXo2TFNtczU1NVloRnRobjFXVjhjaURCcFptODZoSzl0cDgzV29qSlVteFBHazFoWi5WejZNa21kQmpNeUI0VFM1VWJiUXc1NHN6bTh5dk1NZjFmdEdWMnNRVllBeGFlV2hFLlNleUpwWkNJNkltNWxkeTFwWkNJc0luUWlPaUprYlNJc0luTWlPaUpvZEhSd2N6b3ZMMjFsWkdsaGRHOXlMbkp2YjNSemFXUXVZMnh2ZFdRaUxDSmhJanBiSW1ScFpHTnZiVzB2ZGpJaVhYMCIsImJvZHkiOnsiZ29hbF9jb2RlIjoicmVxdWVzdC1tZWRpYXRlIiwiZ29hbCI6IlJlcXVlc3RNZWRpYXRlIiwibGFiZWwiOiJNZWRpYXRvciJ9fQ';
await expect(processMediatorOOB(oob)).rejects.toThrow(
'Invalid DID generation',
);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// handleOOBInvitation.test.ts
import { handleOOBInvitation } from '../services/HandleOOBInvitation';
import { Wallet } from '../services/Wallet';
import { validOutOfBandInvitation, invalidEncodedUrl } from './OOBTestFixtures';
import { invalidEncodedUrl, validOutOfBandInvitation } from './OOBTestFixtures';

beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation(() => {});
Expand Down
2 changes: 1 addition & 1 deletion packages/contact-exchange/src/tests/OOBParser.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { parseOOBInvitation } from '../services/OOBParser';
import { OutOfBandInvitation } from '../services/DIDCommOOBInvitation';
import { parseOOBInvitation } from '../services/OOBParser';
import { validEncodedUrl, validOutOfBandInvitation } from './OOBTestFixtures';

beforeEach(() => {
Expand Down
Loading
Loading