diff --git a/src/model/account/PublicAccount.ts b/src/model/account/PublicAccount.ts index c23eeb028f..3b9ea64166 100644 --- a/src/model/account/PublicAccount.ts +++ b/src/model/account/PublicAccount.ts @@ -14,8 +14,9 @@ * limitations under the License. */ -import {NetworkType} from '../blockchain/NetworkType'; -import {Address} from './Address'; +import { KeyPair, convert } from 'nem2-library'; +import { NetworkType } from '../blockchain/NetworkType'; +import { Address } from './Address'; /** * The public account structure contains account's address and public key. @@ -53,6 +54,44 @@ export class PublicAccount { return new PublicAccount(publicKey, address); } + /** + * Verify a signature. + * + * @param {PublicAccount} publicAccount - The public account to use for verification. + * @param {string} data - The data to verify. + * @param {string} signature - The signature to verify. + * + * @return {boolean} - True if the signature is valid, false otherwise. + */ + static verifySignature(publicAccount: PublicAccount, data: string, signature: string): boolean { + if (!publicAccount || !data || !signature) { + throw new Error('Missing argument'); + } + + if (signature.length !== 128) { + throw new Error('Signature length is incorrect'); + } + + if (!convert.isHexString(signature)) { + throw new Error('Signature must be hexadecimal only'); + } + + // Convert signature key to Uint8Array + const convertedSignature = convert.hexToUint8(signature); + + let convertedData; + + // Convert data to hex if data is not hex + if (!convert.isHexString(data)) { + convertedData = convert.utf8ToHex(data); + } + + // Convert to Uint8Array + convertedData = convert.hexToUint8(convertedData); + + return KeyPair.verify(publicAccount.publicKey, convertedData, convertedSignature); + } + /** * Compares public accounts for equality. * @param publicAccount @@ -61,4 +100,5 @@ export class PublicAccount { equals(publicAccount: PublicAccount) { return this.publicKey === publicAccount.publicKey && this.address.plain() === publicAccount.address.plain(); } + } diff --git a/test/model/account/PublicAccount.spec.ts b/test/model/account/PublicAccount.spec.ts index 9b7ddd3818..86e9006883 100644 --- a/test/model/account/PublicAccount.spec.ts +++ b/test/model/account/PublicAccount.spec.ts @@ -14,9 +14,9 @@ * limitations under the License. */ -import {expect} from 'chai'; -import {PublicAccount} from '../../../src/model/account/PublicAccount'; -import {NetworkType} from '../../../src/model/blockchain/NetworkType'; +import { expect } from 'chai'; +import { PublicAccount } from '../../../src/model/account/PublicAccount'; +import { NetworkType } from '../../../src/model/blockchain/NetworkType'; describe('PublicAccount', () => { const publicKey = 'b4f12e7c9f6946091e2cb8b6d3a12b50d17ccbbf646386ea27ce2946a7423dcf'; @@ -27,3 +27,83 @@ describe('PublicAccount', () => { expect(publicAccount.address.plain()).to.be.equal('SARNASAS2BIAB6LMFA3FPMGBPGIJGK6IJETM3ZSP'); }); }); + +describe('Signature verification', () => { + it('Can verify a signature', () => { + // Arrange:' + const signerPublicAccount = PublicAccount.createFromPublicKey('22816F825B4CACEA334723D51297D8582332D8B875A5829908AAE85831ABB508', + NetworkType.MIJIN_TEST); + const data = 'I am so so so awesome as always'; + const signature = 'B01DCA6484026C2ECDF3C822E64DEAAFC15EBCCE337EEE209C28513CB5351CDED8863A8E7B855CD471B55C91FAE611C548625C9A5916A555A24F72F3526FA508'; // tslint:disable-line + + // Act & Assert: + expect(PublicAccount.verifySignature(signerPublicAccount, data, signature)).equal(true); + }); + + it('Throw error if signature has invalid length', () => { + // Arrange: + const signerPublicAccount = PublicAccount.createFromPublicKey('22816F825B4CACEA334723D51297D8582332D8B875A5829908AAE85831ABB508', + NetworkType.MIJIN_TEST); + const data = 'I am so so so awesome as always'; + const signature = 'B01DCA6484026C2ECDF3C822E64DEAAFC15EBCCE337EEE209C28513CB5351CDED8863A8E7B855CD471B55C91FAE611C5486'; // tslint:disable-line + + // Act & Assert: + expect(() => { PublicAccount.verifySignature(signerPublicAccount, data, signature); }).to.throw('Signature length is incorrect'); + }); + + it('Throw error if signature is not strictly hexadecimal', () => { + // Arrange: + const signerPublicAccount = PublicAccount.createFromPublicKey('22816F825B4CACEA334723D51297D8582332D8B875A5829908AAE85831ABB508', + NetworkType.MIJIN_TEST); + const data = 'I am so so so awesome as always'; + const signature = 'B01DCA6484026C2ECDF3C822E64DEAAFC15EBCCE337EEE209C28513CB5351CDED8863A8E7B855CD471B55C91FAE611C548625C9A5916A555A24F72F35a1wwwww';// tslint:disable-line + + // Act & Assert: + expect(() => { PublicAccount.verifySignature(signerPublicAccount, data, signature); }) + .to.throw('Signature must be hexadecimal only'); + }); + + it('Return false if wrong public key provided', () => { + // Arrange: + const signerPublicAccount = PublicAccount.createFromPublicKey('12816F825B4CACEA334723D51297D8582332D8B875A5829908AAE85831ABB509', + NetworkType.MIJIN_TEST); + const data = 'I am so so so awesome as always'; + const signature = 'B01DCA6484026C2ECDF3C822E64DEAAFC15EBCCE337EEE209C28513CB5351CDED8863A8E7B855CD471B55C91FAE611C548625C9A5916A555A24F72F3526FA508';// tslint:disable-line + + // Act & Assert: + expect(PublicAccount.verifySignature(signerPublicAccount, data, signature)).equal(false); + }); + + it('Return false if data is not corresponding to signature provided', () => { + // Arrange: + const signerPublicAccount = PublicAccount.createFromPublicKey('22816F825B4CACEA334723D51297D8582332D8B875A5829908AAE85831ABB508', + NetworkType.MIJIN_TEST); + const data = 'I am awesome as always'; + const signature = 'B01DCA6484026C2ECDF3C822E64DEAAFC15EBCCE337EEE209C28513CB5351CDED8863A8E7B855CD471B55C91FAE611C548625C9A5916A555A24F72F3526FA508';// tslint:disable-line + + // Act & Assert: + expect(PublicAccount.verifySignature(signerPublicAccount, data, signature)).equal(false); + }); + + it('Return false if signature is not corresponding to data provided', () => { + // Arrange: + const signerPublicAccount = PublicAccount.createFromPublicKey('22816F825B4CACEA334723D51297D8582332D8B875A5829908AAE85831ABB508', + NetworkType.MIJIN_TEST); + const data = 'I am so so so awesome as always'; + const signature = 'A01DCA6484026C2ECDF3C822E64DEAAFC15EBCCE337EEE209C28513CB5351CDED8863A8E7B855CD471B55C91FAE611C548625C9A5916A555A24F72F3526FA509';// tslint:disable-line + + // Act & Assert: + expect(PublicAccount.verifySignature(signerPublicAccount, data, signature)).equal(false); + }); + + it('Throw error if signature verification is missing a parameter', () => { + // Arrange: + const signerPublicAccount = PublicAccount.createFromPublicKey('22816F825B4CACEA334723D51297D8582332D8B875A5829908AAE85831ABB508', + NetworkType.MIJIN_TEST); + const data = ''; + const signature = 'B01DCA6484026C2ECDF3C822E64DEAAFC15EBCCE337EEE209C28513CB5351CDED8863A8E7B855CD471B55C91FAE611C548625C9A5916A555A24F72F3526FA508';// tslint:disable-line + + // Act & Assert: + expect(() => { PublicAccount.verifySignature(signerPublicAccount, data, signature); }).to.throw('Missing argument'); + }); +});