diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f563c5a..aa65f9d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # CHANGELOG +## master + +Contributed: + +- Support signing of raw data via Qr (Thanks to https://github.com/Tbaut) + +Changes: + +- Raw signing interfaces will now always place a `...` wrapper around signed data + + ## 0.38.3 May 31, 2021 Contributed: diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json index 728cd935..7a8f07eb 100644 --- a/packages/extension-base/package.json +++ b/packages/extension-base/package.json @@ -10,6 +10,7 @@ "dependencies": { "@babel/runtime": "^7.14.0", "@polkadot/api": "^4.12.2-6", + "@polkadot/extension-dapp": "^0.38.4-18", "@polkadot/extension-inject": "^0.38.4-18", "@polkadot/keyring": "^6.6.1", "@polkadot/phishing": "^0.6.171", diff --git a/packages/extension-base/src/background/RequestBytesSign.ts b/packages/extension-base/src/background/RequestBytesSign.ts index 850917a2..e0a90614 100644 --- a/packages/extension-base/src/background/RequestBytesSign.ts +++ b/packages/extension-base/src/background/RequestBytesSign.ts @@ -5,8 +5,9 @@ import type { KeyringPair } from '@polkadot/keyring/types'; import type { SignerPayloadRaw } from '@polkadot/types/types'; import type { RequestSign } from './types'; +import { wrapBytes } from '@polkadot/extension-dapp/wrapBytes'; import { TypeRegistry } from '@polkadot/types'; -import { hexToU8a, u8aToHex } from '@polkadot/util'; +import { u8aToHex } from '@polkadot/util'; export default class RequestBytesSign implements RequestSign { public readonly payload: SignerPayloadRaw; @@ -19,7 +20,7 @@ export default class RequestBytesSign implements RequestSign { return { signature: u8aToHex( pair.sign( - hexToU8a(this.payload.data) + wrapBytes(this.payload.data) ) ) }; diff --git a/packages/extension-base/src/utils/canDerive.ts b/packages/extension-base/src/utils/canDerive.ts index ee17be19..5bae4031 100644 --- a/packages/extension-base/src/utils/canDerive.ts +++ b/packages/extension-base/src/utils/canDerive.ts @@ -3,6 +3,6 @@ import type { KeypairType } from '@polkadot/util-crypto/types'; -export default function canDerive (type?: KeypairType): boolean { +export function canDerive (type?: KeypairType): boolean { return !!type && ['ed25519', 'sr25519', 'ecdsa'].includes(type); } diff --git a/packages/extension-base/src/utils/index.ts b/packages/extension-base/src/utils/index.ts index 7bd8f407..a2968738 100644 --- a/packages/extension-base/src/utils/index.ts +++ b/packages/extension-base/src/utils/index.ts @@ -1,4 +1,4 @@ // Copyright 2019-2021 @polkadot/extension authors & contributors // SPDX-License-Identifier: Apache-2.0 -export { default as canDerive } from './canDerive'; +export { canDerive } from './canDerive'; diff --git a/packages/extension-dapp/src/index.ts b/packages/extension-dapp/src/index.ts index 8b856577..65016cc9 100644 --- a/packages/extension-dapp/src/index.ts +++ b/packages/extension-dapp/src/index.ts @@ -8,6 +8,9 @@ import { decodeAddress, encodeAddress } from '@polkadot/util-crypto'; import { documentReadyPromise } from './util'; +// expose utility functions +export { unwrapBytes, wrapBytes } from './wrapBytes'; + // just a helper (otherwise we cast all-over, so shorter and more readable) const win = window as Window & InjectedWindow; diff --git a/packages/extension-dapp/src/wrapBytes.spec.ts b/packages/extension-dapp/src/wrapBytes.spec.ts new file mode 100644 index 00000000..1af4bcd0 --- /dev/null +++ b/packages/extension-dapp/src/wrapBytes.spec.ts @@ -0,0 +1,114 @@ +// Copyright 2019-2021 @polkadot/extension authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { u8aConcat, u8aEq, u8aToString } from '@polkadot/util'; + +import { POSTFIX, PREFIX, unwrapBytes, wrapBytes } from './wrapBytes'; + +const TEST_DATA = 'this is just some random message that we expect to be wrapped along the way'; +const TEST_WRAP_EMPTY = `${u8aToString(PREFIX)}${u8aToString(POSTFIX)}`; +const TEST_WRAP_FULL = `${u8aToString(PREFIX)}${TEST_DATA}${u8aToString(POSTFIX)}`; +const TEST_WARP_HALF_PRE = `${u8aToString(PREFIX)}${TEST_DATA}`; +const TEST_WRAP_HALF_POST = `${TEST_DATA}${u8aToString(POSTFIX)}`; + +describe('wrapBytes', (): void => { + it('wraps empty bytes', (): void => { + expect( + u8aEq( + wrapBytes(new Uint8Array()), + u8aConcat(PREFIX, POSTFIX) + ) + ).toBe(true); + }); + + it('wraps when no wrapping is detected', (): void => { + expect( + u8aToString( + wrapBytes(TEST_DATA) + ) + ).toEqual(TEST_WRAP_FULL); + }); + + it('wraps when only start wrap is detected', (): void => { + expect( + u8aToString( + wrapBytes(TEST_WARP_HALF_PRE) + ) + ).toEqual(`${u8aToString(PREFIX)}${TEST_WARP_HALF_PRE}${u8aToString(POSTFIX)}`); + }); + + it('wraps when only end wrap is detected', (): void => { + expect( + u8aToString( + wrapBytes(TEST_WRAP_HALF_POST) + ) + ).toEqual(`${u8aToString(PREFIX)}${TEST_WRAP_HALF_POST}${u8aToString(POSTFIX)}`); + }); + + it('does not re-wrap when a wrap is already present', (): void => { + expect( + u8aToString( + wrapBytes(TEST_WRAP_FULL) + ) + ).toEqual(TEST_WRAP_FULL); + }); + + it('does not re-wrap when a wrap (empty data) is already present', (): void => { + expect( + u8aToString( + wrapBytes(TEST_WRAP_EMPTY) + ) + ).toEqual(TEST_WRAP_EMPTY); + }); +}); + +describe('unwrapBytes', (): void => { + it('unwraps empty bytes', (): void => { + expect( + u8aEq( + unwrapBytes(new Uint8Array()), + new Uint8Array() + ) + ).toBe(true); + }); + + it('unwraps when no wrapping is detected', (): void => { + expect( + u8aToString( + unwrapBytes(TEST_DATA) + ) + ).toEqual(TEST_DATA); + }); + + it('unwraps when no wrapping is detected (only start)', (): void => { + expect( + u8aToString( + unwrapBytes(TEST_WARP_HALF_PRE) + ) + ).toEqual(TEST_WARP_HALF_PRE); + }); + + it('unwraps when no wrapping is detected (only end)', (): void => { + expect( + u8aToString( + unwrapBytes(TEST_WRAP_HALF_POST) + ) + ).toEqual(TEST_WRAP_HALF_POST); + }); + + it('unwraps when a wrap is present', (): void => { + expect( + u8aToString( + unwrapBytes(TEST_WRAP_FULL) + ) + ).toEqual(TEST_DATA); + }); + + it('unwraps when a an empty wrap is present', (): void => { + expect( + u8aToString( + unwrapBytes(TEST_WRAP_EMPTY) + ) + ).toEqual(''); + }); +}); diff --git a/packages/extension-dapp/src/wrapBytes.ts b/packages/extension-dapp/src/wrapBytes.ts new file mode 100644 index 00000000..7ce606f2 --- /dev/null +++ b/packages/extension-dapp/src/wrapBytes.ts @@ -0,0 +1,31 @@ +// Copyright 2019-2021 @polkadot/extension authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import { u8aConcat, u8aEq, u8aToU8a } from '@polkadot/util'; + +export const PREFIX = u8aToU8a(''); +export const POSTFIX = u8aToU8a(''); + +const WRAP_LEN = PREFIX.length + POSTFIX.length; + +function isWrapped (u8a: Uint8Array): boolean { + return u8a.length >= WRAP_LEN && + u8aEq(u8a.subarray(0, PREFIX.length), PREFIX) && + u8aEq(u8a.slice(-POSTFIX.length), POSTFIX); +} + +export function unwrapBytes (bytes: string | Uint8Array): Uint8Array { + const u8a = u8aToU8a(bytes); + + return isWrapped(u8a) + ? u8a.subarray(PREFIX.length, u8a.length - POSTFIX.length) + : u8a; +} + +export function wrapBytes (bytes: string | Uint8Array): Uint8Array { + const u8a = u8aToU8a(bytes); + + return isWrapped(u8a) + ? u8a + : u8aConcat(PREFIX, u8a, POSTFIX); +} diff --git a/packages/extension-ui/package.json b/packages/extension-ui/package.json index bcc005c8..2f69b92e 100644 --- a/packages/extension-ui/package.json +++ b/packages/extension-ui/package.json @@ -13,6 +13,7 @@ "@fortawesome/free-solid-svg-icons": "^5.15.3", "@fortawesome/react-fontawesome": "^0.1.14", "@polkadot/extension-base": "^0.38.4-18", + "@polkadot/extension-dapp": "^0.38.4-18", "@polkadot/extension-inject": "^0.38.4-18", "@polkadot/hw-ledger": "^6.6.1", "@polkadot/keyring": "^6.6.1", diff --git a/packages/extension-ui/src/Popup/Signing/Qr.tsx b/packages/extension-ui/src/Popup/Signing/Qr.tsx index e67223e4..f592454f 100644 --- a/packages/extension-ui/src/Popup/Signing/Qr.tsx +++ b/packages/extension-ui/src/Popup/Signing/Qr.tsx @@ -6,8 +6,8 @@ import type { ExtrinsicPayload } from '@polkadot/types/interfaces'; import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; +import { wrapBytes } from '@polkadot/extension-dapp/wrapBytes'; import { QrDisplayPayload, QrScanSignature } from '@polkadot/react-qr'; -import { hexToU8a } from '@polkadot/util'; import { Button } from '../../components'; import useTranslation from '../../hooks/useTranslation'; @@ -34,7 +34,7 @@ function Qr ({ address, className, cmd, genesisHash, onSignature, payload }: Pro case CMD_MORTAL: return (payload as ExtrinsicPayload).toU8a(); case CMD_SIGN_MESSAGE: - return hexToU8a(payload as string); + return wrapBytes(payload as string); default: return null; } diff --git a/packages/extension/webpack.shared.cjs b/packages/extension/webpack.shared.cjs index 5d5b4c50..ba825dac 100644 --- a/packages/extension/webpack.shared.cjs +++ b/packages/extension/webpack.shared.cjs @@ -14,6 +14,7 @@ const packages = [ 'extension', 'extension-base', 'extension-chains', + 'extension-dapp', 'extension-inject', 'extension-ui' ]; diff --git a/yarn.lock b/yarn.lock index 787fd531..b3d83b14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2143,6 +2143,7 @@ __metadata: dependencies: "@babel/runtime": ^7.14.0 "@polkadot/api": ^4.12.2-6 + "@polkadot/extension-dapp": ^0.38.4-18 "@polkadot/extension-inject": ^0.38.4-18 "@polkadot/keyring": ^6.6.1 "@polkadot/phishing": ^0.6.171 @@ -2161,7 +2162,7 @@ __metadata: languageName: unknown linkType: soft -"@polkadot/extension-dapp@workspace:packages/extension-dapp": +"@polkadot/extension-dapp@^0.38.4-18, @polkadot/extension-dapp@workspace:packages/extension-dapp": version: 0.0.0-use.local resolution: "@polkadot/extension-dapp@workspace:packages/extension-dapp" dependencies: @@ -2199,6 +2200,7 @@ __metadata: "@fortawesome/free-solid-svg-icons": ^5.15.3 "@fortawesome/react-fontawesome": ^0.1.14 "@polkadot/extension-base": ^0.38.4-18 + "@polkadot/extension-dapp": ^0.38.4-18 "@polkadot/extension-inject": ^0.38.4-18 "@polkadot/hw-ledger": ^6.6.1 "@polkadot/keyring": ^6.6.1