diff --git a/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java b/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java index eff26dbbfd..3e677819b9 100644 --- a/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java +++ b/app/src/main/java/com/alphawallet/app/di/RepositoriesModule.java @@ -244,9 +244,9 @@ SwapService provideSwapService() @Singleton @Provides - AlphaWalletService provideFeemasterService(OkHttpClient okHttpClient, TransactionRepositoryType transactionRepository, Gson gson) + AlphaWalletService provideFeemasterService(OkHttpClient okHttpClient, Gson gson) { - return new AlphaWalletService(okHttpClient, transactionRepository, gson); + return new AlphaWalletService(okHttpClient, gson); } @Singleton diff --git a/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenScriptFile.java b/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenScriptFile.java index 185151659e..6b83abe2f0 100644 --- a/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenScriptFile.java +++ b/app/src/main/java/com/alphawallet/app/entity/tokenscript/TokenScriptFile.java @@ -159,42 +159,17 @@ public boolean isDebug() public void determineSignatureType(XMLDsigDescriptor sigDescriptor) { boolean isDebug = isDebug(); - if (sigDescriptor.result.equals("pass")) - { - if (isDebug) sigDescriptor.type = SigReturnType.DEBUG_SIGNATURE_PASS; - else sigDescriptor.type = SigReturnType.SIGNATURE_PASS; - } - else - { - setFailedIssuer(isDebug, sigDescriptor); - } - } - - private void setFailedIssuer(boolean isDebug, XMLDsigDescriptor sigDescriptor) - { + String keyName; if (isDebug) { - sigDescriptor.keyName = context.getString(R.string.debug_script); + keyName = context.getString(R.string.debug_script); } else { - sigDescriptor.keyName = context.getString(R.string.unsigned_script); + keyName = context.getString(R.string.unsigned_script); } - if (sigDescriptor.subject != null && sigDescriptor.subject.contains("Invalid")) - { - if (isDebug) - sigDescriptor.type = SigReturnType.DEBUG_SIGNATURE_INVALID; - else - sigDescriptor.type = SigReturnType.SIGNATURE_INVALID; - } - else - { - if (isDebug) - sigDescriptor.type = SigReturnType.DEBUG_NO_SIGNATURE; - else - sigDescriptor.type = SigReturnType.NO_SIGNATURE; - } + sigDescriptor.setKeyDetails(isDebug, keyName); } public boolean fileChanged(String fileHash) diff --git a/app/src/main/java/com/alphawallet/app/service/AlphaWalletService.java b/app/src/main/java/com/alphawallet/app/service/AlphaWalletService.java index 3b95c76ec2..cd96fb9f27 100644 --- a/app/src/main/java/com/alphawallet/app/service/AlphaWalletService.java +++ b/app/src/main/java/com/alphawallet/app/service/AlphaWalletService.java @@ -3,24 +3,31 @@ import static com.alphawallet.app.entity.CryptoFunctions.sigFromByteArray; import static com.alphawallet.token.tools.ParseMagicLink.currencyLink; import static com.alphawallet.token.tools.ParseMagicLink.spawnable; +import static org.web3j.protocol.http.HttpService.JSON_MEDIA_TYPE; + +import android.util.Base64; -import com.alphawallet.app.entity.CryptoFunctions; import com.alphawallet.app.entity.Wallet; import com.alphawallet.app.entity.tokens.Ticket; -import com.alphawallet.app.repository.EthereumNetworkRepository; -import com.alphawallet.app.repository.TransactionRepositoryType; import com.alphawallet.app.util.Utils; import com.alphawallet.token.entity.MagicLinkData; import com.alphawallet.token.entity.XMLDsigDescriptor; import com.alphawallet.token.tools.ParseMagicLink; import com.google.gson.Gson; +import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import org.json.JSONObject; +import org.web3j.crypto.Keys; import org.web3j.crypto.Sign; import org.web3j.utils.Numeric; -import java.io.File; +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; import java.math.BigInteger; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,38 +35,47 @@ import io.reactivex.Observable; import io.reactivex.Single; import okhttp3.MediaType; -import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; +import okhttp3.Response; import timber.log.Timber; public class AlphaWalletService { private final OkHttpClient httpClient; - private final TransactionRepositoryType transactionRepository; private final Gson gson; private ParseMagicLink parser; - private static final String API = "api/"; private static final String XML_VERIFIER_ENDPOINT = "https://aw.app/api/v1/verifyXMLDSig"; + private static final String TSML_VERIFIER_ENDPOINT_STAGING = "https://doobtvjcpb8dc.cloudfront.net/tokenscript/validate"; + private static final String TSML_VERIFIER_ENDPOINT = "https://api.smarttokenlabs.com/"; private static final String XML_VERIFIER_PASS = "pass"; private static final MediaType MEDIA_TYPE_TOKENSCRIPT = MediaType.parse("text/xml; charset=UTF-8"); public AlphaWalletService(OkHttpClient httpClient, - TransactionRepositoryType transactionRepository, Gson gson) { this.httpClient = httpClient; - this.transactionRepository = transactionRepository; this.gson = gson; } - private void initParser() + private static class StatusElement { - if (parser == null) + String type; + String status; + String statusText; + String signingKey; + + public XMLDsigDescriptor getXMLDsigDescriptor() { - parser = new ParseMagicLink(new CryptoFunctions(), EthereumNetworkRepository.extraChains()); + XMLDsigDescriptor sig = new XMLDsigDescriptor(); + sig.result = status; + sig.issuer = signingKey; + sig.certificateName = statusText; + sig.keyType = type; + + return sig; } } @@ -82,47 +98,89 @@ public Observable handleFeemasterImport(String url, Wallet wallet, long /** * Use API to determine tokenscript validity - * @param tokenScriptFile + * @param scriptUri + * @param chainId + * @param address * @return */ - public XMLDsigDescriptor checkTokenScriptSignature(File tokenScriptFile) + public XMLDsigDescriptor checkTokenScriptSignature(String scriptUri, long chainId, String address) { XMLDsigDescriptor dsigDescriptor = new XMLDsigDescriptor(); dsigDescriptor.result = "fail"; try { - RequestBody body = RequestBody.Companion.create(tokenScriptFile, MEDIA_TYPE_TOKENSCRIPT); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("sourceType", "scriptUri"); + jsonObject.put("sourceId", chainId + "-" + Keys.toChecksumAddress(address)); + jsonObject.put("sourceUrl", scriptUri); + RequestBody body = RequestBody.create(jsonObject.toString(), JSON_MEDIA_TYPE); - RequestBody requestBody = new MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("file", "tokenscript", body) - .build(); + okhttp3.Response response = getTSValidationCheck(body); - Request request = new Request.Builder().url(XML_VERIFIER_ENDPOINT) - .post(requestBody) - .build(); + if ((response.code() / 100) == 2) + { + String result = response.body().string(); + JsonObject obj = gson.fromJson(result, JsonObject.class); + if (obj.has("error")) + return dsigDescriptor; - okhttp3.Response response = httpClient.newCall(request).execute(); + JsonObject overview = obj.getAsJsonObject("overview"); + if (overview != null) + { + JsonArray statuses = overview.getAsJsonArray("originStatuses"); + if (statuses.size() == 0) + { + return dsigDescriptor; + } + + StatusElement status1 = gson.fromJson(statuses.get(0), StatusElement.class); + return status1.getXMLDsigDescriptor(); + } + } + } + catch (Exception e) + { + Timber.e(e); + } + + return dsigDescriptor; + } + + public XMLDsigDescriptor checkTokenScriptSignature(InputStream inputStream, long chainId, String address) + { + XMLDsigDescriptor dsigDescriptor = new XMLDsigDescriptor(); + dsigDescriptor.result = "fail"; + try + { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("sourceType", "scriptUri"); + jsonObject.put("sourceId", chainId + "-" + Keys.toChecksumAddress(address)); + jsonObject.put("sourceUrl", ""); + jsonObject.put("base64Xml", streamToBase64(inputStream)); + RequestBody body = RequestBody.create(jsonObject.toString(), JSON_MEDIA_TYPE); - String result = response.body().string(); - JsonObject obj = gson.fromJson(result, JsonObject.class); - if (obj.has("error") || !obj.has("result")) return dsigDescriptor; + okhttp3.Response response = getTSValidationCheck(body); - String queryResult = obj.get("result").getAsString(); - if (queryResult.equals(XML_VERIFIER_PASS)) + if ((response.code() / 100) == 2) { - dsigDescriptor = gson.fromJson(result, XMLDsigDescriptor.class); - //interpret subject to get the primary certifying body - String[] certifiers = dsigDescriptor.subject.split(","); - if (certifiers[0] != null && certifiers[0].length() > 3 && certifiers[0].startsWith("CN=")) + String result = response.body().string(); + JsonObject obj = gson.fromJson(result, JsonObject.class); + if (obj.has("error")) + return dsigDescriptor; + + JsonObject overview = obj.getAsJsonObject("overview"); + if (overview != null) { - dsigDescriptor.certificateName = certifiers[0].substring(3); + JsonArray statuses = overview.getAsJsonArray("originStatuses"); + if (statuses.size() == 0) + { + return dsigDescriptor; + } + + StatusElement status1 = gson.fromJson(statuses.get(0), StatusElement.class); + return status1.getXMLDsigDescriptor(); } } - else - { - dsigDescriptor.subject = obj.get("failureReason").getAsString(); - } } catch (Exception e) { @@ -132,6 +190,45 @@ public XMLDsigDescriptor checkTokenScriptSignature(File tokenScriptFile) return dsigDescriptor; } + private String streamToBase64(InputStream inputStream) throws Exception + { + StringBuilder sb = new StringBuilder(); + try (Reader reader = new BufferedReader(new InputStreamReader + (inputStream, StandardCharsets.UTF_8))) + { + int c; + while ((c = reader.read()) != -1) + { + sb.append((char) c); + } + } + + byte[] base64Encoded = Base64.encode(sb.toString().getBytes(StandardCharsets.UTF_8), Base64.DEFAULT); + + return new String(base64Encoded); + } + + private Response getTSValidationCheck(RequestBody body) throws Exception + { + Request request = new Request.Builder().url(TSML_VERIFIER_ENDPOINT) + .post(body) + .build(); + + okhttp3.Response response = httpClient.newCall(request).execute(); + + if ((response.code() / 100) != 2) + { + //try staging endpoint + request = new Request.Builder().url(TSML_VERIFIER_ENDPOINT_STAGING) + .post(body) + .build(); + + response = httpClient.newCall(request).execute(); + } + + return response; + } + private Observable sendFeemasterCurrencyTransaction(String url, long networkId, String address, MagicLinkData order) { return Observable.fromCallable(() -> { diff --git a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java index 3bc28df404..7c6ffa00b5 100644 --- a/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java +++ b/app/src/main/java/com/alphawallet/app/service/AssetDefinitionService.java @@ -139,8 +139,6 @@ public class AssetDefinitionService implements ParseResult, AttributeInterface private static final String BUNDLED_SCRIPT = "bundled"; private static final long CHECK_TX_LOGS_INTERVAL = 20; private static final String EIP5169_ISSUER = "EIP5169-IPFS"; - private static final String EIP5169_CERTIFIER = "Smart Token Labs"; - private static final String EIP5169_KEY_OWNER = "Contract Owner"; //TODO Source this from the contract via owner() private static final String TS_EXTENSION = ".tsml"; private final Context context; private final IPFSServiceType ipfsService; @@ -250,7 +248,7 @@ private List checkRealmScriptsForChanges() handledHashes.add(tsf.calcMD5()); //add the hash of the new file //re-parse script, file hash has changed final TokenDefinition td = parseFile(tsf.getInputStream()); - cacheSignature(tsf) + cacheSignature(tsf, td) .map(definition -> getOriginContracts(td)) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) @@ -291,7 +289,7 @@ private void loadNewFiles(List handledHashes) final String hash = tsf.calcMD5(); if (handledHashes.contains(hash)) return; //already handled this? final TokenDefinition td = parseFile(tsf.getInputStream()); - cacheSignature(file) + cacheSignature(file, td) .map(definition -> getOriginContracts(td)) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) @@ -1177,7 +1175,6 @@ private void loadScriptFromServer(String correctedAddress) if (assetChecked.get(correctedAddress) == null || (System.currentTimeMillis() > (assetChecked.get(correctedAddress) + 1000L * 60L * 60L))) { fetchXMLFromServer(correctedAddress) - .flatMap(this::cacheSignature) .flatMap(this::handleNewTSFile) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -1235,7 +1232,7 @@ private Single handleNewTSFile(File newFile) updateScriptEntriesInRealm(originContracts, isDebugOverride, tsf.calcMD5(), schemaUID); cachedDefinition = td; return tsf; - }).flatMap(tt -> cacheSignature(tsf)) + }).flatMap(tt -> cacheSignature(tsf, td)) .map(a -> fileLoadComplete(originContracts, tsf, td)); } @@ -1368,7 +1365,7 @@ private File storeEntry(Token token, Pair> scriptD } entry.setFileHash(fileHash); - if (scriptData.second.second) entry.setIpfsPath(scriptData.first); //if sourced from IPFS store path + entry.setIpfsPath(scriptData.first); //store scriptUri path entry.setFilePath(storeFile.getAbsolutePath()); entry.setSchemaUID(td.getAttestationSchemaUID()); }); @@ -1378,6 +1375,10 @@ private File storeEntry(Token token, Pair> scriptD Timber.w(e); } + //check signature using the endpoint associated with scriptUri + //otherwise we use the endpoint that takes the full encoded file + + return storeFile; } @@ -2000,7 +2001,7 @@ private boolean isInSecureZone(String file) } /* Add cached signature if uncached files found. */ - private Single cacheSignature(File file) + private Single cacheSignature(File file, TokenDefinition td) { if (file.getName().equals(UNCHANGED_SCRIPT)) { @@ -2018,7 +2019,7 @@ private Single cacheSignature(File file) if (sig == null || sig.keyName == null) { //fetch signature and store in realm - sig = alphaWalletService.checkTokenScriptSignature(tsf); + sig = checkTokenScriptSignature(file, td, ""); tsf.determineSignatureType(sig); storeCertificateData(hash, sig); } @@ -2028,6 +2029,24 @@ private Single cacheSignature(File file) }); } + private XMLDsigDescriptor checkTokenScriptSignature(File file, TokenDefinition td, String scriptUri) + { + XMLDsigDescriptor sig; + ContractInfo info = td.contracts.get(td.holdingToken); + if (TextUtils.isEmpty(scriptUri)) + { + TokenScriptFile tsf = new TokenScriptFile(context, file.getAbsolutePath()); + sig = alphaWalletService.checkTokenScriptSignature(tsf.getInputStream(), info.getfirstChainId(), info.getFirstAddress()); + } + else + { + //String scriptUri, long chainId, String address + sig = alphaWalletService.checkTokenScriptSignature(scriptUri, info.getfirstChainId(), info.getFirstAddress()); + } + + return sig; + } + private void storeCertificateData(String hash, XMLDsigDescriptor sig) throws RealmException { try (Realm realm = realmManager.getRealmInstance(ASSET_DEFINITION_DB)) @@ -2084,15 +2103,7 @@ private XMLDsigDescriptor getCertificateFromRealm(String hash) private XMLDsigDescriptor IPFSSigDescriptor() { - XMLDsigDescriptor sig = new XMLDsigDescriptor(); - sig.issuer = EIP5169_ISSUER; - sig.certificateName = EIP5169_CERTIFIER; - sig.keyName = EIP5169_KEY_OWNER; - sig.keyType = "ECDSA"; - sig.result = "Pass"; - sig.subject = ""; - sig.type = SigReturnType.SIGNATURE_PASS; - return sig; + return new XMLDsigDescriptor(EIP5169_ISSUER); } private boolean checkFileDiff(String address, Pair result) @@ -2508,16 +2519,16 @@ private void notifyNewScript(TokenDefinition tokenDefinition, File file) public Single getSignatureData(Token token) { TokenScriptFile tsf = getTokenScriptFile(token); - return getSignatureData(tsf); + return getSignatureData(tsf, token.tokenInfo.chainId, token.tokenInfo.address); } public Single getSignatureData(long chainId, String contractAddress) { TokenScriptFile tsf = getTokenScriptFile(chainId, contractAddress); - return getSignatureData(tsf); + return getSignatureData(tsf, chainId, contractAddress); } - private Single getSignatureData(TokenScriptFile tsf) + private Single getSignatureData(TokenScriptFile tsf, long chainId, String contractAddress) { return Single.fromCallable(() -> { XMLDsigDescriptor sigDescriptor = new XMLDsigDescriptor(); @@ -2530,7 +2541,7 @@ private Single getSignatureData(TokenScriptFile tsf) XMLDsigDescriptor sig = getCertificateFromRealm(hash); if (sig == null || (sig.result != null && sig.result.equalsIgnoreCase("fail"))) { - sig = alphaWalletService.checkTokenScriptSignature(tsf); + sig = alphaWalletService.checkTokenScriptSignature(tsf.getInputStream(), chainId, contractAddress); tsf.determineSignatureType(sig); storeCertificateData(hash, sig); } @@ -3147,7 +3158,6 @@ public Single checkServerForScript(Token token, MutableLiveData //try the contractURI, then server return fetchTokenScriptFromContract(token, updateFlag) .flatMap(file -> tryServerIfRequired(file, token.getAddress().toLowerCase())) - .flatMap(this::cacheSignature) .flatMap(this::handleNewTSFile) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()); diff --git a/app/src/main/java/com/alphawallet/app/widget/CertifiedToolbarView.java b/app/src/main/java/com/alphawallet/app/widget/CertifiedToolbarView.java index 1bcadfd09c..6b949b0878 100644 --- a/app/src/main/java/com/alphawallet/app/widget/CertifiedToolbarView.java +++ b/app/src/main/java/com/alphawallet/app/widget/CertifiedToolbarView.java @@ -42,6 +42,8 @@ public void onSigData(final XMLDsigDescriptor sigData, final Activity act) SigReturnType type = sigData.type != null ? sigData.type : SigReturnType.NO_TOKENSCRIPT; + downloadSpinner.setVisibility(View.GONE); + lockResource = 0; switch (type) diff --git a/lib/src/main/java/com/alphawallet/token/entity/XMLDsigDescriptor.java b/lib/src/main/java/com/alphawallet/token/entity/XMLDsigDescriptor.java index f0c2e580ee..9fd4e79cf9 100644 --- a/lib/src/main/java/com/alphawallet/token/entity/XMLDsigDescriptor.java +++ b/lib/src/main/java/com/alphawallet/token/entity/XMLDsigDescriptor.java @@ -2,6 +2,10 @@ public class XMLDsigDescriptor { + private static final String EIP5169_CERTIFIER = "Smart Token Labs"; + private static final String EIP5169_KEY_OWNER = "Contract Owner"; //TODO Source this from the contract via owner() + private static final String MATCHES_DEPLOYER = "matches the contract deployer"; + public String result; public String subject; public String keyName; @@ -9,4 +13,70 @@ public class XMLDsigDescriptor public String issuer; public SigReturnType type; public String certificateName = null; + + public XMLDsigDescriptor() + { + + } + + public XMLDsigDescriptor(String issuerText) + { + issuer = issuerText; + certificateName = EIP5169_CERTIFIER; + keyName = EIP5169_KEY_OWNER; + keyType = "ECDSA"; + result = "Pass"; + subject = ""; + type = SigReturnType.SIGNATURE_PASS; + } + + public void setKeyDetails(boolean isDebug, String failKeyName) + { + if (pass()) + { + if (isDebug) + { + this.type = SigReturnType.DEBUG_SIGNATURE_PASS; + } + else + { + this.type = SigReturnType.SIGNATURE_PASS; + } + + if (this.certificateName != null && this.certificateName.contains(MATCHES_DEPLOYER)) + { + this.keyName = EIP5169_KEY_OWNER; + } + } + else + { + setFailedIssuer(isDebug, failKeyName); + } + } + + public boolean pass() + { + return this.result != null && (this.result.equals("pass") || this.result.equals("valid")); + } + + private void setFailedIssuer(boolean isDebug, String failKeyName) + { + this.keyName = failKeyName; + if (this.subject != null && this.subject.contains("Invalid")) + { + if (isDebug) + this.type = SigReturnType.DEBUG_SIGNATURE_INVALID; + else + this.type = SigReturnType.SIGNATURE_INVALID; + } + else + { + if (isDebug) + this.type = SigReturnType.DEBUG_NO_SIGNATURE; + else + this.type = SigReturnType.NO_SIGNATURE; + } + } + + }