From f8e43def2e5093c798308c13101995a562a4d9fa Mon Sep 17 00:00:00 2001 From: Anthony Law Yong Chuan Date: Tue, 7 May 2019 16:11:23 +0800 Subject: [PATCH 01/13] serialize tranaction pass to schema --- src/TransactionQR.ts | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/TransactionQR.ts b/src/TransactionQR.ts index 512ee52..3d2fea8 100644 --- a/src/TransactionQR.ts +++ b/src/TransactionQR.ts @@ -31,7 +31,7 @@ export class TransactionQR extends QRCode implements QRCodeInterface { /** * Construct a Transaction Request QR Code out of the * nem2-sdk Transaction instance. - * + * * @param transaction {Transaction} * @param networkType {NetworkType} * @param chainId {string} @@ -62,25 +62,19 @@ export class TransactionQR extends QRCode implements QRCodeInterface { */ public toJSON(): string { - // take the JSON of the transaction - const txJSON = this.transaction.toJSON().transaction; - - // remove empty `signature` and `signer` fields - if (txJSON.hasOwnProperty('signature') && !txJSON.signature.length) { - delete txJSON['signature']; - } - if (txJSON.hasOwnProperty('signer') && !txJSON.signer.length) { - delete txJSON['signer']; - } + // Serialize the transaction object data. + const txSerialized = this.transaction.serialize(); const jsonSchema = { 'v': 3, 'type': this.type, 'network_id': this.networkType, 'chain_id': this.chainId, - 'data': txJSON, + 'data': { + 'payload': txSerialized + } }; - return JSON.stringify(jsonSchema); + return JSON.stringify(jsonSchema).trim(); } } \ No newline at end of file From 4b0944b33166abcc454289630a1ed8d25f013289 Mon Sep 17 00:00:00 2001 From: Anthony Law Yong Chuan Date: Tue, 7 May 2019 16:12:21 +0800 Subject: [PATCH 02/13] added read json QR function --- src/QRCodeGenerator.ts | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/QRCodeGenerator.ts b/src/QRCodeGenerator.ts index 545536b..4ef5a0c 100644 --- a/src/QRCodeGenerator.ts +++ b/src/QRCodeGenerator.ts @@ -16,12 +16,14 @@ import { NetworkType, Transaction, + TransactionMapping, } from "nem2-sdk"; import {QRCode} from 'qrcode-generator-ts'; // internal dependencies import { QRCodeInterface, + QRCodeType, AccountQR, ContactQR, ObjectQR, @@ -31,7 +33,7 @@ import { /** * Class `QRCodeGenerator` describes a NIP-7 compliant QR Code * generator (factory). - * + * * @since 0.2.0 */ export class QRCodeGenerator { @@ -73,4 +75,38 @@ export class QRCodeGenerator { ): TransactionQR { return new TransactionQR(transaction, networkType, chainId); } + + /** + * Read JSON Content from QRcode. + * @param json {string} + */ + static fromJSON(json:string) :any{ + + if (json == null || json == '') { + throw Error('QR json object is missing'); + } + + const jsonObj = JSON.parse(json || ''); + + switch(jsonObj.type) { + case QRCodeType.AddContact: { + new ContactQR(jsonObj.data.account, jsonObj.network_id, jsonObj.chainId) + } + case QRCodeType.ExportAccount: { + return new AccountQR(jsonObj.data.account,jsonObj.network_id, jsonObj.chainId) + } + case QRCodeType.RequestTransaction: { + let txMapping: Transaction = TransactionMapping.createFromPayload(jsonObj.data.payload); + + return new TransactionQR(txMapping, jsonObj.network_id, jsonObj.chainId) + } + case QRCodeType.RequestCosignature: { + // Todo: In progress; + break; + } + case QRCodeType.ExportObject: { + new ObjectQR(jsonObj.data.object, jsonObj.network_id, jsonObj.chainId); + } + } + } } From 6c15be4056b72ac49d34b48b2c28e271bc09b10c Mon Sep 17 00:00:00 2001 From: Anthony Law Yong Chuan Date: Tue, 7 May 2019 17:27:42 +0800 Subject: [PATCH 03/13] added fromJson's TransactionQR test --- test/QRCodeGenerator.spec.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/test/QRCodeGenerator.spec.ts b/test/QRCodeGenerator.spec.ts index b5d7767..4104abe 100644 --- a/test/QRCodeGenerator.spec.ts +++ b/test/QRCodeGenerator.spec.ts @@ -32,6 +32,8 @@ import { QRCodeGenerator } from "../index"; import { ExpectedObjectBase64, } from './vectors/index'; +import { TransactionQR } from "../src/TransactionQR"; +import { QRCodeType } from "../src/QRCodeType"; describe('QRCodeGenerator -->', () => { @@ -78,4 +80,37 @@ describe('QRCodeGenerator -->', () => { expect(requestTx.toJSON()).to.have.lengthOf.below(2953); }); }); + + describe('fromJson() should', () => { + + it('Read data From TransactionQR', () => { + // Arrange: + const transfer = TransferTransaction.create( + Deadline.create(), + Address.createFromPublicKey( + 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', + NetworkType.MIJIN_TEST + ), + [new Mosaic(new NamespaceId('cat.currency'), UInt64.fromUint(10000000))], + PlainMessage.create('Welcome to NEM!'), + NetworkType.MIJIN_TEST + ); + + const requestTx = QRCodeGenerator.createTransactionRequest(transfer,NetworkType.MIJIN_TEST); + const txJSON = requestTx.toJSON(); + + // Act: + const transactionObj: TransactionQR = QRCodeGenerator.fromJSON(txJSON); + + // Assert: + expect(transactionObj).to.not.be.equal(''); + expect(transactionObj.transaction.toJSON()).to.deep.equal(transfer.toJSON()); + expect(transactionObj.type).to.deep.equal(QRCodeType.RequestTransaction); + }); + + + it('Read data From AccountQR', () => {}); + it('Read data From ContactQR', () => {}); + it('Read data From ObjectQR', () => {}); + }); }); From 2fe4c034e6007b07c3d4475bc0bd1518b0f92280 Mon Sep 17 00:00:00 2001 From: Anthony Law Yong Chuan Date: Tue, 7 May 2019 18:34:28 +0800 Subject: [PATCH 04/13] added fromJson for ContactQR --- src/ContactQR.ts | 8 ++++--- src/QRCodeGenerator.ts | 26 ++++++++++++++++++---- test/QRCodeGenerator.spec.ts | 43 +++++++++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/ContactQR.ts b/src/ContactQR.ts index 4f9b2e6..c2f6a8d 100644 --- a/src/ContactQR.ts +++ b/src/ContactQR.ts @@ -31,7 +31,7 @@ export class ContactQR extends QRCode implements QRCodeInterface { /** * Construct a Contact QR Code out of the * nem2-sdk Account or PublicAccount instance. - * + * * @param account {Account|PublicAccount} * @param networkType {NetworkType} * @param chainId {string} @@ -67,9 +67,11 @@ export class ContactQR extends QRCode implements QRCodeInterface { 'type': this.type, 'network_id': this.networkType, 'chain_id': this.chainId, - 'data': 'not implemented yet', + 'data': { + 'address': this.account + }, }; - return JSON.stringify(jsonSchema); + return JSON.stringify(jsonSchema).trim(); } } \ No newline at end of file diff --git a/src/QRCodeGenerator.ts b/src/QRCodeGenerator.ts index 4ef5a0c..ecffb32 100644 --- a/src/QRCodeGenerator.ts +++ b/src/QRCodeGenerator.ts @@ -17,6 +17,8 @@ import { NetworkType, Transaction, TransactionMapping, + Account, + PublicAccount } from "nem2-sdk"; import {QRCode} from 'qrcode-generator-ts'; @@ -54,7 +56,7 @@ export class QRCodeGenerator { */ public static createExportObject( object: Object, - networkType: NetworkType = NetworkType.TEST_NET, + networkType: NetworkType = NetworkType.MIJIN_TEST, chainId: string = 'E2A9F95E129283EF47B92A62FD748DBA4D32AA718AE6F8AC99C105CFA9F27A31' ): ObjectQR { return new ObjectQR(object, networkType, chainId); @@ -70,12 +72,28 @@ export class QRCodeGenerator { */ public static createTransactionRequest( transaction: Transaction, - networkType: NetworkType = NetworkType.TEST_NET, + networkType: NetworkType = NetworkType.MIJIN_TEST, chainId: string = 'E2A9F95E129283EF47B92A62FD748DBA4D32AA718AE6F8AC99C105CFA9F27A31' ): TransactionQR { return new TransactionQR(transaction, networkType, chainId); } + /** + * Create a Transaction Request QR Code from a Transaction + * instance. + * + * @param transaction {Transaction} + * @param networkType {NetworkType} + * @param chainId {string} + */ + public static createContact( + account: Account | PublicAccount, + networkType: NetworkType = NetworkType.MIJIN_TEST, + chainId: string = 'E2A9F95E129283EF47B92A62FD748DBA4D32AA718AE6F8AC99C105CFA9F27A31' + ): ContactQR { + return new ContactQR(account, networkType, chainId); + } + /** * Read JSON Content from QRcode. * @param json {string} @@ -90,7 +108,7 @@ export class QRCodeGenerator { switch(jsonObj.type) { case QRCodeType.AddContact: { - new ContactQR(jsonObj.data.account, jsonObj.network_id, jsonObj.chainId) + return new ContactQR(jsonObj.data.address, jsonObj.network_id, jsonObj.chainId) } case QRCodeType.ExportAccount: { return new AccountQR(jsonObj.data.account,jsonObj.network_id, jsonObj.chainId) @@ -105,7 +123,7 @@ export class QRCodeGenerator { break; } case QRCodeType.ExportObject: { - new ObjectQR(jsonObj.data.object, jsonObj.network_id, jsonObj.chainId); + return new ObjectQR(jsonObj.data.object, jsonObj.network_id, jsonObj.chainId); } } } diff --git a/test/QRCodeGenerator.spec.ts b/test/QRCodeGenerator.spec.ts index 4104abe..bb4e09b 100644 --- a/test/QRCodeGenerator.spec.ts +++ b/test/QRCodeGenerator.spec.ts @@ -23,6 +23,7 @@ import { UInt64, PlainMessage, NetworkType, + PublicAccount, } from 'nem2-sdk'; // internal dependencies @@ -34,6 +35,7 @@ import { } from './vectors/index'; import { TransactionQR } from "../src/TransactionQR"; import { QRCodeType } from "../src/QRCodeType"; +import { ContactQR } from "../src/ContactQR"; describe('QRCodeGenerator -->', () => { @@ -81,6 +83,26 @@ describe('QRCodeGenerator -->', () => { }); }); + describe('createContact() should', ()=> { + + it('generate correct Base64 representation for TransferTransaction', () => { + // Arrange: + const account = PublicAccount.createFromPublicKey( + 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', + NetworkType.MIJIN_TEST + ); + + // Act: + const createContact = QRCodeGenerator.createContact(account); + const actualBase64 = createContact.toBase64(); + + // Assert: + expect(actualBase64).to.not.be.equal(''); + expect(actualBase64.length).to.not.be.equal(0); + expect(createContact.toJSON()).to.have.lengthOf.below(2953); + }); + }); + describe('fromJson() should', () => { it('Read data From TransactionQR', () => { @@ -108,9 +130,28 @@ describe('QRCodeGenerator -->', () => { expect(transactionObj.type).to.deep.equal(QRCodeType.RequestTransaction); }); + it.only('Read data From ContactQR', () => { + // Arrange: + const account = PublicAccount.createFromPublicKey( + 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', + NetworkType.MIJIN_TEST + ); + + const createContact = QRCodeGenerator.createContact(account,NetworkType.MIJIN_TEST); + const contactJSON = createContact.toJSON(); + + + // Act: + const contactObj: ContactQR = QRCodeGenerator.fromJSON(contactJSON); + + // Assert: + expect(contactObj).to.not.be.equal(''); + expect(contactObj.account.address).to.deep.equal(account.address); + expect(contactObj.type).to.deep.equal(QRCodeType.AddContact); + }); it('Read data From AccountQR', () => {}); - it('Read data From ContactQR', () => {}); + it('Read data From ObjectQR', () => {}); }); }); From 886b310ef525febf8a98f539359aeb689c4c6fe0 Mon Sep 17 00:00:00 2001 From: Anthony Law Yong Chuan Date: Mon, 13 May 2019 17:39:39 +0800 Subject: [PATCH 05/13] WIP export account funtion --- package-lock.json | 4 +-- package.json | 1 + src/AccountQR.ts | 35 +++++++++++++++++++------- src/QRCode.ts | 48 +++++++++++++++++++++++++++++++++++- src/QRCodeGenerator.ts | 18 +++++++++++--- src/QRCodeInterface.ts | 27 +++++++++++++++++++- test/AccountQR.spec.ts | 26 ++++++++++--------- test/QRCodeGenerator.spec.ts | 27 ++++++++++++++++++++ 8 files changed, 158 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index a751c5d..ad1db38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3816,8 +3816,8 @@ } }, "webpack-dev-server": { - "version": ">=3.1.11", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.3.1.tgz", + "version": "1.16.5", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-1.16.5.tgz", "integrity": "sha1-DL1fLSrI1OWTqs1clwLnu9XlmJI=", "requires": { "compression": "^1.5.2", diff --git a/package.json b/package.json index e50e960..404d2c3 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "test": "test" }, "dependencies": { + "crypto-js": "^3.1.9-1", "nem2-sdk": "^0.11.5", "qrcode-generator-ts": "^0.0.4" }, diff --git a/src/AccountQR.ts b/src/AccountQR.ts index fcce2b1..84d56b9 100644 --- a/src/AccountQR.ts +++ b/src/AccountQR.ts @@ -17,6 +17,8 @@ import { Account, PublicAccount, NetworkType, + Wallet, + Password, } from "nem2-sdk"; // internal dependencies @@ -26,12 +28,13 @@ import { QRCodeType, QRCodeSettings, } from '../index'; +import { throwError } from "rxjs"; export class AccountQR extends QRCode implements QRCodeInterface { /** * Construct an Account QR Code out of the * nem2-sdk Account or PublicAccount instance. - * + * * @param transaction {Transaction} * @param networkType {NetworkType} * @param chainId {string} @@ -41,6 +44,11 @@ export class AccountQR extends QRCode implements QRCodeInterface { * @var {Account} */ public readonly account: Account, + /** + * The password for encryption + * @var {Password} + */ + public readonly password: Password, /** * The network type. * @var {NetworkType} @@ -62,14 +70,23 @@ export class AccountQR extends QRCode implements QRCodeInterface { */ public toJSON(): string { - const jsonSchema = { - 'v': 3, - 'type': this.type, - 'network_id': this.networkType, - 'chain_id': this.chainId, - 'data': 'not implemented yet', - }; + if (this.password == null) { + throw Error('Password is missing'); + } + + const encryption = this.AES_PBKF2_encryption(this.password, this.account.privateKey); + + const jsonSchema = { + 'v': 3, + 'type': this.type, + 'network_id': this.networkType, + 'chain_id': this.chainId, + 'data': { + 'priv_key': encryption.encrypted, + 'salt': encryption.salt, + }, + }; - return JSON.stringify(jsonSchema); + return JSON.stringify(jsonSchema); } } \ No newline at end of file diff --git a/src/QRCode.ts b/src/QRCode.ts index 18ce7fb..e0d6ec0 100644 --- a/src/QRCode.ts +++ b/src/QRCode.ts @@ -21,7 +21,10 @@ import { import { NetworkType, + Password } from 'nem2-sdk'; +import {convert,nacl_catapult} from 'nem2-library'; +import * as CryptoJS from "crypto-js"; // internal dependencies import { @@ -35,7 +38,7 @@ export abstract class QRCode implements QRCodeInterface { /** * Construct a QR Code instance out of its base64 * representation and type. - * + * * @param type {QRCodeType} * @param base64 {string} */ @@ -110,4 +113,47 @@ export abstract class QRCode implements QRCodeInterface { QRCodeSettings.MARGIN_PIXEL ); } + + public AES_PBKF2_encryption(password: Password, privateKey: string): any { + const salt = CryptoJS.lib.WordArray.random(256 / 8); + const key = CryptoJS.PBKDF2(password.value, salt, { + keySize: 256 / 32, + iterations: 2000, + }); + + const hex = convert.uint8ToHex(nacl_catapult.randomBytes(16)); + + const encIv = { + iv: CryptoJS.enc.Hex.parse(hex), + }; + + const encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Hex.parse(privateKey), key, encIv); + + return { + encrypted: hex + encrypted.toString(), + salt: salt.toString(), + }; + } + + public AES_PBKF2_decryption(password: Password, json: any): string { + const encryptedData = json; + const salt = CryptoJS.enc.Hex.parse(encryptedData.salt); + const iv = CryptoJS.enc.Hex.parse(encryptedData.encrypted.substring(0, 32)); + const encrypted: string = encryptedData.encrypted.substring(32, 96); + + //generate key + const key = CryptoJS.PBKDF2(password.value, salt, { + keySize: 256 / 32, + iterations: 2000, + }); + + let encIv = { + iv: iv + }; + + let decrypt = CryptoJS.enc.Hex.stringify(CryptoJS.AES.decrypt(encrypted, key, encIv)); + + if (decrypt === "" || (decrypt.length != 64 && decrypt.length != 66)) throw new Error("invalid password"); + return decrypt; + } } \ No newline at end of file diff --git a/src/QRCodeGenerator.ts b/src/QRCodeGenerator.ts index ecffb32..aab1b4e 100644 --- a/src/QRCodeGenerator.ts +++ b/src/QRCodeGenerator.ts @@ -18,7 +18,8 @@ import { Transaction, TransactionMapping, Account, - PublicAccount + PublicAccount, + Password } from "nem2-sdk"; import {QRCode} from 'qrcode-generator-ts'; @@ -94,6 +95,15 @@ export class QRCodeGenerator { return new ContactQR(account, networkType, chainId); } + public static createExportAccount( + account: Account, + password: Password, + networkType: NetworkType = NetworkType.MIJIN_TEST, + chainId: string = 'E2A9F95E129283EF47B92A62FD748DBA4D32AA718AE6F8AC99C105CFA9F27A31', + ): AccountQR { + return new AccountQR(account, password, networkType, chainId); + } + /** * Read JSON Content from QRcode. * @param json {string} @@ -110,9 +120,9 @@ export class QRCodeGenerator { case QRCodeType.AddContact: { return new ContactQR(jsonObj.data.address, jsonObj.network_id, jsonObj.chainId) } - case QRCodeType.ExportAccount: { - return new AccountQR(jsonObj.data.account,jsonObj.network_id, jsonObj.chainId) - } + // case QRCodeType.ExportAccount: { + // return new AccountQR(jsonObj.data.account,jsonObj.network_id, jsonObj.chainId) + // } case QRCodeType.RequestTransaction: { let txMapping: Transaction = TransactionMapping.createFromPayload(jsonObj.data.payload); diff --git a/src/QRCodeInterface.ts b/src/QRCodeInterface.ts index 8d546e8..a14a82f 100644 --- a/src/QRCodeInterface.ts +++ b/src/QRCodeInterface.ts @@ -15,13 +15,17 @@ */ import {QRCode} from 'qrcode-generator-ts'; +import { + Password +} from 'nem2-sdk'; + // internal dependencies import {QRCodeType} from '../index'; /** * Interface `QRCodeInterface` describes rules for the definition * of NIP-7 compliant QR Codes. - * + * * @since 0.2.0 */ export interface QRCodeInterface { @@ -60,4 +64,25 @@ export interface QRCodeInterface { * @return {string} */ toBase64(): string; + + /** + * The `AES_PBKF2_encryption()` method should return encrypted and salt + * + * @param password + * @param privateKey + * + * @returns {json} + */ + AES_PBKF2_encryption(password: Password, privateKey: string): any; + + /** + * The `AES_PBKF2_decryption()` method should return string (privateKey) + * + * @param password + * @param json + * + * @returns {string} + */ + AES_PBKF2_decryption(password: Password, json: any): string; + } diff --git a/test/AccountQR.spec.ts b/test/AccountQR.spec.ts index 79a973b..39a310c 100644 --- a/test/AccountQR.spec.ts +++ b/test/AccountQR.spec.ts @@ -17,6 +17,7 @@ import {expect} from "chai"; import { Account, NetworkType, + Password, } from 'nem2-sdk'; import { QRCode as QRCodeImpl, @@ -25,13 +26,14 @@ import { } from 'qrcode-generator-ts'; // internal dependencies -import { +import { QRCodeInterface, QRCode, QRCodeType, QRCodeSettings, ContactQR, } from "../index"; +import { AccountQR } from "../src/AccountQR"; describe('AccountQR -->', () => { @@ -44,17 +46,19 @@ describe('AccountQR -->', () => { NetworkType.TEST_NET ); + const password = new Password('1234'); + // Act: - const addContact = new ContactQR(account, NetworkType.TEST_NET, ''); - const actualJSON = addContact.toJSON(); - const actualObject = JSON.parse(actualJSON); - - // Assert: - expect(actualObject).to.have.property('v'); - expect(actualObject).to.have.property('type'); - expect(actualObject).to.have.property('network_id'); - expect(actualObject).to.have.property('chain_id'); - expect(actualObject).to.have.property('data'); + const exportAccount = new AccountQR(account, password, NetworkType.TEST_NET, ''); + const actualJSON = exportAccount.toJSON(); + // const actualObject = JSON.parse(actualJSON); + + // // Assert: + // expect(actualObject).to.have.property('v'); + // expect(actualObject).to.have.property('type'); + // expect(actualObject).to.have.property('network_id'); + // expect(actualObject).to.have.property('chain_id'); + // expect(actualObject).to.have.property('data'); }); }); diff --git a/test/QRCodeGenerator.spec.ts b/test/QRCodeGenerator.spec.ts index bb4e09b..4ca0052 100644 --- a/test/QRCodeGenerator.spec.ts +++ b/test/QRCodeGenerator.spec.ts @@ -24,6 +24,8 @@ import { PlainMessage, NetworkType, PublicAccount, + Account, + Password } from 'nem2-sdk'; // internal dependencies @@ -36,6 +38,7 @@ import { import { TransactionQR } from "../src/TransactionQR"; import { QRCodeType } from "../src/QRCodeType"; import { ContactQR } from "../src/ContactQR"; +import { AccountQR } from "../src/AccountQR"; describe('QRCodeGenerator -->', () => { @@ -103,6 +106,30 @@ describe('QRCodeGenerator -->', () => { }); }); + describe.only('createExportAccount() should', ()=> { + + it('generate correct Base64 representation for ExportAccount', () => { + // Arrange: + const account = Account.createFromPrivateKey( + 'F97AE23C2A28ECEDE6F8D6C447C0A10B55C92DDE9316CCD36C3177B073906978', + NetworkType.TEST_NET + ); + const password = new Password('password'); + + + // Act: + const exportAccount = QRCodeGenerator.createExportAccount(account,password); + const actualObject = exportAccount.toJSON(); + // const actualBase64 = exportAccount.base64(); + console.log(exportAccount); + + // Assert: + expect(actualObject).to.not.be.equal(''); + expect(actualObject.length).to.not.be.equal(0); + expect(exportAccount.toJSON()).to.have.lengthOf.below(2953); + }); + }); + describe('fromJson() should', () => { it('Read data From TransactionQR', () => { From cc44245b9fa4fa7a522b2d32eeddafbe9b54753c Mon Sep 17 00:00:00 2001 From: Anthony Law Yong Chuan Date: Tue, 14 May 2019 11:29:43 +0800 Subject: [PATCH 06/13] completed generate exportAccount json --- src/AccountQR.ts | 2 +- test/AccountQR.spec.ts | 20 ++++++++++---------- test/QRCodeGenerator.spec.ts | 19 ++++++------------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/AccountQR.ts b/src/AccountQR.ts index 84d56b9..a8fc094 100644 --- a/src/AccountQR.ts +++ b/src/AccountQR.ts @@ -48,7 +48,7 @@ export class AccountQR extends QRCode implements QRCodeInterface { * The password for encryption * @var {Password} */ - public readonly password: Password, + protected readonly password: Password, /** * The network type. * @var {NetworkType} diff --git a/test/AccountQR.spec.ts b/test/AccountQR.spec.ts index 39a310c..d94e3d6 100644 --- a/test/AccountQR.spec.ts +++ b/test/AccountQR.spec.ts @@ -37,28 +37,28 @@ import { AccountQR } from "../src/AccountQR"; describe('AccountQR -->', () => { - describe('toJSON() should', () => { + describe.only('toJSON() should', () => { it('include mandatory NIP-7 QR Code base fields', () => { // Arrange: const account = Account.createFromPrivateKey( 'F97AE23C2A28ECEDE6F8D6C447C0A10B55C92DDE9316CCD36C3177B073906978', - NetworkType.TEST_NET + NetworkType.MIJIN_TEST ); - const password = new Password('1234'); + const password = new Password('password'); // Act: - const exportAccount = new AccountQR(account, password, NetworkType.TEST_NET, ''); + const exportAccount = new AccountQR(account, password, NetworkType.MIJIN_TEST, ''); const actualJSON = exportAccount.toJSON(); - // const actualObject = JSON.parse(actualJSON); + const actualObject = JSON.parse(actualJSON); // // Assert: - // expect(actualObject).to.have.property('v'); - // expect(actualObject).to.have.property('type'); - // expect(actualObject).to.have.property('network_id'); - // expect(actualObject).to.have.property('chain_id'); - // expect(actualObject).to.have.property('data'); + expect(actualObject).to.have.property('v'); + expect(actualObject).to.have.property('type'); + expect(actualObject).to.have.property('network_id'); + expect(actualObject).to.have.property('chain_id'); + expect(actualObject).to.have.property('data'); }); }); diff --git a/test/QRCodeGenerator.spec.ts b/test/QRCodeGenerator.spec.ts index 4ca0052..fd5590d 100644 --- a/test/QRCodeGenerator.spec.ts +++ b/test/QRCodeGenerator.spec.ts @@ -29,16 +29,12 @@ import { } from 'nem2-sdk'; // internal dependencies -import { QRCodeGenerator } from "../index"; +import { QRCodeGenerator, AccountQR, TransactionQR, QRCodeType, ContactQR } from "../index"; // vectors data import { ExpectedObjectBase64, } from './vectors/index'; -import { TransactionQR } from "../src/TransactionQR"; -import { QRCodeType } from "../src/QRCodeType"; -import { ContactQR } from "../src/ContactQR"; -import { AccountQR } from "../src/AccountQR"; describe('QRCodeGenerator -->', () => { @@ -106,26 +102,23 @@ describe('QRCodeGenerator -->', () => { }); }); - describe.only('createExportAccount() should', ()=> { + describe('createExportAccount() should', ()=> { it('generate correct Base64 representation for ExportAccount', () => { // Arrange: const account = Account.createFromPrivateKey( 'F97AE23C2A28ECEDE6F8D6C447C0A10B55C92DDE9316CCD36C3177B073906978', - NetworkType.TEST_NET + NetworkType.MIJIN_TEST ); const password = new Password('password'); - // Act: const exportAccount = QRCodeGenerator.createExportAccount(account,password); - const actualObject = exportAccount.toJSON(); - // const actualBase64 = exportAccount.base64(); - console.log(exportAccount); + const actualBase64 = exportAccount.toBase64(); // Assert: - expect(actualObject).to.not.be.equal(''); - expect(actualObject.length).to.not.be.equal(0); + expect(actualBase64).to.not.be.equal(''); + expect(actualBase64.length).to.not.be.equal(0); expect(exportAccount.toJSON()).to.have.lengthOf.below(2953); }); }); From 11a1173eb594afd7839632267b0dc60cf2237fef Mon Sep 17 00:00:00 2001 From: Anthony Law Yong Chuan Date: Tue, 14 May 2019 16:29:27 +0800 Subject: [PATCH 07/13] added QRService class --- index.ts | 1 + src/QRService.ts | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 src/QRService.ts diff --git a/index.ts b/index.ts index b859f30..0d2713c 100644 --- a/index.ts +++ b/index.ts @@ -22,3 +22,4 @@ export { ContactQR } from './src/ContactQR'; export { ObjectQR } from './src/ObjectQR'; export { TransactionQR } from './src/TransactionQR'; export { QRCodeGenerator } from './src/QRCodeGenerator'; +export { QRService } from './src/QRService'; diff --git a/src/QRService.ts b/src/QRService.ts new file mode 100644 index 0000000..ea3bb58 --- /dev/null +++ b/src/QRService.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2019 NEM Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License "); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + Password, +} from "nem2-sdk"; +import {convert,nacl_catapult} from 'nem2-library'; +import * as CryptoJS from "crypto-js"; + +export class QRService { + + /** + * AES_PBKF2_encryption will encrypt privateKey with provided password. + * @param password {Password} + * @param privateKey {strinf} + */ + public AES_PBKF2_encryption(password: Password, privateKey: string): any { + const salt = CryptoJS.lib.WordArray.random(256 / 8); + const key = CryptoJS.PBKDF2(password.value, salt, { + keySize: 256 / 32, + iterations: 2000, + }); + + const hex = convert.uint8ToHex(nacl_catapult.randomBytes(16)); + + const encIv = { + iv: CryptoJS.enc.Hex.parse(hex), + }; + + const encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Hex.parse(privateKey), key, encIv); + + return { + encrypted: hex + encrypted.toString(), + salt: salt.toString(), + }; + } + + /** + * AES_PBKF2_decryption will decrypt privateKey with provided password + * @param password + * @param json + */ + public AES_PBKF2_decryption(password: Password, json: any): string { + const encryptedData = json; + const salt = CryptoJS.enc.Hex.parse(encryptedData.data.salt); + const iv = CryptoJS.enc.Hex.parse(encryptedData.data.priv_key.substring(0, 32)); + const encrypted: string = encryptedData.data.priv_key.substring(32, 96); + + //generate key + const key = CryptoJS.PBKDF2(password.value, salt, { + keySize: 256 / 32, + iterations: 2000, + }); + + let encIv = { + iv: iv + }; + + let decrypt = CryptoJS.enc.Hex.stringify(CryptoJS.AES.decrypt(encrypted, key, encIv)); + + if (decrypt === "" || (decrypt.length != 64 && decrypt.length != 66)) throw new Error("invalid password"); + return decrypt; + } +} \ No newline at end of file From 20536dc97ae32e8ba669155e2e4d07b336aebf54 Mon Sep 17 00:00:00 2001 From: Anthony Law Yong Chuan Date: Tue, 14 May 2019 16:39:28 +0800 Subject: [PATCH 08/13] password-protected export account apply --- src/AccountQR.ts | 7 +++--- src/QRCode.ts | 43 ------------------------------------ src/QRCodeGenerator.ts | 21 +++++++++++++----- src/QRCodeInterface.ts | 25 --------------------- test/AccountQR.spec.ts | 9 ++++---- test/QRCodeGenerator.spec.ts | 22 ++++++++++++++++-- 6 files changed, 44 insertions(+), 83 deletions(-) diff --git a/src/AccountQR.ts b/src/AccountQR.ts index a8fc094..078fecb 100644 --- a/src/AccountQR.ts +++ b/src/AccountQR.ts @@ -27,6 +27,7 @@ import { QRCodeInterface, QRCodeType, QRCodeSettings, + QRService } from '../index'; import { throwError } from "rxjs"; @@ -35,7 +36,7 @@ export class AccountQR extends QRCode implements QRCodeInterface { * Construct an Account QR Code out of the * nem2-sdk Account or PublicAccount instance. * - * @param transaction {Transaction} + * @param account {Account} * @param networkType {NetworkType} * @param chainId {string} */ @@ -73,8 +74,8 @@ export class AccountQR extends QRCode implements QRCodeInterface { if (this.password == null) { throw Error('Password is missing'); } - - const encryption = this.AES_PBKF2_encryption(this.password, this.account.privateKey); + const qrService: QRService = new QRService(); + const encryption = qrService.AES_PBKF2_encryption(this.password, this.account.privateKey); const jsonSchema = { 'v': 3, diff --git a/src/QRCode.ts b/src/QRCode.ts index e0d6ec0..64f432e 100644 --- a/src/QRCode.ts +++ b/src/QRCode.ts @@ -113,47 +113,4 @@ export abstract class QRCode implements QRCodeInterface { QRCodeSettings.MARGIN_PIXEL ); } - - public AES_PBKF2_encryption(password: Password, privateKey: string): any { - const salt = CryptoJS.lib.WordArray.random(256 / 8); - const key = CryptoJS.PBKDF2(password.value, salt, { - keySize: 256 / 32, - iterations: 2000, - }); - - const hex = convert.uint8ToHex(nacl_catapult.randomBytes(16)); - - const encIv = { - iv: CryptoJS.enc.Hex.parse(hex), - }; - - const encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Hex.parse(privateKey), key, encIv); - - return { - encrypted: hex + encrypted.toString(), - salt: salt.toString(), - }; - } - - public AES_PBKF2_decryption(password: Password, json: any): string { - const encryptedData = json; - const salt = CryptoJS.enc.Hex.parse(encryptedData.salt); - const iv = CryptoJS.enc.Hex.parse(encryptedData.encrypted.substring(0, 32)); - const encrypted: string = encryptedData.encrypted.substring(32, 96); - - //generate key - const key = CryptoJS.PBKDF2(password.value, salt, { - keySize: 256 / 32, - iterations: 2000, - }); - - let encIv = { - iv: iv - }; - - let decrypt = CryptoJS.enc.Hex.stringify(CryptoJS.AES.decrypt(encrypted, key, encIv)); - - if (decrypt === "" || (decrypt.length != 64 && decrypt.length != 66)) throw new Error("invalid password"); - return decrypt; - } } \ No newline at end of file diff --git a/src/QRCodeGenerator.ts b/src/QRCodeGenerator.ts index aab1b4e..60e8874 100644 --- a/src/QRCodeGenerator.ts +++ b/src/QRCodeGenerator.ts @@ -21,7 +21,7 @@ import { PublicAccount, Password } from "nem2-sdk"; -import {QRCode} from 'qrcode-generator-ts'; +import * as CryptoJS from "crypto-js"; // internal dependencies import { @@ -31,6 +31,7 @@ import { ContactQR, ObjectQR, TransactionQR, + QRService, } from '../index'; /** @@ -108,7 +109,7 @@ export class QRCodeGenerator { * Read JSON Content from QRcode. * @param json {string} */ - static fromJSON(json:string) :any{ + static fromJSON(json:string, password?: Password) :any { if (json == null || json == '') { throw Error('QR json object is missing'); @@ -120,9 +121,19 @@ export class QRCodeGenerator { case QRCodeType.AddContact: { return new ContactQR(jsonObj.data.address, jsonObj.network_id, jsonObj.chainId) } - // case QRCodeType.ExportAccount: { - // return new AccountQR(jsonObj.data.account,jsonObj.network_id, jsonObj.chainId) - // } + case QRCodeType.ExportAccount: { + if (password == null){ + throw Error('Password are required'); + } + + const qrService: QRService = new QRService(); + const privatekey: string = qrService.AES_PBKF2_decryption(password,jsonObj); + + const account = Account.createFromPrivateKey(privatekey, + NetworkType.MIJIN_TEST); + + return new AccountQR(account, password, jsonObj.network_id, jsonObj.chainId) + } case QRCodeType.RequestTransaction: { let txMapping: Transaction = TransactionMapping.createFromPayload(jsonObj.data.payload); diff --git a/src/QRCodeInterface.ts b/src/QRCodeInterface.ts index a14a82f..8e1b110 100644 --- a/src/QRCodeInterface.ts +++ b/src/QRCodeInterface.ts @@ -15,10 +15,6 @@ */ import {QRCode} from 'qrcode-generator-ts'; -import { - Password -} from 'nem2-sdk'; - // internal dependencies import {QRCodeType} from '../index'; @@ -64,25 +60,4 @@ export interface QRCodeInterface { * @return {string} */ toBase64(): string; - - /** - * The `AES_PBKF2_encryption()` method should return encrypted and salt - * - * @param password - * @param privateKey - * - * @returns {json} - */ - AES_PBKF2_encryption(password: Password, privateKey: string): any; - - /** - * The `AES_PBKF2_decryption()` method should return string (privateKey) - * - * @param password - * @param json - * - * @returns {string} - */ - AES_PBKF2_decryption(password: Password, json: any): string; - } diff --git a/test/AccountQR.spec.ts b/test/AccountQR.spec.ts index d94e3d6..17b5787 100644 --- a/test/AccountQR.spec.ts +++ b/test/AccountQR.spec.ts @@ -37,23 +37,22 @@ import { AccountQR } from "../src/AccountQR"; describe('AccountQR -->', () => { - describe.only('toJSON() should', () => { + describe('toJSON() should', () => { - it('include mandatory NIP-7 QR Code base fields', () => { + it.only('include mandatory NIP-7 QR Code base fields', () => { // Arrange: const account = Account.createFromPrivateKey( 'F97AE23C2A28ECEDE6F8D6C447C0A10B55C92DDE9316CCD36C3177B073906978', NetworkType.MIJIN_TEST ); - const password = new Password('password'); // Act: - const exportAccount = new AccountQR(account, password, NetworkType.MIJIN_TEST, ''); + const exportAccount = new AccountQR(account, password, NetworkType.MIJIN_TEST, 'no-chain-id'); const actualJSON = exportAccount.toJSON(); const actualObject = JSON.parse(actualJSON); - // // Assert: + // // // Assert: expect(actualObject).to.have.property('v'); expect(actualObject).to.have.property('type'); expect(actualObject).to.have.property('network_id'); diff --git a/test/QRCodeGenerator.spec.ts b/test/QRCodeGenerator.spec.ts index fd5590d..0677869 100644 --- a/test/QRCodeGenerator.spec.ts +++ b/test/QRCodeGenerator.spec.ts @@ -150,7 +150,7 @@ describe('QRCodeGenerator -->', () => { expect(transactionObj.type).to.deep.equal(QRCodeType.RequestTransaction); }); - it.only('Read data From ContactQR', () => { + it('Read data From ContactQR', () => { // Arrange: const account = PublicAccount.createFromPublicKey( 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', @@ -170,7 +170,25 @@ describe('QRCodeGenerator -->', () => { expect(contactObj.type).to.deep.equal(QRCodeType.AddContact); }); - it('Read data From AccountQR', () => {}); + it('Read data From AccountQR', () => { + // Arrange: + const account = Account.createFromPrivateKey( + 'F97AE23C2A28ECEDE6F8D6C447C0A10B55C92DDE9316CCD36C3177B073906978', + NetworkType.MIJIN_TEST + ); + const password = new Password('password'); + + const exportAccount = QRCodeGenerator.createExportAccount(account,password); + const actualObj = exportAccount.toJSON(); + + // Act: + const accountObj: AccountQR = QRCodeGenerator.fromJSON(actualObj,password); + + // Assert: + expect(accountObj).to.not.be.equal(''); + expect(accountObj.account).to.deep.equal(account); + expect(accountObj.type).to.deep.equal(QRCodeType.ExportAccount); + }); it('Read data From ObjectQR', () => {}); }); From 8dd4dc165e66b6929fbbc6e9f266004227c89fa6 Mon Sep 17 00:00:00 2001 From: Greg S Date: Wed, 22 May 2019 13:54:35 +0200 Subject: [PATCH 09/13] nemtech/NIP#21 : refactor with Schemas implementation, fixed AccountQR, ContactQR and TransactionQR, added CosignatureQR --- index.ts | 20 ++- package-lock.json | 6 + package.json | 1 + src/AccountQR.ts | 65 ++++++---- src/ContactQR.ts | 59 ++++++--- src/CosignatureQR.ts | 101 ++++++++++++++++ src/EncryptedPayload.ts | 82 +++++++++++++ src/ObjectQR.ts | 49 ++++++-- src/QRCode.ts | 33 ++++- src/QRCodeDataSchema.ts | 67 +++++++++++ src/QRCodeGenerator.ts | 86 ++++++++----- src/QRCodeSettings.ts | 14 +-- src/TransactionQR.ts | 52 +++++--- src/schemas/AddContactDataSchema.ts | 82 +++++++++++++ src/schemas/ExportAccountDataSchema.ts | 93 ++++++++++++++ src/schemas/ExportObjectDataSchema.ts | 73 +++++++++++ src/schemas/RequestCosignatureDataSchema.ts | 74 ++++++++++++ src/schemas/RequestTransactionDataSchema.ts | 84 +++++++++++++ src/services/EncryptionService.ts | 127 ++++++++++++++++++++ test/ContactQR.spec.ts | 3 +- test/QRCode.spec.ts | 21 ++-- test/QRCodeGenerator.spec.ts | 24 +++- 22 files changed, 1084 insertions(+), 132 deletions(-) create mode 100644 src/CosignatureQR.ts create mode 100644 src/EncryptedPayload.ts create mode 100644 src/QRCodeDataSchema.ts create mode 100644 src/schemas/AddContactDataSchema.ts create mode 100644 src/schemas/ExportAccountDataSchema.ts create mode 100644 src/schemas/ExportObjectDataSchema.ts create mode 100644 src/schemas/RequestCosignatureDataSchema.ts create mode 100644 src/schemas/RequestTransactionDataSchema.ts create mode 100644 src/services/EncryptionService.ts diff --git a/index.ts b/index.ts index 0d2713c..80eb989 100644 --- a/index.ts +++ b/index.ts @@ -13,13 +13,31 @@ * See the License for the specific language governing permissions and *limitations under the License. */ + +// enumerations / interfaces export { QRCodeType } from './src/QRCodeType'; export { QRCodeSettings } from './src/QRCodeSettings'; export { QRCodeInterface } from './src/QRCodeInterface'; export { QRCode } from './src/QRCode'; + +// encryption +export { EncryptedPayload } from './src/EncryptedPayload'; +export { EncryptionService } from './src/services/EncryptionService'; + +// QR Code data schemas +export { QRCodeDataSchema } from './src/QRCodeDataSchema'; +export { AddContactDataSchema } from './src/schemas/AddContactDataSchema'; +export { ExportAccountDataSchema } from './src/schemas/ExportAccountDataSchema'; +export { ExportObjectDataSchema } from './src/schemas/ExportObjectDataSchema'; +export { RequestCosignatureDataSchema } from './src/schemas/RequestCosignatureDataSchema'; +export { RequestTransactionDataSchema } from './src/schemas/RequestTransactionDataSchema'; + +// specialized QR Code classes export { AccountQR } from './src/AccountQR'; export { ContactQR } from './src/ContactQR'; export { ObjectQR } from './src/ObjectQR'; export { TransactionQR } from './src/TransactionQR'; +export { CosignatureQR } from './src/CosignatureQR'; + +// factory export { QRCodeGenerator } from './src/QRCodeGenerator'; -export { QRService } from './src/QRService'; diff --git a/package-lock.json b/package-lock.json index ad1db38..2570896 100644 --- a/package-lock.json +++ b/package-lock.json @@ -139,6 +139,12 @@ "integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==", "dev": true }, + "@types/node": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.2.tgz", + "integrity": "sha512-5tabW/i+9mhrfEOUcLDu2xBPsHJ+X5Orqy9FKpale3SjDA17j5AEpYq5vfy3oAeAHGcvANRCO3NV3d2D6q3NiA==", + "dev": true + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", diff --git a/package.json b/package.json index 404d2c3..4d7e0f3 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "devDependencies": { "@types/chai": "^4.1.3", "@types/mocha": "^5.2.0", + "@types/node": "^12.0.2", "chai": "^4.1.2", "coveralls": "^3.0.2", "mocha": "^5.2.0", diff --git a/src/AccountQR.ts b/src/AccountQR.ts index 078fecb..8d64384 100644 --- a/src/AccountQR.ts +++ b/src/AccountQR.ts @@ -17,7 +17,6 @@ import { Account, PublicAccount, NetworkType, - Wallet, Password, } from "nem2-sdk"; @@ -27,9 +26,11 @@ import { QRCodeInterface, QRCodeType, QRCodeSettings, - QRService + EncryptionService, + EncryptedPayload, + QRCodeDataSchema, + ExportAccountDataSchema } from '../index'; -import { throwError } from "rxjs"; export class AccountQR extends QRCode implements QRCodeInterface { /** @@ -49,7 +50,7 @@ export class AccountQR extends QRCode implements QRCodeInterface { * The password for encryption * @var {Password} */ - protected readonly password: Password, + public readonly password: Password, /** * The network type. * @var {NetworkType} @@ -64,30 +65,46 @@ export class AccountQR extends QRCode implements QRCodeInterface { } /** - * The `toJSON()` method should return the JSON - * representation of the QR Code content. + * Parse a JSON QR code content into a AccountQR + * object. * - * @return {string} + * @param json {string} + * @param password {Password} + * @return {AccountQR} + * @throws {Error} On empty `json` given. + * @throws {Error} On missing `type` field value. + * @throws {Error} On unrecognized QR code `type` field value. */ - public toJSON(): string { + static fromJSON( + json: string, + password: Password + ): AccountQR { - if (this.password == null) { - throw Error('Password is missing'); - } - const qrService: QRService = new QRService(); - const encryption = qrService.AES_PBKF2_encryption(this.password, this.account.privateKey); + // create the QRCode object from JSON + return ExportAccountDataSchema.parse(json, password); + } - const jsonSchema = { - 'v': 3, - 'type': this.type, - 'network_id': this.networkType, - 'chain_id': this.chainId, - 'data': { - 'priv_key': encryption.encrypted, - 'salt': encryption.salt, - }, - }; + /** + * The `getTypeNumber()` method should return the + * version number for QR codes of the underlying class. + * + * @see https://en.wikipedia.org/wiki/QR_code#Storage + * @return {number} + */ + public getTypeNumber(): number { + // Type version for ContactQR is Version 10 + // This type of QR can hold up to 174 bytes of data. + return 10; + } - return JSON.stringify(jsonSchema); + /** + * The `getSchema()` method should return an instance + * of a sub-class of QRCodeDataSchema which describes + * the QR Code data. + * + * @return {QRCodeDataSchema} + */ + public getSchema(): QRCodeDataSchema { + return new ExportAccountDataSchema(); } } \ No newline at end of file diff --git a/src/ContactQR.ts b/src/ContactQR.ts index c2f6a8d..5d74696 100644 --- a/src/ContactQR.ts +++ b/src/ContactQR.ts @@ -17,6 +17,7 @@ import { Account, PublicAccount, NetworkType, + Address, } from "nem2-sdk"; // internal dependencies @@ -25,6 +26,8 @@ import { QRCodeInterface, QRCodeType, QRCodeSettings, + QRCodeDataSchema, + AddContactDataSchema } from '../index'; export class ContactQR extends QRCode implements QRCodeInterface { @@ -37,8 +40,13 @@ export class ContactQR extends QRCode implements QRCodeInterface { * @param chainId {string} */ constructor(/** + * The contact name. + * @var {string} + */ + public readonly name: string, + /** * The contact account. - * @var {Account|PublicAccount} + * @var {Account|PublicAccount|Address} */ public readonly account: Account | PublicAccount, /** @@ -55,23 +63,44 @@ export class ContactQR extends QRCode implements QRCodeInterface { } /** - * The `toJSON()` method should return the JSON - * representation of the QR Code content. + * Parse a JSON QR code content into a ContactQR + * object. * - * @return {string} + * @param json {string} + * @return {ContactQR} + * @throws {Error} On empty `json` given. + * @throws {Error} On missing `type` field value. + * @throws {Error} On unrecognized QR code `type` field value. */ - public toJSON(): string { + static fromJSON( + json: string + ): ContactQR { - const jsonSchema = { - 'v': 3, - 'type': this.type, - 'network_id': this.networkType, - 'chain_id': this.chainId, - 'data': { - 'address': this.account - }, - }; + // create the QRCode object from JSON + return AddContactDataSchema.parse(json); + } + + /** + * The `getTypeNumber()` method should return the + * version number for QR codes of the underlying class. + * + * @see https://en.wikipedia.org/wiki/QR_code#Storage + * @return {number} + */ + public getTypeNumber(): number { + // Type version for ContactQR is Version 10 + // This type of QR can hold up to 174 bytes of data. + return 10; + } - return JSON.stringify(jsonSchema).trim(); + /** + * The `getSchema()` method should return an instance + * of a sub-class of QRCodeDataSchema which describes + * the QR Code data. + * + * @return {QRCodeDataSchema} + */ + public getSchema(): QRCodeDataSchema { + return new AddContactDataSchema(); } } \ No newline at end of file diff --git a/src/CosignatureQR.ts b/src/CosignatureQR.ts new file mode 100644 index 0000000..ee6bcf4 --- /dev/null +++ b/src/CosignatureQR.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + *limitations under the License. + */ +import { + Account, + PublicAccount, + NetworkType, +} from "nem2-sdk"; + +// internal dependencies +import { + QRCode, + QRCodeInterface, + QRCodeType, + QRCodeSettings, + QRCodeDataSchema, + RequestCosignatureDataSchema +} from '../index'; + +//XXX should it maybe extend TransactionQR to make use of version-40 ? +export class CosignatureQR extends QRCode implements QRCodeInterface { + /** + * Construct a Object QR Code out of the + * JSON object. + * + * @param object {Object} + * @param networkType {NetworkType} + * @param chainId {string} + */ + constructor(/** + * The hash of the transaction to co-sign + * @var {string} + */ + public readonly hash: string, + /** + * The network type. + * @var {NetworkType} + */ + public readonly networkType: NetworkType, + /** + * The chain Id. + * @var {string} + */ + public readonly chainId: string) { + super(QRCodeType.ExportObject, networkType, chainId); + } + + /** + * Parse a JSON QR code content into a CosignatureQR + * object. + * + * @param json {string} + * @return {CosignatureQR} + * @throws {Error} On empty `json` given. + * @throws {Error} On missing `type` field value. + * @throws {Error} On unrecognized QR code `type` field value. + */ + static fromJSON( + json: string + ): CosignatureQR { + + // create the QRCode object from JSON + return RequestCosignatureDataSchema.parse(json); + } + + /** + * The `getTypeNumber()` method should return the + * version number for QR codes of the underlying class. + * + * @see https://en.wikipedia.org/wiki/QR_code#Storage + * @return {number} + */ + public getTypeNumber(): number { + // Type version for ContactQR is Version 10 + // This type of QR can hold up to 174 bytes of data. + return 10; + } + + /** + * The `getSchema()` method should return an instance + * of a sub-class of QRCodeDataSchema which describes + * the QR Code data. + * + * @return {QRCodeDataSchema} + */ + public getSchema(): QRCodeDataSchema { + return new RequestCosignatureDataSchema(); + } +} \ No newline at end of file diff --git a/src/EncryptedPayload.ts b/src/EncryptedPayload.ts new file mode 100644 index 0000000..059b0f1 --- /dev/null +++ b/src/EncryptedPayload.ts @@ -0,0 +1,82 @@ +/** + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + *limitations under the License. + */ +// internal dependencies +import {QRCodeType} from '../index'; + +/** + * Class `EncryptedPayload` describes an encrypted payload + * with salt and ciphertext properties. + * + * @since 0.3.0 + */ +export class EncryptedPayload { + + constructor(/** + * The payload ciphertext. + * The first X bytes represent the IV. + * + * @var {string} + */ + public readonly ciphertext: string, + /** + * The payload salt. + * + * @var {string} + */ + public readonly salt: string) {} + + /** + * Parse a JSON representation of an encrypted + * payload into a `EncryptedPayload` instance. + * + * The provided JSON must contain fields 'ciphertext' + * and 'salt'. + * + * @param {string} json + * @return {EncryptedPayload} + */ + public static fromJSON( + json: string + ): EncryptedPayload { + + if (! json.length) { + throw new Error('JSON argument cannot be empty.'); + } + + // validate JSON + let jsonObject: any; + try { + jsonObject = JSON.parse(json); + } + catch (e) { + // Invalid JSON provided, forward error + throw new Error(e); + } + + // validate obligatory fields + if (!jsonObject.hasOwnProperty('ciphertext')) { + throw new Error("Missing mandatory field 'ciphertext'."); + } + + if (!jsonObject.hasOwnProperty('salt')) { + throw new Error("Missing mandatory field 'salt'."); + } + + return new EncryptedPayload(jsonObject.ciphertext, jsonObject.salt); + } + + +} diff --git a/src/ObjectQR.ts b/src/ObjectQR.ts index 83a3d86..5688b61 100644 --- a/src/ObjectQR.ts +++ b/src/ObjectQR.ts @@ -25,6 +25,8 @@ import { QRCodeInterface, QRCodeType, QRCodeSettings, + QRCodeDataSchema, + ExportObjectDataSchema } from '../index'; export class ObjectQR extends QRCode implements QRCodeInterface { @@ -55,21 +57,44 @@ export class ObjectQR extends QRCode implements QRCodeInterface { } /** - * The `toJSON()` method should return the JSON - * representation of the QR Code content. + * Parse a JSON QR code content into a ObjectQR + * object. * - * @return {string} + * @param json {string} + * @return {ObjectQR} + * @throws {Error} On empty `json` given. + * @throws {Error} On missing `type` field value. + * @throws {Error} On unrecognized QR code `type` field value. */ - public toJSON(): string { + static fromJSON( + json: string + ): ObjectQR { - const jsonSchema = { - 'v': 3, - 'type': this.type, - 'network_id': this.networkType, - 'chain_id': this.chainId, - 'data': this.object, - }; + // create the QRCode object from JSON + return ExportObjectDataSchema.parse(json); + } - return JSON.stringify(jsonSchema); + /** + * The `getTypeNumber()` method should return the + * version number for QR codes of the underlying class. + * + * @see https://en.wikipedia.org/wiki/QR_code#Storage + * @return {number} + */ + public getTypeNumber(): number { + // Type version for ContactQR is Version 10 + // This type of QR can hold up to 174 bytes of data. + return 10; + } + + /** + * The `getSchema()` method should return an instance + * of a sub-class of QRCodeDataSchema which describes + * the QR Code data. + * + * @return {QRCodeDataSchema} + */ + public getSchema(): QRCodeDataSchema { + return new ExportObjectDataSchema(); } } \ No newline at end of file diff --git a/src/QRCode.ts b/src/QRCode.ts index 64f432e..875e185 100644 --- a/src/QRCode.ts +++ b/src/QRCode.ts @@ -31,6 +31,7 @@ import { QRCodeInterface, QRCodeType, QRCodeSettings, + QRCodeDataSchema, } from "../index"; export abstract class QRCode implements QRCodeInterface { @@ -66,14 +67,40 @@ export abstract class QRCode implements QRCodeInterface { } /// region Abstract Methods + /** + * The `getSchema()` method should return an instance + * of a sub-class of QRCodeDataSchema which describes + * the QR Code data. + * + * @return {QRCodeDataSchema} + */ + public abstract getSchema(): QRCodeDataSchema; + /** + * The `getTypeNumber()` method should return the + * version number for QR codes of the underlying class. + * + * @return {number} + */ + public abstract getTypeNumber(): number; + /// end-region Abstract Methods + /** * The `toJSON()` method should return the JSON * representation of the QR Code content. * * @return {string} */ - public abstract toJSON(): string; - /// end-region Abstract Methods + public toJSON(): string { + + // get the QR Code Data Schema + const schema = this.getSchema(); + + // create the JSON object for this QR Code + const json = schema.toObject(this); + + // format to JSON + return JSON.stringify(json); + } /** * The `build()` method should return the QRCode @@ -85,7 +112,7 @@ export abstract class QRCode implements QRCodeInterface { // prepare QR generation const qr = new QRCodeImpl(); - qr.setTypeNumber(QRCodeSettings.VERSION_NUMBER); + qr.setTypeNumber(this.getTypeNumber()); qr.setErrorCorrectLevel(QRCodeSettings.CORRECTION_LEVEL); // get JSON representation diff --git a/src/QRCodeDataSchema.ts b/src/QRCodeDataSchema.ts new file mode 100644 index 0000000..00407d3 --- /dev/null +++ b/src/QRCodeDataSchema.ts @@ -0,0 +1,67 @@ +/** + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + *limitations under the License. + */ +// internal dependencies +import {QRCode} from '../index'; + +/** + * Class `QRCodeDataSchema` describes a QR Code's data + * schema. The schema defines obligatory fields and their + * format. + * + * @since 0.3.0 + */ +export abstract class QRCodeDataSchema { + + /** + * The AccountQR QR Code version + * + * @var {number} + */ + public readonly VERSION = 3; + + constructor() {} + + /// region Abstract Methods + /** + * The `getData()` method returns an object + * that will be stored in the `data` field of + * the underlying QR Code JSON content. + * + * @return {any} + */ + public abstract getData(qr: QRCode): any; + /// end-region Abstract Methods + + /** + * The `toObject()` method returns a JSON object + * with required fields. + * + * @return {any} + */ + public toObject(qr: QRCode): any { + + // read data from child-classes + const data = this.getData(qr); + + return { + "v": this.VERSION, + "type": qr.type, + "network_id": qr.networkType, + "chain_id": qr.chainId, + "data": data + }; + } +} diff --git a/src/QRCodeGenerator.ts b/src/QRCodeGenerator.ts index 60e8874..6ba066c 100644 --- a/src/QRCodeGenerator.ts +++ b/src/QRCodeGenerator.ts @@ -31,7 +31,8 @@ import { ContactQR, ObjectQR, TransactionQR, - QRService, + CosignatureQR, + QRCode } from '../index'; /** @@ -89,11 +90,12 @@ export class QRCodeGenerator { * @param chainId {string} */ public static createContact( + name: string, account: Account | PublicAccount, networkType: NetworkType = NetworkType.MIJIN_TEST, chainId: string = 'E2A9F95E129283EF47B92A62FD748DBA4D32AA718AE6F8AC99C105CFA9F27A31' ): ContactQR { - return new ContactQR(account, networkType, chainId); + return new ContactQR(name, account, networkType, chainId); } public static createExportAccount( @@ -106,46 +108,66 @@ export class QRCodeGenerator { } /** - * Read JSON Content from QRcode. + * Parse a JSON QR code content into a sub-class + * of QRCode. + * * @param json {string} + * @return {QRCode} + * @throws {Error} On empty `json` given. + * @throws {Error} On missing `type` field value. + * @throws {Error} On unrecognized QR code `type` field value. */ - static fromJSON(json:string, password?: Password) :any { + static fromJSON( + json: string, + password: Password | undefined = undefined + ): QRCode { - if (json == null || json == '') { - throw Error('QR json object is missing'); + if (! json.length) { + throw new Error('JSON argument cannot be empty.'); } - const jsonObj = JSON.parse(json || ''); + const jsonObj = JSON.parse(json); + if (!jsonObj.type) { + throw new Error('Missing mandatory field with name "type".'); + } - switch(jsonObj.type) { - case QRCodeType.AddContact: { - return new ContactQR(jsonObj.data.address, jsonObj.network_id, jsonObj.chainId) - } - case QRCodeType.ExportAccount: { - if (password == null){ - throw Error('Password are required'); - } + // We will use the `fromJSON` static implementation + // of specialized QRCode classes (child classes). + // An error will be thrown if the QRCodeType is not + // recognized or invalid. - const qrService: QRService = new QRService(); - const privatekey: string = qrService.AES_PBKF2_decryption(password,jsonObj); + switch (jsonObj.type) { - const account = Account.createFromPrivateKey(privatekey, - NetworkType.MIJIN_TEST); + // create a ContactQR from JSON + case QRCodeType.AddContact: + return ContactQR.fromJSON(json); - return new AccountQR(account, password, jsonObj.network_id, jsonObj.chainId) - } - case QRCodeType.RequestTransaction: { - let txMapping: Transaction = TransactionMapping.createFromPayload(jsonObj.data.payload); + // create an AccountQR from JSON + case QRCodeType.ExportAccount: - return new TransactionQR(txMapping, jsonObj.network_id, jsonObj.chainId) - } - case QRCodeType.RequestCosignature: { - // Todo: In progress; - break; - } - case QRCodeType.ExportObject: { - return new ObjectQR(jsonObj.data.object, jsonObj.network_id, jsonObj.chainId); + // password obligatory for encryption + if (! password) { + throw new Error('Missing password to decrypt AccountQR QR code.'); } - } + + return AccountQR.fromJSON(json, password); + + // create a ObjectQR from JSON + case QRCodeType.ExportObject: + return ObjectQR.fromJSON(json); + + // create a CosignatureQR from JSON + case QRCodeType.RequestCosignature: + return CosignatureQR.fromJSON(json); + + // create a TransactionQR from JSON + case QRCodeType.RequestTransaction: + return TransactionQR.fromJSON(json); + + default: + break; + } + + throw new Error("Unrecognized QR Code 'type': '" + jsonObj.type + "'."); } } diff --git a/src/QRCodeSettings.ts b/src/QRCodeSettings.ts index a206a0a..fb1f92a 100644 --- a/src/QRCodeSettings.ts +++ b/src/QRCodeSettings.ts @@ -36,19 +36,11 @@ export class QRCodeSettings { public static CORRECTION_LEVEL = ErrorCorrectLevel.L; /** - * The QR Code version number. - * - * With `40-L` configuration, the QR Code can contain - * up to `2953` bytes. As defined in the following link, - * this is the maximum storage capacity for our types - * or QR Codes: - * - * https://en.wikipedia.org/wiki/QR_code#Design + * The NEM network QR Code version * - * @see https://en.wikipedia.org/wiki/QR_code#Design - * @var {string} + * @var {number} */ - public static VERSION_NUMBER = 40; + public static VERSION = 3; /** * The QR Code cell size in pixels. diff --git a/src/TransactionQR.ts b/src/TransactionQR.ts index 3d2fea8..5ca1d49 100644 --- a/src/TransactionQR.ts +++ b/src/TransactionQR.ts @@ -25,6 +25,8 @@ import { QRCodeInterface, QRCodeType, QRCodeSettings, + QRCodeDataSchema, + RequestTransactionDataSchema } from '../index'; export class TransactionQR extends QRCode implements QRCodeInterface { @@ -55,26 +57,44 @@ export class TransactionQR extends QRCode implements QRCodeInterface { } /** - * The `toJSON()` method should return the JSON - * representation of the QR Code content. + * Parse a JSON QR code content into a TransactionQR + * object. * - * @return {string} + * @param json {string} + * @return {TransactionQR} + * @throws {Error} On empty `json` given. + * @throws {Error} On missing `type` field value. + * @throws {Error} On unrecognized QR code `type` field value. */ - public toJSON(): string { + static fromJSON( + json: string + ): TransactionQR { - // Serialize the transaction object data. - const txSerialized = this.transaction.serialize(); + // create the QRCode object from JSON + return RequestTransactionDataSchema.parse(json); + } - const jsonSchema = { - 'v': 3, - 'type': this.type, - 'network_id': this.networkType, - 'chain_id': this.chainId, - 'data': { - 'payload': txSerialized - } - }; + /** + * The `getTypeNumber()` method should return the + * version number for QR codes of the underlying class. + * + * @see https://en.wikipedia.org/wiki/QR_code#Storage + * @return {number} + */ + public getTypeNumber(): number { + // Type version for ContactQR is Version 40 + // This type of QR can hold up to 1264 bytes of data. + return 40; + } - return JSON.stringify(jsonSchema).trim(); + /** + * The `getSchema()` method should return an instance + * of a sub-class of QRCodeDataSchema which describes + * the QR Code data. + * + * @return {QRCodeDataSchema} + */ + public getSchema(): QRCodeDataSchema { + return new RequestTransactionDataSchema(); } } \ No newline at end of file diff --git a/src/schemas/AddContactDataSchema.ts b/src/schemas/AddContactDataSchema.ts new file mode 100644 index 0000000..8e7e7db --- /dev/null +++ b/src/schemas/AddContactDataSchema.ts @@ -0,0 +1,82 @@ +/** + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + *limitations under the License. + */ +import { + Address, + Account, + PublicAccount, +} from "nem2-sdk"; + +// internal dependencies +import { + QRCodeDataSchema, + QRCode, + QRCodeType, + ContactQR +} from '../../index'; + +/** + * Class `AddContactDataSchema` describes a contact + * add QR code data schema. + * + * @since 0.3.0 + */ +export class AddContactDataSchema extends QRCodeDataSchema { + + /** + * The `getData()` method returns an object + * that will be stored in the `data` field of + * the underlying QR Code JSON content. + * + * @return {any} + */ + public getData(qr: ContactQR): any { + return { + "name": qr.name, + "publicKey": qr.account.publicKey.toString(), + }; + } + + /** + * Parse a JSON QR code content into a ContactQR + * object. + * + * @param json {string} + * @return {ContactQR} + * @throws {Error} On empty `json` given. + * @throws {Error} On missing `type` field value. + * @throws {Error} On unrecognized QR code `type` field value. + */ + static parse( + json: string + ): ContactQR { + if (! json.length) { + throw Error('JSON argument cannot be empty.'); + } + + const jsonObj = JSON.parse(json); + if (!jsonObj.type || jsonObj.type !== QRCodeType.AddContact) { + throw Error('Invalid type field value for ContactQR.'); + } + + // read contact data + const name = jsonObj.data.name; + const network = jsonObj.network_id; + const account = PublicAccount.createFromPublicKey(jsonObj.data.publicKey, network); + const chainId = jsonObj.chain_id; + + return new ContactQR(name, account, network, chainId); + } +} diff --git a/src/schemas/ExportAccountDataSchema.ts b/src/schemas/ExportAccountDataSchema.ts new file mode 100644 index 0000000..6413034 --- /dev/null +++ b/src/schemas/ExportAccountDataSchema.ts @@ -0,0 +1,93 @@ +/** + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + *limitations under the License. + */ +import { + Account, + PublicAccount, + NetworkType, + Password, +} from "nem2-sdk"; + +// internal dependencies +import { + QRCodeDataSchema, + QRCode, + QRCodeType, + AccountQR, + EncryptionService, + EncryptedPayload, +} from '../../index'; + +/** + * Class `ExportAccountDataSchema` describes an export + * account QR code data schema. + * + * @since 0.3.0 + */ +export class ExportAccountDataSchema extends QRCodeDataSchema { + + /** + * The `getData()` method returns an object + * that will be stored in the `data` field of + * the underlying QR Code JSON content. + * + * @return {any} + */ + public getData(qr: AccountQR): any { + + // we will store a password encrypted copy of the private key + const encrypted = EncryptionService.encrypt(qr.account.privateKey, qr.password); + + return { + "ciphertext": encrypted.ciphertext, + "salt": encrypted.salt, + }; + } + + /** + * Parse a JSON QR code content into a AccountQR + * object. + * + * @param json {string} + * @param password {Password} + * @return {AccountQR} + * @throws {Error} On empty `json` given. + * @throws {Error} On missing `type` field value. + * @throws {Error} On unrecognized QR code `type` field value. + */ + static parse( + json: string, + password: Password + ): AccountQR { + if (! json.length) { + throw Error('JSON argument cannot be empty.'); + } + + const jsonObj = JSON.parse(json); + if (!jsonObj.type || jsonObj.type !== QRCodeType.ExportAccount) { + throw Error('Invalid type field value for AccountQR.'); + } + + // decrypt private key + const payload = new EncryptedPayload(jsonObj.data.ciphertext, jsonObj.data.salt); + const privKey = EncryptionService.decrypt(payload, password); + const network = jsonObj.network_id; + const chainId = jsonObj.chain_id; + + // create account + const account = Account.createFromPrivateKey(privKey, network); + return new AccountQR(account, password, network, chainId); + } +} diff --git a/src/schemas/ExportObjectDataSchema.ts b/src/schemas/ExportObjectDataSchema.ts new file mode 100644 index 0000000..a853ac1 --- /dev/null +++ b/src/schemas/ExportObjectDataSchema.ts @@ -0,0 +1,73 @@ +/** + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + *limitations under the License. + */ +// internal dependencies +import { + QRCodeDataSchema, + QRCode, + QRCodeType, + ObjectQR +} from '../../index'; + +/** + * Class `ExportObjectDataSchema` describes an export + * object QR code data schema. + * + * @since 0.3.0 + */ +export class ExportObjectDataSchema extends QRCodeDataSchema { + + /** + * The `getData()` method returns an object + * that will be stored in the `data` field of + * the underlying QR Code JSON content. + * + * @return {any} + */ + public getData(qr: ObjectQR): any { + + return qr.object; + } + + /** + * Parse a JSON QR code content into a ObjectQR + * object. + * + * @param json {string} + * @return {ObjectQR} + * @throws {Error} On empty `json` given. + * @throws {Error} On missing `type` field value. + * @throws {Error} On unrecognized QR code `type` field value. + */ + static parse( + json: string + ): ObjectQR { + if (! json.length) { + throw Error('JSON argument cannot be empty.'); + } + + const jsonObj = JSON.parse(json); + if (!jsonObj.type || jsonObj.type !== QRCodeType.ExportObject) { + throw Error('Invalid type field value for ObjectQR.'); + } + + // read contact data + const obj = jsonObj.data; + const network = jsonObj.network_id; + const chainId = jsonObj.chain_id; + + return new ObjectQR(obj, network, chainId); + } +} diff --git a/src/schemas/RequestCosignatureDataSchema.ts b/src/schemas/RequestCosignatureDataSchema.ts new file mode 100644 index 0000000..057339f --- /dev/null +++ b/src/schemas/RequestCosignatureDataSchema.ts @@ -0,0 +1,74 @@ +/** + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + *limitations under the License. + */ +// internal dependencies +import { + QRCodeDataSchema, + QRCode, + QRCodeType, + CosignatureQR +} from '../../index'; + +/** + * Class `RequestCosignatureDataSchema` describes a transaction + * request QR code data schema. + * + * @since 0.3.0 + */ +export class RequestCosignatureDataSchema extends QRCodeDataSchema { + + /** + * The `getData()` method returns an object + * that will be stored in the `data` field of + * the underlying QR Code JSON content. + * + * @return {any} + */ + public getData(qr: CosignatureQR): any { + return { + "hash": qr.hash + }; + } + + /** + * Parse a JSON QR code content into a CosignatureQR + * object. + * + * @param json {string} + * @return {CosignatureQR} + * @throws {Error} On empty `json` given. + * @throws {Error} On missing `type` field value. + * @throws {Error} On unrecognized QR code `type` field value. + */ + static parse( + json: string + ): CosignatureQR { + if (! json.length) { + throw Error('JSON argument cannot be empty.'); + } + + const jsonObj = JSON.parse(json); + if (!jsonObj.type || jsonObj.type !== QRCodeType.RequestCosignature) { + throw Error('Invalid type field value for CosignatureQR.'); + } + + // read contact data + const hash = jsonObj.data.hash; + const network = jsonObj.network_id; + const chainId = jsonObj.chain_id; + + return new CosignatureQR(hash, network, chainId); + } +} diff --git a/src/schemas/RequestTransactionDataSchema.ts b/src/schemas/RequestTransactionDataSchema.ts new file mode 100644 index 0000000..37b33f7 --- /dev/null +++ b/src/schemas/RequestTransactionDataSchema.ts @@ -0,0 +1,84 @@ +/** + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + *limitations under the License. + */ +import { + NetworkType, + TransactionMapping, + Transaction, +} from "nem2-sdk"; + +// internal dependencies +import { + QRCodeDataSchema, + QRCode, + QRCodeType, + TransactionQR +} from '../../index'; + +/** + * Class `RequestTransactionDataSchema` describes a transaction + * request QR code data schema. + * + * @since 0.3.0 + */ +export class RequestTransactionDataSchema extends QRCodeDataSchema { + + /** + * The `getData()` method returns an object + * that will be stored in the `data` field of + * the underlying QR Code JSON content. + * + * @return {any} + */ + public getData(qr: TransactionQR): any { + + // serialize the transaction object data. + const payload = qr.transaction.serialize(); + + return { + "payload": payload + }; + } + + /** + * Parse a JSON QR code content into a TransactionQR + * object. + * + * @param json {string} + * @return {TransactionQR} + * @throws {Error} On empty `json` given. + * @throws {Error} On missing `type` field value. + * @throws {Error} On unrecognized QR code `type` field value. + */ + static parse( + json: string + ): TransactionQR { + if (! json.length) { + throw Error('JSON argument cannot be empty.'); + } + + const jsonObj = JSON.parse(json); + if (!jsonObj.type || jsonObj.type !== QRCodeType.RequestTransaction) { + throw Error('Invalid type field value for TransactionQR.'); + } + + // read contact data + const transaction = TransactionMapping.createFromPayload(jsonObj.data.payload); + const network = jsonObj.network_id; + const chainId = jsonObj.chain_id; + + return new TransactionQR(transaction, network, chainId); + } +} diff --git a/src/services/EncryptionService.ts b/src/services/EncryptionService.ts new file mode 100644 index 0000000..048f4fb --- /dev/null +++ b/src/services/EncryptionService.ts @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2019 NEM Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License "); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + convert, + nacl_catapult, + sha3Hasher +} from 'nem2-library'; +import { + Password, +} from "nem2-sdk"; +import * as CryptoJS from "crypto-js"; + +// internal dependencies +import { + EncryptedPayload +} from '../../index'; + +/** + * Class `EncryptionService` describes a high level service + * for encryption/decryption of data. + * + * Implemented algorithms for encryption/decryption include: + * - AES with PBKDF2 (Password-Based Key Derivation Function) + * + * @since 0.3.0 + */ +export class EncryptionService { + + /** + * The `encrypt` method will encrypt given `data` raw string + * with given `password` password. + * + * First we generate a random salt of 32 bytes, then we iterate + * 2000 times with PBKDF2 and encrypt with AES. + * + * @param password {Password} + * @param data {string} + */ + public static encrypt( + data: string, + password: Password + ): EncryptedPayload { + + // create random salt (32 bytes) + const salt = CryptoJS.lib.WordArray.random(32); + + // derive key of 8 bytes with 2000 iterations of PBKDF2 + const key = CryptoJS.PBKDF2(password.value, salt, { + keySize: 8, + iterations: 2000, + }); + + // create encryption input vector of 16 bytes (iv) + const iv = CryptoJS.lib.WordArray.random(16); + + // format IV for crypto-js encryption + const encIv = { + iv: iv + }; + + // encrypt with AES + const dataBin = CryptoJS.lib.WordArray.create(data); + const encrypted = CryptoJS.AES.encrypt(dataBin, key, encIv); + + // create our `EncryptedPayload` + const ciphertext = iv.toString() + encrypted.toString(); + const used_salt = salt.toString(); + + return new EncryptedPayload(ciphertext, used_salt); + } + + /** + * AES_PBKF2_decryption will decrypt privateKey with provided password + * @param password + * @param json + */ + public static decrypt( + payloadOrJson: EncryptedPayload | string, + password: Password + ): string { + + let payload: EncryptedPayload; + + // parse input if necessary + if (payloadOrJson instanceof EncryptedPayload) { + payload = payloadOrJson; + } + else { + payload = EncryptedPayload.fromJSON(payloadOrJson); + } + + // read payload + const salt = CryptoJS.enc.Hex.parse(payload.salt); + const priv = payload.ciphertext; + + // read encryption configuration + const iv: string = CryptoJS.enc.Hex.parse(priv.substring(0, 32)); + const cipher: string = priv.substring(32, 96); + + const encIv = { + iv: iv + }; + + // re-generate key (PBKDF2) + const key = CryptoJS.PBKDF2(password.value, salt, { + keySize: 8, + iterations: 2000, + }); + + // decrypt and return + const decrypted = CryptoJS.AES.decrypt(cipher, key, encIv); + return decrypted.toString(); + } +} \ No newline at end of file diff --git a/test/ContactQR.spec.ts b/test/ContactQR.spec.ts index c74f0c3..90b11ac 100644 --- a/test/ContactQR.spec.ts +++ b/test/ContactQR.spec.ts @@ -39,13 +39,14 @@ describe('ContactQR -->', () => { it('include mandatory NIP-7 QR Code base fields', () => { // Arrange: + const name = 'test-contact-1'; const account = PublicAccount.createFromPublicKey( 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', NetworkType.TEST_NET ); // Act: - const addContact = new ContactQR(account, NetworkType.TEST_NET, ''); + const addContact = new ContactQR(name, account, NetworkType.TEST_NET, ''); const actualJSON = addContact.toJSON(); const actualObject = JSON.parse(actualJSON); diff --git a/test/QRCode.spec.ts b/test/QRCode.spec.ts index cca45b2..f6868d6 100644 --- a/test/QRCode.spec.ts +++ b/test/QRCode.spec.ts @@ -36,6 +36,8 @@ import { QRCode, QRCodeType, QRCodeSettings, + QRCodeDataSchema, + ExportObjectDataSchema, } from "../index"; /// region Mock for QRCode specialization @@ -48,15 +50,12 @@ class FakeQR extends QRCode implements QRCodeInterface { super(QRCodeType.ExportObject, networkType, chainId); } - public toJSON(): string { - const jsonSchema = { - 'v': 3, - 'type': this.type, - 'network_id': this.networkType, - 'chain_id': this.chainId, - 'data': this.object, - }; - return JSON.stringify(jsonSchema); + public getSchema(): QRCodeDataSchema { + return new ExportObjectDataSchema(); + } + + public getTypeNumber(): number { + return 10; } } /// end-region Mock for QRCode specialization @@ -101,14 +100,14 @@ describe('QRCode -->', () => { it('set correct settings for QR Code generation', () => { // Arrange: const object = {"test": "test"}; - const modulesCount = QRCodeSettings.VERSION_NUMBER * 4 + 17; + const modulesCount = 10 * 4 + 17; // Act: const fakeQR = new FakeQR(object, NetworkType.TEST_NET, 'no-chain-id'); const implQR = fakeQR.build(); // Assert: - expect(implQR.getTypeNumber()).to.be.equal(QRCodeSettings.VERSION_NUMBER); + expect(implQR.getTypeNumber()).to.be.equal(10); expect(implQR.getErrorCorrectLevel()).to.be.equal(QRCodeSettings.CORRECTION_LEVEL); expect(implQR.getModuleCount()).to.be.equal(modulesCount); }); diff --git a/test/QRCodeGenerator.spec.ts b/test/QRCodeGenerator.spec.ts index 0677869..391b276 100644 --- a/test/QRCodeGenerator.spec.ts +++ b/test/QRCodeGenerator.spec.ts @@ -29,7 +29,13 @@ import { } from 'nem2-sdk'; // internal dependencies -import { QRCodeGenerator, AccountQR, TransactionQR, QRCodeType, ContactQR } from "../index"; +import { + QRCodeGenerator, + AccountQR, + TransactionQR, + QRCodeType, + ContactQR +} from "../index"; // vectors data import { @@ -86,13 +92,14 @@ describe('QRCodeGenerator -->', () => { it('generate correct Base64 representation for TransferTransaction', () => { // Arrange: + const name = 'test-contact-1'; const account = PublicAccount.createFromPublicKey( 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', NetworkType.MIJIN_TEST ); // Act: - const createContact = QRCodeGenerator.createContact(account); + const createContact = QRCodeGenerator.createContact(name, account); const actualBase64 = createContact.toBase64(); // Assert: @@ -142,7 +149,7 @@ describe('QRCodeGenerator -->', () => { const txJSON = requestTx.toJSON(); // Act: - const transactionObj: TransactionQR = QRCodeGenerator.fromJSON(txJSON); + const transactionObj: TransactionQR = QRCodeGenerator.fromJSON(txJSON) as TransactionQR; // Assert: expect(transactionObj).to.not.be.equal(''); @@ -152,17 +159,22 @@ describe('QRCodeGenerator -->', () => { it('Read data From ContactQR', () => { // Arrange: + const name = 'test-contact-1'; const account = PublicAccount.createFromPublicKey( 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', NetworkType.MIJIN_TEST ); - const createContact = QRCodeGenerator.createContact(account,NetworkType.MIJIN_TEST); + const createContact = QRCodeGenerator.createContact( + name, + account, + NetworkType.MIJIN_TEST + ); const contactJSON = createContact.toJSON(); // Act: - const contactObj: ContactQR = QRCodeGenerator.fromJSON(contactJSON); + const contactObj: ContactQR = QRCodeGenerator.fromJSON(contactJSON) as ContactQR; // Assert: expect(contactObj).to.not.be.equal(''); @@ -182,7 +194,7 @@ describe('QRCodeGenerator -->', () => { const actualObj = exportAccount.toJSON(); // Act: - const accountObj: AccountQR = QRCodeGenerator.fromJSON(actualObj,password); + const accountObj: AccountQR = QRCodeGenerator.fromJSON(actualObj,password) as AccountQR; // Assert: expect(accountObj).to.not.be.equal(''); From 23a1294398151d6a2785a8cc4467d91cf9bc7373 Mon Sep 17 00:00:00 2001 From: Greg S Date: Thu, 23 May 2019 14:24:18 +0200 Subject: [PATCH 10/13] nemtech/NIP#21 : add specialized schema unit tests, fixed CosignatureQR, TransactionQR --- index.ts | 2 +- src/CosignatureQR.ts | 33 +++--- src/TransactionQR.ts | 10 +- src/schemas/AddContactDataSchema.ts | 4 + src/schemas/ExportAccountDataSchema.ts | 4 + src/schemas/ExportObjectDataSchema.ts | 4 + src/schemas/RequestCosignatureDataSchema.ts | 33 +++--- src/schemas/RequestTransactionDataSchema.ts | 4 + test/AccountQR.spec.ts | 22 +++- test/ContactQR.spec.ts | 18 +++ test/CosignatureQR.spec.ts | 115 ++++++++++++++++++++ test/TransactionQR.spec.ts | 22 ++++ 12 files changed, 233 insertions(+), 38 deletions(-) create mode 100644 test/CosignatureQR.spec.ts diff --git a/index.ts b/index.ts index 80eb989..d94a13a 100644 --- a/index.ts +++ b/index.ts @@ -29,8 +29,8 @@ export { QRCodeDataSchema } from './src/QRCodeDataSchema'; export { AddContactDataSchema } from './src/schemas/AddContactDataSchema'; export { ExportAccountDataSchema } from './src/schemas/ExportAccountDataSchema'; export { ExportObjectDataSchema } from './src/schemas/ExportObjectDataSchema'; -export { RequestCosignatureDataSchema } from './src/schemas/RequestCosignatureDataSchema'; export { RequestTransactionDataSchema } from './src/schemas/RequestTransactionDataSchema'; +export { RequestCosignatureDataSchema } from './src/schemas/RequestCosignatureDataSchema'; // specialized QR Code classes export { AccountQR } from './src/AccountQR'; diff --git a/src/CosignatureQR.ts b/src/CosignatureQR.ts index ee6bcf4..2d75de5 100644 --- a/src/CosignatureQR.ts +++ b/src/CosignatureQR.ts @@ -14,9 +14,10 @@ *limitations under the License. */ import { - Account, - PublicAccount, NetworkType, + TransactionMapping, + Transaction, + AggregateTransaction, } from "nem2-sdk"; // internal dependencies @@ -26,24 +27,24 @@ import { QRCodeType, QRCodeSettings, QRCodeDataSchema, - RequestCosignatureDataSchema + RequestCosignatureDataSchema, + TransactionQR, } from '../index'; -//XXX should it maybe extend TransactionQR to make use of version-40 ? -export class CosignatureQR extends QRCode implements QRCodeInterface { +export class CosignatureQR extends TransactionQR implements QRCodeInterface { /** - * Construct a Object QR Code out of the - * JSON object. - * - * @param object {Object} + * Construct a Transaction Request QR Code out of the + * nem2-sdk Transaction instance. + * + * @param transaction {Transaction} * @param networkType {NetworkType} * @param chainId {string} */ constructor(/** - * The hash of the transaction to co-sign - * @var {string} + * The transaction for the request. + * @var {AggregateTransaction} */ - public readonly hash: string, + public readonly transaction: AggregateTransaction, /** * The network type. * @var {NetworkType} @@ -54,7 +55,7 @@ export class CosignatureQR extends QRCode implements QRCodeInterface { * @var {string} */ public readonly chainId: string) { - super(QRCodeType.ExportObject, networkType, chainId); + super(transaction, networkType, chainId, QRCodeType.RequestCosignature); } /** @@ -83,9 +84,9 @@ export class CosignatureQR extends QRCode implements QRCodeInterface { * @return {number} */ public getTypeNumber(): number { - // Type version for ContactQR is Version 10 - // This type of QR can hold up to 174 bytes of data. - return 10; + // Type version for ContactQR is Version 40 + // This type of QR can hold up to 1264 bytes of data. + return 40; } /** diff --git a/src/TransactionQR.ts b/src/TransactionQR.ts index 5ca1d49..d5efba1 100644 --- a/src/TransactionQR.ts +++ b/src/TransactionQR.ts @@ -52,8 +52,14 @@ export class TransactionQR extends QRCode implements QRCodeInterface { * The chain Id. * @var {string} */ - public readonly chainId: string) { - super(QRCodeType.RequestTransaction, networkType, chainId); + public readonly chainId: string, + /** + * The QR Code Type + * + * @var {QRCodeType} + */ + public readonly type: QRCodeType = QRCodeType.RequestTransaction) { + super(type, networkType, chainId); } /** diff --git a/src/schemas/AddContactDataSchema.ts b/src/schemas/AddContactDataSchema.ts index 8e7e7db..0123237 100644 --- a/src/schemas/AddContactDataSchema.ts +++ b/src/schemas/AddContactDataSchema.ts @@ -35,6 +35,10 @@ import { */ export class AddContactDataSchema extends QRCodeDataSchema { + constructor() { + super(); + } + /** * The `getData()` method returns an object * that will be stored in the `data` field of diff --git a/src/schemas/ExportAccountDataSchema.ts b/src/schemas/ExportAccountDataSchema.ts index 6413034..a82bdc4 100644 --- a/src/schemas/ExportAccountDataSchema.ts +++ b/src/schemas/ExportAccountDataSchema.ts @@ -38,6 +38,10 @@ import { */ export class ExportAccountDataSchema extends QRCodeDataSchema { + constructor() { + super(); + } + /** * The `getData()` method returns an object * that will be stored in the `data` field of diff --git a/src/schemas/ExportObjectDataSchema.ts b/src/schemas/ExportObjectDataSchema.ts index a853ac1..84bd700 100644 --- a/src/schemas/ExportObjectDataSchema.ts +++ b/src/schemas/ExportObjectDataSchema.ts @@ -29,6 +29,10 @@ import { */ export class ExportObjectDataSchema extends QRCodeDataSchema { + constructor() { + super(); + } + /** * The `getData()` method returns an object * that will be stored in the `data` field of diff --git a/src/schemas/RequestCosignatureDataSchema.ts b/src/schemas/RequestCosignatureDataSchema.ts index 057339f..39f89bb 100644 --- a/src/schemas/RequestCosignatureDataSchema.ts +++ b/src/schemas/RequestCosignatureDataSchema.ts @@ -13,33 +13,32 @@ * See the License for the specific language governing permissions and *limitations under the License. */ +import { + NetworkType, + TransactionMapping, + Transaction, + AggregateTransaction, +} from "nem2-sdk"; + // internal dependencies import { QRCodeDataSchema, QRCode, QRCodeType, - CosignatureQR + CosignatureQR, + RequestTransactionDataSchema, } from '../../index'; /** * Class `RequestCosignatureDataSchema` describes a transaction - * request QR code data schema. + * cosignature request QR code data schema. * * @since 0.3.0 */ -export class RequestCosignatureDataSchema extends QRCodeDataSchema { +export class RequestCosignatureDataSchema extends RequestTransactionDataSchema { - /** - * The `getData()` method returns an object - * that will be stored in the `data` field of - * the underlying QR Code JSON content. - * - * @return {any} - */ - public getData(qr: CosignatureQR): any { - return { - "hash": qr.hash - }; + constructor() { + super(); } /** @@ -60,15 +59,15 @@ export class RequestCosignatureDataSchema extends QRCodeDataSchema { } const jsonObj = JSON.parse(json); - if (!jsonObj.type || jsonObj.type !== QRCodeType.RequestCosignature) { + if (!jsonObj.type || jsonObj.type !== QRCodeType.RequestTransaction) { throw Error('Invalid type field value for CosignatureQR.'); } // read contact data - const hash = jsonObj.data.hash; + const transaction = TransactionMapping.createFromPayload(jsonObj.data.payload); const network = jsonObj.network_id; const chainId = jsonObj.chain_id; - return new CosignatureQR(hash, network, chainId); + return new CosignatureQR(transaction as AggregateTransaction, network, chainId); } } diff --git a/src/schemas/RequestTransactionDataSchema.ts b/src/schemas/RequestTransactionDataSchema.ts index 37b33f7..17b0ca8 100644 --- a/src/schemas/RequestTransactionDataSchema.ts +++ b/src/schemas/RequestTransactionDataSchema.ts @@ -35,6 +35,10 @@ import { */ export class RequestTransactionDataSchema extends QRCodeDataSchema { + constructor() { + super(); + } + /** * The `getData()` method returns an object * that will be stored in the `data` field of diff --git a/test/AccountQR.spec.ts b/test/AccountQR.spec.ts index 17b5787..55ec23e 100644 --- a/test/AccountQR.spec.ts +++ b/test/AccountQR.spec.ts @@ -39,7 +39,7 @@ describe('AccountQR -->', () => { describe('toJSON() should', () => { - it.only('include mandatory NIP-7 QR Code base fields', () => { + it('include mandatory NIP-7 QR Code base fields', () => { // Arrange: const account = Account.createFromPrivateKey( 'F97AE23C2A28ECEDE6F8D6C447C0A10B55C92DDE9316CCD36C3177B073906978', @@ -52,13 +52,31 @@ describe('AccountQR -->', () => { const actualJSON = exportAccount.toJSON(); const actualObject = JSON.parse(actualJSON); - // // // Assert: + // Assert: expect(actualObject).to.have.property('v'); expect(actualObject).to.have.property('type'); expect(actualObject).to.have.property('network_id'); expect(actualObject).to.have.property('chain_id'); expect(actualObject).to.have.property('data'); }); + + it('include specialized schema fields', () => { + // Arrange: + const account = Account.createFromPrivateKey( + 'F97AE23C2A28ECEDE6F8D6C447C0A10B55C92DDE9316CCD36C3177B073906978', + NetworkType.MIJIN_TEST + ); + const password = new Password('password'); + + // Act: + const exportAccount = new AccountQR(account, password, NetworkType.MIJIN_TEST, 'no-chain-id'); + const actualJSON = exportAccount.toJSON(); + const actualObject = JSON.parse(actualJSON); + + // Assert: + expect(actualObject.data).to.have.property('ciphertext'); + expect(actualObject.data).to.have.property('salt'); + }); }); }); diff --git a/test/ContactQR.spec.ts b/test/ContactQR.spec.ts index 90b11ac..2fccdc5 100644 --- a/test/ContactQR.spec.ts +++ b/test/ContactQR.spec.ts @@ -57,6 +57,24 @@ describe('ContactQR -->', () => { expect(actualObject).to.have.property('chain_id'); expect(actualObject).to.have.property('data'); }); + + it('include specialized schema fields', () => { + // Arrange: + const name = 'test-contact-1'; + const account = PublicAccount.createFromPublicKey( + 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', + NetworkType.TEST_NET + ); + + // Act: + const addContact = new ContactQR(name, account, NetworkType.TEST_NET, ''); + const actualJSON = addContact.toJSON(); + const actualObject = JSON.parse(actualJSON); + + // Assert: + expect(actualObject.data).to.have.property('name'); + expect(actualObject.data).to.have.property('publicKey'); + }); }); }); diff --git a/test/CosignatureQR.spec.ts b/test/CosignatureQR.spec.ts new file mode 100644 index 0000000..65f20c4 --- /dev/null +++ b/test/CosignatureQR.spec.ts @@ -0,0 +1,115 @@ +/** + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + *limitations under the License. + */ +import {expect} from "chai"; +import { + AggregateTransaction, + TransferTransaction, + Deadline, + Address, + Mosaic, + NamespaceId, + UInt64, + PlainMessage, + NetworkType, + PublicAccount, +} from 'nem2-sdk'; +import { + QRCode as QRCodeImpl, + QR8BitByte, + ErrorCorrectLevel, +} from 'qrcode-generator-ts'; + +// internal dependencies +import { + QRCodeInterface, + QRCode, + QRCodeType, + QRCodeSettings, + CosignatureQR, +} from "../index"; + +describe('CosignatureQR -->', () => { + + describe('toJSON() should', () => { + + it('include mandatory NIP-7 QR Code base fields', () => { + // Arrange: + const account = PublicAccount.createFromPublicKey( + 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', + NetworkType.MIJIN_TEST + ); + const transfer = TransferTransaction.create( + Deadline.create(), + Address.createFromPublicKey( + 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', + NetworkType.MIJIN_TEST + ), + [new Mosaic(new NamespaceId('cat.currency'), UInt64.fromUint(10000000))], + PlainMessage.create('Welcome to NEM!'), + NetworkType.MIJIN_TEST + ); + const bonded = AggregateTransaction.createBonded( + Deadline.create(), + [transfer.toAggregate(account)], + NetworkType.MIJIN_TEST + ); + + // Act: + const requestTx = new CosignatureQR(bonded, NetworkType.TEST_NET, ''); + const actualJSON = requestTx.toJSON(); + const actualObject = JSON.parse(actualJSON); + + // Assert: + expect(actualObject).to.have.property('v'); + expect(actualObject).to.have.property('type'); + expect(actualObject).to.have.property('network_id'); + expect(actualObject).to.have.property('chain_id'); + expect(actualObject).to.have.property('data'); + }); + + it('include specialized schema fields', () => { + // Arrange: + const account = PublicAccount.createFromPublicKey( + 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', + NetworkType.MIJIN_TEST + ); + const transfer = TransferTransaction.create( + Deadline.create(), + Address.createFromPublicKey( + 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', + NetworkType.MIJIN_TEST + ), + [new Mosaic(new NamespaceId('cat.currency'), UInt64.fromUint(10000000))], + PlainMessage.create('Welcome to NEM!'), + NetworkType.MIJIN_TEST + ); + const bonded = AggregateTransaction.createBonded( + Deadline.create(), + [transfer.toAggregate(account)], + NetworkType.MIJIN_TEST + ); + + // Act: + const requestTx = new CosignatureQR(bonded, NetworkType.TEST_NET, ''); + const actualJSON = requestTx.toJSON(); + const actualObject = JSON.parse(actualJSON); + + // Assert: + expect(actualObject.data).to.have.property('payload'); + }); + }); + +}); diff --git a/test/TransactionQR.spec.ts b/test/TransactionQR.spec.ts index 281f71c..57a7b02 100644 --- a/test/TransactionQR.spec.ts +++ b/test/TransactionQR.spec.ts @@ -68,6 +68,28 @@ describe('TransactionQR -->', () => { expect(actualObject).to.have.property('chain_id'); expect(actualObject).to.have.property('data'); }); + + it('include specialized schema fields', () => { + // Arrange: + const transfer = TransferTransaction.create( + Deadline.create(), + Address.createFromPublicKey( + 'C5C55181284607954E56CD46DE85F4F3EF4CC713CC2B95000FA741998558D268', + NetworkType.MIJIN_TEST + ), + [new Mosaic(new NamespaceId('cat.currency'), UInt64.fromUint(10000000))], + PlainMessage.create('Welcome to NEM!'), + NetworkType.MIJIN_TEST + ); + + // Act: + const requestTx = new TransactionQR(transfer, NetworkType.TEST_NET, ''); + const actualJSON = requestTx.toJSON(); + const actualObject = JSON.parse(actualJSON); + + // Assert: + expect(actualObject.data).to.have.property('payload'); + }); }); }); From ca6eb07d4e42c32dd3c58c80051393290b693cbf Mon Sep 17 00:00:00 2001 From: Greg S Date: Mon, 27 May 2019 20:53:57 +0200 Subject: [PATCH 11/13] nemtech/NIP#21 : WIP on unit tests, fixed AccountQR, WIP on EncryptionService --- index.ts | 8 +-- src/AccountQR.ts | 4 +- src/ContactQR.ts | 2 +- src/CosignatureQR.ts | 2 +- src/EncryptedPayload.ts | 1 - src/ObjectQR.ts | 2 +- src/QRCode.ts | 2 +- src/QRCodeGenerator.ts | 68 ++++++++++++++--------- src/QRCodeSettings.ts | 7 --- src/QRService.ts | 77 -------------------------- src/TransactionQR.ts | 2 +- src/schemas/ExportAccountDataSchema.ts | 4 ++ test/AccountQR.spec.ts | 12 +--- test/ContactQR.spec.ts | 11 +--- test/CosignatureQR.spec.ts | 6 +- test/EncryptedPayload.spec.ts | 75 +++++++++++++++++++++++++ test/EncryptionService.spec.ts | 76 +++++++++++++++++++++++++ test/QRCode.spec.ts | 9 +-- test/QRCodeGenerator.spec.ts | 53 ++++++++++-------- test/TransactionQR.spec.ts | 11 +--- test/vectors/OBJECT.ts | 16 ------ test/vectors/index.ts | 16 ------ 22 files changed, 244 insertions(+), 220 deletions(-) delete mode 100644 src/QRService.ts create mode 100644 test/EncryptedPayload.spec.ts create mode 100644 test/EncryptionService.spec.ts delete mode 100644 test/vectors/OBJECT.ts delete mode 100644 test/vectors/index.ts diff --git a/index.ts b/index.ts index d94a13a..0767a12 100644 --- a/index.ts +++ b/index.ts @@ -20,10 +20,6 @@ export { QRCodeSettings } from './src/QRCodeSettings'; export { QRCodeInterface } from './src/QRCodeInterface'; export { QRCode } from './src/QRCode'; -// encryption -export { EncryptedPayload } from './src/EncryptedPayload'; -export { EncryptionService } from './src/services/EncryptionService'; - // QR Code data schemas export { QRCodeDataSchema } from './src/QRCodeDataSchema'; export { AddContactDataSchema } from './src/schemas/AddContactDataSchema'; @@ -32,6 +28,10 @@ export { ExportObjectDataSchema } from './src/schemas/ExportObjectDataSchema'; export { RequestTransactionDataSchema } from './src/schemas/RequestTransactionDataSchema'; export { RequestCosignatureDataSchema } from './src/schemas/RequestCosignatureDataSchema'; +// encryption +export { EncryptedPayload } from './src/EncryptedPayload'; +export { EncryptionService } from './src/services/EncryptionService'; + // specialized QR Code classes export { AccountQR } from './src/AccountQR'; export { ContactQR } from './src/ContactQR'; diff --git a/src/AccountQR.ts b/src/AccountQR.ts index 8d64384..7da0dfd 100644 --- a/src/AccountQR.ts +++ b/src/AccountQR.ts @@ -94,7 +94,7 @@ export class AccountQR extends QRCode implements QRCodeInterface { public getTypeNumber(): number { // Type version for ContactQR is Version 10 // This type of QR can hold up to 174 bytes of data. - return 10; + return 20; } /** @@ -107,4 +107,4 @@ export class AccountQR extends QRCode implements QRCodeInterface { public getSchema(): QRCodeDataSchema { return new ExportAccountDataSchema(); } -} \ No newline at end of file +} diff --git a/src/ContactQR.ts b/src/ContactQR.ts index 5d74696..6e14500 100644 --- a/src/ContactQR.ts +++ b/src/ContactQR.ts @@ -103,4 +103,4 @@ export class ContactQR extends QRCode implements QRCodeInterface { public getSchema(): QRCodeDataSchema { return new AddContactDataSchema(); } -} \ No newline at end of file +} diff --git a/src/CosignatureQR.ts b/src/CosignatureQR.ts index 2d75de5..463444b 100644 --- a/src/CosignatureQR.ts +++ b/src/CosignatureQR.ts @@ -99,4 +99,4 @@ export class CosignatureQR extends TransactionQR implements QRCodeInterface { public getSchema(): QRCodeDataSchema { return new RequestCosignatureDataSchema(); } -} \ No newline at end of file +} diff --git a/src/EncryptedPayload.ts b/src/EncryptedPayload.ts index 059b0f1..d822bfc 100644 --- a/src/EncryptedPayload.ts +++ b/src/EncryptedPayload.ts @@ -78,5 +78,4 @@ export class EncryptedPayload { return new EncryptedPayload(jsonObject.ciphertext, jsonObject.salt); } - } diff --git a/src/ObjectQR.ts b/src/ObjectQR.ts index 5688b61..64ce12a 100644 --- a/src/ObjectQR.ts +++ b/src/ObjectQR.ts @@ -97,4 +97,4 @@ export class ObjectQR extends QRCode implements QRCodeInterface { public getSchema(): QRCodeDataSchema { return new ExportObjectDataSchema(); } -} \ No newline at end of file +} diff --git a/src/QRCode.ts b/src/QRCode.ts index 875e185..060c607 100644 --- a/src/QRCode.ts +++ b/src/QRCode.ts @@ -140,4 +140,4 @@ export abstract class QRCode implements QRCodeInterface { QRCodeSettings.MARGIN_PIXEL ); } -} \ No newline at end of file +} diff --git a/src/QRCodeGenerator.ts b/src/QRCodeGenerator.ts index 6ba066c..0d68c5e 100644 --- a/src/QRCodeGenerator.ts +++ b/src/QRCodeGenerator.ts @@ -21,18 +21,17 @@ import { PublicAccount, Password } from "nem2-sdk"; -import * as CryptoJS from "crypto-js"; // internal dependencies import { QRCodeInterface, QRCodeType, + QRCode, AccountQR, ContactQR, ObjectQR, TransactionQR, - CosignatureQR, - QRCode + CosignatureQR } from '../index'; /** @@ -53,6 +52,7 @@ export class QRCodeGenerator { /** * Create a JSON object QR Code from a JSON object. * + * @see {ObjectQR} * @param object {Object} * @param networkType {NetworkType} * @param chainId {string} @@ -66,47 +66,58 @@ export class QRCodeGenerator { } /** - * Create a Transaction Request QR Code from a Transaction - * instance. + * Create a Contact QR Code from a contact name + * and account. * + * @see {ContactQR} * @param transaction {Transaction} * @param networkType {NetworkType} * @param chainId {string} */ - public static createTransactionRequest( - transaction: Transaction, + public static createAddContact( + name: string, + account: Account | PublicAccount, networkType: NetworkType = NetworkType.MIJIN_TEST, chainId: string = 'E2A9F95E129283EF47B92A62FD748DBA4D32AA718AE6F8AC99C105CFA9F27A31' - ): TransactionQR { - return new TransactionQR(transaction, networkType, chainId); + ): ContactQR { + return new ContactQR(name, account, networkType, chainId); } /** * Create a Transaction Request QR Code from a Transaction * instance. * + * @see {AccountQR} * @param transaction {Transaction} * @param networkType {NetworkType} * @param chainId {string} */ - public static createContact( - name: string, - account: Account | PublicAccount, - networkType: NetworkType = NetworkType.MIJIN_TEST, - chainId: string = 'E2A9F95E129283EF47B92A62FD748DBA4D32AA718AE6F8AC99C105CFA9F27A31' - ): ContactQR { - return new ContactQR(name, account, networkType, chainId); - } - public static createExportAccount( account: Account, password: Password, networkType: NetworkType = NetworkType.MIJIN_TEST, - chainId: string = 'E2A9F95E129283EF47B92A62FD748DBA4D32AA718AE6F8AC99C105CFA9F27A31', + chainId: string = 'E2A9F95E129283EF47B92A62FD748DBA4D32AA718AE6F8AC99C105CFA9F27A31' ): AccountQR { return new AccountQR(account, password, networkType, chainId); } + /** + * Create a Transaction Request QR Code from a Transaction + * instance. + * + * @see {TransactionQR} + * @param transaction {Transaction} + * @param networkType {NetworkType} + * @param chainId {string} + */ + public static createTransactionRequest( + transaction: Transaction, + networkType: NetworkType = NetworkType.MIJIN_TEST, + chainId: string = 'E2A9F95E129283EF47B92A62FD748DBA4D32AA718AE6F8AC99C105CFA9F27A31' + ): TransactionQR { + return new TransactionQR(transaction, networkType, chainId); + } + /** * Parse a JSON QR code content into a sub-class * of QRCode. @@ -126,9 +137,16 @@ export class QRCodeGenerator { throw new Error('JSON argument cannot be empty.'); } - const jsonObj = JSON.parse(json); - if (!jsonObj.type) { - throw new Error('Missing mandatory field with name "type".'); + let jsonObject: any; + try { + jsonObject = JSON.parse(json); + if (!jsonObject.type) { + throw new Error('Missing mandatory field with name "type".'); + } + } + catch(e) { + // Invalid JSON provided, forward error + throw new Error(e); } // We will use the `fromJSON` static implementation @@ -136,7 +154,7 @@ export class QRCodeGenerator { // An error will be thrown if the QRCodeType is not // recognized or invalid. - switch (jsonObj.type) { + switch (jsonObject.type) { // create a ContactQR from JSON case QRCodeType.AddContact: @@ -151,7 +169,7 @@ export class QRCodeGenerator { } return AccountQR.fromJSON(json, password); - + // create a ObjectQR from JSON case QRCodeType.ExportObject: return ObjectQR.fromJSON(json); @@ -168,6 +186,6 @@ export class QRCodeGenerator { break; } - throw new Error("Unrecognized QR Code 'type': '" + jsonObj.type + "'."); + throw new Error("Unrecognized QR Code 'type': '" + jsonObject.type + "'."); } } diff --git a/src/QRCodeSettings.ts b/src/QRCodeSettings.ts index fb1f92a..595bb9c 100644 --- a/src/QRCodeSettings.ts +++ b/src/QRCodeSettings.ts @@ -35,13 +35,6 @@ export class QRCodeSettings { */ public static CORRECTION_LEVEL = ErrorCorrectLevel.L; - /** - * The NEM network QR Code version - * - * @var {number} - */ - public static VERSION = 3; - /** * The QR Code cell size in pixels. * diff --git a/src/QRService.ts b/src/QRService.ts deleted file mode 100644 index ea3bb58..0000000 --- a/src/QRService.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2019 NEM Foundation - * - * Licensed under the Apache License, Version 2.0 (the "License "); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - Password, -} from "nem2-sdk"; -import {convert,nacl_catapult} from 'nem2-library'; -import * as CryptoJS from "crypto-js"; - -export class QRService { - - /** - * AES_PBKF2_encryption will encrypt privateKey with provided password. - * @param password {Password} - * @param privateKey {strinf} - */ - public AES_PBKF2_encryption(password: Password, privateKey: string): any { - const salt = CryptoJS.lib.WordArray.random(256 / 8); - const key = CryptoJS.PBKDF2(password.value, salt, { - keySize: 256 / 32, - iterations: 2000, - }); - - const hex = convert.uint8ToHex(nacl_catapult.randomBytes(16)); - - const encIv = { - iv: CryptoJS.enc.Hex.parse(hex), - }; - - const encrypted = CryptoJS.AES.encrypt(CryptoJS.enc.Hex.parse(privateKey), key, encIv); - - return { - encrypted: hex + encrypted.toString(), - salt: salt.toString(), - }; - } - - /** - * AES_PBKF2_decryption will decrypt privateKey with provided password - * @param password - * @param json - */ - public AES_PBKF2_decryption(password: Password, json: any): string { - const encryptedData = json; - const salt = CryptoJS.enc.Hex.parse(encryptedData.data.salt); - const iv = CryptoJS.enc.Hex.parse(encryptedData.data.priv_key.substring(0, 32)); - const encrypted: string = encryptedData.data.priv_key.substring(32, 96); - - //generate key - const key = CryptoJS.PBKDF2(password.value, salt, { - keySize: 256 / 32, - iterations: 2000, - }); - - let encIv = { - iv: iv - }; - - let decrypt = CryptoJS.enc.Hex.stringify(CryptoJS.AES.decrypt(encrypted, key, encIv)); - - if (decrypt === "" || (decrypt.length != 64 && decrypt.length != 66)) throw new Error("invalid password"); - return decrypt; - } -} \ No newline at end of file diff --git a/src/TransactionQR.ts b/src/TransactionQR.ts index d5efba1..232a8d6 100644 --- a/src/TransactionQR.ts +++ b/src/TransactionQR.ts @@ -103,4 +103,4 @@ export class TransactionQR extends QRCode implements QRCodeInterface { public getSchema(): QRCodeDataSchema { return new RequestTransactionDataSchema(); } -} \ No newline at end of file +} diff --git a/src/schemas/ExportAccountDataSchema.ts b/src/schemas/ExportAccountDataSchema.ts index a82bdc4..c03e4e7 100644 --- a/src/schemas/ExportAccountDataSchema.ts +++ b/src/schemas/ExportAccountDataSchema.ts @@ -84,12 +84,16 @@ export class ExportAccountDataSchema extends QRCodeDataSchema { throw Error('Invalid type field value for AccountQR.'); } + console.log("JSON: ", jsonObj); + // decrypt private key const payload = new EncryptedPayload(jsonObj.data.ciphertext, jsonObj.data.salt); const privKey = EncryptionService.decrypt(payload, password); const network = jsonObj.network_id; const chainId = jsonObj.chain_id; + console.log("data: ", privKey); + // create account const account = Account.createFromPrivateKey(privKey, network); return new AccountQR(account, password, network, chainId); diff --git a/test/AccountQR.spec.ts b/test/AccountQR.spec.ts index 55ec23e..bc581ef 100644 --- a/test/AccountQR.spec.ts +++ b/test/AccountQR.spec.ts @@ -19,21 +19,11 @@ import { NetworkType, Password, } from 'nem2-sdk'; -import { - QRCode as QRCodeImpl, - QR8BitByte, - ErrorCorrectLevel, -} from 'qrcode-generator-ts'; // internal dependencies import { - QRCodeInterface, - QRCode, - QRCodeType, - QRCodeSettings, - ContactQR, + AccountQR, } from "../index"; -import { AccountQR } from "../src/AccountQR"; describe('AccountQR -->', () => { diff --git a/test/ContactQR.spec.ts b/test/ContactQR.spec.ts index 2fccdc5..cc8bd41 100644 --- a/test/ContactQR.spec.ts +++ b/test/ContactQR.spec.ts @@ -18,18 +18,9 @@ import { PublicAccount, NetworkType, } from 'nem2-sdk'; -import { - QRCode as QRCodeImpl, - QR8BitByte, - ErrorCorrectLevel, -} from 'qrcode-generator-ts'; // internal dependencies -import { - QRCodeInterface, - QRCode, - QRCodeType, - QRCodeSettings, +import { ContactQR, } from "../index"; diff --git a/test/CosignatureQR.spec.ts b/test/CosignatureQR.spec.ts index 65f20c4..db4b8c9 100644 --- a/test/CosignatureQR.spec.ts +++ b/test/CosignatureQR.spec.ts @@ -33,11 +33,7 @@ import { } from 'qrcode-generator-ts'; // internal dependencies -import { - QRCodeInterface, - QRCode, - QRCodeType, - QRCodeSettings, +import { CosignatureQR, } from "../index"; diff --git a/test/EncryptedPayload.spec.ts b/test/EncryptedPayload.spec.ts new file mode 100644 index 0000000..356cd30 --- /dev/null +++ b/test/EncryptedPayload.spec.ts @@ -0,0 +1,75 @@ +/** + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + *limitations under the License. + */ +import {expect} from "chai"; + +// internal dependencies +import { + EncryptedPayload, +} from "../index"; + +describe('EncryptedPayload -->', () => { + + describe('fromJSON() should', () => { + + it('throw on empty JSON', () => { + // Arrange: + const json = ''; + + // Act + Assert + expect((function () { + const payload = EncryptedPayload.fromJSON(json); + })).to.throw('JSON argument cannot be empty.'); + }); + + it('throw on missing ciphertext property', () => { + // Arrange: + const json = '{"salt": "00"}'; + + // Act + Assert + expect((function () { + const payload = EncryptedPayload.fromJSON(json); + })).to.throw('Missing mandatory field \'ciphertext\'.'); + }); + + it('throw on missing salt property', () => { + // Arrange: + const json = '{"ciphertext": "00"}'; + + // Act + Assert + expect((function () { + const payload = EncryptedPayload.fromJSON(json); + })).to.throw('Missing mandatory field \'salt\'.'); + }); + + it('create complete object', () => { + // Arrange: + const json = { + ciphertext: "zyFIAqnq8fihaJFqgH9gVKGT1Aa8dbxXqrcWb//Ckv7R/DJDgdXOY8ejc6KNURPGujULpv0fQnN87AQFldmCgkGYq0CBSHwhOhyCvEBK18g=", + salt: "12345678901234567890123456789012" + }; + + // Act + const payload = EncryptedPayload.fromJSON(JSON.stringify(json)); + + // Assert + expect(payload.ciphertext).to.not.be.undefined; + expect(payload.ciphertext).to.be.equal(json.ciphertext); + expect(payload.salt).to.not.be.undefined; + expect(payload.salt).to.be.equal(json.salt); + }); + }); + +}); diff --git a/test/EncryptionService.spec.ts b/test/EncryptionService.spec.ts new file mode 100644 index 0000000..95fce30 --- /dev/null +++ b/test/EncryptionService.spec.ts @@ -0,0 +1,76 @@ +/** + * Copyright 2019 NEM + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + *limitations under the License. + */ +import {expect} from "chai"; +import {Password} from 'nem2-sdk'; + +// internal dependencies +import { + EncryptionService, + EncryptedPayload, +} from "../index"; + +describe('EncryptionService -->', () => { + + describe('encrypt() should', () => { + + it('should create encrypted payload with salt', () => { + // Arrange: + const data = 'this will be encrypted.'; + const pass = 'password'; + + // Act + const encrypted = EncryptionService.encrypt(data, new Password(pass)); + + // Asset + expect(encrypted.ciphertext).to.not.be.undefined; + expect(encrypted.salt).to.not.be.undefined; + expect(encrypted.salt).to.have.lengthOf(64); + }); + + it('should create correctly sized ciphertext and salt', () => { + // Arrange: + const data = 'this will be encrypted.'; + const pass = 'password'; + + // Act + const encrypted = EncryptionService.encrypt(data, new Password(pass)); + + // Asset + expect(encrypted.ciphertext).to.have.lengthOf(160); + expect(encrypted.salt).to.have.lengthOf(64); + }); + + it('should always create different ciphertext with salt', () => { + // Arrange: + const data = 'this will be encrypted.'; + const pass = 'password'; + + // Act + const encrypted_1 = EncryptionService.encrypt(data, new Password(pass)); + const encrypted_2 = EncryptionService.encrypt(data, new Password(pass)); + const encrypted_3 = EncryptionService.encrypt(data, new Password(pass)); + + // Asset + expect(encrypted_1).to.not.be.equal(encrypted_2); + expect(encrypted_1).to.not.be.equal(encrypted_3); + expect(encrypted_2).to.not.be.equal(encrypted_3); + expect(encrypted_1.salt).to.have.lengthOf(64); + expect(encrypted_2.salt).to.have.lengthOf(64); + expect(encrypted_3.salt).to.have.lengthOf(64); + }); + }); + +}); diff --git a/test/QRCode.spec.ts b/test/QRCode.spec.ts index f6868d6..85d3892 100644 --- a/test/QRCode.spec.ts +++ b/test/QRCode.spec.ts @@ -15,13 +15,6 @@ */ import {expect} from "chai"; import { - TransferTransaction, - Deadline, - Address, - Mosaic, - NamespaceId, - UInt64, - PlainMessage, NetworkType, } from 'nem2-sdk'; import { @@ -31,7 +24,7 @@ import { } from 'qrcode-generator-ts'; // internal dependencies -import { +import { QRCodeInterface, QRCode, QRCodeType, diff --git a/test/QRCodeGenerator.spec.ts b/test/QRCodeGenerator.spec.ts index 391b276..cd9b1a6 100644 --- a/test/QRCodeGenerator.spec.ts +++ b/test/QRCodeGenerator.spec.ts @@ -30,33 +30,39 @@ import { // internal dependencies import { + QRCodeType, QRCodeGenerator, + ContactQR, AccountQR, TransactionQR, - QRCodeType, - ContactQR } from "../index"; -// vectors data -import { - ExpectedObjectBase64, -} from './vectors/index'; - describe('QRCodeGenerator -->', () => { describe('createExportObject() should', () => { - it('generate correct Base64 representation for {test: test}', () => { + + it('use default values for network_id and chain_id', () => { // Arrange: - const object = {"test": "test"}; - const qrcode = QRCodeGenerator.createExportObject(object, NetworkType.TEST_NET, 'no-chain-id'); + const object = {}; // Act: - const base64 = qrcode.toBase64(); + const objectQR = QRCodeGenerator.createExportObject(object); // Assert: - expect(base64).to.not.be.equal(''); - expect(base64.length).to.not.be.equal(0); - expect(base64).to.be.equal(ExpectedObjectBase64); + expect(objectQR.networkType).to.be.equal(NetworkType.MIJIN_TEST); + expect(objectQR.chainId).to.not.be.undefined; + expect(objectQR.chainId).to.have.lengthOf(64); + }); + + it('fill object property correctly with {test: test}', () => { + // Arrange: + const object = {test: "test"}; + + // Act: + const objectQR = QRCodeGenerator.createExportObject(object); + + // Assert: + expect(objectQR.object).to.deep.equal(object); }); }); @@ -88,7 +94,7 @@ describe('QRCodeGenerator -->', () => { }); }); - describe('createContact() should', ()=> { + describe('createAddContact() should', () => { it('generate correct Base64 representation for TransferTransaction', () => { // Arrange: @@ -99,7 +105,7 @@ describe('QRCodeGenerator -->', () => { ); // Act: - const createContact = QRCodeGenerator.createContact(name, account); + const createContact = QRCodeGenerator.createAddContact(name, account); const actualBase64 = createContact.toBase64(); // Assert: @@ -109,7 +115,7 @@ describe('QRCodeGenerator -->', () => { }); }); - describe('createExportAccount() should', ()=> { + describe('createExportAccount() should', () => { it('generate correct Base64 representation for ExportAccount', () => { // Arrange: @@ -120,7 +126,7 @@ describe('QRCodeGenerator -->', () => { const password = new Password('password'); // Act: - const exportAccount = QRCodeGenerator.createExportAccount(account,password); + const exportAccount = QRCodeGenerator.createExportAccount(account, password); const actualBase64 = exportAccount.toBase64(); // Assert: @@ -165,7 +171,7 @@ describe('QRCodeGenerator -->', () => { NetworkType.MIJIN_TEST ); - const createContact = QRCodeGenerator.createContact( + const createContact = QRCodeGenerator.createAddContact( name, account, NetworkType.MIJIN_TEST @@ -181,7 +187,7 @@ describe('QRCodeGenerator -->', () => { expect(contactObj.account.address).to.deep.equal(account.address); expect(contactObj.type).to.deep.equal(QRCodeType.AddContact); }); - +/* it('Read data From AccountQR', () => { // Arrange: const account = Account.createFromPrivateKey( @@ -190,18 +196,19 @@ describe('QRCodeGenerator -->', () => { ); const password = new Password('password'); - const exportAccount = QRCodeGenerator.createExportAccount(account,password); + const exportAccount = QRCodeGenerator.createExportAccount(account, password); const actualObj = exportAccount.toJSON(); // Act: - const accountObj: AccountQR = QRCodeGenerator.fromJSON(actualObj,password) as AccountQR; + const accountObj: AccountQR = QRCodeGenerator.fromJSON(actualObj, password) as AccountQR; // Assert: expect(accountObj).to.not.be.equal(''); expect(accountObj.account).to.deep.equal(account); expect(accountObj.type).to.deep.equal(QRCodeType.ExportAccount); }); - +*/ it('Read data From ObjectQR', () => {}); }); + }); diff --git a/test/TransactionQR.spec.ts b/test/TransactionQR.spec.ts index 57a7b02..accf6b4 100644 --- a/test/TransactionQR.spec.ts +++ b/test/TransactionQR.spec.ts @@ -24,18 +24,9 @@ import { PlainMessage, NetworkType, } from 'nem2-sdk'; -import { - QRCode as QRCodeImpl, - QR8BitByte, - ErrorCorrectLevel, -} from 'qrcode-generator-ts'; // internal dependencies -import { - QRCodeInterface, - QRCode, - QRCodeType, - QRCodeSettings, +import { TransactionQR, } from "../index"; diff --git a/test/vectors/OBJECT.ts b/test/vectors/OBJECT.ts deleted file mode 100644 index a93418b..0000000 --- a/test/vectors/OBJECT.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright 2019 NEM - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - *limitations under the License. - */ -export const ExpectedObjectBase64 = "data:image/gif;base64,R0lGODdhfwN/A4AAAAAAAP///ywAAAAAfwN/AwAC/4yPqcvtD6OctNqLs968+w+G4kiW5omm6sq27gvH8kzX9o3n+s73/g8MCofEovGITCqXzKbzCY1Kp9Sq9YrNarfcrvcLDosngLL5jE6r1Qk0eQ2PywEOd8N+wBvi7bNIr8CHANg3l1bnxwG4aEinmJgHuScZCFdYdunIQDho2WmWyRkhOgn6ibnSeHi6Gmm6qUpJ+qD69koiCHsbEEsZKus52tuLOFzLams8XIyq21z6zBsMrfkxm0vNHKvdwbh7vOEdfT0t/e2LfFcu3lpBfr57ovwLH00/l4kBDjHbXZ7Pal+lNfeq0VLWSBvCdeicLUyoLl46V+MYSgzXkBo7hf8QHfqrZ1CgPmAgOZacuOAdG5TmVlpQGTLjiHkBSdqraQjgy478ZH50mdLmtohAW7Y7+BDbwKQEWZpkWlRn0JMUj/a7AHMj0aEeH1HV+A+rUKlLrY7d+jQmyKtd22ZLZawg2YJK2U7FJ8FuBqVkaTpkpxdq2KqCvyItPPitW6NynXq9SfjsX55lf6qtmPPx5c1p+8rMKhnsxZ6jGTsu4Vc0Zsiq5czNS5mbCb4sU5cF7BOnYNkYSweOrZsz2tqUaUdeBrvp4saLjSsmvfpoMoPTmWtwfju0UZivhWX2Hr16cN53uT7fLvT39+vaQZem2z43eOqn3X2W39p9+OFV4a//vf/QdNxtNeBxrJk2n3UU6OWZcuw5eJhZ//mmnYCJVVYfhgbS1x+BxaUH4nvBMcjfefpxWF5dFYpFoYgJlhghdiq2uN+GuNGIkIU4dgajjPgh2NpILpanoJBReWiYj4bpeGRzP9544IkzRgclitlBaKRwHU42JHoQatVblD+SpyF0riFZI5HEreelmAEm1+R5Ra4ZJ5ARzvniThMeyKSVClZJJok2Prmik1hmaBug48F5ZphayrnofFM+uheAfDJKZlondmdiiEUpuVCfmUaq5oajGlrnqWbiad+hXKbZppVgtopXnpCiKh2icdEpYaojjtljoUEyOimnC1q6WbG//7o6rKm8+gcrtJQK+iCs1MaaZYPWXjjrscx6IBKpV4aaoba5vmpejBa5WWe33QZ6oap2XiuerbG+y6uyg347qbue9irrh0vOFl+XrN75bL7IGssjasA5i26ihO7p57K1mqlvigsnvOWq4tZb6q3WTEzlugFT7C9y6P5Z8MV2SmsuCylfujKL2ybJrbDqGmxzoxrjyBTDCncZNLEmNxzyvDqbe22BIoNLcrJHB8tmlaACPLPUXyKb8csE48wzxLRO2/K5nYbdLC4Ppz1ujuUOTXN+a7eNr9hlKj0wrk1v3PFMUcOsK8plh2t311brLFDdaoMdd9vZcnx2u0ujWbGjV/9LuWvf8H4rLXaAa/4x5Hg3rqie7FL6x98Wow43wCxvXZix9FJNbsTAgi5p5nK/Wfi/Rde876dcq263pgLffHrpY18tdO6CM6716c3/LDnFTE+tN/Zs95uU7Ld7HLzPdH/vtfPErC4x7CXHTnnniMfbep+Jl400tj0z/7b50bseP+2VU2+20fEvfN57HvKw5jvdle9pjwvf5eC3HKOpbHe1214CeQdA9+WNbZvzlQO1J8D6eY5yTlugt4RXtcHxjYEkXKH9+ja/Ha3uetZD3/FO1jiQ3WtuhAPf8hSIGARCL4giBOLwiPYv+d0wZu0Lnf5KOLvepXCILtwbm2h4wK7/YXGAFlQfVAqYQ0zZkF9zk5fxphjE9WURMUWcoAlHCEMIUlB8WWujBwEIRfK9zmVZyyPadihDKaqRi3eL4SCnd7eDKQ+Q5yOg6GyDyC328IUYm9wHvahBPurRf/JKXxoJ+cANju+PmRTiGt2onkNq5nNPOxwVRdnJnNXwkppEI9ncGMnWTRKOXSRd93RpRFJyEkalLKUn92fH/zFSmWesJQoHaUhlLrKBcwTj/vpoyaQpMoljdCYdVXjF/CXtdxmUYLRaaEtjgtAy1mymKdO4xWQqUZi0fKcklyjG4vWMlW+84CnPaa9tznKO0TScPwNoRWjyUI6UNKE6X1lBh+4z/3DJu+EnIYpDQrrTMsfEJiqJx8GJOhJ3kfvmQCUaQX3G0YvkXCYT62lG/0HSieUk6Si9+dBnMjOfrTyiNy/6z4wq7qY7tZ0tPdpIFq70jggbaU/p+VSX7VGjPE0kOiOqxfvJUqrDtKmoZLpQWIrTpcQcqyt9icSTnhWhVQwrU1NJVaPitH9LLao2xxrLcAoyrj60qlzdtleOblV8HWSd6dhqQMQC9K5/zStaFZrWxyITmGQMZDWbGFDMKpWgIFzkUHWYy5ReFni9tGs/IwtPcUVxtHgEaQg148ekFtK1Gy2rVykrWR0aFJcUvaZPpRm1z3YTlNysKzttO7bakrahCf/N6Dyxuk6tVparuAptTcepwJf2VbmM3awAeYnSkiKVsL3l58EQmdW/7laVxl0lck8o1MF6lnwldGdHs7mz6lEXu2LVX2F3iV+44k+0s9Vrdwus02K+d631i2lgzZmu8Wq3UkBj4yPpd2HoTbiOGT7ghjHc3hCX9MMa7vAtPWxiEqNYxK9lMYddDOIRp3jGMC5xjSEb0ZyadMUyvvGJf6xiINO4x0RucZFffOQYG3nJSGaykpsM5SdL2cZJprKTrRxlLINzN1cFbJV5fGUwZ1nMUyazls9s5jQL2cdBbvOQw7zmL8cZzm5m85vHPGc86ziUXLazn+VcZ0Dfucx5JnT/oOk8aDQXWtGH1nOi1dxoQz960ZCeNBG7fMxIM3rSmq70nxH9aUeHWtKj3nSpPS3oU1N61Z1mNadfXc9JxkDA8i1utYBrYFBz1rIIJm/2OKfa3653sv5971eru+AAd7bWpvbueekqXgsvd8Bj8C6AWbrJ2zq11/ecrnNnWFVZB7vC6YR2fr9tubbyesLypClzSf1D1E51z8xm74G9QGtvT1OklfR2kK8tPXDbS9wPVi+h8x3wdPO6vqJrt0pLe+hj93XYl572YKttVrfa071PVHdQqd1r8DIYwtaMZjxJbmyUI5vAwi14SF2ua/jCdd7DBfjGCYzv4Nab2Ld+Lrrl/2zz/zJ05MW2aabN3WC8apbbye4vxN09bHarfOLLvmgyGU6DmfqY6M7uMr0X/tt2djXesl3tuR0b4YPaHLyF1bHP0xvtj8q76X3u92KXKWrWnnauBge7WvGpcJ22VOowF7u2W1tuh/kb01v2MlGPW1etI57v/IXu3FkubbsbdteoJqvRAT95xf6Y6+E991qD3tuhxta0LTV70j8vdxyny7z8/m52GY9JsMYe1wkPPcGf3vfOm9zrqdf4ybdt9oL+cuXX3fvPS+92pR8W65wf/fJDD/m4y9a+0Y18MHNL9y9O3fnD33r39Y65xXs/sdZ9fM0vXnxRrj5LR1e8fuOb+P/q8zmAulX70K/vfNF3eAGIe/vlOPFHeXAGd54XVWkHVUQ1eOVlbq6HgNbHW+vXe7YHbAP3Pur3cpV3c8m1dDIDfyymYLAne6DFcQ14gs2Xfj+lc/kHNUDlQv+GXyoIcxGIgc9HetS0ZAJlb01VWs31eikAcqr3fTxYRtJHdX63feNWUbuHcDtmWtyHQX4FdDdYVTQXax4net0WRsxHfl44c1DIf+FHgdPnhSUHeiH3fw9IeNrna8DneyYzhQtIf0CFdsfXdZmVgwDIh0y3bT6IhB7YUEJ3Ujj4gShwhB1YdmT4hmHYhBkoee9Gbo+4gdXXflaYYxK4g5uIho2Heg//p4AM1XYViIjgt1dEOIJwYYiFiImUuIQD2H9OOHtmyIkhKHN2eHuNtXSrN1/61osuGIMZ6IOpyHqOuHmHuIKlaItsmIk/aIbUh3eN92yDiIsN527XiH1bSHxdiFr7Z3hDaGsQGIl8dYA4542/JonSaILbWHsSNnZjGI2wqIvQmIDcE447F3HwSIrcJYSm53/1qHH2OFX7Bo6S1XKLmHyuxXba+I7/GI/WOI8vyHvLqI/GOFyBKHIFCW/pqHl5SJD7iG0X2I0WZ4vBeH+VyIUhmXJ+CIKsiIIYaYrhRpFiaJG1GIVBiI9UeJDFKHp5B5JnJ11vBZSnd47i6InkmIjv/0eSdweEVZhxTcmBz9iKjmKDVjmTHSl/vLhuGzmBScmPb8aNzNhxl9dzNnlUXqlmC1h+fXhr1IiMp2h+XzmArZd5Q/l1UIl0BXiGnxaV/ciOc0iAtFh7dOkCZYh8FnVw58eA7rg490eIJZhqDFmDfomUubZ/9TeJf6l/DqmM8weRMWl885g6aAl9IaCY6MeYiuaWbZiGLqmOg4mYzqiRdwmAFdd8e7mMgZiLqwmZZlmZpZeZt1lkp6mKcDmDyfmYyueajvmWqRl4z6eTX2hpl4mbV6ibeumUCaaWRnmFtbkpFalsiUWNC0lhzBmb8FWdGamL42WPUeeKkjmRr9iYdv9pbbmZl1GJl6jJlZ2Ik2FHnk6ngQYomvZ3kaCoAipJg8QljCk5jI95jJjJlo5HcaWJkvRZdJ15h7AZmuVYnbUpkik4moWpnL83lD0IAwzaoDe5m7hFmCjqYJ9ZkpbnnTAVilSZnyPJkxTabClakxF5nEXpW1Y2jtKpdxJXoJ7JiOXYorlHio0YngKXlmcZi+L3hyZJjz2aneDJpXUIpUO6ivSljArKlDfKgqYJYc0Fn9k2Mn/3pPK4XGAYo7O4XT4qePoZptWIn5aoobLJoUfpoV1Zo8lIqHy5oc1ZpmpqpYaKphK6ok4ap3L6oju4dkF6b935TveFUSD3k31algj/CaaPGp8g+p1K+IDu2WRHyqaY6qdF2o5mCqR/NJ7BJ5WYd6iwap2HJaJXSYypOqiImqm8upTmCKf5aKoz6XOCJYWiiKEgGKK/eI7IiY5oZ5HreaCkqZlkqpWASpSG6YuAuY5Vmqa2+qmkmqzlqnkNqZ1VB6DqSpwfqqNYmJ7VmqM7SZgTKqRL6qnjGpyh6pjLSpb+Opl9yqkkWpfGGa+59nYVSqf9uq9x+KrAyazCSq+b6ap7WKyVKJPQOn4OF5fu2psvKYLK6pfn6qBfaqICCaztuqc6CK/AWauLKKstyZ6+yqexGn6sGqEdK4jeCrL1Kp/gqpy9upWtibBZaLCC/9qyNpqyjsez/imgEpmoOSmthXqwPCecAYmeAMmRa3ivqPqEYxqNtGeyVWmALTirsneeDgtbYkmgUqqQIiuxI0qT21qovImyZSmYZLerdPizF0uZOYt/SbuLhoheB9Wwl2iBBjq1NLumQJm49vmUT1u3RNqTMzuxepqn8yqebgqpn7ix2GmZ5vm4obuupLuwhpuLQcuxp/uQ4eqtU2i0Jzmc2AqdHnmsWHqmWhutROt+tDm6S0u5Y9u7rYq4U8m6J3u1bKu67lmxTIqHUFeeyGqkb5urSkm2Tsui3ApmdguMi3q2lSqH2pukkUu86mmnhOu6PQu7GUul37qjf7q6Pv85sHPKhCxLvzC7tW3KuPyph+PXoQyrvLq6uI/qmwRbrOArufnLT5TKtaALsbP7vBXMo3+7nLdoqbori+zXvEEZwFXLtGlbwGY7qugoqgGpsdNJuw4cvxBcu4ersEF7pD2ppDULloN5qd2KupvLuNtJw3iaoKf6nxYKibRKWy6MjRl6q6yJUb8bsqD6wSt8YEUsl0Qqt1BLvZ36vg/6SUo6wCTcpQaMvgmYwDApuE2br9nIwvepvmp8xUxMxWnspRgcvVn8usVLxkx8rYA4xWW8xmNswsaKwH2JxjWMxQ0MvA/sohEMx6obxONLx9IrqWUrwKdbsNw7kMyZuSLbtpb/TKxiqIg9jGdWe7QXrK+l67zpq7UqGsqpi7dirIn+2r1wyMbke7MJ2coRisg4Krq4ylSp/K9hm61LCpCmfKdojMfP6oZcrMYHDMIXjKLh67mYi4rN2J+dK8srq7YprLnUGsPWOrgw/MLClsTKPM7uC6HOrLNjnMfbzJLDq81NvLaFS8/F+Z68vMA5LIfibMvAPLIAncVn7MT4SrG0nM5aSs0fJ8EjXKcE7MvJ3M/+6Lc2jM2cu6nTSsyebMHBfJgl6s/728iM/MPnLNH9u8nUGZYejJWAjLGVW8SZ/LTIXL8z6sNXatMSW6rZm7ux/NCrPM+ae6EuG4RDbbz0Sqef/8vTpsvKlzubRfs1RZ2uioqakxvUbdh+BnnNp1zIS3y+G7yWXrzLTG298/ywGo22P+1qfMzMfavCJNsCDArAezrHV5eEOV2iO3130TnIo0ijy7u9jWvEu+u/8AyaPu3Wqjq4bm3T65l9rzrXXV3FivzXd9uOWs3Vlm2/shu82nql5gvZmgrBRq3Bw1q+YLuvip284nrIShyZY62yfL2zl0zUKp3WMfyaMtjXzJzNai3Xot3IpD3Nh+24P4qyExxzo4zDaojE0oy0vuvafuPToF3YDviuoR3RSJ2/ql3Heo3BqwrXwty10/iR7IquLH3CNH3DH8u8gY3XC8rB0dyWGP/9zjPd1uFN3hANuLtt3Pj9yoELxnQL0rTN0CU93a4au5Cb3dDr30YY30LtrM2q0ByN2ArM1vqN3bYbuOjp2PLsqPnc38w932K9wx5NtbjczW771HH94F1c4J+tzud91Actwlpqs/zdee9dsh4r4rC91lE84isZ44GM4jib4nnb4E164GKdlZo84S875Mr9wcNN1/HbalLe0qJM4JY74DX+4vZc4kz6m5jMqL2b1Q492lGOs7lt2FUuyd595s291yndvkst5Hocz9kd2YONrwI4v9Btq99c5jcd0KbNvis942ReeFb+xwCt0wGb5Jg9y3Kc1y2+zkettnvOmfW86XP/6dJjedtTroXuHKwZvdqqjMLry+Ziq9njDbySfuiUntqW3sHXnelbXHdbqqvT+8uwXNx1bNGL3toLXsmJ/r+NruGRHLPyarFxPudkbc+JTeusnueTjsvbCeG13Z5oDuqdadcEatXW7eN+zZ3C/tW/qrDgbe5vHeo63O3Jzeheza+qPuR9rNv5jdNE7OKmrrJYPpz+Xu7/7uFRK9nSfee5vuoCK+tiquCVbbP1Pcq4i8obDeV2vscMz+mKS9HszuMn6r3fLb4dv7K8/qYHz7sZrt3yPrQNGMZUWN1xDK/RG/HmXPHPftXXe+GGm+ApL/DDfu1KRvCafeNRLd8nn/CA/1znNk/Jtf3ykLzP0znzBi7jra7xEYvvF7+1AK/uJ12E8P7mIY/y8rBzvQ3rXnvLwWnFSdj0UizJMo/QUs/lmU3SFsvgiwnKhr71HC+/G97Rc/a1/wy0wI3kbf+N6O73G0/2EU6/dY29nFyBK6/rc1/osQ7VAWrjpzr0Wf7z7KPl+Cvv21650Ozpej7w1uz5hj/H3k3jFKzHjJ/Znd7elS/y1e7IUg34kS/Exdz1Mqp7cwvTCX3zi9/lm9/dYC/tGI68sW38T1/ZHH7Xqqn4Yj6gBS/eY6/mpA73j532v4/ndD7br/7uyj7yzL70l63iIt/LLI/Roh/dTU7kte/j4P+85vs5/CiN4aMf+Oba+7ve985e9AHe1JWO/B/vzctP+Lrs/Lsn/2Bt4rMP6f+t6df/5+pdn8XP/SKNtYtd/3se4E1d6cj/8d68/ISvy65e0FosvOtey/3Oz/DfwsKLzmxv6E7tt9H5yc0e74Zut2Mug3Xv69KfsKU9zIFevS6vz70e9oh+j7EP/y0svOjM9obu1H4bnZ/c7PFu6HY75jJY974u/Qlb2sMc6NXr8vrc62GP6PcY+/DfwsKLzmxv6E7tt9H5yc0e74Zut2Mug3Xv69KfsKU9zIFevS6vz70e9oh+j7EP/y0svOjM9obu1H4bnZ/c7PFu6HY75jJY977/Lv0JW9rDHOjV6/L63Othj+j3GPvw38LCi85sb+hO7bfR+cnNHu+GbrdjLoN17+vSn7ClPcyBPup4X/XpXezibvYrLtM6jqS5rP6tn8hV/fZT39s6LsP3PNWBeb+GXv13X88hLf3/jYz2btb8rOurn8Hpr/faT/O1/uEx/dFZivBOD/UX7dw87Py3H+xALvSFb/BDrPL07f2vb9IZnP56r/00X+sfHtMfnaUI7/RQf9HOzcPOf/vBDuRCX/gGP8QqT9/e//omncHpr/faT/O1/uEx/dFZivBOD/UX7dw87Py3H+xALvSFb/BDrPL07f2vb9IZnP56r/00X+sfHtMf/52lCO/0UH/Rzs3Dzn/7wQ7kQl/4MlDLwe/b4W/I6n/q9Zu1vX3WUxql3D7xJ17TW33SPqv7W13Qgq3UBj3t4Tyf416hYS7ovT7JyN3y3M3ngg3EuH6RtJ/C4R7sJ+2zur/VBS3YSm3Q0x7O8znuFRrmgt7rk4zcLc/dfC7YQIzrF0n7KRzuwX7SPqv7W13Qgq3UBj3t4Tyf416hYS7ovT7JyN3y3M3ngg3EuH6RtJ/C4R7sJ+2zur/VBS3YSm3Q0x7O8znuFRrmgt7rk4zcLc/dfC7YQIzrF0n7KRzuwX7SPqv7W13Qgq3UBj3t4WzhFV3ANE37AB/05K7jPG//ff/+6W4OuB1++mh//vZv0P3/6c4t4QZ93+5e4e5t4RVdwDRN+wAf9OSu4zxv/33+6W4OuB1++mh//vZv0P3/6c4t4QZ93+5e4e5t4RVdwDRN+wAf9OSu4zxv/33+6W4OuB1++mh//vZv0P3/6c4t4QZ93+5e4e5t4RVdwDRN+wAf9OSu4zxv/33+6W4OuB1++mh//vZv0P3/6c4t4QZ93+5e4e5t4RVdwDRN+wAf9OSu4zxv/33+6W4OuB1++mh//vZv0P3/6c4t4QZ93+5e4e7d+FTP5FeftcdN3JN/zPsezoMOxDCq6cT94VRu7LZv7H0+n++fwgqP8eHu1BX9+Pv/js91f7zEPfnHvO/hPOhADKOaTtwfTuXGbvvG3ufz+f4prPAYH+5OXdGPv+/4XPfHS9yTf8z7Hs6DDsQwqunE/eFUbuy2b+x9Pp/vn8IKj/Hh7tQV/fj7js91f7zEPfnHvO/hPOhADKOaTtwfTuXGbvvG3ufz+f4prPAYH+5OXdGPv+/4XPfHS9yTf8z7Hs6DDsQwqunE/eFUbuy2b+x9Pp/vn8IKj/Hh7tSoTq3kXvfEf/heXvS9Lf44btvq3/1VbWIwz82JzMPmvdnJPui4HcKofusnbv/tfML4b/8iDe5XP+b3vu44btvq3/1VbWIwz82JzMPmvdnJPui4HcKo/37rJ27/7XzC+G//Ig3uVz/m977uOG7b6t/9VW1iMM/NiczD5r3ZyT7ouB3CqH7rJ27/7XzC+G//Ig3uVz/m977uOG7b6t/9VW1iMM/NiczD5r3ZyT7ouB3CqH7rJ27/7XzC+G//Ig3uVz/m977uOG7b6t/9VW1iMM/NiczD5r3ZyT7ouB3CqH7rJz7vU+rlYI7WDg+jMU3xBv387l2fzZ77qP3ay4y19x2l9P/1T+r66j/xjRrT+8+/4k/7+Lz2wE7xBv387l2fzZ77qP3ay4y19x2l9P/1T+r66j/xjRrT+8+/4k/7+Lz2wE7xBv387l2fzZ77qP3ay4y19x2l9P//9U/q+uo/8Y0a0/vPv+JP+/i89sBO8Qb9/O5dn82e+6j92suMtfcdpfT/9U/q+uo/8Y0a0/vPv+JP+/i89sBO8Qb9/O5dn82e+6j92suMtfcdpfT/9U/q+uo/8Y0a0/Xp+ybv/G7e8FQt/O2O/nBdzN58/p1d08g+79qO6xdJ+5/864K93ide09+P/oWfweH+0nTf08U/+Ib+6HFr0jFH3dYft/t+1rsXmJ8+8QIN/IBO/OU/xI4+6IMs0wqv4S0s90iP6x8e9FqP2hOd82BevdFuYtg+1S0f0xRf64BO/OU/xI4+6IMs0wqv4S0s90iP6x8e9FqP2hOd82BevdH/bmLYPtUtH9MUX+uATvzlP8SOPuiDLNMKr+EtLPdIj+sfHvRaj9oTnfNgXr3RbmLYPtUtH9MUX+uATvzlP8SOPuiDvPtAz8+E3tgCztk4HvcFffiED9bAvv9nPfECvf5nv/21btusH+Ssfsw03+bf2/is3smDj+oBbvHqffakl/efDNRDLLxg3ebpnpe/feL2r/R222q6X85wH9P1v96sbe2E3tgCztk4HvcFffiED9bAvv9nPfECvf5nv/21btusH+Ssfsw03+bf2/is3smDj+oBbvHqffakl/efDNRDLLxg3ebpnpe/feL2r/R222q6X85wH9P1v96sbe2E3tgC/87ZOB73BX34hA/WwL7/Zz3xAr3+Z7/9tW7brB/krH7MNN/m3wv9l3/iaH/+Nf3pl87/rJzCfs73bI/+BQzzAB7vyx76kS7guqz1QC/4QeX2ab39bQ7sW73fcA3xV4/cde/rav3oci707A3+Fv/JdL/YW37WE9/u6q/tG9/1Sf3poY/5oh633H7p/M/KKeznfM/26F/AMA/g8b7soR/pAq7LWg/0gh9Ubp/W29/mwL7V+w3XEH/1yF33vq7Wjy7nQs/e4G/xn0z3i73lZz3x7a7+2r7xXZ/Unx76mC/qccvtl87/rJzCfs73bI/+BQzzAB7vyx76kS7guqz1QC/4Qf/l9mlN3Xm58+Hu/rRfzJ/8vZrq/6uO7Sn90n2O6KVNuCRPuDDPzUKMjOpd6ZPK9ser9g0ttexc2pP86Dxs79Rf+Lre23pb+PV83Na/e9AM89wsxMio3pU+qWx/vGrf0FLLzqU9yY/Ow/ZO/YWv672tt4Vfz8dt/bsHzTDPzUKMjOpd6ZPK9ser9g0ttexc2pP86Dxs79Rf+Lre23pb+PV83Na/e9AM89wsxMio3pU+qWx/vGrf0FLLzqU9yY/Ow/ZO/YWv672tt4Vfz8dt/bsHzTDPzUKMjOpd6ZPK9taey97Oz/Ua7ust97mv6AXN/7sHsKc+yccN/JNf/Wwf8/f/e+49DMWdPcnFTM76/99KjemYb+Govvv0H6Wlju1THZhvb/vG/r3Q39RvX+tUT+INbfGfLNLPyf0m/udn/cwlzN5yn/uKXtD8v3sAe+qTfNzAP/nVz/Yxf7/n3sNQ3NmTXMzkrP//rdSYjvkWjuq7T/9RWurYPtWB+fa2b+zfC/1N/fa1TvUk3tAW/8ki/Zzcb+J/ftbPXMLsLfe5r+gFzf+7B7CnPsnHDfyTX/1sH/P3e+49DMWdPcnFTM6NqtS1DuK1Ts/hTuX9vvFQrPfkrvuYj/nMj/uBrs/uXfj1DLCcj/qIj9bWzvP1O9Ae/tudT676W/6AXuT/vfqkDPY+/yuzyy/3uU/vg5/sZ7/9103a6+3up87zpAz0iJ7q4G7h1g7/LQwCzp/9g+7qGH/86W79nM/6Jp/t/S74fW7IRQ7Wwr/i5A7rdb/st9/yMgu37Yz+FF/rrC7TUMzNW53e+uzehV/PAMv5qI/4aG3tPF+/A+3hv9355Kq/5Q/oRf7fq0/KYO+zMrv8cp/79D74yX7223/dpL3e7n7qPL/3j+3nhJ61wf7a6V+vVz7oME/VZsz8hKzWSm3yMX31A/34FTrQ7Q/Xrv7aEm/Qz623Z8+39TnD9Znjlc72Bb3Zk839xl7rgG7t8I/PSm3QFTrQ7Q/Xrv7aEm/Qz623Z8+39f85w/WZ45XO9gW92ZPN/cZe64Bu7fCPz0pt0BU60O0P167+2hJv0M+tt2fPt/U5w/WZ45XO9gW92ZPN/cZe64Bu7fCPz0pt0BU60O0P167+2hJv0M+tt2fPt/U5w/WZ45XO9gW92ZPN/cZe64Bu7fCPz0pt0BU60O0P164+A/Ye/OXv29Gv+v3/6buX9Pxe/uM+7Z9c/Dz8yTM8/ba97E/M/LruyrJvzDVg78Ff/r4d/arf/5++e0nP7+U/7tP+ycXPw588w9Nv28v+xMyv664s+8ZcA/Ye/OXv29Gv+v3/6buX9Pxe/uM+7Z9c/Dz8yTM8/ba97E/M/LruyrJvzDX/YO/BX/6+Hf2q3/+fvntJz+/lP+7T/snFz8OfPMPTb9vL/sTMr+uuLPvGXAP2Hvzl79vRr/r9/+m7l/T8Xv7jPu2fXPw8/MkzPP22vexPzPy67sqyb8zN2NjVK/1gPam2X+ulHXP1Te6uvuPjz/a+LvSmr7S/rta778eWT/f0T/hfL+HAbuEYz7cwOqmZf9qtD6P2XuuTL9O43dD3Dv41z//ZH7ZjLrU4n96LzfzzjrUmzfPDfOLNPKlm7eHkvvYGDfyTL9O43dD3Dv41z//ZH7ZjLrU4n96LzfzzjrUmzfPDfOLNPKlm7eHkvvYGDfyTL9O43dD3Dv41z//ZH7Zj/y61OJ/ei8388461Js3zw3zizTypZu3h5L72Bg38ky/TuN3Q9w7+Nc//2R+2Yy61OJ/ei8388461DY3eek/yhFvK+szaFX316c/NuK7Wu1/vfoz9mG7ISmvsff7pp477ifroTG76JY/1IT35vX3WE2/1Xg7i6d736r3VJzzmp07I5Q/oSmvsff7pp477ifroTG76JY/1IT35vX3WE2/1Xg7i6d736r3VJzzmp07I5Q/oSmvsff7pp477ifroTG76JY/1IT35vX3WE2/1Xg7i6d736r3VJzzmp07I5Q/oSmvsff7pp477ifroTG76JY/1IT35vX3WE2/1Xg7i6d736v+91Sc85qdOyOUP6Epr7H3+6aeO+4n66Exu+vmP/rGfy8T//rh9/oRc/kX/0m1e66Utw7Z92q3P/MOMznff2dyM+UjP/Hvvze/ewu3u69Iv94Vfr8GO6rT/0iZP5bVe2jJs26fd+sw/zOh8953NzZiP9My/99787i3c7r4u/XJf+PUa7KhO+y9t8lRe66Utw7Z92q3P/MOMznff2dyM+UjP/Hvvze/ewu3u69Iv94Vfr8GO6rT/0iZP5bVe2jJs26fd+sw/zOh8953NzZiP9My/99787i3c7r4u/XJf+PUa7KhO+y9t8lRe66Utw7Z92q3P/MOMznff2dyM+UjP/Hv/783vLrPnr+tHf+KUT+j8m/5IivvCm6iPfvvOj/MVjesuX72Hr8swr+v4DPOE+8nX/cm2X/Cw3+v6C+Jf/PYzHLb4v/ePfvvOj/MVjesuX72Hr8swr+v4DPOE+8nX/cm2X/Cw3+v6C+Jf/PYzHLb4v/ePfvvOj/MVjesuX72Hr8swr+v4DPOE+8nX/cm2X/Cw3+v6C+Jf/PYzHLb4v/ePfvvOj/MVjesuX72Hr8swr+v4DPOE+8nX/cm2X/Cw3+v6C+Jf/PYzHLb4v/ePfvvOj/MVjesuX72Hr8swr+v4DPOE+8nXDfPN3O/83MwFv+oQP/2sLtNQzPP1fNYVnvNg/07vhD2pi/34To/9wQ/yuRrO8e7ghM3tl57qbu7w34/xro7xjy7ncd/8fe/Nfiy8ugzEzE/KJt3ZlM/3TV3AO6/956+/IH/v686/xaz7mI/5kS7guvzJZi3EoA/UDer6517TSM/PfN/UBbzz2n/++gvy977u/FvMuo/5mB/pAq7Ln2zWQgz6QN2grn/uNY30/Mz3TV3AO6/956+/IH/v686/xaz7mI/5kS7guvzJZi3EoA/UDer6517TSM/PfN/UBbzzYr/kZn31IV3a9TrmMkh6yA3zhAvz807iaD3Zci70hW/wh9/jx9/sog73tH/rCPrStm/WFL/NUf/FJlb3k//N9g4P1/jcum8/w4Ou3tCP8xV9vxgf4CTux43a4Zwt+bWc7n6s+YQs7kmN67b95xCv70ve5un+8enfw9o/6qhf6eAu6nBP+7eOoC9t+2ZN8dsc9V9sYnU/2Wzv8HCNz6379jM86OoN/Thf0feL8QFO4n7cqB3O2ZJfy+nux5pPyOKe1Lhu238O8fq+5G2e7h+f/j2s/aOO+pUO7qIO97R/6z2e5rvr/g//3At9kZ8c/EWP3Okfczv/yd9b6dnPz52d8XAf0/oO9zGN+PTO5EiP6PFvYr9t978O6MQf7fxMyLY92Tycwq7f8xhP0/VN3daP/MRd9FTe7/FO/OW/2YD/C/m1/OU4DujEH+38TMi2Pdk8nMKu3/MYT9P1Td3Wj/zEXfRU3u/xTvzlv9mAC/m1/OU4DujEH+38TMi2Pdk8nMKu3/MYT9P1Td3Wj/zEXfRU3u/xTvzlv9mAC/m1/OU4DujEH+38TMi2Pdk8nMKu3/MYT9P1Td3Wj/zEXfRU3u/xTvzSn/ND3MY43/D2L9I8fPzm/8hufu6E3rq5L+C6HPXp7v1p/cnbHPUzPOjkTPpQj/gNLYOCfvr2r/RBn/56r/Dqz+S4XtqEG534vPrcDKMyu9jxTq6TLNNKbfI7/+2O3+N5v/19PuiwXvYW7utUz+S4XtqEG534vPrcDKMy/7vY8U6ukyzTSm3yO//tjt/jeb/9fT7osF72Fu7rVM/kuF7ahBud+Lz63AyjMrvY8U6ukyzTSm3yO//tjt/jeb/9fT7osF72Fu7rVM/kuF7ahBud+Lz63AyjMrvY8U6ukyzTSm3yMb3xwT7Ri1z/6c/3cL/9Bm392n71yB30LSzQDX3vlj/DhU/IWK/UBt3/+n7sgrzxwT7Ri1z/6c/3cL/9Bm392n71yB30LSzQDX3vlj/DhU/IWK/UBt3/+n7sgrzxwT7Ri1z/6c/3cL/9Bm392n71yB30LSzQDX3vlj/DhU/IWK/UBt3/+n7sgrzxwT7Ri1z/6c/3cL/9Bm392v9+9cgd9C0s0A1975Y/w4VPyFiv1Abd//p+7IK88cE+0Ytc/+nP93C//QZt/dp+9cgd9C0s0A1975Y/w4VPyFiv1Abd//p+7IJczux8j5V++8iI7VM9a/1f+HzP9ug/6LCu+vt+zDRv7EE+qfYN7SFt325+2s8Z1rVu26/Nm6S9A59s0Kgd85iP+YOe92HeyRYP8SY2qaR9668f/UEP+VmL26AL+l9syDPwyQaN2jGP+Zg/6Hkf5p1s8RBvYpNK2rf++tEf9JCftbgNuqD/xYY8A59s0Kgd85iP+YOe92HeyRYP8SY2qaR9668f/UEP+VmL26AL+l9syDPwyQaN2jH/j/mYP+h5H+adbPEQb2KTStq3/vrRL6tU/usuH/1pXtXzb/E+K7OoPdkkXvUxj+ta7OixT7ikXf1sb+17z9hIf9F3L8Pl79vnP+81b6/YX+tg/vHhHvQhDeZVH/O4rsWOHvuES9rVz/bWvveMjfQXffcyXP6+ff7zXvP2iv21DuYfH+5BH9JgXvUxj+ta7OixT7ikXf1sb+17z9hIf9F3L8Pl79vnP+81b6/YX+tg/vHhHvQhDeZVH/O4rsWOHvuES9rVz/bWvveMjfQXffcyXP6+ff7zXvP2iv21DuYfH+5BH9JgXvUxj+ta7OixT7ikXf1sb+17z9hIf9F3D+DM/0/oWUt62I7ak33pfB7tJgbxor7kbd7vF03Y0575GF/dUa/0u0///y2rZb/VzV/1BW30qD3Zl87n0W5iEC/qS97m/X7RhD3tmY/x1R31Sr/79P/fslr2W938VV/QRo/ak33pfB7tJgbxor7kbd7vF03Y0575GF/dUa/0u0///y2rZb/VzV/1BW30qD3Zl87n0W5iEC/qS97m/X7RhD3tmY/x1R31Sr/79P/fslr2W938VV/QRo/ak33pfB7tJgbxor7kbd7vF03Y0575GF/dUa/0u0///+3/y13kuvz3Zo3rQl/4eo/PrRu2Yp+r6Z/BnXzsUdr/zF+//LvzLR/TFP8f/PzO6mLv3Lu3t1eP2zTv+GBt8si981AM4O0/6GEemN8f8wXMvzvf8jFN8cHP76wu9s69e3t79bhN844P1iaP3DsPxQDe/oMe5oH5/TFfwPy78y0f0xQf/PzO6mLv3Lu3t1eP2zTv+GBt8si981AM4O0/6GEemN8f8wXMvzvf8jFN8cHP76wu9s69e3t79bhN844P1iaP3DsPxQDe/oMe5oH5/TFfwPy78y0f0xQf/Pwe7UgP/YPf/IuN7ADe/iZW9qX/+sRd9C+d7kf866ye1EZuyBWN+Ipe8kLP3ona9YNst8ftx9iv9Olfr3+f7qit/hMfpfW+2B5O7q0G2vj/L7RTHeyvnf4Bv/ddP8h2e9x+jP1Kn/71+vfpjtrqP/FRWu+L7eHk3mqgjf9CO9XB/trpH/B73/WDbLfH7cfYr/TpX69/n+6orf4TH6X1vtgeTu6tBtr4L7RTHeyvnf4Bv/ddP8h2e9x+jP1Kn/71+vfpjtrqP/FRWu+L7eHk3mqgjf9CO9XB/trpH/B73/WgD9gaXvevHexKa/Kabv1Pnu7we/o1He897/Fxq+glT/V3L7Q9/etxP9ELv/cy7coaXvevHexKa/Kabv1Pnu7we/o1He897/Fxq+glT/V3L7Q9/etxP9ELv/cy7coaXvevHexKa/Kabv1Pnu7we/o1/x3vPe/xcavoJU/1dy+0Pf3rcT/RC7/3Mu3KGl73rx3sSmvymm79T57u8Hv6NR3vPe/xcavoJU/1dy+0Pf3rcT/RC7/3Mu3KGl73rx3sSmvymm79T57u8Hv6NR3vPe/xcavoJU/1dy+0Pf3rcT/RCy/D0V7pNY/1SV2f1y2z10/8gE78q+azqz/MOQ/mU/3JJm/etm3thPvJQG3bqP/9+Q+42f7lyOiz/Cuz10/8gE78q+azqz/MOQ/mU/3JJm/etm3thPvJQG3bqP/9+Q+42f7lyOiz/Cuz10/8gE78q+azqz/MOQ/mU/3JJm/etm3thPvJQG3bqP/9+Q+42f7lyP/os/wrs9dP/IBO/Kvms6s/zDkP5lP9ySZv3rZt7YT7yUBt26j//fkPuNn+5cjos/wrs9dP/IBO/Kvms6s/zDkP5lP9ySZv3rZt7YT7yUBt26j//YEK99wN6Pst6pyf7JV+//Vf9ltu778O5keM6Yhe2oR79CfO9yRO87Uu9IMO+u080YFe9aju/FjrxypP/j4rs06f82B+/ckOozF99S+d78L739487Uqd7qPu2fc81Yk/+Ki/5UAc7xjP+1Y/63J+j4Kf7rztx8JL+roM6ztPzirP28tv9Wlt3oDu8OXM5AWMz2Gu+/Hu7Zgf6edPyGpt9mDd5rZv7Ah/xOb/xIX/39l839TRncKgb9DP7c2QHtj8S+7qjfmYH+nnT8hqbfZg3ea2b+wIf8Tm/8SF39l839TRncIyDdrHfd/r7rP+n+aDP9ls7/BvD9RgjtXfv+PcbOGo7v7H3P+Rfv6EXNp7L/boHfCETu6Z7/Fsj/pXH5jf7+vvv9C+/fzoLOdC/9GnH3MvrfSuz/omz79dL/8q/+78m+0z/P0Vjej3yPz1S/sL7dvPj85yLvQfffox99JK7/qsb/L82/Xyr/Lvzr/ZPsPfX9GIfo/MX7+0v9C+/fzoLOdC/9GnH3MvrfSuz/omz79dL/8q/+78m+0z/P0Vjej3yPz1S/sL7dvPj85y/y70H336MffSSu/6rG/y/Nv1j83AUkvYkC7QvJ3SxYzPrQvXMM/3d8/NsIbqu3/9FX32NCjTZz3xJ07VMD7xbSy1hA3pAs3bKV3M+Ny6cA3zfH/33AxrqL7711/RZ0+DMn3WE3/iVA3jE9/GUkvYkC7QvJ3SxYzPrQvXMM/3d8/NsIbqu3/9FX32NCjTZz3xJ07VMD7xbSy1hA3pAs3bKV3M+Ny6cA3zfH/33AxrqL7711/RZ0+DMn3WE3/iVA3jE9/GUkvYkC7QvJ3SxYzPrQvXMM/3d8/NsIbqu3/9FX32NCjTZz3xJ07VMN76mrrc6v/Emrr7Js3NuH7yDGzx2f/eomMe1l/e8iQ/78cu0BVK7q7v7dOvxYIMoySfy6h/9WHe2P0//5wP5Km+5ECtxbpc96ft3LsH+iYf0vyu1lFv/rl61qfO99oO/cfe64Qr6IL8/cR/8gxs8dneomMe1l/e8iQ/78cu0BVK7q7v7dOvxYIMoySfy6h/9WHe2P0//5wP5Km+5ECtxbpc96ft3LsH+iYf0vyu1lFv/rl61qfO99oO/cfe64Qr6IL8/cR/8gxs8dneomMe1l/e8iQ/78cu0BVK7q7v7dOvxYts5OWM8w1fr1Hv+Nhv4sk+/1aH6KXN87Z5kS28yHAN8wBezpde/uLeyU2N9FtOrS1//Cb/vuN6v/r1u/ODrOnrf/UxjfmID7fNbuExj/mFP/7I//yv3/9Iv+XU2vLHb+I7rverX787P8iavv5XH9OYj/hw2+wWHvOYX/jjj/zP//r9j/RbTq0tf/wmvuN6v/r1u/ODrOnrf/UxjfmID7fNbuExj/mFP/7I//yv3/9Iv+XU2vLHb+I7rverX787P8iavv5XH9OYj/hw2+wWHvOYX/jjj/zP//r9j/RbDvpTr9Rmzfwoz92ljfsnXs+Pnv3TD+izj9rob/rpDfTxftrH3ut8D/dp/uQGHf1BD/nun/nq/8SCn+7nL8M/PskDbcizj9rob/rpDfTxftrH3ut8D/dp//7kBh39QQ/57p/56v/Egp/u5y/DPz7JA23Is4/a6G/66Q308X7ax97rfA/3af7kBh39QQ/57p/56v/Egp/u5y/DPz7JA23Is4/a6G/66Q308X7ax97rfA/3af7kBh39QQ/57p/56v/Egp/u5y/DPz7JA23Is4/a6G/66Q308X7ax97rfA/3af7kBh39/h/ud0zZKN/CPq+/qybojl76Ah39qz/MgW7SAJ778X3rCTvEcj/993zfnK39Ji3D5Q/idI/WQB73qL76aL/8WYrppd/jVP7Gvv/Fol7hvQ7/uw/0icz2516v4X7HlI3yLezz+rtqgu7opS/Q0b/6wxzoJv8N4Lkf37eesEMs99N/z/fN2dpv0jJc/iBO92gN5HGP6quP9sufpZhe+j1O5W/s+18s6hXe6/C/+0CfyGx/7vUa7ndM2Sjfwj6vv6sm6I5e+gId/as/zIFu0gCe+/F96wk7xHI//fd835wd4Jyv4byvy5Oq+vUe3H6cpUBd/vY9qU9u4jvezNdN7uT89VPa+eoP92Eu215+8pK/85OK7TRv7FOPz9n+5dv/5ZMK2vLv7k/+66v2yUBd/kI//5yf3qN+vPVeock/qXwO5vUe3H6cpUBd/vY9qU9u4jvezNdN7uT89VPa+eoP92Eu215+8pK/85OK7TRv7FOPz9n+5dv//+WTCtry7+5P/uur9slAXf5CP/+cn96jfrz1XqHJP6l8Dub1Htx+nKVAXf72PalPbuI73szXTe7k/PVT2vnqD/dhLttQv9VAzv/QXvc7XtPxfrv7Tu582/g/Xr+wr/LPLeizfuxgHfz37NCP3clVXfgyTPWPDO16u/FOfdo8DOf8PMkdvvBen97kT9Nhjtv7fuTb3Njr6/rnzvM9LP47f9wVmrUyDfoijfPHu9gb7/aqBs2qT/OAjerpj/JZa94XufPp38PhvvOrbt78zu/i7rMyy9tAf/XIvfN2e+Wf7ubnTtW/DvKsvPaAveP6e899H9NIb/rq/8jQrrcb79Sn/83DcM7Pk9zhC+/16U3+NB3muL3vR77Njb2+rn/uPE/Kk1rqtZzrTj3ZPBzgWAv0pd/jn2zsIv3kta7Fn29itE/yVG3i6l/NnS/7k9zyk/r3wa/FVu/og/7tOr/8Vs/Znczk+4/c+Kze06/FC6+/gH7Sdb/j3NyMADupMlj3zQ/8H576H1/LU5/6H4/c9b3Qao2MYS6zKd3C2G/WH+3e9TnDH33ihEuDfx/8Wmz1jj7o367zy2/1nN3JTL7/yI3P6j39Wrzw+gvoJ133O87NzQiwkyqDdd/8wP/hqf/xtTz1qf/xyF3fC63WyBjmMpvSLYz9Zv3R7l2fM/zRJ064O/9/6ydu/wZd7GB+xJjOzwFPuCQ/73ef9czfw5n/5/J/+gZvx/qe/SaW7V9+zNx+/7Ef8AGv67/N2Wft3Etu0M+f1rx/4twc76gP1xLf5/Npx/qe/SaW7V9+zNx+/7Ef8AGv67/N2Wft3Etu0M+f1rx/4twc76gP1xLf5/Npx/qe/SaW7V9+zNx+/7Ef8AGv67/N2Wft3Etu0M+f1rx/4twc76gP1xLf5/Npx/qe/SaW7V9+zNx+/7Ef8AGv67/N2Wft3Etu0M+f1rx/4twc76gP1xLf5/Npx/qe/SaW7V9+zJStvyA/+MnejJr+/Oj8yBN/4oSb98Is0zouw2Ae/eH/XPiTHODs7NvRD/PzXuGnT8jUPtW8j84k/vzo/MgTf+KEm/fCLNM6LsNgHv3hXPiTHODs7NvRD/PzXuGnT8jUPtW8j84k/vzo/MgTf+KEm/fCLNM6LsNgHv3hXPiTHODs7NvRD/PzXuGnT8jUPtW8j84k/vzo/MgTf+KEm/fCLNM6LsNgHv3hXPiTHODs7NvRD/PzXuGnT8jUPtW8j84k/vzo/MgTf+KEm/fCLNM6LsNgHv3hXPiTHODs7NvRD/PzXuGnn8tALv12L/yB/dJTT9A8n8sxz8/23+a/Hvfnnsv7vf+y7e6n3tm5fO5er+F5z/snrvcK77fevO9hDvkp/7zcGG/vyo/88e3+5h3/+r57oP/rF7nzrtzzGp73vH/ieq/wfuvN+x7mkJ/Cy43x9q78yB/f7m/e8a/vuwf6v36RO+/KPa/hec/7J673Cu+33rzvYQ75KbzcGG/vyo/88e3+5h3/+r57oP/rF7nzrtzzGp73vH/ieq/wfuvN+x7mkJ/Cy43x9q78yB/f7m/e8a/vuwf6v36RO+/KPa/hec/7K07yVD3DJXz1UW/Q52/MMPrYYx769Y/PjT3VSp3jQuzUvs7/rW/6+x379t/OXH/2x+z0gnz1UW/Q52/MMPrYYx769Y/PjT3VSp3jQuzUvs7/rW/6+x379t/OXH/2x//s9IJ89VFv0OdvzDD62GMe+vWPz4091Uqd40Ls1L7O/61v+vsd+/bfzlx/9sfs9IJ89VFv0OdvzDD62GMe+vWPz4091Uqd40Ls1L7O/61v+vsd+/bfzlx/9sfs9IJ89VFv0OdvzDD62GMe+vWPz4091Uqd40Ls1L7O/61v+vsd+/bfzkUu97Cm4TKL2rOv6Khfwhsf7jqe8TUP5gLO2e5f95Mt4QY9qdfv8Owt95gf7w2P+6p2zMTN6krd51se7nxbwkl+3EDPz2jP294fpTRv1rk/qUeM6bge7Zgf7w2P+6p2zMTN6krd51se7nxbwkl+3EDPz2jP294fpTRv1rn/P6lHjOm4Hu2YH+8Nj/uqdszEzepK3edbHu58W8JJftxAz89oz9veH6U0b9a5P6lHjOm4Hu2YH+8Nj/uqdszEzepK3edbHu58W8JJftxAz89oz9veH6U0b9a5P6lHjOm4Hu2YH++vrfA7HnMQz97gD+7IHnMvrfQ77+f2b9AUDtS+bdKEXP6p/uT5js5yXv6TH+6z5tAy7f7IHZj12c6AW8o0r/Q77+f2b9AUDtS+bdKEXP6p/uT5js5yXv6TH+6z5tAy7f7IHZj12c6AW8o0r/Q77+f2b9AUDtS+bdKEXP6p/uT5js5yXv6TH+6z5tAy7f7IHZj12c6AW8o0r/Q7/+/n9m/QFA7Uvm3ShFz+qf7k+Y7Ocl7+kx/us+bQMu3+yB2Y9dnOgFvKNK/0O+/n9m/QFA7Uvm3ShFz+qf7k+Y7Ocl7+kx/uwszAMijo/53UJbzd6zvmu6u3iC79nK3UfT79F5n3je30ifr3ys/OgB7zhc/3ck7Pfo728Pv19975vn7P9B390Vn3QF7+hx+lQI/sSFrTSR6YZz/Q5Xzq9TvQ2w26fo728Pv19975vn7P9B390Vn3QF7+hx+lQI/sSFrTSR6YZz/Q5Xzq9TvQ2w26fo728Pv19975vn7P9B390Vn3QF7+hx+lQI/sSFrTSR6YZz/Q5Xzq9TvQ2w26fv+O9vD79ffe+b5+z/Qd/dFZ90Be/ocfpUCP7Eha00kemGc/0OV86vU70CWM7PYf/Fps9Wk9qdlu4vk/2XAvlOHu/zId0qyu8Bqu3vUZVGfN+ad96oQO+3JvyMfrx1af1pOa7Sae/5MN90IZ7v4v0yHN6gqv4epdn0F11px/2qdO6LAv94Z8vH5s9Wk9qdlu4vk/2XAvlOHu/zId0qyu8Bqu3vUZVGfN+ad96oQO+3JvyMfrx1af1pOa7Sae/5MN90IZ7v4v0yHN6gqv4epdn0F11px/2qdO6LAv94Z8vH5s9Wk9qdlu4vk/2XAvlOHu/zId0qyu8Bqu3vUZVGfN+af/feqEDvs5f5HOKdK7q+O57OubPdHtvuPj/+QmX934r7/3XOzRLvhBxbdbjfrsvfBCK+CJarcAK+Eivbs6nsu+vtkT3e47Pv5PbvLVjf/6e8/FHu2CH1R8u9Woz94LL7QCnqh2C7ASLtK7q+O57OubPdHtvuPj/+QmX934r7/3XOzRLvhBxbdbjfrsvfBCK+CJarcAK+Eivbs6nsu+vtkT3e47Pv5PbvLVjf/6e8/FHu2CH1R8u9Woz94LL7QCnqh2C7ASLtK7q+O57OubPdHtvuPj/+QmX934r7/3XOzRLvhBxbdbjfrsvfBCK+CJysBWZ2JUbsaRDvb1Tc6cjeNU/y+1bn68WH31bm/3Sh+d5JywF7nQH27vvw7yj/yk7r/91L/dYF/f5MzZOE71Uuvmx4vVV+/2dq/00UnOCXuRC/3h9v7rIP/IT+r+20/92w329U3OnI3jVC+1bn68WH31bm/3Sh+d5JywF7nQH27vvw7yj/yk7r/91L/dYF/f5MzZOE71Uuvmx4vVV+/2dq/00UnOCXuRC/3h9v7rIP/IT+r+20/92w329U3OnI3jVC+1bn68WH31bm/3Sh+d5JywF7nQH27vvw7yj8zD/r/pdT/R7R6ozo3zx+vHVv/fQimzpR6YsR/w887kMBrT9e/ne5/UFn7uAY/2H7/pdT/R7f8eqM6N88frx1b/30Ips6UemLEf8PPO5DAa0/Xv53uf1BZ+7gGP9h+/6XU/0e0eqM6N88frx1b/30Ips6UemLEf8PPO5DAa0/Xv53uf1BZ+7gGP9h+/6XU/0e0eqM6N88frx1b/30Ips6UemLEf8PPO5DAa0/Xv53uf1BZ+7gGP9h+/6XU/0e0eqM6N88frx1b/30Ips6UemLEf8PPO5DAa0/Xv53uf1BZ+7ijvnDDIyvXts5/c7KZf0IIdneqdyExO8X0Oo8IMxbjv5Re59r8u/Zo/zP/t/9V95MKvy12P26j92qS39lNv9j0O2p187GBt/s9c6c4dtxR+3Tsv08v/HfNbLvnOme5+fOIAXvoISnprP/Vm3+Og3cnHDtbm/8yV7txxS+HXvfMyvdwxv+WS75zp7scnDuClj6Ckt/ZTb/Y9DtqdfOxgbf7PXOnOHbcUft07L9PLHfNbLvnOme5+fOIAXvoISnprP/Vm3+Og3cnHDtbm/8yV7txxS+HXvfMyvdwxr+/gvv9aX6Gyjc4V/vmffuyvv9iGDORDnNZZ28noncvoD/2nrvd+rus+S9CdXa+Zv+NZX8D47Orq3/13T8jR/viLbchAPsRpnbWdjN65jP7Qf+p67+e67rME3dn1mvk7nvUFjM+urv7df/eEHO2Pv9iGDORDnNZZ28no/53L6A/9p673fq7rPkvQnV2vmb/jWV/A+Ozq6t/9d0/I0f74i23IQD7EaZ21nYzeuYz+0H/qeu/nuu6zBN3Z9Zr5O571BYzPrq7+3X/3hBztj7/YhgzkQ5zWWdvJ6J3L6A/9p673fq7rPkvQnS20At7Gcbu+7n/W9y68bRz6BdzCva731Q/upo/35w/gSN+Mux/9n6z8cZvSn1zW94zVhZ/LEw3W+Z7W5v3+JJ/1Zx/T0M/D1ArzhB7OhV+v652wao3/AL7/5C75sirbnA3FKI/txC39nC3+AFvz5U/PQS/2p57xPKzw5Lr37o/c3ly9/H/qVN3shV+/O5/m0E7yxv/c+OVvryeO8jEN/TxMrTBP6OFc+PW63gmr1vgP4PtP7pIvq7LN2VCM8thO3NLP2eIPsDVf/vQc9GJ/6hnPwwpPrnvv/sjtzdXL/6cO//Kv8lV/vHD76+Lu/gBvtzouw4Kt8MkOoz7722BtxvsvzHt79dTd/1ud7Bd9+6582oRN70+e7wk7xAgK80I70oXf2c28ze7v/rItvP/97bZv7AWv+7j+/lCcwTiO9dxd/qVN+TEX00QP80I70oXf2c28ze7v/rItvP/97bZv7AWv+7j+/lCcwTiO9dxd/qVN+TEX00QP80I70oXf2c28ze7v/rItvP/97bZv7AWv+7j+/lD/nME4jvXcXf6lTfkxF9NED/NCO9KF39nNvM3u7/6yLbz//e22b+wFr/u4/v53/PGZ/+egb9bff8IPT+HCL8jRDfnIKNudv+Pwn/6EW8o03+/lfOyJypuf3M4l//5/P8MlXM6nrut1P9Fgne7Efc8Cntaw3/k7Dv/pT7ilTPP9Xs7Hnqi8+cntXPLv//czXMLlfOq6XvcTDdbpTtz3LOBpDfudv+Pwn/6EW8o03+/lfOyJypuf3M4l//5/P8MlXM6nrut1P9Fgne7Efc8Cntaw3/k7Dv/pT7ilTPP9Xs7Hnqi8+cntXPLv//czXMLlfOq6XvcTDdbpTtz3LOBpDfud/7/j8J/+hFvKNN/v5Xzsicqbn9zOFW36qP/4U53tIm3xAR633r/igxzgMljdreazm6765z/vu2e3Tk2uWY/40W+3dpv0YD7V2S7SFh/gcev9Kz7IAS6D1d1qPrvpqn/+8757duvU5Jr1iB/9dmu3SQ/mU53tIm3xAR633r/igxzgMljdreazm6765z/vu2e3Tk2uWY/40W+3dpv0YD7V2S7SFh/gcev9Kz7IAS6D1d1qPrvpqn/+8757duvU5Jr1iB/9dmu3SQ/mU53tIm3xAR633r/igxzgMljdreazm6765z/vu2e3Tk2uWY/40a/jKM/7qnbrjQrzAG76t8vt3f9/9/M++EDu8iPd+LYdqM5t8a7M9YWfy8su50Ps8/U8yEr95cmv1jLt9qoGzQst/WBt1o3v8iPd+LYdqM5t8a7M9YWfy8su50Ps8/U8yEr95cmv1jLt9qoGzQst/WBt1o3v8iPd+LYdqM5t8a7M9YWfy8su50Ps8/U8yEr95cmv1jLt9qoGzQst/WBt1o3v8iPd+LYdqM5t8a7M9YWfy8su50Ps8/U8yEr95cmv1jLt9qoGzQst/WBt1o3v8iPd+LYdqM5t8a7M9YWfy8su50PcqC0fnYIO3zU/xOB//0IcmEL87X0e7xou6AtPymDvs+YN8oT9/An74QzMwwzs3Nr/vtWJKeDt7rfmzed2rKnf3ufxruGCvvCkDPY+a94gT9jPn7AfzsA8zMDOre1bnZgC3u5+a958bsea+u19Hu8aLugLT8pg77PmDfKE/fwJ++EMzMMM7NzavtWJKeDt7rfmzed2rKnf3ufxruGCvvCkDPY+a94gT9jPn7AfzsA8zMDOre1bnZgC3u5+a958bsea+u19Hu8aLugLT8pg77PmDfKE/fwJ++EMzMMyTdrfnutOfe6Ey791X/LlD/K75/6QL8xOzfrtPNGcvbcXvbvhHp11n+xXz79JbcjH68dWr/KTuu8SX/yXvtlKW9b3TNz3DOlprd5Xr/3FPvmbXvfJ/371/JvUhny8fmz1Kj+p+y7xxX/pm620ZX3PxH3PkJ7W6n312l/sk7/pdZ/sV8+/SW3Ix+vHVq/yk7rvEl/8l77ZSlvW90zc9wzpaa3eV6/9xT75m173yX71/JvUhny8fmz1Kj+p+y7xxX/pm620ZX3PxH3PkJ7W6n312l/sk7/pdZ/sV8+/yLj2gK20Sr/e2J/vvQ7/j42M1I3a5z7Mnb/je7/2+d7ruUz8dozrm938aO3r5Y/1+Izjgp3+AV+/m57CTs31MJrttY71To3+H73w867thnzSoB3sGo7PVO77ba7z3q/ywe30UcrtNQ/o6P/R7s7kpa/yo47xWi/guv8M+Tsv6FGq6AXN/5eP/W2u896v8sHt9FHK7TUP6Oj/0e7O5KWv8qOO8Vov4LoM+Tsv6FGq6AXN/5eP/W2u896v8sHt9FHK7TUP6Oj/0e7O5KWv8qOO8Vov4LoM+Tsv6FGq6AXN/5eP/W0O1OJ+1lLLzkZ/xL9u39Bu3ocPAsI9qby9vp3M+Sec9CAP7TG91fvNz4TL3UOsy5BP+6Ucwsv+5FOPz+Z9+CAg3JPK2+vbyZx/wkkP8tAe01u93/xMuNw9xLoM+bRfyiG87E8+9fhs3ocPAsI9qby9vp3M+Sec9CAP7TG91fvNz4TL3UOsy5BP+6Ucwsv+5FOPz+Z9+CD/INyTytvr28mcf8JJD/LQHtNbvd/8TLjcPcS6DPm0X8ohvOxPPvX4bN6HDwLCPam8vb6dzPknnPQgD+0xvdX7zc+Ey91DvOKu7vClL8j369nln+rcb/6XLv2nX78tT93WX81RWvWsD9T3aOSDHvXBP/muPNlSK+d8zurhztgwGuBsf+5678oVDdfbb+xmXfhZj+tYn/R8Xv7Sb/XojLXVi/Wuj/6f7uY9P/vVi/Xi3/LVj95C6/QnTrjZbvIpHJjxjvqaCrB3P8zojLXVi/Wuj/6f7uY9P/vVi/Xi3/LVj95C6/QnTrjZbvIpHJjxjvqaCrB3P8zojLXVi/Wuj/6f/+7mPT/71Yv14t/y1Y/eQuv0J0642W7yKRyY8Y76mgqwdz/M6Iy11Yv1ewujdT/ZMgj5ZV//dqvUZj3oBJ3LSlvr4u7+zhlUuA236f78pA/W5p+rUGzMW772Zk3xQXXl3y/7e+/+gVnCG6/Uyg/trZa1BI32cJvuz0/6YG3+uQrFxrzla2/WFB9UV/79sr/37h+YJbzxSq380N5qWUvQaA+36f78pA/W5p+rUGzMW772Zk3xQXXl3y/7e+/+gVnCG6/Uyg/trZa1BI32cJvuz0/6YG3+uQrFxrzla2/WFB9UV/79sr/37h+YJbzxSq380N5qWUvQaA+36f78pA/W5v+fq1BszBb+5+EM1/bu+AIN/O9v9gtPuEK58+EemPGO+vxcv+Te4azd8IRL3dZf8+9P8jX99s1ewrGv95186dLP2UnP56tm3kMcpVXP+kA9+RDf/tMf7Z++5Mq/e4Oc+Xhv0sO84vbu+AIN/O9v9gtPuEK58+EemPGO+vxcv+Te4azd8IRL3dZf8+9P8jX99s1ewrGv95186dLP2UnP56tm3kMcpVXP+kA9+RDf/tMf7Z++5Mq/e4Oc+Xhv0sO84vbu+AIN/O9v9gtPuEK58+EemPGO+vxcv+Te4azd8IRL3dZf8+9P8lRt61ev8H9O+9hO3NIf6FNt71N/5LZ+vyX/b6+B/vw97rPQ7JxlLdh177cQX/iE28InTtW+j+mCT/dTDc3eTPNlzf/QLrOjXtHbHaf+v/PJb/TPn7Au//H8S9P8u7dIX8C0j+3ELf2BPtX2PvVHbuv3W/L2GujP3+M+C83OWdaCXfd+C/GFT7gtfOJU7fuYLvh0P9XQ7M00X9b8D+0yO+oVvd1x6v87n/xG//wJ6/Ifz780zb97i/QFTPvYTtzSH+hTbe9Tf+S2fr8lb6+B/vw97rPQ7JxlLdh177cQ/9G93sxvDINs7+tFT/LGvOUy3cmD/+eZf9KuXNFJzv6CjOuT3/VJveV+bsz7b97UfsRvDINs7+tFT/LG/7zlMt3Jg//nmX/SrlzRSc7+gozrk9/1Sb3lfm7M+2/e1H7EbwyDbO/rRU/yxrzlMt3Jg//nmX/SrlzRSc7+gozrk9/1Sb3lfm7M+2/e1H7EbwyDbO/rRU/yxrzlMt3Jg//nmX/SrlzRSc7+gozrk9/1Sb3lfm7M+2/e1H7EbwyDbO/rRU/yxrzlMt3Jg//nmX/SrlzRSc7+gozrk9/1Sb3lfm7M+9/hiRrOFl7y1N7/Quz+v/3foF/8xx6lwD/EJ673u8/tu6vp+573bo/9ZlzAHX76AP7RCIr/kzzmzzn4Hi/nWL/75x/ww9zr9Wq3ja32Ix3vZl7/dhvuso397f9c0ZjfjGJ/6bat/ruX1GcP8EnN3u49/3E71QAfmJ++u5q+73nv9thvxgXc4acP4B+NoPg/yWP+nIPv8XKO9bt//gE/zL1er3bb2Go/0vFu5vVvt+Eu29jfzhWN+c0o9pdu2+q/e0l99gCf1Ozt3vMft1MN8IH56bur6fue926P/WZcwMlf/nb89rZv0PfN2tYe9vPf+vXZzrLPzfxc07gu2Lcu91vOm9G50NJ/+rqO7aA7yIH50b2u6+R+/DAet+Ff/07N9W9v0JDe63yPtYuN+BTeoo+O3kLb9zS43Oce8Eg6/m7u8N+P8Vkb7oJu9Z2/4/Ua0psdqDzM3XaM+BT/3qKPjt5C2/c0uNznHvBIOv5u7vDfj/FZG+6CbvWdv+P1GtKbHag8zN12jPgU3qKPjt5C2/c0uNznHvBIOv5u7vDfj/FZG+6CbvWdv+P1GtKbHag8zN12jPgU3qKPjt5C2/c06P/sz9nFTN0NXeH/TeUmvuxWJ+rPDKOf7LRnzfROb/VefpFpHrdTrfXLr/k8T/mdPf7zSvL6a9uB6txyTu30794XDe0hLe5nzfROb/VefpFpHrdTrfXLr/k8T/mdPf7zSvL6a9uB6txyTu30794XDe0hLe5nzfROb/VefpFpHrdTrfXLr/k8T/mdPf7zSvL6a9uB6txyTu30794X/w3tIS3uZ830Tm/1Xn6RaR63U631y6/5PE/5nT3+80ry+mvbgercck7t9O/eFw3tIS3uZ830Tm/1Xn6RaR63U631y6/5hOzyfoz9Mwyj7q/jPazw1l6/m57C317rVB/WdL/+Fp7spq/+ci7uoG/+cg7o+537KY3cganvNQ/oHn//hU/59bvpKfzttU71YU3362/hyW766i/n4g765i/ngL7fuZ/SyB2Y+l7zgO7x91/4lF+/m57C317rVB/WdL/+Fp7spq/+ci7uoG/+cg7o+537KY3cganvNQ/oHn//hU/59bvpKfzttU71YU3362/hyW766i/n4g765i/ngL7fuf+f0sgdmPpe84Du8fdf+JRfv5uewt9e61Qf1nS//hae7Kav/nIu7qBv/nIO6Pu9/5L/7bkOzc5p4udO6NXP5PtP3VOd+Yae1Ozd67oO8fF+0q4O5OJO0KT83LLd61Td54Jv4s1foclf9Hy71cTP/8fe63u/+vC//bWe6pzv2dL/9bc/5s+Z/YVPuLDO3VQP7byvy9z94dSN4IqO6reepcUftx8/yIF59nq7/z7r1CXP54Jt9nbvtPhP1X0u+Cbe/BWa/EXPt1tN/Px/7L2+96sP/9tf66nO+Z4t/V9/+2P+nNlf+IQL69xN9dDO+7rM3R9O3Qiu6Kh+61la/HH78YP/HJhnr7f777NOXfJ8Lthmb/dOC8W2qdZQzM1w3fK9TdBez/UwGuDozc0WnuymX+RtvOTKX/PR/vYwzs58fpG8b/lfDvNe/+daH6fur9TmL7XIT+EF77MKD+TRTvT17c0CfvqT7JxfDvv6r/kxB/GYz/w8X7806P5Kbf5Si/wUXvA+q/BAHu1EX9/eLOCnP8nO+eWwr/+aH3MQj/nMz/P1S4Pur9TmL7XIT+EF77MKD+TRTvT17c0CfvqT7JxfDvv6r/kxB/GYz/w8X7806P5Kbf5Si/wUXvA+q/BAHu1EX9/eLOCnP8nO+eWwr/+ab8zTf/I4XvROrf5LHlSB+dFW/w/1Ybv78c23gx7T/BxzvS35Mkvvbm7t9evqsw/8/L/kJp/0Q6z/C0/oOj7+tx+Y80/i+27eVC+1u+f+0emcb9zmBQ/7C1+vBG2bWqzy7+78cJ/tti/86Azupj/ZTF74Gf/MWx2oMrj2U1/3gercFk/yYV+fys/OpU3K7+78cJ/tti/86Azupj/ZTF74Gf/MWx2oMrj2U1/3gercFk/yYV+fys/OpU3K7+78cJ/tti/86Azupj/ZTF74Gf/MWx2oMrj2U1/3gercFk/yYf/Ri7zdVa/hyT/Egsz8YR/vqL7Mdw/gF211Juaze6vvYV3wWZv34czeYA3Y6P/95JrL5/9O1eY/8YLM/GEf76i+zHcP4BdtdSbms3ur72Fd8Fmb9+HM3mAN2Oj//eSay+dO1eY/8YLM/GEf76i+zHcP4BdtdSbms3ur72Fd8Fmb9+HM3mAN2Oj//eSay+dO1eY/8YLM/GEf76i+zHcP4BdtdSbms3ur72Fd8Fmb9+HM3mAN2Oj//eSay+dO1eY/8YLM/GEf76i+zHcP4BdtdSbms3ur72Fd8Fmb9+HM3mAN2Oh/9Z1895P82yvu+qeN/JCe1rAvvP/N3an+5FO/0HGP/lvu/BIO2Kf9zPUf9MG+3/xcr9+u9I8d9LRP8vAP+8L739yd6k8+9Qsd9+i/5c4v4YD/fdrPXP9BH+z7zc/1+u1K/9hBT/skD/+wL7z/zd2p/uRTv9Bxj/5b7vwSDtin/cz1H/TBvt/8XK/frvSPHfS0T/LwD/vC+9/cnepPPvULHffov+XOL+GAfdrPXP9BH+z7zc/1+u1K/9hBT/skD/+wL7z/zd2p/uRTv9Bxj/5b7vwSDtin/cz1H/TpH/C5rOHZft1lH+8Nn8FnbfGQ781T7eqnzeQwSs6OrqnrTfiNGuZ8W/oC3f/s/d+S/+3GrvRmf+IZfNYWD/nePNWuftpMDqPk7Oiaut6E36hhzrelL9D9z97/LfnfbuxKb/YnnsFnbfGQ781T7eqnzeQwSs6O/66p6034jRrmfFv6At3/7P3fkv/txq70Zn/iGXzWFg/53jzVrn7aTA6j5OzomrrehN+oYc63pS/Q/c/e/y35327sSm/2J57BZ23xkO/NU+3qp83kMErOjq6p6034jRrmfFv6At33MV3AMB/29Vn82v74H//Jwk/sNe/yxa7Wwc76ju/l75/C633iMrzZSlvrQyy82A/sBQzzYV+fxa/tj//xnyz8xF7zLl/sah3srO/4Xv7+KbzeJy7Dm620tT7Ewov9wF7AMB/29Vn82v74H//Jwk/sNe/yxa7Wwc76ju/l75/C633iMrzZSlvrQyy82A/sBQzzYV+fxa/tj//xn/8s/MRe8y5f7God7Kzv+F7+/im83icuw5uttLU+xMKL/cBewDAf9vVZ/Nr++B//ycJP7DXv8sWu1sHO+o7v5e+fwut94jK82Upb60MsvIvM/OPPw0C88ZLf2+5f9x6P8w0f9huvt4Pu9o5+0T+68/4vs/Ue/SQP/zuf/uPPw0C88ZLf2+5f9x6P8w0f9huvt4Pu9o5+0T+68/4vs/Ue/SQP/zuf/uPPw0C88ZLf2+5f9x6P8w0f9huvt4Pu9o5+0T+68/4vs/Ue/SQP/zuf/uPPw0C88ZLf2+5f9x6P8w0f9huvt4Pu9o5+0T+68/4vs/Ue/SQP/zuf/uPPw0C88ZL/39vuX/cej/MNH/Ybr7eD7vaOftE/uvP+L7P1Hv0kD/87r/1HrPTr7eiID7cwGLfP/99QLLRA3/ib7bfmTc/V/+Rf/slmXenOHbf6DAI7L9P4z81G7uEt3Ki/3aiu7vHgjuxIqr/2ytnCLPZwX8wQL+o43/BhH/tKbuLkOv5u/uerb8zsrf/rfuTmL+eTn/T0fOTmf/9J/ti3nrBq/e3fi/iQTvoI2uGdP9GEf/qTDLDOHdZ0//z/DcVCC/SNv9l+a970XP1P/uWfbNaV7txxq88gsPMyjf/cbOQe3sKN+tuN6uoeD+7IjqT6a6+cLcxiD/fFDPGijvMNH/axr+Qm/37uaE//Kk/zv374cs/eYG3Wg272/y3oYP3r1P7xkp/329/sQvzo4G76k83kFu63ui/4Bt33EF/px56oZz2vrn7uuaz+l17a43/38767wc767ezwlQ7upj/ZTG7hfqv7gm/QfQ/xlX7siXrW8+rq557L6n/ppT3+dz/vuxvsrN/ODl/p4G76k83kFu63ui/4Bt33EF/px56oZz2vrn7uuaz+l17a43/38767wc767ezwlQ7upj/ZTG7hfqv7gm/QfQ/xlX7siXrW8+rq557L6n/ppT3+dz/vuxvsrN/ODl/p4G76k83kFo739N3TdP/8bXzqAP7Rgc3dWuzeHw3W1P+P66tm71Mv03C+/2kO7vG+4/DvnI6P/TDu5hh/5O1s7Tw/zOiMtRXKv3W/481sxvP/nHAP+aCv9Att3zjv6/dM3OK++vWbtbDe8q5+wo096pONtRXKv3W/481sxvP/nHAP+aCv9Att3zjv6/dM3OK++vWbtbDe8q5+wo096pONtRXKv3W/481sxvP/nHAP+aCv9Att3zjv6/dM3OK++vWbtbDe8q5+wo096pONtRXKv3W/481sxvP/nHAP+aCv9Att3zjv6/dM3OK++oT70mb86XKO9c4ft07v8+PP/WUN6NZu/8Lv5fbN9ED/9sCu73Iu7qBv/nIu7rIKxJrq/7z/D9bfO+jpn8sn7fzabsjWbv/C7+X2zfRA//bAru9yLu6gb/5yLu6yCsSa6v+8D9bfO+jpn8sn7fzabsjWbv/C7+X2zfRA//bAru9yLu6gb/5yLu6yCsSa6v+8D9bfO+jpn8sn7fzabsjWbv/C7+X2zfRA//bAru9yLu6gb/5yLu6yCsSa6v+8D9bfO+jpn8sn7fzabsjWbv/C7+X2zfRA//bAru9yLu6gb/5yLu6yWsvp/txZy5v4jO003863C/yljfJZ6//Q3MJyn8hPbuLkOswg0NsETcpYHe+eDejqj7Vqn9LOOalKv/OtttC+Hf2//d9jHvpb7bf13eGW385+/xud3jztnSy1gz/ZYe3kQC3uk6r6Df2cl+7b0f/b/z3mob/VflvfHW757ey30enN097JUjv4kx3WTg7U4j6pqt/Qz3npvh39v/3fYx76W+239d3hlt/Ofhud3jztnSy1gz/ZYe3kQC3uk6r6Df2cl+7b0f/b/z3mob/VflvfHW757ey30enN097Jcn74urzzEu/4jv7pT4zrm73j8L/9Sh/mMA/gmG/kjS/uZq/yij7ZEv7lsO/uEz/rVvfRmq/36U/Igq3po/7ng5z51t7ZPA/gmG/kjS/uZq/yij7ZEv7lsO/uEz/rVvfRmq/36U/Igq3po/7ng5z51t7ZPA/gmP9v5I0v7mav8oo+2RL+5bDv7hM/61b30Zqv9+lPyIKt6aP+54Oc+dbe2TwP4Jhv5I0v7mav8oo+2RL+5bDv7hM/61b30Zqv9+lPyIKt6aP+54Oc+dbe2TwP4Jhv5I0v7mav8oo+2RL+5bDv7hPf6ygPxPVp8scP2LeL1duN2q8t6PBd4Sov4KdPyL4d/aBv1vru3BZP8oQus8Vu31P639QN9DCq1Drv9I7e/vN/9xm8zExe6c49pVna5jqP1udez61bwK48++c/75ef1tn+vTCq1Drv9I7e/vN/9xm8zExe6c49pVna5jqP1udez61bwK48++c/75ef1tn+vTCq1Dr/7/SO3v7zf/cZvMxMXunOPaVZ2uY6j9bnXs+tW8CuPPvnP++Xn9bZ/r0wqtQ67/SO3v7zf/cZvMxMXunOPaVZ2uY6j9bnXs+tW8CuPNnP/PZmXP8A6+anzeSifuycndQXPaWBXu/eL9BTrd7IXq+SH+46HvDzDvd5n/65fNKybfWnX8/ICNp7O/8V/vn6fvm6HOa3Lve4DuaUjaTNPPVR386ojuPiHu46zvO6DuvJf5GwXt/ZbvJmD9btfNrIH6ff/uvRjutgTtlI2sxTH/XtjOo4Lu7hruM8r+uwnvwXCev1ne0mb/Zg3c6njfxx+u2/Hu24DuaUjaTNPPVR386o/47j4h7uOs7zug7ryX+RsF7f2W7yZg/W7XzayB+n3/7r0Y7rYE7ZSNrMUx/17YzqOC7uwX689N33kC+r0QnnmL/dKe38bD/Zgz/Zu/folx73MQ/9JE7fVa/+d8/3oX/22R78fG7H5Zyrd/yRAIvzgbp7UW/+xx6lND/16T/vIS77e8+btVzwvG/5vm/7Sl/LCL/Y5Zyrd/yRAIvzgbp7UW/+xx6lND/16T/vIS77e8+btVzwvG/5vm/7Sl/LCL/Y5Zyrd/yRAIvzgbp7UW/+xx6lND/16T/vIS77e8+btVzwvG/5vm/7Sl/LCL/Y5Zyrd/yRAIvzgbp7UW/+xx6lNP8/9ek/7yEu+3vPm7Vc8Lxv+b5v+0of4Kfu9enN2zSf7qPO9fvfyTUP5n0P54Oe7UAt2A9f9eSa9du9/ldf/dD+6Gxv5t+P8byp+/G+7Lfvs6seneq91erPw8F+2jUP4l+u/Wjtt9FZ9zvezGVt23/uzaj950Bc6RXe+eSay/vNz2h/3+s+0GF71k989q6841m/3et/9dUP7Y/O9mb+/RjPm7of78t++z676tGp3lut/jwc7Kdd8yD+5dqP1n4bnXW/481c1rb9596M2n8OxJVe4Z1Prrm83/yM9ve97gMdtmf9xGfvyjue9du9/ldf/dD+6NmvqestyPGe3sTN6gz/zMpZG9KbfbxxCsWE28LY36LhruNISrjOKdJPPOhj7uYazr+Bqe/OffmzDu7xrv4Snu7PH9iTCsWE28LY36LhruNISrjOKdJPPOhj7uYazr+Bqe/OffmzDu7xrv4Snu7PH9iTCsWE28LY36LhruNISrjOKdJPPOhj7uYazr+Bqe/OffmzDu7xrv4Snu7PH9iTCsWE28LY36LhruNISrjOKdJPPOhj7uYazr+Bqe/OffmzDu7xrv4Snu7PH9iTCsWE28LY36LhruNISrjOKdJPPOhj7uY7XtNwDfmPLrUkfv2ePcQ+7/Wo7tQNj6S4D9+tr+8yqN71H+5nfeyBXuzl/1/aufznjS3gnf/nmn79nj3EPu/1qO7UDY+kuA/fra/vMqje9R/uZ33sgV7s5V/aufznjS3gnf/nmn79nj3EPu/1qO7UDY+kuA/fra/vMqje9R/uZ33sgV7s5V/aufznjS3gnf/nmn79nj3EPu/1qO7UDY+kuA/fra/vMqje9R/uZ33sgV7s5V/aufznjS3gnf/nmn79nj3EPu/1qO7UDY+kuA/fra/vMqje9R/uZ33sgV7s5V/aGfzo2Q/XLQzWJr/eqiaUUAzgpq/hrtzz557LqF/pu3fc9F3eiu7ZQ0zsFi//s579cN3CYG3y661qQgnFAG76Gu7KPX/uuYz6lf++e8dN3+Wt6J49xMRu8fI/69kP1y0M1ia/3qomlFAM4Kav4a7c8+eey6hf6bt33PRd3oru2UNM7BYv/7Oe/XDdwmBt8uutakIJxQBu+hruyj1/7rmM+pW+e8dN3+Wt6J49xMRu8fI/69kP1y0M1ia/3qomlFAM4Kav4a7c8+eey6hf6bt33PRd3oru2UNM7BbPwKFf+PYf/LZt6I8u5yA+w/Ee84Vfr11/1vfu7t0vtRav1L5vxvMP7phP8cDe/vFO/C6/78jt/+m/90ffqK6f7B994vO+e6D/64A++4re8ycc7rp/0axM5c2O9Oxt9yaP40P83zCf8bmq8JPN/e3/rP5Sy/l/fuu9Dv/BPvvcHrfAT8+PDvcyC7qDrOlTzd22beiPLucgPsPxHvOFX69df9b37u7dL7UWr9S+b8bzD+6YT/HA3v7xTvwuv+/I7f/pv/dH36iun+wffeLzvnug/+uAPvuK3vMnHO66f9GsTOXNXsBZS91H3KK3jv2YfvWJX+E5/+F6i/jrH92TCuvmXfSTSvKdPcmp78eO/ulY++4L3aC3jv2YfvWJX+E5/+F6i/jrH92TCuvmXfSTSvKdPcmp78eO/ulY++4L3aC3jv2YfvWJX+E5/+F6i/jrH92TCuvmXfSTSvKdPcmp78eO/ulY++4L3aC3jv2YfvWJ/1/hOf/heov46x/dkwrr5l30k0rynT3Jqe/Hjv7pWPvuC92gt479mH71iV/hOf/heov46x/dkwrr5l30k0rynT3Jqe/HAo3VmorjLt/QNX/PJo2kMZf3Cq+0Bj3VZc/P9j+p6e7Qj93JWHvfi2zhGA/wrfv4z03TO//wDV3V213eo67h5Gz3Is3KzmnQITzZ3N/OMc/PhPvJ7cz19a/jhEz1Em7yKRzg4A7XMM/3Ie63pUzzyp+rrYvrk2rbf77z6U/5hPvJ7cz19a/jhEz1Em7yKRzg4A7XMM/3Ie63pUzzyp+rrYvrk2rbf77z6U/5hPvJ7cz19a/jhEz1Em7yKf8c4OAO1zDP9yHut6VM88qfq62L65Nq23++8+lP+YT7ye3M9fWv481c8J8M1KvWwmBt0OSf7U5e66U9/pf/3+E+yOlP+ZQ/ySmM26id/7dr/exc9DKL1cw/7zUP6Kgf7wUN5vuetciI3Pgs/yvuypMN9/gc0oDu7d//58XszaUe5jKL1cw/7zUP6Kgf7wUN5vuetciI3Pgs/yvuypMN9/gc0oDu7d//58XszaUe5jKL1cw/7zUP6Kgf7wUN5vuetciI3Pgs/yvuypMN9/gc0oDu7d//58XszaUe5jKL1cw/7zUP6Kgf7wUN5vuetciI3Pgs/yvuypMN9/gc0oDu7d///+fF7M2lHuY++9KYXp+1fvj6r8tpfvmnz/PzzsMw38ON7fSJ+vftXNCCnfSCreOEftZh/etaLLyBDr+df7vcDu2kfeuJ2thVP/tO//lwDfN7//ftXNCCnfSCreOEftZh/etaLLyBDr+df7vcDu2kfeuJ2thVP/tO//lwDfN7//ftXNCCnfSCreOEftZh/etaLLyBDr+df7vcDu2kfeuJ2thVP/tO//lwDfN7//ftXNCCnfSCreOEftZh/etaLLyBDr+df7vcDu2kfeuJ2thVP/tO//lwDfN7//ftXNCCnfSCreOEftZh/eta7O7aru/H3qhK/b3x3vN/DvGD/u0G/+30jdrJz2xiC63WyIjcwuznna3rLezuUmt1JYzr5a/WV174fM/2Dv/9Hj/4yR7dsv35mG/Ip33q837q9QrNNK3e84/e8I/txF30Cj/7QB/vPf/nED/o327QTt+onfzMJrbQao2MyC3Mft7Zut7C7i61VlfCuF7+an3lhc/3bO/w3+/xg5/s0S3bn4/5hnzapz7vp16v0EzT6j3/6A3/2E7cRa/wsw/08d7zfw7xg/7tBu30jdrJz2xiC63WyIjcwuznna3rLezuVU30d3z+e+/NQF/px87Zskru/EvQxiz4QcW3zM/z887Dn9zs++/U6K+p7t/YoAvwrYv4FF7rQv/P/Pb/6/fM234svMKry2l+99wM167c88v+zGfvs/Y+9YFp4SV/z2pP2cY8/awe0mDu9I2KjFSu/IS9/ldf/VW9/8X8yc2+/06N/prq/o0NugDfuohP4bUu9Mxv/79+z7ztx8IrvLqc5nfPzXDtyj2/7M989j5r71MfmBZe8ves9pRtzNPP6iEN5k7fqMhI5cpP2Ot/9dVf1ftfzJ/c7Pvv1Oivqe4f0oI9qSTf2T3MwM95+b2O9n4syBu/3Nbe2Xw/r64+2eB+9WM+pcSO3vXMt6Uv0NE/qa5e0O+v8EqL6YhO9U/+xiYu+4QM5qg90Z2/4wC+5fae60cO2IC787r/v9WnPalu7utYf8duXNrN/L2a2thTreP6W/TF7M2jPtngfvVjPqXEjt71zLelL9DRP6muXtDvr/BKi+mITvVP/sYmLvuEDOaoPdGdv+MAvuX2nutHDtiAu/O6v9WnPalu7utYf8duXNrN/L2a2thTreP6W/TF7M2jPtngfvVjPqXEjt71zLel7+hEr9TU//jEzf+s3OEJK/Tff7vAf5FH3+t8z/movsyX3+v6y//zyt0gf+wgUPbQz8Mkr+sy6/0CDbpKXfCfLHzAf5FH3+t8z/movsyX3+v6y//zyt0gf+wgUPbQz8Mkr+sy6/0CDbpKXfCfLHzAf5FH3+t8z/mo/77Ml9/r+sv/88rdIH/sIFD20M/DJK/rMuv9Ag26Sl3wnyx8wH+RR9/rfM/5qL7Ml9/r+sv/88rdIH/sIFD20M/DJK/rMuv9Ag26Sl3wnyx8wH+RR9/rfM/5qL7Ml9/r+sv/88rdIH/sIKD6I435F438fvzfx//rYB7+8W7mMJr4NU/1x37iVG3sjg/+uZr51u71Me/hPnvMUx3O+u7cl4/OOH/uug7aOA7iQM3nm43x1A23wW/H37/j4+/m1u71Me/hPnvMUx3O+u7cl4/OOH/uug7aOA7iQM3nm43x1A23wW/H37/j4+/m1u71Me/hPnvMUx3O+u7cl4/OOH/uuv8O2jgO4kDN55uN8dQNt8Fvx9+/4+Pv5tbu9THv4T57zFMdzvru3JePzjh/7roO2jgO4kDN55uN8dQNt8Fvx9+/4+Pv5tbu9THv4T4r09481X8fVHdcoeR+1tz/xsFf/vYapddv6Aof85XOw+rN/M1c67a97FNK7FP633Xv2UOcpf3+6dD+yUB9kTF99dtP99Z/6kiqv0I/6MWs3szfzLVu28s+pcQ+pf9d9549xFna758O7Z8M1BcZ01e//XRv/aeOpPor9INezOrN/M1c67a97FNK7FP633Xv2UOcpf3+6dD+yUB9kTF99dtP99Z/6kiqv0I/6MWs3szfzLVu28v/PqXEPqX/XfeePcRZ2u+fDu2fDNQXGdNXv/10b/2njqT6K/SDXszqzfzNXOu2vexTSuxTCgKyTfhpvfOSD/m7D/QfDQKrz824rtapX+xq3dhTTYN/b+wzbGIQX/877/x3r79CH+lVD+T8fvin7/U7bvBD7OjzX+GOjuhq3dhTTYN/b+wzbGIQX/877/x3r79CH+lVD+T8fvin7/U7bvBD7OjzX+GOjuhq3dhTTYN/b+wzbGIQX/877/x3r79CH+lVD+T8fvin7/U7bvBD7OjzX+GOjuhq3dhTTYN/b+wzbGIQX/877/x3r79CH+lVD+T8fvin7/U7bvBD7OjzX+GO/47oat3YU02Df2/sM2xiEF//O+/8dx/2FK/8nO/r5Q/oHj/4J2z2rO3wbw/Yzd/TQG3b5Jr1uH74PW7e0d74gC77xpzk4t/J8wrEsQ//MJ/xcK/e9+vwbw/Yzd/TQG3b5Jr1uH74PW7e0d74gC77xpzk4t/J8wrEsQ//MJ/xcK/e9+vwbw/Yzd/TQG3b5Jr1uH74PW7e0d74gC77xpzk4t/J8wrEsQ//MJ/xcK/e9+vwbw/Yzd/TQG3b5Jr1uH74PW7e0d74gC77xpzk4t/J8wrEsQ//MJ/xcK/e9+vwbw/Yzd/TQG3b5Jr1uH74PW7e0d74gC77xhzpH7/2fb7d9P/O/Y4f6CbNzbkf3+udsIIt9jiv/ncP4JV+7IF+/nwP9zGN9M3o+qwP1NG+3fTO/Y4f6CbNzbkf3+udsIIt9jiv/ncP4JV+7IF+/nwP9zGN9M3o+qwP1NG+3fTO/Y4f6CbNzbkf3+udsIIt9jiv/ncP4JV+7IF+/nwP9zGN9M3o+qwP1NG+3fTO/Y4f6CbNzbkf3+udsIIt9jiv/ncP4JV+7IF+/nwP9zGN9M3o+qwP1NG+3fTO/Y4f6CbNzbkf3+udsIIt9jiv/ncP4JV+7IF+/nwP9zGN9M0Y0pud3lVv+Duf+ede05FevdH+6W7u8W7e8w1P6Gc9pYQf6Cat69z/XfTIONBG/tGNqvrPne3bLP7k7vre/vbFv3s6Hvb32/CEftZTSviBbtK6zt1Fj4wDbeQf3aiq/9zZvs3iT+6u7+1vX/y7p+Nhf78NT+hnPaWEH+gmrevcXfTIONBG/tGNqvrPne3bLP7k7vre/vbFv3s6Hvb32/CEftZTSviBbtK6zt1Fj4wDbeQf3aiq/9zZvs3iT+6u7+1vX/y7p+Nhf78NT+hnPaWEH+gmrevcXfTIONBG/tFpffSOzs+2eZGu7OtUz+SCr/T82/LOaewGrc+9Tsj8L7V3n8sxX/8BDu7ITrg+i4xl7+HRmcLH/fGrTt15ae+/fpGJ/+QGrc+9/07I/C+1d5/LMV//AQ7uyE64PouMZe/h0ZnCx/3xq07deWnvv36Rif/kBq3PvU7I/C+1d5/LMV//AQ7uyE64PouMZe/h0ZnCx/3xq07deWnvv36Rif/kBq3PvU7I/C+1d5/LMV//AQ7uyE64PouMZe/h0ZnCx/3xq07deWnvv36Rif/kBq3PvU7I/C+1d5/LMV//AQ7uyE64Pgv6Bq3P/13Mzk/i63/1O0/uncz26o/8i/39s9//7T/oYd5q9m77Jk/uztnn+g73vN/us1+9Rd/bx43g9/36Tv/6cDvDzO/1y377j77kwmf9g4/qEF/OTK7vcM/77T771Vv0vX3cCP9+36/v9K8PtzPM/F6/7Lf/6EsufNY/+KgO8eXM5PoO97zf7rNfvUXf28eN4Pf9+k7/+nA7w8zv9ct++4++5MJn/YOP6hBfzkyu73DP++0++9Vb9L193Ah+36/v9K8PtzPM/F6/7Lf/6EsufNY/+KgO8eXM5PrO+X++5wo/+4sd7z3/5/7v1Kwv/ITfqJ2s7QWs3sw/76der42N1pPNymv/xl9cwrhe/mpN0LpezDA/zJbfzmb+/YYu+WMugy3M2ek/76der42N1pPNymv/xl9cwrhe/mpN0LpezDA/zJbfzmb+/YYu+WMugy3M2ek/76der42N1pPNymv/xl9cwrj/Xv5qTdC6XswwP8yW385m/v2GLvljLoMtzNnpP++nXq+NjdaTzcpr/8ZfXMK4Xv5qTdC6XswwP8yW385m/v2GLvljLoMtzNnpP++nXq+NjdaTzcpr7/gIesfnv/cpTNBor+jn3tkZD/cxbWL2nu8nbv/N7uHkXvb139j9//Z0P9XOf/s6TsiAjvqIP9Vlj+vvH+aaTu/PCfebvv21/uHIONDMP8w+z80lPOgEPcyL/NE9Xves3++GjPGC/vnxbuZCDMXGnPsU7vu1/uHIONDMP8w+z80lPOgEPcyL/NE9Xves3++GjPGC/vnxbuZCDMXGnPsU7vu1/uHIONDMP8w+/8/NJTzoBD3Mi/zRPV73rN/vhozxgv758W7mQgzFxpz7FO77tf7hyDjQzD/MPs/NJTzoBI32FA7j2p7I6C20xM3/Ex+lxV7ak6zjw+zohQ/glX77jy7nm53efmz3Br3+MBruj17zfG70H8/d94zWp438U13LbZ7uH+//IQ3yPLzc6L/dWC34xm7Ggj/1kg/FuA/WMKjtiYzeQkvc/D/xUVrspT3JOj7Mjl74AF7pt//ocr7Z6e3Hdm/Q6w+j4f7oNc/nRv/x3H3PaH3ayD/Vtdzm6f7x/h/SIM/Dy43+243Vgm/sZiz4Uy/5UIz7YA2D2p7I6C20xM3/Ex+lxV7ak/+s48Ps6IUP4JV++48u55ud3n5s9wa9/jAa7o9++0Gf+ZON/ED/0Sder+H+6HJO9aeOpGh/31Gal9Qd/Zu+/cEv7nz70Vkq/IQPAsVM8rous6M+2Xefy3/+6HJO9aeOpGh/31Gal9Qd/Zu+/cEv7nz70Vkq/IQPAsVM8rous6M+2Xefy3/+6HJO9aeOpGh/31Gal9Qd/Zu+/cEv7nz70Vkq/IQPAsVM8rous6M+2Xefy3/+6HJO9aeOpGh/31Gal9Qd/Zu+/cEv7nz70Vkq/IQPAsVM8rous6M+2Xefy3/+6HJO9aeOpGh/31Gal9Qd/Zu+/cEv7nz70Vkq/K8/0pj/f9HI78ecffy/bts93/wCrsu7T+FAPcQnTtXG7vgqD/arPtBGDmvkWr+kLdPaT+9TytnH/+u23fPNL+C6vPsUDtRDfOJUbeyOr/Jgv+oDbeSwRq71S9oyrf30PqWcffy/bts93/wCrsu7T+FAPcQnTtXG7vgqD/arPtBGDmvkWr+kLdPaT+9TytnH/+u23fPNL+C6vPsUDtRDfOJUbeyOr/Jgv+oDbeSwRq71S9oyrf30PqWcffy/bts93/wCrsu7T+FAPcQnTtXG7vgqD/arPtBGDmvkiqTNrPzV3Ou2ec9HbOzpPu3Ruf3pDr+vj9qyD/+p3/+VnquNPdXkrum8/12hMA//3jzVfx9Uj33Wp67r9l7W5W/HGx/SIA/u9Znuy8/aGA/zxlz4k2zvIo21Tp/W3jzVfx9Uj33Wp67r9l7W5W/HGx/SIA/u9Znuy8/aGA/zxlz4k2zvIo21Tp/W3jzVfx9Uj33Wp67r9l7W5W/HGx/SIA/u9Znuy8/aGA/zxlz4k2zvIo21Tp/W3jzVfx9Uj33Wp67r9l7W5W/HGx/SIA/u9Znuy8/aGA/zxlz4k2zvIo21Tt/G0K7jhGveLk/Ze//ozzz/uRr0ndz6YWv2hC+8nP3wFRrTJXz1Mb3xOt7M26zw1s7zhCz9nL39vl/rtr3jAY/2YK/6wG/bqP+O3DJr/cfe65QP/6AP1EN84noP+tus8NbO84Qs/Zy9/b5f67a94wGP9mCv+sBv26iO3DJr/cfe65QP/6AP1EN84noP+tus8NbO84Qs/Zy9/b5f67a94wGP9mCv+sBv26iO3DJr/cfe65QP/6AP1EN84noP+tus8NbO84Qs/Zy9/b5f67a94wGP9mCv+sBv26iO3DJr/cfe65QP/6AP1EN84noP+tuc+YGKtTTf5phe+Dxfz0l9vzGf+/0//Sef/qQ86hNN+qxd8vcM9NBv8TIL6VC/3VV/7oTb8slvx1ue1Pcb87nf/9N/8ulPyqM+0aTP2iV/z0AP/RYvs5AO9dv/XfXnTrgtn/x2vOVJfb8xn/v9P/0nn/6kPOoTTfqsXfL3DPTQb/EyC+lQv91Vf+6E2/LJb8dbntT3G/O53//Tf/LpT8qjPtGkz9olf89AD/0WL7OQDvXbXfXnTrgtn/x2vOVJfb8xn/v9P/0nn/6kPOoTTfqsXfL3DPTQb/EyC+k+T7gt7OiJDO2/veL82/LOiemGXPL8H9a+X+u2zfoIv/xyX/iEXNr2/+u27fGcP9nc3+bCD/5hDeO5Cs0wT7jc/f4prP15uf21btusj/DLL/eFT8ilbf+/btsez/mTzf1tLvzgH9YwnqvQDPOEy93vn8Lan5fbX+u2zfoIv/xy/1/4hFza9v/rtu3xnD/Z3N/mwg/+YQ3juQrNME+43P3+Kaz9ebn9tW7brI/wyy/3hU/IpW3/v27bHs/5k839bS784B/WMJ6r0AzzhMvd75/C2p+X21/rts36CL/8cl/4hFza9v/rtu3xnD/ZU5rz/H/p5X+Run+/Hs/2E42gnXzs2K/0duv+/LvnY17ViH6R5N7JPAzzSKq/71/31t7MwM7es47zHs/2E42gnXzs2K/0duv+/LvnY17ViH6R5N7JPAzzSKq/71/31t7MwM7es47zHs/2E42gnXzs2K/0duv+/LvnY17ViH6R5N7JPAzzSKq/71/31t7MwM7es47zHv/P9hONoJ187Niv9Hbr/vy752Ne1Yh+keTeyTwM80iqv+9f99bezMDO3rOO8x7P9hONoJ187Niv9Hbr/vy752Ne1Yh+keTeyTwM80gqtBRe8GlO2B+555pO81Ov+v2//5uO3H7O87pe3SF9kbpfzhaP2x/vzW58z6V+9L1uzGcP56Ke/bFvzI/f//u/6cjt5zyv69Ud0hep++Vs8bj98d7sxvdc6kff68Z89nAu6tkf+8b8+P2//5uO3H7O87pe3SF9kbpfzhaP2x/vzW58z6V+9L1uzGcP56Ke/bFvzI/f//u/6cjt5zyv69Ud0hep++Vs8bj98d7sxvdc6kff68b/fPZwLurZH/vG/Pj9v/+bjtx+zvO6Xt0hfZG6X84Wj9sf781u/OEAO6U+z81wncK3Lry63MKOzs98z8Nmz9oen6vhzsC5areMXf8Pn9ItH/XAjvh+7PPcDNcpfOvCq8st7Oj8zPc8bPas7fG5Gu4MnKt2y9j1//Ap3fJRD+yI78c+z81wncK3Lry63MKOzs98z8Nmz9oen6vhzsC5areMXf8Pn9ItH/XAjvh+7PPcDNcpfOvCq8st7Oj8zPc8bPas7fG5Gu4MnKt2y9j1//Ap3fJRD+yI78c+z81wncK3Lry63MKOzs98z8Nmz9oen6vhzsC5areMXf8Pn9ItH/Um/8+b5j3EJ66/pZ3Lhn78QF3aXu+3qu/HXl702Q7U4s6/q2//Bk3vUyrISe7nAU/o/Kv1Uw3NO5/+8B/0QHy/fqv6fuzlRZ/tQC3u/Lv69m/Q9D6lgpzkfh7whM6/Wj/V0Lzz6Q//QQ/E9+u3qu/HXl702Q7U4s6/q2//Bk3vUyrISe7nAU/o/Kv1Uw3NO5/+8B/0QHy/fqv6fuzlRZ/tQC3u/Lv69m/Q9D6lgpzkfh7whM6/Wj/V0Lzz6Q//QQ/E9+u3qu/HXl702Q7U4s6/q2//Bk3vUyrISe7nAV+vrl/RhjzZT/7G6e70r9/Qg5/epY7Pfg7/+NzYU63e7V/6qv+W/l5/7p09yfLv6I0v2FFv7G+c7k7/+g09+Old6vjs5/CPz4091erd/qWvaunv9efe2ZMs/47e+IId9cb+xunu9K/f0IOf3qWOz34O//jc2FOt3u1f+qqW/l5/7p09yfLv6I0v2FFv7G+c7k7/+g09+Old6vjs5/CPz4091erd/qWvaunv9efe2ZMs/47e+IId9cb+xunu9K/f0IOf3qWOz34O//jc2FOt3u1f+qqW/l5/7p09ySGtxY3qyueu5OZP4vcdpc9N5el+3+79/TuO9k6varLK3Vhvt3ub+xSu81U/2XDP+8S+5CZe8h9u78Ifpc9N5el+3+79/TuO9k7/r2qyyt1Yb7d7m/sUrvNVP9lwz/vEvuQmXvIfbu/CH6XPTeXpft/u/f07jvZOr2qyyt1Yb7d7m/sUrvNVP9lwz/vEvuQmXvIfbu/CH6XPTeXpft/u/f07jvZOr2qyyt1Yb7d7m/sUrvNVP9lwz/vEvuQmXvIfbu/CH6XPTeXpft/u/f07jvZOr2qyyt1Yb7d7m/sUrvNVj/eKfu7GjOx7H5283+tCG/4fDf65mv4oD+uf7PuYDqOaTu+EXb3vn8KBWf+wvvr2/+uAvt+iXvPvT9AyXPSP7f9BH9I/DuDMT8jlj/Uv3ea2T/3x7tkXWcwyi9on/dK1XvThTnquHKiP/8zkzej/QR/SPw7gzE/I5Y/1L93mtk/98e7ZF1nMMovaJ/3StV704U56rhyoj8zkzej/QR/SPw7gzE/I5Y/1L93mtk/98e7ZF1nMMovaJ/3StV704U56rhyoj8zkzej/QR/SPw7gzE/I5Y/1L93mtk/98e7ZF1nMMovayY74i+3hO1/fro7xQIzrqb7kQW7052+bpY37iXrrwrvirWbeHx70IT3Egoz0Ro7rtn3acl7aYY/5ua/o+x3vgIvPzmn7X47c22/QaI3qMk3Qubzj48/KMmvSnR3wAc/NMMrATL7d5F/f2W7Q+37WEm77X47c22/QaI3qMk3Qubzj48/KMv9r0p0d8AHPzTDKwEy+3eRf39lu0Pt+1hJu+1+O3Ntv0GiN6jJN0Lm84+PPyjJr0p0d8AHPzTDKwEy+3eRf39lu0Pt+1hJu+1+O3Ntv0GiN6jJN0Lm84+PPyjJr0nof9N68/vXp+Lq8+oQs7knP73a88Xfc9+a92UXe+RVd+JOs3oU/ydDc8kf/36l//oRsr50/2ZzP9YJf8KR963I/n2rd9Qqv/nI++bt/31FK7xW+4vzr59xs4cs++Ok96hjfyZeP/Qi/78e92Bt/x31v3ptd5J1f0YU/yepd+JMMzS1/9P+d+udPyPba+ZPN+Vwv+AVP2rcu9/Op1l2v8Oov55P/v/v3HaX0XuErzr9+zs0WvuyDn96jjvGdfPnYj/D7ftyLvfF33PfmvdlF3vkVXfiTrN6FP8nQ3PJH3+6oX86hX87ODe2uvuMBn8GBWek8TPKTrNQGff2efZHIvfYi/cRXj9w7z/763/mAK9s5f4+ivntif+wIGpiVzsMkP8lKbdDX79kXidxrL9JPfPXIvfPsr/+dD7iynfP3KOq7J/bHjqCBWek8TPKTrNQGff2efZHIvfYi/cRXj9w7z/763/mAK9s5f4+ivntif+wIGpiVzsMkP8lKbdDX79kXidxrL9JPfPXIvfPsr/+dD7iynfP3KOq7J/bHjqCBWek8TPKT/6zUBn39nn2RyL32Iv3EV4/cO8/++v/fPiv5Y875Rc7ZOL7Zhp756g/3zlnr4r7z6U/K+57C4d71661qsirbKt/T7eztH233Bh2nXW/2Ko/V293TfZ77U43Pgxz1Bt33tez4At3QrX+/fqv6+67juezx2f/4cdr1Zq/yWL3dPd3nuT/V+DzIUW/QfV/Lji/QDd369+u3qr/vOp7LHp/9jx+nXW/2Ko/V293TfZ77U43Pgxz1Bt33tez4At3QrX+/fqv6+67juezx2f/4cdr1Zq/yWL3dPd3nuT/V+DzIUW/QfV/Lji/QDd369+u3qr/vOi7D/C7Y4czeVt/rk+znk/985LZuyOmt6Kxv4hif/PFf/0HPwMjf0yYP8ftf3TpO1WUN8o/Mw/g/yX4+yUdu64ac3orO+iaO8ckf//Uf9AyM/D1t8hC//9Wt41Rd1iD/yDyM/5Ps55N85LZuyOmt6Kxv4hif/PFf/0HPwMjf0yYP8ftf3TpO1WUN8o/Mw/g/yX4+yUdu64ac3orO+iaO8ckf//Uf9AyM/D1t8hC//9Wt41Rd1iD/yDyM/5Ps55N85LZuyOmt6Kxv4hif/PFf/0HPwMjf0yYP8ftf3a5v+ImPtVVf8kJfwt+P/rgO6CUv9KaP6qW872e95Ep/1vcu0HAL7BRf6z/OzRZ+vGpfoa7/TK7jX9XMj/bAL/3/DfMZf+wJG+0mxr9HD9b5js7IT++sPNCwZujhjs/cjfW83+7o//2TbfH4b/BCb/qoXsr7ftZLrvRnfe8CDbfATvG1/uPcbOHHq/YV6srkOv5VzfxoD/zS/98wn/HHnrDRbmL8e/Rgne/ojPz0zsoDDWuGHu74zN1Yz/vtjv7fP9kWj/8GL/Smj+qlvO9nveRKf9b3LtBwC+wUX+s/zs1Ib+ENb8yY7+FwXp8GTd/7bvaEH+hYvdWnfeqTHO4xvfFH/r01UPZbnvxD3Khaj9YN7/UTLff1adD0ve9mT/iBjtVbfdqnPsnhHtMbf+TfWwNlv+XJ/z/Ejar1aN3wXj/Rcl+fBk3f+272hB/oWL3Vp33qkxzuMb3xR/69NVD2W578Q9yoWo/WDe/1Ey339WnQ9L3vZk/4gY7VW33apz7J4R7TG3/k31sDZb/lyT/Ejar1aN3wXj/Rcl+fBk3f+272hB/oWL3Vp33qkxzuMb3xR/69NVD2W578Q4zOT2zhgQr3vH/i9j/1LU/OX//kjg/+85r3m67e7T/oNAiwzs3kJkb7zn/sjm7Iho7/hPz+AMvkhnzCBD3+T+744D+veb/p6t3+g06DAOvcTG5itO/8x+7ohmzo+E/I7w+wTG7IJ0zQ4//kjg/+85r3m67e7T/oNAiwzv/N5CZG+85/7I5uyIaO/4T8/gDL5IZ8wgQ9/k/u+OA/r3m/6erd/oNOgwDr3ExuYrTv/Mfu6IZs6PhPyO8PsExuyCdM0OP/5I4P/vOa95uu3u0/6DQIsM7N5CZG+85/7F7u8j2d7nAr0s/MzwBe/wTt9X5b9uzt5XbMz7Yp7jvv/1Re6yD/5I4P/k+O6VdPep/c50g/6KD/60K/3VOd9wof8+zt5XbMz7Yp7jvv/1Re6yD/5I4P/k+O6VdPep/c50g/6KD/60K/3VOd9wof8+zt5XbMz7Yp7jvv/1Re6yD/5I4P/k+O6VdPep/c50g/6KD/60K/3VOd9wof8+zt5Xb/zM+2Ke477/9UXusg/+SOD/5PjulXT3qf3OdIP+ig/+tCv91TnfcKH/Ps7eV2zM+2Ke477/9UXusg/+SOD/5PjunIjqT1GuzEX9o1XfgAbuGG/u0Gvf5Xz93vn/fLfPfNbP7HvuKSL/7FrN6R7vQLj6T1GuzEX9o1XfgAbuGG/u0Gvf5Xz93vn/fLfPfNbP7HvuKSL/7FrN6R7vQLj6T1GuzEX9o1XfgAbuGG/u0Gvf5Xz93vn/fLfPfNbP7HvuKSL/7FrN6R7vQLj6T1GuzEX9o1XfgAbuGG/u0Gvf5Xz93vn/fLfPfNbP7HvuKSL/7FrN6R7vQLj6T1GuzEX9o1/134AG7hhv7tBr3+V8/d75/3y3z3zWz+x77iki/+xazeke70r//8pA/19Q/wf//Foh7WmM78lI/7jh7pH5/+9j/1mk7+q578YL7+V2/vM/z4z0/6UF//AP/3XyzqYY3pzE/5uO/okf7x6W//U6/p5L/qyQ/m63/19j7Dj//8pA/19Q/wf//Foh7WmM78lI/7jh7pH5/+9j/1mk7+q578YL7+V2/vM/z4z0/6UF//AP/3XyzqYY3pzE/5uO/okf7x6W//U6/p5L/qyQ/m63/19j7Dj//8pA/19Q/wf//Foh7WmM78lI/7jh7pH5/+9j/1mk7+q578YL7+V2/v6T7SZ/+Pz59s0BQO7PXPvzuf/vCPjKD98FMN86R8/oR+1jJY93+u/XBb65Of+qiN6qV8/ZO9e4PM2Jra9VFv/rl661Eq4FHq9Cp//oR+1jJY93+u/XBb65Of+qiN6qV8/ZO9e4PM2Jra9VFv/rl661Eq4FHq9Cp//oR+1jJY93+u/XBb65Of+qiN6qV8/ZO9e4PM2Jra9VFv/rl661Eq4FHq9Cp//oR+1jJY93+u/XBb65Of+qiN6qV8/ZO9e4PM2Jra9VFv/rl661Eq4FHq9Cp//oR+1jJY93+u/XBb65Of+sRt26xv0BUK56Yf87jO/8e+8J1dz4++e5K/9ul+/kKL1pP/vXvub7cyjduKnv+G/uhynuoSXvA7f/SnP+9MjuydXc+PvnuSv/bpfv5Ci9aTvXvub7cyjduKnv+G/uhynuoSXvA7f/SnP+9MjuydXc+PvnuSv/bpfv5Ci9aTvXvub7cyjduKnv+G/uhynuoSXvA7f/SnP+9MjuydXc+PvnuSv/bpfv5Ci9aTvXvub7cyjduKnv+G/uhynuoSXvA7f/SnP+9MjuydXc+PvnuSv/bpfv5Ci9aTvXvub7cyjduKnv+GTtCEDOgVbWKu7OvS7+hCHOwTTfpff/spnPl+m/fy/98L7dsV2vLhLMTQXPb6zv1m3fir5sq+Lv2OLsTBPtGk///1t5/Cme+3eS///73Qvl2hLR/OQgzNZa/v3G/Wjb9qruzr0u/oQhzsE036X3/7KZz5fpv38v/fC+3bFdry4SzE0Fz2+s79Zt34q+bKvi79ji7EwT7RpP/1t5/Cme+3eS///73Qvl2hLR/OQgzNZa/v3G/Wjb9qruzr0u/oQhzsE036X3/7KZz5fpv38v/fC+3bFdry4SzE0Fz2mH/1eX/WVQ2j0bnzZX/RrW/IPe/xbJ//zb/vALvkOY74al/sqXX6GZz875/mMiiz+vz1nD/R/w3zuYzq7q/UM/z9QA7m9T7ShV/Pj+7cYW37X2zkjf/hVB5Ukh/ut+7ogh9UMv+t4/BfzN4M9Nvt9Imqt8ie9fEOuHD+6YSd0mEu9ndP+fB/3EC//zG9/5u+82V/0a1vyD3v8Wyf/82/7wC75DmO+Gpf7Kl1+hmc/O+f5jIos/r89Zw/0f8N87mM6u6v1DP8/UAO5vU+0oVfz4/u3GFt+19s5I3/4WHO+4Hu/aQP/nK+2agulAHOymsv/GAd/O8vs/cdpcVe2uN/+w8P9ELcydlv4b7+/mMO7bcO/nK+2agulAHOymsv/GAd/O8vs/cdpcVe2uN/+w8P9ELcydlv4b7+/mMO7bcO/nK+2agulAHOymsv/GAd/O8vs/cdpcVe2uN/+w8P9ELcydlv4b7//v5jDu23Dv5yvtmoLpQBzsprL/xgHfzvL7P3HaXFXtrjf/sPD/RC3MnZb+G+/v5jDu23Dv5yvtmoLpQBzsprL/xgHfzvL7P3HaXFXtrjf/sPD/RC3MnZb+GBCu2r7/XJLsR7ftZsf9pYS+9uTvzRbvqvHc6ifuwJy+qdfOzrvswV7u5w/8m2b+ulj/3FL+F9DqMwP+9YS+9uTvzRbvqvHc6ifuwJy+qdfOzrvswV7u5w/8m2b+ulj/3FL+F9DqMwP+9YS+9uTvzRbvqvHc6ifuwJy+qdfOzrvswV7u5w/8m2b+ulj/3FL+F9DqMwP+9YS+9uTvzRbvqvHc6ifuwJ/8vqnXzs677MFe7ucP/Jtm/rpY/9xS/hfQ6jMD/vWEvvbk780W76rx3Oon7sCcvqnXzs677MFe7ucP/J7ZzeFbrM956oTr3f/Iz7wgvfp7731E3zBn3ECD/Vwn3fvV6vj93JYf29uP7h1Jr4FU7scg7yof/4PS3Sdy+0Tp/z0f74NF/rqdXr9eznk8y3SZ7+hPvbch/poKu3/NzMZszeVr/usB+lU02tMJ/xx/76NF/rqdXr9eznk8y3SZ7+hPvbch/poKu3/NzMZszeVr/usB+lU02tMJ/xx/76NF/rqdXr9eznk8y3SZ7+hPvbch/poKu3/NzMZszeVr/usB+lU/9NrTCf8cf++jRf66nV6/Xs55PMt0me/oT723If6fEN8Ek96DqO+1BP8WZt5IU/yaCf746+/7Lt6PVZ/HEL/EJv+jvOzduN1n+u8EUO/ugN4BY+2Xcf8Lpe90XO2enfzPnu6Psv245en8Uft8Av9Ka/49y83Wj95wpf5OCP3gBu4ZN99wGv63Vf5Jyd/s2c746+/7Lt6PVZ/HEL/EJv+jvOzduN1n+u8EUO/ugN4BY+2Xcf8Lpe90XO2enfzPnu6Psv245en8Uft8Av9Ka/49y83Wj95wpf5OCP3gBu4ZN99wGv63Vf5Jyd/s2c746+/7Lt6PVZ/HEL/EJv+jvOzdv/jdZ/Lv+JyrcwmvrVe5HmffjEvrug/+sf7v8A6+bLjvNCgNzfDtTUvu+Mre+sbN4XKehpjdx5H+7mffjEvrug/+sf7v8A6+bLjvNCgNzfDtTUvu+Mre+sbN4XKehpjdx5H+7mffjEvrug/+sf7v8A6+bLjvNCgNzfDtTUvu+Mre+sbN4XKehpjdx5H+7mffjEvrug/+sf7v8A6+bLjvNCgNzfDtTUvu+Mre+sbN4XKehpjdx5H+7mffjEvrug/+sf7v8A6+bLjvNCgNzfDtTUvu+Mre+sLLPFPvnhzvu6zPu9nss7Pu9P6tS+zup+bvBFn/jQzt22/eeDzJvCvdj7/8/ATwrFKO+zrZvInL/jWR/p0U+DTu3rrO7nBl/0iQ/t3G3bfz7IvCnci73/DPykUIzyPtu6icz5O571kR79NOjUvs7qfm7wRZ/40M7dtv3ng8ybwr3Y+8/ATwrFKO+zrZvInL/jWR/p0U+DTu3rrO7nBl/0iQ/t3G3bfz7IvCnci73/DPykUIzyPtu6icz5O571kR79NOjUvs7qfm7wRZ/40M7dtv3ng8ybwr3Y+8/AT2oDKdz1fAvXKez+4TzonTz46X39fz7IoO/7b+z4YC387U6ue+/8MqjeiL/YkVrNpx/wuRyoUjuvro7xnTz46X39fz7IoO/7b+z4YC387f9Ornvv/DKo3oi/2JFazacf8LkcqFI7r66O8Z08+Ol9/X8+yKDv+2/s+GAt/O1Ornvv/DKo3oi/2JFazacf8LkcqFI7r66O8Z08+Ol9/X8+yKDv+2/s+GAt/O1Ornvv/DKo3oi/2JFazacf8LkcqFI7r66O8Z08+Ol9/X8+yKDv+2/s+GAt/O1Ornvv/DKo3oi/2IJv4qdd82D+/ITv5fz/5GXN/85t8Z/8vYbs7YMOxShv3mo9yO6fwuzf65RvzBRv6/sP+Xpb6XJu35yv4Wtf/Bb/yd9ryN4+6FCM8uat1oPs/inM/r1O+cZM8ba+/5Cvt5Uu5/bN+Rq+9sVv8Z///L2G7O2DDsUob95qPcjun8Ls3+uUb8wUb+v7D/l6W+lybt+cr+FrX/wW/8nfa8jePuhQjPLmrdaD7P4pzP69TvnGTPG2vv+Qr7eVLuf2zfkavvbFb/Gf/L2G7O2DDsUob95qPcjun8Ls3+uUb8zfT64BP+81D+ZwC9is7/swXtXML8Os7vqoz95WL9DV6/I0n+vHDbfAXumtb8iz7/To3P0yaO++D9is7/swXtXML8Os7vqoz95WL9DV6/I0n+vHDbfAXumtb8iz7/To3P0yaO++D9is7/swXtXML8Os7vqoz95WL9DV6/I0n+vHDbfAXumtb8iz7/To3P0yaO++/w/YrO/7MF7VzC/DrO76qM/eVi/Q1evyNJ/rxw23wF7prW/Is+/06Nz9Mmjvvg/YrO/7MF7VzC/DrO76qM/eVi/Q1evyNJ/rxw23wF7prW/Is3/EBQ/7Kk/f9D3qxM//cB+dKQzFhDupv63LyA3NVF7wm772Zmzh6L/V6e30YK30sq3y9E3fo078/A/30ZnCUEy4k/rbuozc0EzlBb/pa2/GFo7+W53eTg/WSi/bKk/f9D3qxM//cB+dKQzFhDupv63LyA3NVF7wm772Zmzh6L/V6e30YK30sq3y9E3fo078/A/30ZnCUEy4k/rbuozc0EzlBb/pa2/GFo7+W53eTv8P1kov2ypP3/Q96sTP/3AfnSkMxYQ7qb+ty8gNzVRe8Ju+9mZs4ei/1ent9GD9vezdqATN87q+8+F+5AhP4eZ/7O2e/wV9kbDf69xs+p7t2yZN6K6P/h/N2eEu+f6f/PZd1RRv4njv/br82yc+yTJL2Z1dv7Df69xs+p7t2yZN6K6P/h/N2eEu+f6f/PZd1RRv4njv/br82yc+yTJL2Z1dv7Df69xs+p7t2yZN6K6P/h/N2eEu+f6f/PZd1RRv4njv/br82yc+yTJL2Z1dv7Df69xs+p7t2yZN6K6P/h/N2eEu+f6f/PZd1RRv4njv/br82yc+yTJL2Z1dv7Df69z/bPqe7dsmTeiuj/4fzdnhLvl+jqSEK+g9Hp07r/uV/uQFr/v6zsP438zb7PacLdPBftK9fetff/9b3fDNPPWw36igzcAyuPO6X+lPXvC6r+88jP/NvM1uz9kyHewn3du3/vX3v9UN38xTD/uNCtoMLIM7r/uV/uQFr/v6zsP438zb7PacLdPBftK9fetff/9b3fDNPPWw36igzcAyuPO6X+lPXvC6r+88jP/NvM1uz9kyHewn3du3/vX3v9UN38xTD/uNCtoMLIM7r/uV/uQFr/v6zsP438zb7PacLdPBftK9fetff/9b3fA8D/9+jqSEm/chbd/ZP/0gj/zWv3vy/9+o2u/QAI//Wf/9sk/Igi2rYa7pQM/8uXza996okB/1ZV30xbzMzg33sq35+nvP1fv+5j3uHx/OiVzz9tqo327QQM/8uXza996okB/1ZV30xbzMzg33sq35+nvP1fv+5j3uHx/OiVzz9tqo327QQM/8uXza996okB/1ZV30xbzMzg33sq35+nvP1fv+5j3uHx/OiVzz9tqo327QQM/8uXza996okB/1ZV30xbzMzg33sq35+nvP1fv+5j3uHx/OiVzz9tqo327QQP/49K3oPY/357/3D7/+0I/zDe/1Dt+Me2v6XG/6sz/S9Y/jYE7ZhLzZSmvsJp7ez737QA/XdfSf/4Db2/I/6xM/61P633tr+lxv+rM/0vWP42BO2YS82Upr7Cae3s+9+0AP13Wf/4Db2/I/6xM/61P633tr+lxv+rM/0vWP42BO2YS82Upr7Cae3s+9+0AP13Wf/4Db2/I/6xM/61P633tr+lxv+rM/0vWP42BO2YS82Upr7Cae3s+9+0AP13Wf/4Db2/I/6xM/61P633tr+lxv+rM/0vWP42BO2YS82Upr7CaOcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRiHcRg3AQUAADs="; diff --git a/test/vectors/index.ts b/test/vectors/index.ts deleted file mode 100644 index bb94d19..0000000 --- a/test/vectors/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright 2019 NEM - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - *limitations under the License. - */ -export { ExpectedObjectBase64 } from './OBJECT'; From 991fc7df96aa2f143ca6ca04df1a9a561a0e7ea8 Mon Sep 17 00:00:00 2001 From: Greg S Date: Mon, 27 May 2019 21:27:08 +0200 Subject: [PATCH 12/13] nemtech/NIP#21 : fixed EncryptionService encrypt() and decrypt(), added unit tests for encryption --- src/services/EncryptionService.ts | 33 +++++++++++++++---------------- test/EncryptionService.spec.ts | 24 ++++++++++++++++++---- 2 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/services/EncryptionService.ts b/src/services/EncryptionService.ts index 048f4fb..0612b0a 100644 --- a/src/services/EncryptionService.ts +++ b/src/services/EncryptionService.ts @@ -66,18 +66,16 @@ export class EncryptionService { // create encryption input vector of 16 bytes (iv) const iv = CryptoJS.lib.WordArray.random(16); - // format IV for crypto-js encryption - const encIv = { - iv: iv - }; - // encrypt with AES - const dataBin = CryptoJS.lib.WordArray.create(data); - const encrypted = CryptoJS.AES.encrypt(dataBin, key, encIv); + const encrypted = CryptoJS.AES.encrypt(data, key, { + iv: iv, + padding: CryptoJS.pad.Pkcs7, + mode: CryptoJS.mode.CBC + }); - // create our `EncryptedPayload` + // create our `EncryptedPayload` (16 bytes iv as hex || cipher text) const ciphertext = iv.toString() + encrypted.toString(); - const used_salt = salt.toString(); + const used_salt = CryptoJS.enc.Hex.stringify(salt); return new EncryptedPayload(ciphertext, used_salt); } @@ -107,12 +105,8 @@ export class EncryptionService { const priv = payload.ciphertext; // read encryption configuration - const iv: string = CryptoJS.enc.Hex.parse(priv.substring(0, 32)); - const cipher: string = priv.substring(32, 96); - - const encIv = { - iv: iv - }; + const iv: string = CryptoJS.enc.Hex.parse(priv.substr(0, 32)); + const cipher: string = priv.substr(32); // re-generate key (PBKDF2) const key = CryptoJS.PBKDF2(password.value, salt, { @@ -121,7 +115,12 @@ export class EncryptionService { }); // decrypt and return - const decrypted = CryptoJS.AES.decrypt(cipher, key, encIv); - return decrypted.toString(); + const decrypted = CryptoJS.AES.decrypt(cipher, key, { + iv: iv, + padding: CryptoJS.pad.Pkcs7, + mode: CryptoJS.mode.CBC + }); + + return decrypted.toString(CryptoJS.enc.Utf8); } } \ No newline at end of file diff --git a/test/EncryptionService.spec.ts b/test/EncryptionService.spec.ts index 95fce30..615e19e 100644 --- a/test/EncryptionService.spec.ts +++ b/test/EncryptionService.spec.ts @@ -34,7 +34,7 @@ describe('EncryptionService -->', () => { // Act const encrypted = EncryptionService.encrypt(data, new Password(pass)); - // Asset + // Assert expect(encrypted.ciphertext).to.not.be.undefined; expect(encrypted.salt).to.not.be.undefined; expect(encrypted.salt).to.have.lengthOf(64); @@ -48,8 +48,8 @@ describe('EncryptionService -->', () => { // Act const encrypted = EncryptionService.encrypt(data, new Password(pass)); - // Asset - expect(encrypted.ciphertext).to.have.lengthOf(160); + // Assert + expect(encrypted.ciphertext).to.have.lengthOf(76); expect(encrypted.salt).to.have.lengthOf(64); }); @@ -63,7 +63,7 @@ describe('EncryptionService -->', () => { const encrypted_2 = EncryptionService.encrypt(data, new Password(pass)); const encrypted_3 = EncryptionService.encrypt(data, new Password(pass)); - // Asset + // Assert expect(encrypted_1).to.not.be.equal(encrypted_2); expect(encrypted_1).to.not.be.equal(encrypted_3); expect(encrypted_2).to.not.be.equal(encrypted_3); @@ -73,4 +73,20 @@ describe('EncryptionService -->', () => { }); }); + describe('decrypt() should', () => { + + it('should decrypt ciphertext correctly', () => { + // Arrange: + const data = 'this will be encrypted'; + const pass = 'password'; + + // Act + const encrypted = EncryptionService.encrypt(data, new Password(pass)); + const decrypted = EncryptionService.decrypt(encrypted, new Password(pass)); + + // Assert + expect(decrypted).to.be.equal(data); + }); + }); + }); From 70354f7536b731adad4edd694e5952772a07fc5e Mon Sep 17 00:00:00 2001 From: Greg S Date: Mon, 27 May 2019 21:28:23 +0200 Subject: [PATCH 13/13] nemtech/NIP#21 : fixed AccountQR and QRCodeGenerator.fromJSON for AccountQR --- CHANGELOG.md | 28 +++++++++++++++++++++----- README.md | 1 + src/schemas/ExportAccountDataSchema.ts | 4 ---- test/QRCodeGenerator.spec.ts | 4 ++-- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cbb66a..9813198 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,32 @@ # CHANGELOG -## v0.1.0 +## v0.3.0 -- added qr-library. -- generate QRcode by string. -- generate image base64 string by string. +- Added class `EncryptedPayload` +- Added class `EncryptionService` +- Added data schemas structure with `QRCodeDataSchema` +- Added data schema `AddContactDataSchema` +- Added data schema `ExportAccountDataSchema` +- Added data schema `RequestCosignatureDataSchema` child of Transaction data schema +- Added data schema `RequestTransactionDataSchema` child of Transaction data schema +- Unit tests for AccountQR, ContactQR, TransactionQR, CosignatureQR +- Modified QRCode.toJSON() logic to make use of `build()` method +- Fixed `AccountQR` generation of encrypted private keys for accounts +- Fixed `ContactQR` to also hold `name` information (optional) +- Fixed QR Code `TypeNumber`: ContactQR uses type 10, AccountQR type 20, TransactionQR type 40. +- Removed class `QRService` +- Removed encryption from `QRService` in `AccountQR` ## v0.2.0 + - added QRcode base class - added AccountQR Class - added ContactQR Class - added TransactionQR Class -- added ObjectQR Class \ No newline at end of file +- added ObjectQR Class + +## v0.1.0 + +- added qr-library. +- generate QRcode by string. +- generate image base64 string by string. diff --git a/README.md b/README.md index beda9b1..94fd806 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ The produced Base64 encoded payload can be used to display the QR Code. An examp Important versions listed below. Refer to the [Changelog](CHANGELOG.md) for a full history of the project. +- [0.3.0](CHANGELOG.md) - 2019-05-27 - [0.2.0](CHANGELOG.md) - 2019-05-01 - [0.1.0](CHANGELOG.md) - 2019-04-20 diff --git a/src/schemas/ExportAccountDataSchema.ts b/src/schemas/ExportAccountDataSchema.ts index c03e4e7..a82bdc4 100644 --- a/src/schemas/ExportAccountDataSchema.ts +++ b/src/schemas/ExportAccountDataSchema.ts @@ -84,16 +84,12 @@ export class ExportAccountDataSchema extends QRCodeDataSchema { throw Error('Invalid type field value for AccountQR.'); } - console.log("JSON: ", jsonObj); - // decrypt private key const payload = new EncryptedPayload(jsonObj.data.ciphertext, jsonObj.data.salt); const privKey = EncryptionService.decrypt(payload, password); const network = jsonObj.network_id; const chainId = jsonObj.chain_id; - console.log("data: ", privKey); - // create account const account = Account.createFromPrivateKey(privKey, network); return new AccountQR(account, password, network, chainId); diff --git a/test/QRCodeGenerator.spec.ts b/test/QRCodeGenerator.spec.ts index cd9b1a6..ba1c028 100644 --- a/test/QRCodeGenerator.spec.ts +++ b/test/QRCodeGenerator.spec.ts @@ -187,7 +187,7 @@ describe('QRCodeGenerator -->', () => { expect(contactObj.account.address).to.deep.equal(account.address); expect(contactObj.type).to.deep.equal(QRCodeType.AddContact); }); -/* + it('Read data From AccountQR', () => { // Arrange: const account = Account.createFromPrivateKey( @@ -207,7 +207,7 @@ describe('QRCodeGenerator -->', () => { expect(accountObj.account).to.deep.equal(account); expect(accountObj.type).to.deep.equal(QRCodeType.ExportAccount); }); -*/ + it('Read data From ObjectQR', () => {}); });