diff --git a/src/DecentralizedID.ts b/src/DecentralizedID.ts new file mode 100644 index 0000000..c5905a5 --- /dev/null +++ b/src/DecentralizedID.ts @@ -0,0 +1,51 @@ +import { InvalidDIDError, InvalidDIDTypeError } from './error'; + +export class DecentralizedID { + public static fromAddress(address: string): DecentralizedID { + return new DecentralizedID('btc-addr', address); + } + + public static fromPublicKey(publicKey: string): DecentralizedID { + return new DecentralizedID('ecdsa-pub', publicKey); + } + + public static fromString(str: string): DecentralizedID { + const didParts = str.split(':'); + + if (didParts.length !== 3) { + throw new InvalidDIDError(str, 'Decentralized IDs must have 3 parts'); + } + + if (didParts[0].toLowerCase() !== 'did') { + throw new InvalidDIDError(str, 'Decentralized IDs must start with "did"'); + } + + const type = didParts[1]; + const identifier = didParts[2]; + return new DecentralizedID(type, identifier); + } + + private readonly type: string; + private readonly identifier: string; + + public constructor(type: string, identifier: string) { + this.type = type; + this.identifier = identifier; + } + + public getAddress() { + if(this.type == 'btc-addr') { + return this.identifier; + } else { + throw new InvalidDIDTypeError(this.type, 'btc-addr'); + } + } + + public getType(): string { + return this.type; + } + + public toString(): string { + return `did:${this.type}:${this.identifier}`; + } +} diff --git a/src/auth/messages.ts b/src/auth/messages.ts index e3128e9..156e843 100644 --- a/src/auth/messages.ts +++ b/src/auth/messages.ts @@ -3,7 +3,7 @@ import { SECP256K1Client, TokenSigner } from 'jsontokens'; import { DEFAULT_SCOPE } from '../constants'; import { decryptECIES, encryptECIES, publicKeyToAddress } from '../crypto'; import { DebugType, Logger } from '../debug'; -import { makeDIDFromAddress } from '../dids'; +import { DecentralizedID } from '../DecentralizedID'; import { ProfileJson } from '../profile/schema/Profile.json'; import { makeUUID4, nextHour, nextMonth } from '../utils'; import { generateAndStoreTransitKey } from './app'; @@ -66,7 +66,7 @@ export function makeAuthRequest( const publicKey = SECP256K1Client.derivePublicKey(transitPrivateKey); payload.public_keys = [publicKey]; const address = publicKeyToAddress(publicKey); - payload.iss = makeDIDFromAddress(address); + payload.iss = DecentralizedID.fromAddress(address).toString(); /* Sign and return the token */ const tokenSigner = new TokenSigner('ES256k', transitPrivateKey); @@ -164,7 +164,7 @@ export function makeAuthResponse( core_token: coreTokenPayload, exp: Math.floor(expiresAt / 1000), // JWT times are in seconds iat: Math.floor(new Date().getTime() / 1000), // JWT times are in seconds - iss: makeDIDFromAddress(address), + iss: DecentralizedID.fromAddress(address).toString(), jti: makeUUID4(), private_key: privateKeyPayload, profile, diff --git a/src/auth/verification.ts b/src/auth/verification.ts index e948e72..5144e2c 100644 --- a/src/auth/verification.ts +++ b/src/auth/verification.ts @@ -1,7 +1,7 @@ import { decodeToken, TokenVerifier } from 'jsontokens'; import { publicKeyToAddress } from '../crypto'; -import { getAddressFromDID } from '../dids'; +import { DecentralizedID } from '../DecentralizedID'; import { MultiplePublicKeysNotSupportedError } from '../error'; import { isSameOriginAbsoluteUrl } from '../utils'; import { fetchAppManifest } from './provider'; @@ -49,7 +49,7 @@ export function doSignaturesMatchPublicKeys(token: string) { export function doPublicKeysMatchIssuer(token: string) { const payload = decodeToken(token).payload; const publicKeys = payload.public_keys; - const addressFromIssuer = getAddressFromDID(payload.iss); + const addressFromIssuer = DecentralizedID.fromString(payload.iss).getAddress(); if (publicKeys.length === 1) { const addressFromPublicKeys = publicKeyToAddress(publicKeys[0]); @@ -105,7 +105,7 @@ export function doPublicKeysMatchUsername(token: string, nameLookupURL: string) .then(responseJSON => { if (responseJSON.hasOwnProperty('address')) { const nameOwningAddress = responseJSON.address; - const addressFromIssuer = getAddressFromDID(payload.iss); + const addressFromIssuer = DecentralizedID.fromString(payload.iss).getAddress(); if (nameOwningAddress === addressFromIssuer) { resolve(true); } else { diff --git a/src/dids.ts b/src/dids.ts deleted file mode 100644 index bda0835..0000000 --- a/src/dids.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { InvalidDIDError } from './error'; - -export function makeDIDFromAddress(address: string) { - return `did:btc-addr:${address}`; -} - -export function makeDIDFromPublicKey(publicKey: string) { - return `did:ecdsa-pub:${publicKey}`; -} - -export function getDIDType(decentralizedID: string) { - const didParts = decentralizedID.split(':'); - - if (didParts.length !== 3) { - throw new InvalidDIDError(decentralizedID, 'Decentralized IDs must have 3 parts'); - } - - if (didParts[0].toLowerCase() !== 'did') { - throw new InvalidDIDError(decentralizedID, 'Decentralized IDs must start with "did"'); - } - - return didParts[1].toLowerCase(); -} - -export function getAddressFromDID(decentralizedID: string) { - const didType = getDIDType(decentralizedID); - if (didType === 'btc-addr') { - return decentralizedID.split(':')[2]; - } else { - return null; - } -} diff --git a/src/error/InvalidDIDTypeError.ts b/src/error/InvalidDIDTypeError.ts new file mode 100644 index 0000000..0592e70 --- /dev/null +++ b/src/error/InvalidDIDTypeError.ts @@ -0,0 +1,14 @@ +export class InvalidDIDTypeError extends Error { + public readonly name: string = 'InvalidDIDTypeError'; + public readonly message: string; + public readonly actualType: string; + public readonly expectedType: string; + + constructor(actualType: string, expectedType: string) { + super(); + + this.message = `The given DID type "${actualType}" did not match the expected "${expectedType}"`; + this.actualType = actualType; + this.expectedType = expectedType; + } +} diff --git a/src/error/index.ts b/src/error/index.ts index bd48b4c..54bb706 100644 --- a/src/error/index.ts +++ b/src/error/index.ts @@ -1,6 +1,7 @@ // ./ export { DidNotSatisfyJsonSchemaError } from './DidNotSatisfyJsonSchemaError'; export { InvalidDIDError } from './InvalidDIDError'; +export { InvalidDIDTypeError } from './InvalidDIDTypeError'; export { InvalidParameterError } from './InvalidParameterError'; export { NotImplementedError } from './NotImplementedError'; diff --git a/tests/unitTests/DecentralizedID.spec.ts b/tests/unitTests/DecentralizedID.spec.ts new file mode 100644 index 0000000..c83f1bb --- /dev/null +++ b/tests/unitTests/DecentralizedID.spec.ts @@ -0,0 +1,78 @@ +import * as chai from 'chai'; + +import { correct, incorrect } from '../fun'; + +import { DecentralizedID } from '../../src/DecentralizedID'; +import { InvalidDIDError, InvalidDIDTypeError } from '../../src/error'; + +describe('DecentralizedID', () => { + const didWithBtcAddr = new DecentralizedID('btc-addr', '1111111111111111111114oLvT2'); + const didWithPubKey = new DecentralizedID('ecdsa-pub', '000000000000000000000000000000000000000000000000000000000000000000') + + const btcAddr = '1111111111111111111114oLvT2'; + const pubKey = '000000000000000000000000000000000000000000000000000000000000000000'; + + const didStringWithBtcAddr = 'did:btc-addr:1111111111111111111114oLvT2'; + const didStringWithPubKey = 'did:ecdsa-pub:000000000000000000000000000000000000000000000000000000000000000000'; + + describe('fromAddress', () => { + it(`creates ${correct()} DID objects from an address`, () => { + const did = DecentralizedID.fromAddress('1111111111111111111114oLvT2'); + + chai.expect(did).to.deep.equal(didWithBtcAddr); + }); + }); + + describe('fromPublicKey', () => { + it(`creates ${correct()} DID Objects from a public key`, () => { + const did = DecentralizedID.fromPublicKey('000000000000000000000000000000000000000000000000000000000000000000'); + + chai.expect(did).to.deep.equal(didWithPubKey); + }); + }); + + describe('fromString', () => { + it(`creates ${correct()} DID objects from a string`, () => { + chai.expect(DecentralizedID.fromString(didStringWithBtcAddr)).to.deep.equal(didWithBtcAddr); + chai.expect(DecentralizedID.fromString(didStringWithPubKey)).to.deep.equal(didWithPubKey); + }); + + it(`fails on ${incorrect()} inputs`, () => { + chai.expect(() => DecentralizedID.fromString('did:1:2:3')).to.throw(InvalidDIDError); + chai.expect(() => DecentralizedID.fromString('did:1')).to.throw(InvalidDIDError); + chai.expect(() => DecentralizedID.fromString('')).to.throw(InvalidDIDError); + chai.expect(() => DecentralizedID.fromString('no-did:1:2')).to.throw(InvalidDIDError); + }); + }); + + describe('constructor', () => { + it(`creates ${correct()} DID objects`, () => { + chai.expect(new DecentralizedID('btc-addr', btcAddr)).to.deep.equal(didWithBtcAddr); + chai.expect(new DecentralizedID('ecdsa-pub', pubKey)).to.deep.equal(didWithPubKey); + }); + }); + + describe('getAddress', () => { + it(`retrieves the ${correct()} address of a DID object`, () => { + chai.expect(didWithBtcAddr.getAddress()).to.equal(btcAddr); + }); + + it(`fails on ${incorrect()} inputs`, () => { + chai.expect(() => didWithPubKey.getAddress()).to.throw(InvalidDIDTypeError); + }); + }); + + describe('getType', () => { + it(`retrieves the ${correct()} type of a DID object`, () => { + chai.expect(didWithBtcAddr.getType()).to.equal('btc-addr'); + chai.expect(didWithPubKey.getType()).to.equal('ecdsa-pub'); + }); + }); + + describe('toString', () => { + it(`creates ${correct()} DID strings from DID objects`, () => { + chai.expect(didWithBtcAddr.toString()).to.equal(didStringWithBtcAddr); + chai.expect(didWithPubKey.toString()).to.equal(didStringWithPubKey); + }); + }); +});