Skip to content

Commit

Permalink
feat: Add did import api [DEV-3487] (#457)
Browse files Browse the repository at this point in the history
* feat: Add key/import api

* Update routes

* Update description

* Update auth middleware

* Add salt and fix issues

* feat: Add did import api

* Update auth for routes

* fix: Fix decryption bug

* feat: Add import did and key validators

* Make type as const
  • Loading branch information
DaevMithran authored Jan 4, 2024
1 parent d7256f4 commit 75232f0
Show file tree
Hide file tree
Showing 15 changed files with 354 additions and 72 deletions.
111 changes: 61 additions & 50 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@veramo/did-resolver": "^5.5.3",
"@veramo/key-manager": "^5.5.3",
"@veramo/kms-local": "^5.5.3",
"@veramo/utils": "^5.5.3",
"@verida/account-node": "^2.3.9",
"@verida/client-ts": "^2.3.9",
"@verida/encryption-utils": "^2.2.3",
Expand Down
3 changes: 2 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,13 @@ class App {

// Keys API
app.post('/key/create', new KeyController().createKey);
app.post('/key/import', new KeyController().importKey);
app.post('/key/import', KeyController.importKeyValidator, new KeyController().importKey);
app.get('/key/read/:kid', new KeyController().getKey);

// DIDs API
app.post('/did/create', DIDController.createDIDValidator, new DIDController().createDid);
app.post('/did/update', DIDController.updateDIDValidator, new DIDController().updateDid);
app.post('/did/import', DIDController.importDIDValidator, new DIDController().importDid);
app.post('/did/deactivate/:did', DIDController.deactivateDIDValidator, new DIDController().deactivateDid);
app.get('/did/list', new DIDController().getDids);
app.get('/did/search/:did', new DIDController().resolveDidUrl);
Expand Down
137 changes: 134 additions & 3 deletions src/controllers/did.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import {
} from '@cheqd/sdk';
import { StatusCodes } from 'http-status-codes';
import { IdentityServiceStrategySetup } from '../services/identity/index.js';
import { generateDidDoc, getQueryParams } from '../helpers/helpers.js';
import { decryptPrivateKey, generateDidDoc, getQueryParams } from '../helpers/helpers.js';
import { bases } from 'multiformats/basics';
import { base64ToBytes } from 'did-jwt';
import type { CreateDidRequestBody } from '../types/shared.js';

import type { CreateDidRequestBody, KeyImportRequest } from '../types/shared.js';
import { check, validationResult, param } from './validator/index.js';
import type { IKey, RequireOnly } from '@veramo/core';
import { extractPublicKeyHex } from '@veramo/utils';

export class DIDController {
// ToDo: improve validation in a "bail" fashion
Expand Down Expand Up @@ -78,6 +79,26 @@ export class DIDController {

public static deactivateDIDValidator = [param('did').exists().isString().isDID().bail()];

public static importDIDValidator = [
check('did').isDID().bail(),
check('controllerKeyId').optional().isString().withMessage('controllerKeyId should be a string').bail(),
check('keys')
.isArray()
.withMessage('Keys should be an array of KeyImportRequest objects used in the DID-VerificationMethod')
.custom((value) => {
return value.every(
(item: KeyImportRequest) =>
item.privateKeyHex &&
typeof item.encrypted === 'boolean' &&
(item.encrypted === true ? item.ivHex && item.salt : true)
);
})
.withMessage(
'KeyImportRequest object is invalid, privateKeyHex is required, Property ivHex, salt is required when encrypted is set to true'
)
.bail(),
];

/**
* @openapi
*
Expand Down Expand Up @@ -324,6 +345,116 @@ export class DIDController {
}
}

/**
* @openapi
*
* /did/import:
* post:
* tags: [ DID ]
* summary: Import a DID Document.
* description: This endpoint imports a decentralized identifier associated with the user's account for custodian-mode clients.
* requestBody:
* content:
* application/x-www-form-urlencoded:
* schema:
* $ref: '#/components/schemas/DidImportRequest'
* application/json:
* schema:
* $ref: '#/components/schemas/DidImportRequest'
* responses:
* 200:
* description: The request was successful.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/DidResult'
* 400:
* description: A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/InvalidRequest'
* example:
* error: InvalidRequest
* 401:
* $ref: '#/components/schemas/UnauthorizedError'
* 500:
* description: An internal error has occurred. Additional state information plus metadata may be available in the response body.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/InvalidRequest'
* example:
* error: Internal Error
*/
public async importDid(request: Request, response: Response) {
// validate request
const result = validationResult(request);

// handle error
if (!result.isEmpty()) {
return response.status(StatusCodes.BAD_REQUEST).json({ error: result.array().pop()?.msg });
}

try {
const { did, controllerKeyId, keys } = request.body;
const { didDocument } = await new IdentityServiceStrategySetup().agent.resolveDid(did);
if (!didDocument || !didDocument.verificationMethod || didDocument.verificationMethod.length === 0) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: `Invalid request: Invalid did document for ${did}`,
});
}
const publicKeyHexs: string[] = [
...new Set(
didDocument.verificationMethod.map((vm) => extractPublicKeyHex(vm)).filter((pk) => pk) || []
),
];

const keysToImport: RequireOnly<IKey, 'privateKeyHex' | 'type'>[] = [];
if (keys && keys.length === publicKeyHexs.length) {
// import keys
keysToImport.push(
...(await Promise.all(
keys.map(async (key: any) => {
const { type, encrypted, ivHex, salt } = key;
let { privateKeyHex } = key;
if (encrypted) {
if (ivHex && salt) {
privateKeyHex = toString(
await decryptPrivateKey(privateKeyHex, ivHex, salt),
'hex'
);
} else {
throw new Error(
`Invalid request: Property ivHex, salt is required when encrypted is set to true`
);
}
}

return {
type: type || 'Ed25519',
privateKeyHex,
};
})
))
);
} else if (keys) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: `Invalid request: Provide all the required keys`,
});
}

