Skip to content

Commit 9a06d59

Browse files
authored
Merge pull request #626 from TokenScript/OH_UN_flow_solana
feat: 🎸 added sign/verify for flow and solana, fixes
2 parents 38cd041 + f1236aa commit 9a06d59

File tree

10 files changed

+322
-85
lines changed

10 files changed

+322
-85
lines changed

package-lock.json

Lines changed: 32 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"homepage": "https://github.com/TokenScript/token-negotiator#readme",
4343
"dependencies": {
4444
"@onflow/fcl": "^1.3.2",
45+
"@onflow/types": "^1.0.5",
4546
"@peculiar/asn1-schema": "^2.2.0",
4647
"@tokenscript/attestation": "0.4.3",
4748
"@toruslabs/torus-embed": "^1.25.0",
@@ -51,6 +52,7 @@
5152
"ethers": "^5.4.0",
5253
"pvutils": "^1.0.17",
5354
"text-encoding": "^0.7.0",
55+
"tweetnacl": "^1.0.3",
5456
"web3-eth-accounts": "^1.7.4"
5557
},
5658
"devDependencies": {

src/client/auth/signedUNChallenge.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AbstractAuthentication, AuthenticationMethod, AuthenticationResult } fr
22
import { AuthenticateInterface, OffChainTokenConfig, OnChainTokenConfig } from '../interface'
33
import { SafeConnectProvider } from '../../wallet/SafeConnectProvider'
44
import { UN, UNInterface } from './util/UN'
5+
import { logger } from '../../utils'
56

