diff --git a/apps/wallet-mobile/ios/Podfile.lock b/apps/wallet-mobile/ios/Podfile.lock index 8efec1c935..4a95e284f1 100644 --- a/apps/wallet-mobile/ios/Podfile.lock +++ b/apps/wallet-mobile/ios/Podfile.lock @@ -354,6 +354,8 @@ PODS: - React-Core - react-native-haskell-shelley (6.0.0-alpha.9): - React + - react-native-message_signing-library (1.0.2): + - React - react-native-mmkv (2.11.0): - MMKV (>= 1.2.13) - React-Core @@ -588,6 +590,7 @@ DEPENDENCIES: - react-native-ble-plx (from `../../../node_modules/react-native-ble-plx`) - react-native-config (from `../../../node_modules/react-native-config`) - "react-native-haskell-shelley (from `../../../node_modules/@emurgo/csl-mobile-bridge`)" + - "react-native-message_signing-library (from `../../../node_modules/@emurgo/msl-mobile-bridge`)" - react-native-mmkv (from `../../../node_modules/react-native-mmkv`) - react-native-pager-view (from `../../../node_modules/react-native-pager-view`) - react-native-quick-base64 (from `../../../node_modules/react-native-quick-base64`) @@ -738,6 +741,8 @@ EXTERNAL SOURCES: :path: "../../../node_modules/react-native-config" react-native-haskell-shelley: :path: "../../../node_modules/@emurgo/csl-mobile-bridge" + react-native-message_signing-library: + :path: "../../../node_modules/@emurgo/msl-mobile-bridge" react-native-mmkv: :path: "../../../node_modules/react-native-mmkv" react-native-pager-view: @@ -872,6 +877,7 @@ SPEC CHECKSUMS: react-native-ble-plx: f10240444452dfb2d2a13a0e4f58d7783e92d76e react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8 react-native-haskell-shelley: 4015aef14eca3ecf5e8d8718e8d1b47cdcf60f84 + react-native-message_signing-library: e81294a3cd40d74d3d83f0a1de72aa319238f09a react-native-mmkv: e97c0c79403fb94577e5d902ab1ebd42b0715b43 react-native-pager-view: 0ccb8bf60e2ebd38b1f3669fa3650ecce81db2df react-native-quick-base64: 62290829c619fbabca4c41cfec75ae759d08fc1c diff --git a/apps/wallet-mobile/jestSetup.ts b/apps/wallet-mobile/jestSetup.ts index fc6ef21b1d..2b8ed0ccc9 100644 --- a/apps/wallet-mobile/jestSetup.ts +++ b/apps/wallet-mobile/jestSetup.ts @@ -16,6 +16,9 @@ jest.mock('react-native-ble-plx', () => ({})) jest.mock('@react-native-async-storage/async-storage', () => require('@react-native-async-storage/async-storage/jest/async-storage-mock'), ) + +jest.mock('@emurgo/cross-msl-mobile', () => require('@emurgo/cross-msl-nodejs')) + jest.mock('react-native-keychain', () => ({ resetGenericPassword: jest.fn(), })) @@ -57,4 +60,3 @@ jest.mock('react-native-localize', () => ({ usesAutoDateAndTime: () => true, usesAutoTimeZone: () => true, })) - diff --git a/apps/wallet-mobile/package.json b/apps/wallet-mobile/package.json index 81a20b5fbe..524881bf21 100644 --- a/apps/wallet-mobile/package.json +++ b/apps/wallet-mobile/package.json @@ -96,7 +96,9 @@ "@emurgo/cip4-js": "1.0.7", "@emurgo/cross-csl-core": "4.4.0", "@emurgo/cross-csl-mobile": "4.4.0", + "@emurgo/cross-msl-mobile": "^1.0.0", "@emurgo/csl-mobile-bridge": "6.0.0-alpha.9", + "@emurgo/msl-mobile-bridge": "^1.0.2", "@emurgo/react-native-blockies-svg": "^0.0.2", "@emurgo/react-native-hid": "^5.15.6", "@emurgo/yoroi-lib": "0.15.4-alpha.7", @@ -217,6 +219,7 @@ "@babel/runtime": "^7.20.0", "@emurgo/cardano-serialization-lib-nodejs": "12.0.0-alpha.28", "@emurgo/cross-csl-nodejs": "4.4.0", + "@emurgo/cross-msl-nodejs": "^1.0.0", "@formatjs/cli": "^6.1.0", "@formatjs/ts-transformer": "^3.13.0", "@react-navigation/devtools": "^6.0.13", diff --git a/apps/wallet-mobile/src/features/Discover/common/helpers.ts b/apps/wallet-mobile/src/features/Discover/common/helpers.ts index 51b1f6f699..cd2e347d50 100644 --- a/apps/wallet-mobile/src/features/Discover/common/helpers.ts +++ b/apps/wallet-mobile/src/features/Discover/common/helpers.ts @@ -2,7 +2,7 @@ import {connectionStorageMaker, dappConnectorApiMaker, dappConnectorMaker, Resol import {DappConnector} from '@yoroi/dapp-connector' import {App} from '@yoroi/types' -import {cip30ExtensionMaker} from '../../../yoroi-wallets/cardano/cip30' +import {cip30ExtensionMaker} from '../../../yoroi-wallets/cardano/cip30/cip30' import {YoroiWallet} from '../../../yoroi-wallets/cardano/types' export const validUrl = (url: string) => { diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30.test.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30.test.ts similarity index 77% rename from apps/wallet-mobile/src/yoroi-wallets/cardano/cip30.test.ts rename to apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30.test.ts index 0610a492d5..ac06e57f80 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30.test.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30.test.ts @@ -1,7 +1,7 @@ -import {mocks} from '../mocks' -import {getMasterKeyFromMnemonic} from './byron/util' +import {mocks} from '../../mocks' +import {getMasterKeyFromMnemonic} from '../byron/util' +import {YoroiWallet} from '../types' import {cip30ExtensionMaker} from './cip30' -import {YoroiWallet} from './types' describe('cip30ExtensionMaker', () => { it('should support submitTx', async () => { @@ -76,9 +76,26 @@ describe('cip30ExtensionMaker', () => { it('should support signTx', async () => { const cip30 = cip30ExtensionMaker(mocks.wallet) - const result = await cip30.signTx(await getMasterKeyFromMnemonic(mnemonic), txCbor, true) + const rootKey = await getMasterKeyFromMnemonic(mnemonic) + const result = await cip30.signTx(rootKey, txCbor, true) expect(result).toBeDefined() }) + + it('should support signData', async () => { + const rootKey = await getMasterKeyFromMnemonic(mnemonic) + const message = 'whatever' + const addressBech32 = + 'addr1qynqc23tpx4dqps6xgqy9s2l3xz5fxu734wwmzj9uddn0h2z6epfcukqmswgwwfruxh7gaddv9x0d5awccwahnhwleqqc4zkh4' + + const cip30 = cip30ExtensionMaker(mocks.wallet) + const result = await cip30.signData(rootKey, addressBech32, message) + + expect(result).toEqual({ + signature: + '845846a201276761646472657373583901260c2a2b09aad0061a320042c15f8985449b9e8d5ced8a45e35b37dd42d6429c72c0dc1c873923e1afe475ad614cf6d3aec61ddbceeefe40a166686173686564f448776861746576657258406a16fcb9cc6c3f7d83fdd623e8896d4b81c0ef6a9fb68d916794e2e4c3c0766666b485f71c6f1f56241cb30905cc18618c7e95721dba3e91bcd9918f51e8b90a', + key: 'a4010103272006215820be6a4d7e9789dd7458049c971ad783107e7f041c6f1f542b4530d63d5fe5afb1', + }) + }) }) const mnemonic = [ diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30.ts similarity index 88% rename from apps/wallet-mobile/src/yoroi-wallets/cardano/cip30.ts rename to apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30.ts index f5457a20bc..d41cbf06ee 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30.ts @@ -1,5 +1,6 @@ import * as CSL from '@emurgo/cross-csl-core' import {WasmModuleProxy} from '@emurgo/cross-csl-core' +import {init} from '@emurgo/cross-msl-mobile' import {RemoteUnspentOutput, signRawTransaction, UtxoAsset} from '@emurgo/yoroi-lib' import {normalizeToAddress} from '@emurgo/yoroi-lib/dist/internals/utils/addresses' import {parseTokenList} from '@emurgo/yoroi-lib/dist/internals/utils/assets' @@ -9,22 +10,23 @@ import {BigNumber} from 'bignumber.js' import {Buffer} from 'buffer' import _ from 'lodash' -import {RawUtxo, YoroiSignedTx, YoroiUnsignedTx} from '../types' -import {asQuantity, Utxos} from '../utils' -import {Cardano, CardanoMobile} from '../wallets' -import {toAssetNameHex, toPolicyId} from './api' -import {getTransactionSigners} from './common/signatureUtils' -import {Pagination, YoroiWallet} from './types' -import {createRawTxSigningKey, identifierToCardanoAsset} from './utils' -import {collateralConfig, findCollateralCandidates, utxosMaker} from './utxoManager/utxos' -import {wrappedCsl} from './wrappedCsl' +import {RawUtxo, YoroiSignedTx, YoroiUnsignedTx} from '../../types' +import {asQuantity, Utxos} from '../../utils' +import {Cardano, CardanoMobile} from '../../wallets' +import {toAssetNameHex, toPolicyId} from '../api' +import * as cip8 from '../cip8/cip8' +import {getDerivationPathForAddress, getTransactionSigners} from '../common/signatureUtils' +import {Pagination, YoroiWallet} from '../types' +import {createRawTxSigningKey, identifierToCardanoAsset} from '../utils' +import {collateralConfig, findCollateralCandidates, utxosMaker} from '../utxoManager/utxos' +import {wrappedCsl as getCSL} from '../wrappedCsl' + +const MSL = init('msl') export const cip30ExtensionMaker = (wallet: YoroiWallet) => { return new CIP30Extension(wallet) } -const getCSL = () => wrappedCsl() - const copy = async Promise}>( creator: {fromHex: (hex: string) => Promise}, value: T, @@ -129,13 +131,33 @@ class CIP30Extension { return txId } - async signData(_rootKey: string, address: string, _payload: string): Promise<{signature: string; key: string}> { + async signData(rootKey: string, address: string, payload: string): Promise<{signature: string; key: string}> { const {csl, release} = getCSL() try { + const payloadInBytes = Buffer.from(payload, 'utf-8') + const normalisedAddress = await normalizeToAddress(csl, address) const bech32Address = await normalisedAddress?.toBech32(undefined) - if (!bech32Address) throw new Error('Invalid wallet state') - throw new Error('Not implemented') + if (!bech32Address || !normalisedAddress) throw new Error('Invalid address') + + const path = getDerivationPathForAddress(bech32Address, this.wallet, true) + const signingKey = await createRawTxSigningKey(rootKey, path) + const coseSign1 = await cip8.sign(Buffer.from(await normalisedAddress.toHex(), 'hex'), signingKey, payloadInBytes) + const key = await MSL.COSEKey.new(await MSL.Label.fromKeyType(MSL.KeyType.OKP)) + await key.setAlgorithmId(await MSL.Label.fromAlgorithmId(MSL.AlgorithmId.EdDSA)) + await key.setHeader( + await MSL.Label.newInt(await MSL.Int.newNegative(await MSL.BigNum.fromStr('1'))), + await MSL.CBORValue.newInt(await MSL.Int.newI32(6)), + ) + await key.setHeader( + await MSL.Label.newInt(await MSL.Int.newNegative(await MSL.BigNum.fromStr('2'))), + await MSL.CBORValue.newBytes(await (await signingKey.toPublic()).asBytes()), + ) + + return { + signature: Buffer.from(await coseSign1.toBytes()).toString('hex'), + key: Buffer.from(await key.toBytes()).toString('hex'), + } } finally { release() } diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/cip8/cip8.test.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/cip8/cip8.test.ts new file mode 100644 index 0000000000..46512b6d83 --- /dev/null +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/cip8/cip8.test.ts @@ -0,0 +1,36 @@ +import {normalizeToAddress} from '@emurgo/yoroi-lib/dist/internals/utils/addresses' +import assert from 'assert' +import {Buffer} from 'buffer' + +import {getMasterKeyFromMnemonic} from '../byron/util' +import {harden} from '../common/signatureUtils' +import {createRawTxSigningKey} from '../utils' +import {wrappedCsl} from '../wrappedCsl' +import * as cip8 from './cip8' + +describe('CIP8', () => { + it('should support signing', async () => { + const {csl} = wrappedCsl() + const bech32 = + 'addr1qynqc23tpx4dqps6xgqy9s2l3xz5fxu734wwmzj9uddn0h2z6epfcukqmswgwwfruxh7gaddv9x0d5awccwahnhwleqqc4zkh4' + const payload = 'whatever' + const rootKey = await getMasterKeyFromMnemonic(mnemonic) + const path = [harden(1852), harden(1815), harden(0), 0, 0] + const signingKey = await createRawTxSigningKey(rootKey, path) + + const payloadInBytes = Buffer.from(payload, 'utf-8') + const normalisedAddress = await normalizeToAddress(csl, bech32) + assert(normalisedAddress != null) + const coseSign1 = await cip8.sign(Buffer.from(await normalisedAddress.toHex(), 'hex'), signingKey, payloadInBytes) + const signature = Buffer.from(await coseSign1.toBytes()).toString('hex') + expect(signature).toEqual( + '845846a201276761646472657373583901260c2a2b09aad0061a320042c15f8985449b9e8d5ced8a45e35b37dd42d6429c72c0dc1c873923e1afe475ad614cf6d3aec61ddbceeefe40a166686173686564f448776861746576657258406a16fcb9cc6c3f7d83fdd623e8896d4b81c0ef6a9fb68d916794e2e4c3c0766666b485f71c6f1f56241cb30905cc18618c7e95721dba3e91bcd9918f51e8b90a', + ) + }) +}) + +const mnemonic = [ + 'dry balcony arctic what garbage sort', + 'cart shine egg lamp manual bottom', + 'slide assault bus', +].join(' ') diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/cip8/cip8.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/cip8/cip8.ts new file mode 100644 index 0000000000..8f9f56885f --- /dev/null +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/cip8/cip8.ts @@ -0,0 +1,18 @@ +import {PrivateKey} from '@emurgo/cross-csl-core' +import {init} from '@emurgo/cross-msl-mobile' +import {Buffer} from 'buffer' + +const MSL = init('cip8') + +export const sign = async (address: Buffer, signKey: PrivateKey, payload: Buffer) => { + const protectedHeader = await MSL.HeaderMap.new() + await protectedHeader.setAlgorithmId(await MSL.Label.fromAlgorithmId(MSL.AlgorithmId.EdDSA)) + await protectedHeader.setHeader(await MSL.Label.newText('address'), await MSL.CBORValue.newBytes(address)) + const protectedSerialized = await MSL.ProtectedHeaderMap.new(protectedHeader) + const unprotected = await MSL.HeaderMap.new() + const headers = await MSL.Headers.new(protectedSerialized, unprotected) + const builder = await MSL.COSESign1Builder.new(headers, payload, false) + const toSign = await (await builder.makeDataToSign()).toBytes() + const signedSigStruct = await (await signKey.sign(toSign)).toBytes() + return builder.build(signedSigStruct) +} diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/common/signatureUtils.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/common/signatureUtils.ts index 430a5abe34..6abacd4abc 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/common/signatureUtils.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/common/signatureUtils.ts @@ -92,7 +92,7 @@ const arePathsEqual = (path1: number[], path2: number[]) => { return path1.every((value, index) => value === path2[index]) && path1.length === path2.length } -const getDerivationPathForAddress = (address: string, wallet: YoroiWallet, partial = false) => { +export const getDerivationPathForAddress = (address: string, wallet: YoroiWallet, partial = false) => { const internalIndex = wallet.internalAddresses.indexOf(address) const externalIndex = wallet.externalAddresses.indexOf(address) const purpose = isHaskellShelley(wallet.walletImplementationId) diff --git a/yarn.lock b/yarn.lock index f4535a9742..dd6999af1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2054,6 +2054,11 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46" integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== +"@emurgo/cardano-message-signing-nodejs@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emurgo/cardano-message-signing-nodejs/-/cardano-message-signing-nodejs-1.0.1.tgz#b2fa1f7541055a6c4b8e805492b1a9362bea5835" + integrity sha512-PoKh1tQnJX18f8iEr8Jk1KXxKCn9eqaSslMI1pyOJvYRJhQVDLCh0+9YReufjp0oFJIY1ShcrR+4/WnECVZUKQ== + "@emurgo/cardano-serialization-lib-nodejs@12.0.0-alpha.26": version "12.0.0-alpha.26" resolved "https://registry.yarnpkg.com/@emurgo/cardano-serialization-lib-nodejs/-/cardano-serialization-lib-nodejs-12.0.0-alpha.26.tgz#4e5fea7ba6d72888e21c5a9bea6bcff367834a4b" @@ -2111,6 +2116,36 @@ "@emurgo/cardano-serialization-lib-nodejs" "12.0.0-alpha.26" "@emurgo/cross-csl-core" "4.4.0" +"@emurgo/cross-msl-core@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emurgo/cross-msl-core/-/cross-msl-core-1.0.0.tgz#f66ec9491362e9d60c7c6142875af849ca788c98" + integrity sha512-X98tmVLYSpcLggky3R7iuvb9no/LdoPCR8YXH/CjVj4U5YoR9/6Xf2PQOxSQCBCeExHZ7fDo+gefwoSX6yRjow== + dependencies: + "@cardano-foundation/ledgerjs-hw-app-cardano" "^5.0.0" + "@types/mocha" "^9.1.1" + axios "^0.24.0" + bech32 "^2.0.0" + bignumber.js "^9.0.1" + blake2b "^2.1.4" + hash-wasm "^4.9.0" + mocha "^10.0.0" + +"@emurgo/cross-msl-mobile@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emurgo/cross-msl-mobile/-/cross-msl-mobile-1.0.0.tgz#677434190af90ed43fac46dd6c4c331ec0a0bcce" + integrity sha512-pHMc6P0TjskUAmeI52BId+w253M0EceolnN2m8xdFdqMw4FmrrdpLbfP4uqACHWiM3Vy419SzSczv1r/BHqCNw== + dependencies: + "@emurgo/cross-msl-core" "1.0.0" + "@emurgo/msl-mobile-bridge" "1.0.1" + +"@emurgo/cross-msl-nodejs@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@emurgo/cross-msl-nodejs/-/cross-msl-nodejs-1.0.0.tgz#3079042d10c9b4d723fe7e77fc9c85559e8c3ee5" + integrity sha512-M2qxS8fo6SSddUXSjnjDNQY1Of/Ro+3a2HUZhTcvN/RRa3JLKU36i3YUqSpMhTxHN0GwCM6QCE7LddpZthpJeA== + dependencies: + "@emurgo/cardano-message-signing-nodejs" "1.0.1" + "@emurgo/cross-msl-core" "1.0.0" + "@emurgo/csl-mobile-bridge@6.0.0-alpha.7": version "6.0.0-alpha.7" resolved "https://registry.yarnpkg.com/@emurgo/csl-mobile-bridge/-/csl-mobile-bridge-6.0.0-alpha.7.tgz#63349d0f2e70d568ce628849c8d8b085892531aa" @@ -2125,6 +2160,20 @@ dependencies: base-64 "0.1.0" +"@emurgo/msl-mobile-bridge@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@emurgo/msl-mobile-bridge/-/msl-mobile-bridge-1.0.1.tgz#4a912e182118dc4462e90cbfda5ebedf6048b4e6" + integrity sha512-ed0KobdbEmNParq33pTkYOMWrF8kMP6R3HdIPDy/YycwW9nvvWlWhlpUR4wKDjM3qQzhE/rN6gb/dG8rwWa4IQ== + dependencies: + base-64 "0.1.0" + +"@emurgo/msl-mobile-bridge@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@emurgo/msl-mobile-bridge/-/msl-mobile-bridge-1.0.2.tgz#3241ff5728a7b34b890641843b36c62b6876d09f" + integrity sha512-RQ+X8ovF09jPr3xwG1XVAr/O2YFVZFDGALbLPMEjszvSw12Su3FNgLGvMwXfSvL9T/csR0BF1NqkGauJONedmw== + dependencies: + base-64 "0.1.0" + "@emurgo/react-native-blockies-svg@^0.0.2": version "0.0.2" resolved "https://registry.yarnpkg.com/@emurgo/react-native-blockies-svg/-/react-native-blockies-svg-0.0.2.tgz#afe2ce2776b0c6430ce3bc9a94ceb4f9fcb302f9"