From e013aaec23f0f77caedc10081de191a2d6f314f8 Mon Sep 17 00:00:00 2001 From: Hidde-Jan Jongsma Date: Thu, 14 May 2020 17:07:32 +0200 Subject: [PATCH] Add connector registration in organization service --- src/connectors/connectors.module.ts | 1 + src/connectors/connectors.service.ts | 9 + src/connectors/irma/irma.service.ts | 1 + .../jolocom/jolocom-credential-type.entity.ts | 33 ++ .../jolocom/jolocom-wallet.entity.ts | 54 +++ src/connectors/jolocom/jolocom.module.ts | 5 + src/connectors/jolocom/jolocom.service.ts | 326 +++++++++++++++++- src/organizations/organizations.module.ts | 3 +- src/organizations/organizations.service.ts | 25 +- 9 files changed, 448 insertions(+), 9 deletions(-) create mode 100644 src/connectors/jolocom/jolocom-credential-type.entity.ts diff --git a/src/connectors/connectors.module.ts b/src/connectors/connectors.module.ts index de014ce..0efc49b 100644 --- a/src/connectors/connectors.module.ts +++ b/src/connectors/connectors.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; + import { JolocomModule } from './jolocom/jolocom.module'; import { IrmaModule } from './irma/irma.module'; import { ConnectorsService } from './connectors.service'; diff --git a/src/connectors/connectors.service.ts b/src/connectors/connectors.service.ts index f52a348..9b06dcf 100644 --- a/src/connectors/connectors.service.ts +++ b/src/connectors/connectors.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { JolocomService } from './jolocom/jolocom.service'; import { IrmaService } from './irma/irma.service'; import { ConnectorService } from './connector-service.interface'; +import { Organization } from '../organizations/organization.entity'; @Injectable() export class ConnectorsService { @@ -18,4 +19,12 @@ export class ConnectorsService { getConnector(type: string) { return this.connectors.find(connector => connector.type === type); } + + async registerOrganization(organization: Organization) { + await Promise.all( + this.connectors.map( + async connector => await connector.registerOrganization(organization), + ), + ); + } } diff --git a/src/connectors/irma/irma.service.ts b/src/connectors/irma/irma.service.ts index 5444212..41150bb 100644 --- a/src/connectors/irma/irma.service.ts +++ b/src/connectors/irma/irma.service.ts @@ -7,6 +7,7 @@ export class IrmaService implements ConnectorService { type = 'irma'; async registerOrganization(organization: Organization) { + // We don't need to do anything for IRMA. return; } } diff --git a/src/connectors/jolocom/jolocom-credential-type.entity.ts b/src/connectors/jolocom/jolocom-credential-type.entity.ts new file mode 100644 index 0000000..9e286d6 --- /dev/null +++ b/src/connectors/jolocom/jolocom-credential-type.entity.ts @@ -0,0 +1,33 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + ManyToOne, + Column, +} from 'typeorm'; +import { JolocomWallet } from './jolocom-wallet.entity'; +import { BaseMetadata } from 'cred-types-jolocom-core/js/types'; + +@Entity() +export class JolocomCredentialType extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @ManyToOne( + () => JolocomWallet, + wallet => wallet.credentialOffers, + ) + wallet: JolocomWallet; + + @Column({ unique: true }) + type: string; + + @Column() + name: string; + + @Column('simple-json') + context: BaseMetadata['context']; + + @Column('simple-json') + claimInterface: BaseMetadata['claimInterface']; +} diff --git a/src/connectors/jolocom/jolocom-wallet.entity.ts b/src/connectors/jolocom/jolocom-wallet.entity.ts index e69de29..0d8f757 100644 --- a/src/connectors/jolocom/jolocom-wallet.entity.ts +++ b/src/connectors/jolocom/jolocom-wallet.entity.ts @@ -0,0 +1,54 @@ +import { + BaseEntity, + Entity, + PrimaryGeneratedColumn, + Column, + OneToOne, + JoinColumn, + OneToMany, +} from 'typeorm'; +import { randomBytes } from 'crypto'; + +import { Organization } from '../../organizations/organization.entity'; +import { JolocomCredentialType } from './jolocom-credential-type.entity'; + +const JOLOCOM_WALLET_SEED_BYTES = 32; +const JOLOCOM_WALLET_PASSWORD_BYTES = 16; + +@Entity() +export class JolocomWallet extends BaseEntity { + @PrimaryGeneratedColumn() + id: number; + + @Column({ update: false }) + encryptedSeedHex: string; + + @Column({ update: false }) + password: string; + + @OneToOne(() => Organization) + @JoinColumn() + organization: Organization; + + @OneToMany( + () => JolocomCredentialType, + credentialOffer => credentialOffer.wallet, + ) + credentialOffers: JolocomCredentialType[]; + + static randomPassword() { + return randomBytes(JOLOCOM_WALLET_PASSWORD_BYTES).toString('hex'); + } + + static randomSeed() { + return randomBytes(JOLOCOM_WALLET_SEED_BYTES); + } + + get encryptedSeed(): Buffer { + return Buffer.from(this.encryptedSeedHex, 'hex'); + } + + set encryptedSeed(seed: Buffer) { + this.encryptedSeedHex = seed.toString('hex'); + } +} diff --git a/src/connectors/jolocom/jolocom.module.ts b/src/connectors/jolocom/jolocom.module.ts index 4c50046..5d479fa 100644 --- a/src/connectors/jolocom/jolocom.module.ts +++ b/src/connectors/jolocom/jolocom.module.ts @@ -1,7 +1,12 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + import { JolocomService } from './jolocom.service'; +import { JolocomWallet } from './jolocom-wallet.entity'; +import { JolocomCredentialType } from './jolocom-credential-type.entity'; @Module({ + imports: [TypeOrmModule.forFeature([JolocomWallet, JolocomCredentialType])], providers: [JolocomService], exports: [JolocomService], }) diff --git a/src/connectors/jolocom/jolocom.service.ts b/src/connectors/jolocom/jolocom.service.ts index c5386d2..90cecc2 100644 --- a/src/connectors/jolocom/jolocom.service.ts +++ b/src/connectors/jolocom/jolocom.service.ts @@ -1,12 +1,332 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; +import { JolocomLib } from 'jolocom-lib'; +import { JolocomRegistry } from 'jolocom-lib/js/registries/jolocomRegistry'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + import { ConnectorService } from '../connector-service.interface'; -import { Organization } from 'src/organizations/organization.entity'; +import { Organization } from '../../organizations/organization.entity'; +import { JolocomWallet } from './jolocom-wallet.entity'; +import { JolocomCredentialType } from './jolocom-credential-type.entity'; @Injectable() export class JolocomService implements ConnectorService { type = 'jolocom'; + private registry: JolocomRegistry; + private logger: Logger; + + constructor( + @InjectRepository(JolocomWallet) + private walletRepository: Repository, + ) { + this.registry = JolocomLib.registries.jolocom.create(); + this.logger = new Logger(JolocomService.name); + } + + /* ConnectorService methods */ + async registerOrganization(organization: Organization) { - return; + const wallet = await this.createWalletForOrganization(organization); + await this.fuelWallet(wallet); + await this.registerWallet(wallet); + } + + async createWalletForOrganization(organization: Organization) { + this.logger.log(`Creating wallet for ${organization.name}`); + const seed = JolocomWallet.randomSeed(); + const password = JolocomWallet.randomPassword(); + const keyProvider = JolocomLib.KeyProvider.fromSeed(seed, password); + + const wallet = new JolocomWallet(); + wallet.organization = organization; + wallet.password = password; + wallet.encryptedSeedHex = keyProvider.encryptedSeed; // Already in hex format + + await this.walletRepository.save(wallet); + this.logger.log(`Created wallet for ${organization.name}`); + return wallet; + } + + async registerWallet(wallet: JolocomWallet) { + this.logger.log(`Wallet registration started`); + const keyProvider = this.getKeyProvider(wallet); + const identityWallet = await this.registry.create( + keyProvider, + wallet.password, + ); + this.logger.log(`Wallet registration successful`); + + // TODO: Maybe we shouldn't return here. + return identityWallet; + } + + async getIdentityWallet(wallet: JolocomWallet) { + const keyProvider = this.getKeyProvider(wallet); + const identityWallet = await this.registry.authenticate(keyProvider, { + derivationPath: JolocomLib.KeyTypes.jolocomIdentityKey, + encryptionPass: wallet.password, + }); + return identityWallet; + } + + async fuelWallet(wallet: JolocomWallet) { + this.logger.log(`Wallet fueling started`); + const publicEthKey = this.getPublicEthKey(wallet); + await JolocomLib.util.fuelKeyWithEther(publicEthKey); + this.logger.log(`Wallet fueling started`); + } + + getPublicEthKey(wallet: JolocomWallet) { + const keyProvider = this.getKeyProvider(wallet); + return keyProvider.getPublicKey({ + encryptionPass: wallet.password, + derivationPath: JolocomLib.KeyTypes.ethereumKey, + }); + } + + getKeyProvider(wallet: JolocomWallet) { + return new JolocomLib.KeyProvider(wallet.encryptedSeed); } + + /* Jolocom specific */ + // public async processCredentialIssueRequest( + // request: CredentialIssueRequest, + // response: Response, + // ): Promise { + // const credOffer = await this.createCredentialOfferToken(request); + // const token = credOffer.encode(); + + // const viewData = { + // requestId: request.requestId, + // qr: await QRCode.toDataURL(token), + // }; + // console.log('Jolocom CredentialOfferToken: ', credOffer.encode()); + + // return response.render('jolocom/issue', viewData); + // } + + // public async processCredentialVerifyRequest( + // verifyRequest: CredentialVerifyRequest, + // response: Response, + // ): Promise { + // // Create Jolocom interaction token + // const credRequestToken = await this.createCredentialRequestToken( + // verifyRequest, + // ); + + // const jwt = credRequestToken.encode(); + + // // Render interaction token (qr code) to user + // const viewData = { + // requestId: verifyRequest.requestId, + // qr: await QRCode.toDataURL(jwt), + // }; + + // return response.render('jolocom/verify', viewData); + // } + + /** + * Instantiate a Jolocom IdentityWallet + * This wallet must already be registered on Etherium + * + * @param organization the organization for which an identifity wallet is instantiated + */ + // protected async getIdentityWallet( + // organization: Organization, + // ): Promise { + // const password = organization.walletConfigs.jolocom!.password; + // const seed = Buffer.from(organization.walletConfigs.jolocom!.seed, 'hex'); + + // /** + // * From Jolocom Documentation: + // * You will need to instantiate a Key Provider using the seed used for identity creation + // * We are currently working on simplifying, and optimising this part of the api + // */ + // const vaultedKeyProvider = JolocomLib.KeyProvider.fromSeed(seed, password); + // const registry = JolocomLib.registries.jolocom.create(); + + // return await registry.authenticate(vaultedKeyProvider, { + // derivationPath: JolocomLib.KeyTypes.jolocomIdentityKey, + // encryptionPass: password, + // }); + // } + + // /** + // * Construct a Jolocom CredentialOffer interaction token + // * + // * @param issueRequest the credential issue request + // */ + // protected async createCredentialOfferToken( + // issueRequest: CredentialIssueRequest, + // ): Promise> { + // const issuer = issueRequest.getIssuer(); + // const identityWallet = await this.getIdentityWallet(issuer); + // const { credentialOffers, password } = issuer.walletConfigs.jolocom!; + + // // Get the Jolocom offeredType and metadata from configured list with credential offers + // const { + // schema: { type: offeredType }, + // metadata = {}, + // } = credentialOffers[issueRequest.credentialType]; // Use the credential type URI as defined in the CredentialIssueRequest + + // // Return a Jolocom CredentialOffert interaction token + // return await identityWallet.create.interactionTokens.request.offer( + // { + // callbackURL: SSIServiceApp.getUrl( + // `/connectors/jolocom/issue/${issueRequest.requestId}`, + // ), + // offeredCredentials: [ + // { + // type: offeredType[offeredType.length - 1], + // ...metadata, + // }, + // ], + // }, + // password, + // ); + // } + + // /** + // * Construct a Jolocom CredentialReceive interaction token + // * to actual issue the credential to the user's wallet app + // * + // * @param issueRequest the credential issue request + // * @param jolocomOfferResponse the Jolocom CredentialOfferResponse that is received from the user's wallet app + // */ + // public async createCredential( + // issueRequest: CredentialIssueRequest, + // jolocomOfferResponse: JSONWebToken, + // ): Promise> { + // const issuer = issueRequest.getIssuer(); // the organization that wants to issue a credential + // const identityWallet = await this.getIdentityWallet(issuer); // jolocom identity wallet of the issuer + // const password = issuer.walletConfigs.jolocom!.password; // password to sign credential + // const subject = jolocomOfferResponse.issuer; // the wallet app that wants to receive a credential + // const credentialType = issuer.walletConfigs.jolocom!.credentialOffers[ + // issueRequest.credentialType + // ]; + + // // Create actual credential (with data) + // const credential = await identityWallet.create.signedCredential( + // { + // metadata: credentialType.schema, + // claim: { + // ...issueRequest.credentialData, + // }, + // subject: keyIdToDid(subject), + // }, + // password, + // ); + + // // Wrap credential in Jolocom interaction token object + // return await identityWallet.create.interactionTokens.response.issue( + // { + // signedCredentials: [credential.toJSON()], + // }, + // password, + // jolocomOfferResponse, + // ); + // } + + // /** + // * Construct a Jolocom CredentialRequest interaction token + // * + // * @param verifyRequest the credential verify request + // */ + // protected async createCredentialRequestToken( + // verifyRequest: CredentialVerifyRequest, + // ): Promise> { + // const verifier = verifyRequest.getVerifier(); + // const identityWallet = await this.getIdentityWallet(verifier); + // const password = verifier.walletConfigs.jolocom!.password; + // const credentialRequestToken = await identityWallet.create.interactionTokens.request.share( + // { + // callbackURL: SSIServiceApp.getUrl( + // `/connectors/jolocom/verify/${verifyRequest.requestId}`, + // ), + // credentialRequirements: [ + // { + // type: ['Credential', verifyRequest.credentialType], + // constraints: [ + // // TODO: implement check on allowed issuers + // // constraintFunctions.is( + // // "issuer", + // // "did:jolo:ed19430d6e28057194870dc9b19c1ca2ad099ff090b52350add129f1049bb65d" + // // ) + // ], + // }, + // ], + // }, + // password, + // ); + + // // Save CredentialRequest because it is needed to verify a CredentialResponse token in the + // // next step. See method receiveCredential() below + // Store.set(credentialRequestToken.nonce, credentialRequestToken.encode()); + + // return credentialRequestToken; + // } + + // public async receiveCredential( + // verifyRequest: CredentialVerifyRequest, + // token: string, + // ): Promise { + // const jolocomCredentialResponse = JolocomLib.parse.interactionToken.fromJWT( + // token, + // ); + + // // TODO: check if this is needed here or can be done in JolocomConnector.receiveCredential() method + // if (!JolocomLib.util.validateDigestable(jolocomCredentialResponse)) { + // throw new Error('Invalid signature'); + // // res.status(401).send("Invalid signature on interaction token"); + // } + + // const identityWallet = await this.getIdentityWallet( + // verifyRequest.getVerifier(), + // ); + + // // Get the CredentialShareToken (issued in previous interaction step) + // const jolocomCredentialRequestJWT: string = Store.get( + // jolocomCredentialResponse.nonce, + // ); + + // const jolocomCredentialRequest: JSONWebToken = JolocomLib.parse.interactionToken.fromJWT( + // jolocomCredentialRequestJWT, + // ); + + // // The validate method will ensure the response contains a valid signature, is not expired, + // // lists our did in the aud (audience) section, and contains the same jti (nonce) as the request. + // // TODO: move this to route middleware + // await identityWallet.validateJWT( + // jolocomCredentialResponse, + // jolocomCredentialRequest, + // ); + + // const credentialResponse = jolocomCredentialResponse.interactionToken as CredentialResponse; + + // // We check against the request we created in a previous step + // const validResponse = credentialResponse.satisfiesRequest( + // jolocomCredentialRequest.interactionToken, + // ); + + // if (!validResponse) { + // throw new Error('Incorrect credential received'); + // } + + // // Validate the provided credentials + // const providedCredentials = credentialResponse.suppliedCredentials; + // const signatureValidationResults = await JolocomLib.util.validateDigestables( + // providedCredentials, + // ); + + // if (signatureValidationResults.every(result => result === true)) { + // // The credentials can be used + // const data = providedCredentials.map(credential => credential.toJSON()); + + // // Handle the data in the provided credentials + // return verifyRequest.processCredentialData(data); + // } else { + // throw new Error('Not all provided credentials are valid'); + // } + // } } diff --git a/src/organizations/organizations.module.ts b/src/organizations/organizations.module.ts index 77244e5..9296655 100644 --- a/src/organizations/organizations.module.ts +++ b/src/organizations/organizations.module.ts @@ -4,9 +4,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { OrganizationsController } from './organizations.controller'; import { OrganizationsService } from './organizations.service'; import { Organization } from './organization.entity'; +import { ConnectorsModule } from 'src/connectors/connectors.module'; @Module({ - imports: [TypeOrmModule.forFeature([Organization])], + imports: [TypeOrmModule.forFeature([Organization]), ConnectorsModule], controllers: [OrganizationsController], providers: [OrganizationsService], }) diff --git a/src/organizations/organizations.service.ts b/src/organizations/organizations.service.ts index 30f88db..e91b377 100644 --- a/src/organizations/organizations.service.ts +++ b/src/organizations/organizations.service.ts @@ -1,24 +1,39 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Organization } from './organization.entity'; +import { ConnectorsService } from 'src/connectors/connectors.service'; @Injectable() export class OrganizationsService { + logger: Logger; + constructor( @InjectRepository(Organization) private organizationsRepository: Repository, - ) {} + private connectorsService: ConnectorsService, + ) { + this.logger = new Logger(Organization.name); + } - findAll() { + async findAll() { return this.organizationsRepository.find(); } - createFromName(name: string) { + async findByIdentifier(uuid: string) { + const results = await this.organizationsRepository.find({ take: 1 }); + return results[0]; + } + + async createFromName(name: string) { + this.logger.log(`Creating organization with name ${name}`); const organization = new Organization(); organization.name = name; organization.sharedSecret = Organization.randomSecret(); - return this.organizationsRepository.save(organization); + await this.organizationsRepository.save(organization); + await this.connectorsService.registerOrganization(organization); + this.logger.log(`Created organization (id: ${organization.id})`); + return organization; } } -- GitLab