const identifier = await new IdentityServiceStrategySetup(
response.locals.customer.customerId
).agent.importDid(did, keys, controllerKeyId, response.locals.customer);
return response.status(StatusCodes.OK).json(identifier);
} catch (error) {
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: `Internal error: ${(error as Error)?.message || error}`,
});
}
}

/**
* @openapi
*
Expand Down
11 changes: 11 additions & 0 deletions src/controllers/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,19 @@ import { StatusCodes } from 'http-status-codes';
import { IdentityServiceStrategySetup } from '../services/identity/index.js';
import { decryptPrivateKey } from '../helpers/helpers.js';
import { toString } from 'uint8arrays';
import { check } from 'express-validator';

export class KeyController {
public static importKeyValidator = [
check('privateKeyHex').isString().withMessage('privateKeyHex is required').bail(),
check('encrypted')
.isBoolean()
.withMessage('encrypted is required')
.custom((value, { req }) => (value === true ? req.ivHex && req.salt : true))
.withMessage('Property ivHex, salt is required when encrypted is set to true')
.bail(),
];

/**
* @openapi
*
Expand Down
2 changes: 1 addition & 1 deletion src/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ export async function decryptPrivateKey(encryptedPrivateKeyHex: string, ivHex: s
throw new Error('Missing encryption secret');
}
// derive key from passphrase
const derivedKey = await deriveSymmetricKeyFromSecret(salt, process.env.ENCRYPTION_SECRET);
const derivedKey = await deriveSymmetricKeyFromSecret(process.env.ENCRYPTION_SECRET, salt);

// unwrap encrypted key with iv
const encryptedKey = Buffer.from(encryptedPrivateKeyHex, 'hex');
Expand Down
2 changes: 2 additions & 0 deletions src/middleware/auth/routes/did-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export class DidAuthHandler extends BaseAuthHandler {
this.registerRoute('/did/update', 'POST', 'update:did:mainnet');
this.registerRoute('/did/deactivate', 'POST', 'deactivate:did:testnet');
this.registerRoute('/did/deactivate', 'POST', 'deactivate:did:mainnet');
this.registerRoute('/did/import', 'POST', 'import:did:testnet');
this.registerRoute('/did/import', 'POST', 'import:did:mainnet');
// Unauthorized routes
this.registerRoute('/did/search/(.*)', 'GET', '', { allowUnauthorized: true, skipNamespace: true });
}
Expand Down
5 changes: 3 additions & 2 deletions src/services/identity/abstract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type {
DIDDocument,
DIDResolutionResult,
IIdentifier,
IKey,
IVerifyResult,
ManagedKeyInfo,
PresentationPayload,
Expand Down Expand Up @@ -78,8 +79,8 @@ export abstract class AbstractIdentityService implements IIdentityService {

importDid(
did: string,
privateKeyHex: string,
publicKeyHex: string,
keys: Pick<IKey, 'privateKeyHex' | 'type'>[],
controllerKeyId: string,
customer: CustomerEntity
): Promise<IIdentifier> {
throw new Error(`Not supported`);
Expand Down
17 changes: 10 additions & 7 deletions src/services/identity/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {
ICreateVerifiablePresentationArgs,
IDIDManager,
IIdentifier,
IKey,
IKeyManager,
IResolver,
IVerifyResult,
MinimalImportableIdentifier,
MinimalImportableKey,
PresentationPayload,
TAgent,
VerifiableCredential,
Expand Down Expand Up @@ -244,21 +244,24 @@ export class Veramo {
async importDid(
agent: TAgent<IDIDManager>,
did: string,
privateKeyHex: string,
publicKeyHex: string
keys: Pick<IKey, 'privateKeyHex' | 'type'>[],
controllerKeyId: string | undefined
): Promise<IIdentifier> {
const [kms] = await agent.keyManagerGetKeyManagementSystems();

if (!did.match(DefaultDidUrlPattern)) {
throw new Error('Invalid DID');
}

const key: MinimalImportableKey = { kms: kms, kid: publicKeyHex, type: 'Ed25519', privateKeyHex, publicKeyHex };

const identifier: IIdentifier = await agent.didManagerImport({
keys: [key],
keys: keys.map((key) => {
return {
...key,
kms,
};
}),
did,
controllerKeyId: key.kid,
controllerKeyId,
} as MinimalImportableIdentifier);

return identifier;
Expand Down
8 changes: 7 additions & 1 deletion src/services/identity/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
DIDDocument,
DIDResolutionResult,
IIdentifier,
IKey,
IVerifyResult,
ManagedKeyInfo,
PresentationPayload,
Expand Down Expand Up @@ -74,7 +75,12 @@ export interface IIdentityService {
resolveDid(did: string): Promise<DIDResolutionResult>;
resolve(didUrl: string): Promise<Response>;
getDid(did: string, customer: CustomerEntity): Promise<any>;
importDid(did: string, privateKeyHex: string, publicKeyHex: string, customer: CustomerEntity): Promise<IIdentifier>;
importDid(
did: string,
keys: Pick<IKey, 'privateKeyHex' | 'type'>[],
controllerKeyId: string | undefined,
customer: CustomerEntity
): Promise<IIdentifier>;
createResource(network: string, payload: ResourcePayload, customer: CustomerEntity): Promise<any>;
createCredential(
credential: CredentialPayload,
Expand Down
12 changes: 9 additions & 3 deletions src/services/identity/local.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as dotenv from 'dotenv';
import type { IIdentifier, CredentialPayload, VerifiableCredential, IVerifyResult } from '@veramo/core';
import type { IIdentifier, CredentialPayload, VerifiableCredential, IVerifyResult, TKeyType } from '@veramo/core';

Check failure on line 2 in src/services/identity/local.ts

View workflow job for this annotation

GitHub Actions / Build & Test / Build Node.js

'TKeyType' is declared but never used.
import { MemoryPrivateKeyStore } from '@veramo/key-manager';
import { KeyManagementSystem } from '@veramo/kms-local';
import {
Expand Down Expand Up @@ -122,11 +122,17 @@ export class LocalIdentityService extends DefaultIdentityService {
try {
return await this.getDid(ISSUER_DID);
} catch {
const key = {
kid: ISSUER_PUBLIC_KEY_HEX,
type: 'Ed25519' as const,
privateKeyHex: ISSUER_PRIVATE_KEY_HEX,
publicKeyHex: ISSUER_PUBLIC_KEY_HEX,
};
const identifier: IIdentifier = await Veramo.instance.importDid(
this.initAgent(),
ISSUER_DID,
ISSUER_PRIVATE_KEY_HEX,
ISSUER_PUBLIC_KEY_HEX
[key],
key.kid
);
return identifier;
}
Expand Down
8 changes: 4 additions & 4 deletions src/services/identity/postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
CredentialPayload,
DIDDocument,
IIdentifier,
IKey,
IVerifyResult,
PresentationPayload,
VerifiableCredential,
Expand Down Expand Up @@ -289,16 +290,15 @@ export class PostgresIdentityService extends DefaultIdentityService {

async importDid(
did: string,
privateKeyHex: string,
publicKeyHex: string,
keys: Pick<IKey, 'privateKeyHex' | 'type'>[],
controllerKeyId: string | undefined,
customer: CustomerEntity
): Promise<IIdentifier> {
if (!did.match(DefaultDidUrlPattern)) {
throw new Error('Invalid DID');
}

// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const identifier: IIdentifier = await Veramo.instance.importDid(this.agent!, did, privateKeyHex, publicKeyHex);
const identifier: IIdentifier = await Veramo.instance.importDid(this.agent!, did, keys, controllerKeyId);
await IdentifierService.instance.update(identifier.did, customer);
return identifier;
}
Expand Down
Loading

0 comments on commit 75232f0

Please sign in to comment.