@microsoft/useragent-sdk
Version:
SDK for building decentralized identity wallets and enterprise agents.
175 lines (152 loc) • 6.74 kB
text/typescript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
require('es6-promise').polyfill();
import base64url from 'base64url';
import 'isomorphic-fetch';
import Identifier from '../Identifier';
import IdentifierDocument from '../IdentifierDocument';
import UserAgentError from '../UserAgentError';
import UserAgentOptions from '../UserAgentOptions';
import IRegistrar from './IRegistrar';
import Multihash from './Multihash';
import CryptoFactory from '../crypto/plugin/CryptoFactory';
import RegistrarCryptoOptions from './RegistrarCryptoOptions';
import IPayloadProtectionOptions from '../crypto/protocols/IPayloadProtectionOptions';
import PayloadSigningStrategy from '../crypto/strategies/PayloadSigningStrategy';
import JoseProtocol from '../crypto/protocols/jose/JoseProtocol';
const cloneDeep = require('lodash/fp/cloneDeep');
declare var fetch: any;
/**
* Registrar implementation for the Sidetree (ION) network
*/
export default class SidetreeRegistrar implements IRegistrar {
private timeoutInMilliseconds: number;
private options: UserAgentOptions;
private cryptoFactory: CryptoFactory;
/**
* Crypto options for the Registrar
*/
public registrarCryptoOptions: RegistrarCryptoOptions;
/**
* Constructs a new instance of the Sidetree registrar
* @param url to the registration endpoint at the registrar
* @param options to configure the registrar.
*/
constructor (public url: string, options: UserAgentOptions) {
// Set options. Stringify to avoid circular exception during serialization of this object.
if (!(options && options.keyStore)) {
throw new UserAgentError('options and options.keyStore need to be defined');
}
this.options = options;
this.cryptoFactory = options.cryptoFactory;
this.registrarCryptoOptions = options.registrar ? options.registrar.registrarCryptoOptions : new RegistrarCryptoOptions(this.cryptoFactory, new JoseProtocol());
// Format the url
this.url = `${url.replace(/\/?$/, '/')}`;
this.timeoutInMilliseconds =
1000 *
(!options || !options.timeoutInSeconds
? 30
: options.timeoutInSeconds);
}
/**
* Prepare the document for registration
* @param document Document to format
*/
public prepareDocForRegistration (document: IdentifierDocument): IdentifierDocument {
delete document.id;
return document;
}
/**
* Registers the identifier document on the ledger
* returning the identifier generated by the registrar.
* @param identifierDocument to register.
* @param keyReference Reference to the identifier for the signing key.
*/
public async register (
identifierDocument: IdentifierDocument,
keyReference: string
): Promise<Identifier> {
return new Promise(async (resolve, reject) => {
const timer = setTimeout(
() =>
reject(new UserAgentError('Fetch timed out.')),
this.timeoutInMilliseconds
);
// prepare document for registration
identifierDocument = this.prepareDocForRegistration(identifierDocument);
let bodyString = JSON.stringify(identifierDocument);
console.debug(bodyString);
// registration with signed message for bodyString
const encodedBodyString = await this.sign(keyReference, bodyString);
const fetchOptions = {
method: 'POST',
body: encodedBodyString,
headers: {
'Content-Type': 'application/json',
'Content-Length': encodedBodyString.length.toString()
}
};
// Now call the actual fetch with the updated options
const response = await fetch(this.url, fetchOptions);
// Got a response so clear the timer
clearTimeout(timer);
if (!response.ok) {
let msg: string;
if (response.body) {
msg = response.body.read().toString();;
} else {
msg = `Status ${response.status}`;
}
const error = new UserAgentError(
`Failed to register the identifier document: ${msg}`
);
reject(error);
return;
}
const responseJson = IdentifierDocument.fromJSON(await response.json());
const identifier = new Identifier(responseJson, <UserAgentOptions> this.options);
resolve(identifier);
});
}
/**
* Sign the registration payload
* @param keyReference Reference to signature key
* @param bodyString original payload to sign
*/
public async sign(keyReference: string, bodyString: string): Promise<string> {
const strategy: PayloadSigningStrategy = this.registrarCryptoOptions.protectionStrategy.payloadSigningStrategy;
const options: IPayloadProtectionOptions = strategy.protectionOptions;
const protocol = options.payloadProtection;
const signature = await protocol.sign(keyReference, Buffer.from(bodyString), strategy.serializationFormat, options);
return protocol.serialize(signature, strategy.serializationFormat, options);
}
/**
* Uses the specified input to create a basic Sidetree
* compliant identifier document and then hashes the document
* in accordance with the Sidetree protocol specification
* to generate and return the identifier.
*
* @param identifierDocument for which to generate the identifier.
*/
public async generateIdentifier (identifierDocument: IdentifierDocument
): Promise<Identifier> {
if (!Array.isArray(identifierDocument.publicKeys) || identifierDocument.publicKeys.length === 0) {
throw new UserAgentError('At least one public key must be specified in the identifier document.');
}
// The genesis document is used for generating the hash,
// but we need to ensure that the id property of the document
// if specified is removed beforehand.
const genesisDocument = cloneDeep(identifierDocument);
genesisDocument.id = undefined;
// Hash the document JSON
const documentBuffer = Buffer.from(JSON.stringify(genesisDocument));
const hashedDocument = Multihash.hash(documentBuffer, 18);
const encodedDocument = base64url.encode(hashedDocument);
// Now update the identifier property in
// the genesis document
genesisDocument.id = `did:ion:test:${encodedDocument}`;
return new Identifier(genesisDocument);
}
}