67
export class SignedUNChallenge extends AbstractAuthentication implements AuthenticationMethod {
78
TYPE = 'signedUN'
@@ -15,25 +16,30 @@ export class SignedUNChallenge extends AbstractAuthentication implements Authent
1516
let web3WalletProvider = await this.client.getWalletProvider()
1617

1718
// TODO: Update once Flow & Solana signing support is added
18-
if (web3WalletProvider.getConnectedWalletData('evm').length === 0) {
19+
let connection = web3WalletProvider.getSingleSignatureCompatibleConnection();
20+
if (!connection) {
1921
throw new Error('WALLET_REQUIRED')
2022
}
2123

22-
let address = web3WalletProvider.getConnectedWalletAddresses('evm')[0]
24+
let address = connection.address
25+
26+
if (!address) {
27+
throw new Error('WALLET_ADDRESS_REQUIRED')
28+
}
2329

2430
let currentProof: AuthenticationResult | null = this.getSavedProof(address)
2531

2632
if (currentProof) {
2733
let unChallenge = currentProof?.data as UNInterface
2834

29-
if (unChallenge.expiration < Date.now() || UN.recoverAddress(unChallenge) !== address.toLowerCase()) {
35+
if (unChallenge.expiration < Date.now() || !UN.verifySignature(unChallenge)) {
3036
this.deleteProof(address)
3137
currentProof = null
3238
}
3339
}
3440

3541
if (!currentProof) {
36-
let walletConnection = web3WalletProvider.getWalletProvider(address)
42+
let walletConnection = connection.provider
3743

3844
currentProof = {
3945
type: this.TYPE,
@@ -46,8 +52,10 @@ export class SignedUNChallenge extends AbstractAuthentication implements Authent
4652
let endpoint = request.options?.unEndPoint ?? SignedUNChallenge.DEFAULT_ENDPOINT
4753

4854
const challenge = await UN.getNewUN(endpoint)
55+
challenge.address = address
4956
let signature
5057

58+
logger(2, 'challenge', challenge)
5159
if (walletConnection instanceof SafeConnectProvider) {
5260
signature = await walletConnection.signUNChallenge(challenge)
5361
} else {
@@ -56,14 +64,11 @@ export class SignedUNChallenge extends AbstractAuthentication implements Authent
5664

5765
// Check that recovered address matches the signature of the requested address
5866
challenge.signature = signature
59-
let recoveredAddr = UN.recoverAddress(challenge)
60-
61-
if (recoveredAddr !== address.toLowerCase()) {
67+
challenge.blockchain = connection.blockchain
68+
if (!(await UN.verifySignature(challenge))) {
6269
throw new Error('Address signature is invalid')
6370
}
6471

65-
challenge.address = recoveredAddr
66-
6772
currentProof.data = challenge
6873

6974
this.saveProof(address, currentProof)

src/client/auth/util/UN.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
import { ethers } from 'ethers'
2+
import { sign } from 'tweetnacl'
3+
import { base58ToUint8Array, hexStringToUint8Array, strToHexStr, strToUtfBytes } from '../../../utils'
4+
import * as flowTypes from '@onflow/types'
25

36
export interface UNInterface {
47
expiration: number
@@ -8,6 +11,7 @@ export interface UNInterface {
811
messageToSign: string
912
address?: string
1013
signature?: string
14+
blockchain?: string
1115
}
1216

1317
export class UN {
@@ -21,12 +25,73 @@ export class UN {
2125
}
2226
}
2327

24-
public static recoverAddress(un: UNInterface) {
28+
public static async verifySignature(un: UNInterface) {
2529
if (!un.signature) throw new Error('Null signature')
2630

27-
const msgHash = ethers.utils.hashMessage(un.messageToSign)
28-
const msgHashBytes = ethers.utils.arrayify(msgHash)
29-
return ethers.utils.recoverAddress(msgHashBytes, un.signature).toLowerCase()
31+
if (un.blockchain === 'solana') {
32+
return await sign.detached.verify(
33+
strToUtfBytes(un.messageToSign),
34+
hexStringToUint8Array(un.signature),
35+
base58ToUint8Array(un.address),
36+
)
37+
} else if (un.blockchain === 'flow') {
38+
const flowProvider = await import('../../../wallet/FlowProvider')
39+
const fcl = flowProvider.getFlowProvider()
40+
41+
try {
42+
const response = await fcl
43+
.send([
44+
fcl.script`
45+
pub fun main(address: Address, sig: String, msg: String): Bool {
46+
let account = getAccount(address)
47+
let sig = sig.decodeHex()
48+
let msg = msg.decodeHex()
49+
let isValid = false
50+
var keyNumber = account.keys.count
51+
var res: Bool = false
52+
while keyNumber > 0 {
53+
let accountKey = account.keys.get(keyIndex: Int(keyNumber - 1)) ?? panic("This keyIndex does not exist in this account")
54+
let key = accountKey.publicKey
55+
if key.verify(
56+
signature: sig,
57+
signedData: msg,
58+
domainSeparationTag: "FLOW-V0.0-user",
59+
hashAlgorithm: HashAlgorithm.SHA3_256
60+
) {
61+
res = true
62+
break
63+
}
64+
keyNumber = keyNumber - 1
65+
}
66+
return res
67+
}
68+
`,
69+
fcl.args([
70+
fcl.arg(un.address, flowTypes.Address),
71+
fcl.arg(un.signature, flowTypes.String),
72+
fcl.arg(strToHexStr(un.messageToSign), flowTypes.String),
73+
]),
74+
])
75+
.then(fcl.decode)
76+
if (response) {
77+
return true
78+
}
79+
} catch (e) {
80+
console.log('Flow address recover error')
81+
}
82+
return false
83+
} else if (!un.blockchain || un.blockchain === 'evm') {
84+
const msgHash = ethers.utils.hashMessage(un.messageToSign)
85+
const msgHashBytes = ethers.utils.arrayify(msgHash)
86+
let recoveredAddr = ethers.utils.recoverAddress(msgHashBytes, un.signature).toLowerCase()
87+
if (recoveredAddr === un.address.toLowerCase()) {
88+
return true
89+
}
90+
} else {
91+
throw new Error(`Blockchain "${un.blockchain}" not supported`)
92+
}
93+
// we should not be here
94+
return false
3095
}
3196

3297
public static async validateChallenge(endPoint: string, data: UNInterface) {

src/client/interface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import { SafeConnectOptions } from '../wallet/SafeConnectProvider'
44
import { BrowserDataInterface } from '../utils/support/isSupported'
55
import { WalletConnection } from '../wallet/Web3WalletProvider'
66

7+
// add new blockchain to both rows
78
export type SupportedBlockchainsParam = 'evm' | 'flow' | 'solana'
9+
export const SignatureSupportedBlockchainsParamList = ['evm', 'flow', 'solana']
810

911
export interface OffChainTokenConfig extends IssuerConfigInterface {
1012
onChain: false

src/outlet/auth-handler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ export class AuthHandler {
304304
iframe.src = this.attestationOrigin ?? ''
305305
iframe.style.width = '800px'
306306
iframe.style.height = '800px'
307+
iframe.style.maxHeight = '100vh'
307308
iframe.style.maxWidth = '100%'
308309
iframe.style.background = '#fff'
309310

src/outlet/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export interface OutletInterface {
4343
export const defaultConfig = {
4444
tokenUrlName: 'ticket',
4545
tokenSecretName: 'secret',
46-
tokenIdName: 'id',
46+
tokenIdName: 'mail',
4747
unsignedTokenDataName: 'ticket',
4848
itemStorageKey: 'dcTokens',
4949
signedTokenWhitelist: [],

src/utils/index.ts

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,14 +85,7 @@ export const waitForElementToExist = (selector: string): Promise<Element> => {
8585

8686
export type ErrorType = 'warning' | 'info' | 'error'
8787

88-
export const errorHandler = (
89-
error: any,
90-
type: ErrorType,
91-
action?: Function | null,
92-
data?: unknown,
93-
log = true,
94-
throwError = false,
95-
) => {
88+
export const errorHandler = (error: any, type: ErrorType, action?: Function | null, data?: unknown, log = true, throwError = false) => {
9689
let errorMsg
9790

9891
if (typeof error === 'object') {
@@ -139,11 +132,7 @@ export const tokenRequest = async (query: string, silenceRequestError: boolean)
139132
* @param additionalParams
140133
* @param namespace
141134
*/
142-
export const removeUrlSearchParams = (
143-
params: URLSearchParams,
144-
additionalParams: string[] = [],
145-
namespace: string | null = URLNS,
146-
) => {
135+
export const removeUrlSearchParams = (params: URLSearchParams, additionalParams: string[] = [], namespace: string | null = URLNS) => {
147136
if (namespace)
148137
for (let key of Array.from(params.keys())) {
149138
// Iterator needs to be converted to array since we are deleting keys
@@ -156,3 +145,54 @@ export const removeUrlSearchParams = (
156145

157146
return params
158147
}
148+
149+
export const base58ToUint8Array = (base58String) => {
150+
const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
151+
const base = alphabet.length
152+
const bytes = [0]
153+
for (let i = 0; i < base58String.length; i++) {
154+
const char = base58String[i]
155+
const charIndex = alphabet.indexOf(char)
156+
if (charIndex === -1) throw new Error(`Invalid Base58 character '${char}'`)
157+
for (let j = 0; j < bytes.length; j++) bytes[j] *= base
158+
bytes[0] += charIndex
159+
let carry = 0
160+
for (let j = 0; j < bytes.length; j++) {
161+
bytes[j] += carry
162+
carry = bytes[j] >> 8
163+
bytes[j] &= 0xff
164+
}
165+
while (carry) {
166+
bytes.push(carry & 0xff)
167+
carry >>= 8
168+
}
169+
}
170+
return new Uint8Array(bytes.reverse())
171+
}
172+
173+
export const strToHexStr = (str: string): string => {
174+
if (typeof Buffer !== 'undefined') {
175+
return Buffer.from(str).toString('hex')
176+
} else {
177+
return Array.from(strToUtfBytes(str))
178+
.map((b) => b.toString(16).padStart(2, '0'))
179+
.join('')
180+
}
181+
}
182+
183+
export const strToUtfBytes = (str: string): Uint8Array => {
184+
const encoder = new TextEncoder()
185+
return encoder.encode(str)
186+
}
187+
188+
export const hexStringToUint8Array = (hexString: string): Uint8Array => {
189+
if (hexString.length % 2 === 1) {
190+
throw new Error('Wrong Hex String')
191+
}
192+
const uint8Array = new Uint8Array(hexString.length / 2)
193+
194+
for (let i = 0; i < hexString.length; i += 2) {
195+
uint8Array[i / 2] = parseInt(hexString.slice(i, i + 2), 16)
196+
}
197+
return uint8Array
198+
}

0 commit comments

Comments
 (0)