From 9f9bc4793d734ae55703b7793807e039a163f233 Mon Sep 17 00:00:00 2001 From: m2049r Date: Mon, 17 Sep 2018 09:03:07 +0200 Subject: [PATCH] OpenAlias support for XMR & BTC (#404) * with support for OpenAlias QR Codes --- app/build.gradle | 5 + app/src/main/AndroidManifest.xml | 1 + .../com/m2049r/xmrwallet/WalletActivity.java | 2 +- .../m2049r/xmrwallet/data/BarcodeData.java | 79 +++++- .../send/SendAddressWizardFragment.java | 72 ++++- .../xmrwallet/util/OpenAliasHelper.java | 245 ++++++++++++++++++ app/src/main/res/values-de/strings.xml | 11 +- app/src/main/res/values-el/strings.xml | 7 +- app/src/main/res/values-es/strings.xml | 7 +- app/src/main/res/values-fr/strings.xml | 7 +- app/src/main/res/values-hu/strings.xml | 7 +- app/src/main/res/values-it/strings.xml | 7 +- app/src/main/res/values-nb/strings.xml | 7 +- app/src/main/res/values-pt/strings.xml | 7 +- app/src/main/res/values-ro/strings.xml | 7 +- app/src/main/res/values-ru/strings.xml | 7 +- app/src/main/res/values-sk/strings.xml | 7 +- app/src/main/res/values-sv/strings.xml | 7 +- app/src/main/res/values-zh-rCN/strings.xml | 7 +- app/src/main/res/values-zh-rTW/strings.xml | 7 +- app/src/main/res/values/strings.xml | 6 +- .../xmrwallet/util/OpenAliasHelperTest.java | 168 ++++++++++++ 22 files changed, 657 insertions(+), 23 deletions(-) create mode 100644 app/src/main/java/com/m2049r/xmrwallet/util/OpenAliasHelper.java create mode 100644 app/src/test/java/com/m2049r/xmrwallet/util/OpenAliasHelperTest.java diff --git a/app/build.gradle b/app/build.gradle index 0ae8adf0..e4d0292f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -82,6 +82,11 @@ dependencies { implementation 'com.nulab-inc:zxcvbn:1.2.3' + implementation 'dnsjava:dnsjava:2.1.8' + //implementation "org.minidns:minidns-hla:0.3.2" + implementation 'org.jitsi:dnssecjava:1.1.3' + implementation 'org.slf4j:slf4j-nop:1.7.25' + testImplementation "junit:junit:$rootProject.ext.junitVersion" testImplementation "org.mockito:mockito-all:$rootProject.ext.mockitoVersion" testImplementation "com.squareup.okhttp3:mockwebserver:$rootProject.ext.okHttpVersion" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 805781ce..ef4be985 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ package="com.m2049r.xmrwallet"> + diff --git a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java index 8d8cdc0d..8308fe60 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java +++ b/app/src/main/java/com/m2049r/xmrwallet/WalletActivity.java @@ -273,6 +273,7 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste break; case Toolbar.BUTTON_CANCEL: onDisposeRequest(); + Helper.hideKeyboard(WalletActivity.this); WalletActivity.super.onBackPressed(); break; case Toolbar.BUTTON_CLOSE: @@ -920,7 +921,6 @@ public class WalletActivity extends BaseActivity implements WalletFragment.Liste drawer.closeDrawer(GravityCompat.START); return; } - final Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_container); if (fragment instanceof OnBackPressedListener) { if (!((OnBackPressedListener) fragment).onBackPressed()) { diff --git a/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java b/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java index 635bbadf..10b13b29 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java +++ b/app/src/main/java/com/m2049r/xmrwallet/data/BarcodeData.java @@ -20,6 +20,7 @@ import android.net.Uri; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.util.BitcoinAddressValidator; +import com.m2049r.xmrwallet.util.OpenAliasHelper; import java.util.HashMap; import java.util.Map; @@ -32,6 +33,9 @@ public class BarcodeData { public static final String XMR_AMOUNT = "tx_amount"; public static final String XMR_DESCRIPTION = "tx_description"; + public static final String OA_XMR_ASSET = "xmr"; + public static final String OA_BTC_ASSET = "btc"; + static final String BTC_SCHEME = "bitcoin:"; static final String BTC_AMOUNT = "amount"; @@ -40,10 +44,12 @@ public class BarcodeData { } public Asset asset = null; + public String addressName = null; public String address = null; public String paymentId = null; public String amount = null; public String description = null; + public boolean isSecure = true; public BarcodeData(String uri) { this.asset = asset; @@ -76,6 +82,14 @@ public class BarcodeData { this.amount = amount; } + public void setAddressName(String name) { + addressName = name; + } + + public void isSecure(boolean isSecure) { + this.isSecure = isSecure; + } + public Uri getUri() { return Uri.parse(getUriString()); } @@ -95,7 +109,7 @@ public class BarcodeData { first = false; sb.append(BarcodeData.XMR_DESCRIPTION).append('=').append(Uri.encode(description)); } - if (!amount.isEmpty()) { + if ((amount != null) && !amount.isEmpty()) { sb.append(first ? "?" : "&"); sb.append(BarcodeData.XMR_AMOUNT).append('=').append(amount); } @@ -113,10 +127,14 @@ public class BarcodeData { if (bcData == null) { bcData = parseBitcoinUri(qrCode); } - // check for naked btc addres + // check for naked btc address if (bcData == null) { bcData = parseBitcoinNaked(qrCode); } + // check for OpenAlias + if (bcData == null) { + bcData = parseOpenAlias(qrCode); + } return bcData; } @@ -238,4 +256,61 @@ public class BarcodeData { return new BarcodeData(BarcodeData.Asset.BTC, address); } + + static public BarcodeData parseOpenAlias(String oaString) { + Timber.d("parseOpenAlias=%s", oaString); + if (oaString == null) return null; + + Map oaAttrs = OpenAliasHelper.parse(oaString); + if (oaAttrs == null) return null; + + String oaAsset = oaAttrs.get(OpenAliasHelper.OA1_ASSET); + if (oaAsset == null) return null; + + String address = oaAttrs.get(OpenAliasHelper.OA1_ADDRESS); + if (address == null) return null; + + Asset asset; + if (OA_XMR_ASSET.equals(oaAsset)) { + if (!Wallet.isAddressValid(address)) { + Timber.d("XMR address invalid"); + return null; + } + asset = Asset.XMR; + } else if (OA_BTC_ASSET.equals(oaAsset)) { + if (!BitcoinAddressValidator.validate(address)) { + Timber.d("BTC address invalid"); + return null; + } + asset = Asset.BTC; + } else { + Timber.i("Unsupported OpenAlias asset %s", oaAsset); + return null; + } + + String paymentId = oaAttrs.get(OpenAliasHelper.OA1_PAYMENTID); + String description = oaAttrs.get(OpenAliasHelper.OA1_DESCRIPTION); + if (description == null) { + description = oaAttrs.get(OpenAliasHelper.OA1_NAME); + } + String amount = oaAttrs.get(OpenAliasHelper.OA1_AMOUNT); + String addressName = oaAttrs.get(OpenAliasHelper.OA1_NAME); + + if (amount != null) { + try { + Double.parseDouble(amount); + } catch (NumberFormatException ex) { + Timber.d(ex.getLocalizedMessage()); + return null; // we have an amount but its not a number! + } + } + if ((paymentId != null) && !Wallet.isPaymentIdValid(paymentId)) { + Timber.d("paymentId invalid"); + return null; + } + + BarcodeData bc = new BarcodeData(asset, address, paymentId, description, amount); + bc.setAddressName(addressName); + return bc; + } } \ No newline at end of file diff --git a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java index 331e16a4..8b54116b 100644 --- a/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java +++ b/app/src/main/java/com/m2049r/xmrwallet/fragment/send/SendAddressWizardFragment.java @@ -25,6 +25,7 @@ import android.text.Editable; import android.text.Html; import android.text.InputType; import android.text.TextWatcher; +import android.util.Patterns; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; @@ -41,8 +42,11 @@ import com.m2049r.xmrwallet.data.TxDataBtc; import com.m2049r.xmrwallet.model.Wallet; import com.m2049r.xmrwallet.util.BitcoinAddressValidator; import com.m2049r.xmrwallet.util.Helper; +import com.m2049r.xmrwallet.util.OpenAliasHelper; import com.m2049r.xmrwallet.util.UserNotes; +import java.util.Map; + import timber.log.Timber; public class SendAddressWizardFragment extends SendWizardFragment { @@ -83,6 +87,8 @@ public class SendAddressWizardFragment extends SendWizardFragment { private TextView tvXmrTo; private View llXmrTo; + private boolean resolvingOA = false; + OnScanListener onScanListener; public interface OnScanListener { @@ -108,7 +114,11 @@ public class SendAddressWizardFragment extends SendWizardFragment { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if ((event != null && (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) && (event.getAction() == KeyEvent.ACTION_DOWN)) || (actionId == EditorInfo.IME_ACTION_NEXT)) { - if (checkAddress()) { + String dnsOA = dnsFromOpenAlias(etAddress.getEditText().getText().toString()); + Timber.d("OpenAlias is %s", dnsOA); + if (dnsOA != null) { + processOpenAlias(dnsOA); + } else if (checkAddress()) { if (llPaymentId.getVisibility() == View.VISIBLE) { etPaymentId.requestFocus(); } else { @@ -230,6 +240,38 @@ public class SendAddressWizardFragment extends SendWizardFragment { return view; } + private void processOpenAlias(String dnsOA) { + if (resolvingOA) return; // already resolving - just wait + if (dnsOA != null) { + resolvingOA = true; + etAddress.setError(getString(R.string.send_address_resolve_openalias)); + OpenAliasHelper.resolve(dnsOA, new OpenAliasHelper.OnResolvedListener() { + @Override + public void onResolved(Map dataMap) { + resolvingOA = false; + BarcodeData barcodeData = dataMap.get(BarcodeData.Asset.XMR); + if (barcodeData == null) barcodeData = dataMap.get(BarcodeData.Asset.BTC); + if (barcodeData != null) { + Timber.d("DNSSEC=%b, %s", barcodeData.isSecure, barcodeData.address); + processScannedData(barcodeData); + etDummy.requestFocus(); + Helper.hideKeyboard(getActivity()); + } else { + etAddress.setError(getString(R.string.send_address_not_openalias)); + Timber.d("NO XMR OPENALIAS TXT FOUND"); + } + } + + @Override + public void onFailure() { + resolvingOA = false; + etAddress.setError(getString(R.string.send_address_not_openalias)); + Timber.e("OA FAILED"); + } + }); + } // else ignore + } + private boolean checkAddressNoError() { String address = etAddress.getEditText().getText().toString(); return Wallet.isAddressValid(address) @@ -276,12 +318,21 @@ public class SendAddressWizardFragment extends SendWizardFragment { return ok; } + private void shakeAddress() { + etAddress.startAnimation(Helper.getShakeAnimation(getContext())); + } + @Override public boolean onValidateFields() { boolean ok = true; if (!checkAddressNoError()) { - etAddress.startAnimation(Helper.getShakeAnimation(getContext())); + shakeAddress(); ok = false; + String dnsOA = dnsFromOpenAlias(etAddress.getEditText().getText().toString()); + Timber.d("OpenAlias is %s", dnsOA); + if (dnsOA != null) { + processOpenAlias(dnsOA); + } } if (!checkPaymentId()) { etPaymentId.startAnimation(Helper.getShakeAnimation(getContext())); @@ -336,7 +387,12 @@ public class SendAddressWizardFragment extends SendWizardFragment { String scannedAddress = barcodeData.address; if (scannedAddress != null) { etAddress.getEditText().setText(scannedAddress); - checkAddress(); + if (checkAddress()) { + if (!barcodeData.isSecure) + etAddress.setError(getString(R.string.send_address_no_dnssec)); + else + etAddress.setError(getString(R.string.send_address_openalias)); + } } else { etAddress.getEditText().getText().clear(); etAddress.setError(null); @@ -367,4 +423,14 @@ public class SendAddressWizardFragment extends SendWizardFragment { Helper.hideKeyboard(getActivity()); etDummy.requestFocus(); } + + String dnsFromOpenAlias(String openalias) { + Timber.d("checking openalias candidate %s", openalias); + if (Patterns.DOMAIN_NAME.matcher(openalias).matches()) return openalias; + if (Patterns.EMAIL_ADDRESS.matcher(openalias).matches()) { + openalias = openalias.replaceFirst("@", "."); + if (Patterns.DOMAIN_NAME.matcher(openalias).matches()) return openalias; + } + return null; // not an openalias + } } diff --git a/app/src/main/java/com/m2049r/xmrwallet/util/OpenAliasHelper.java b/app/src/main/java/com/m2049r/xmrwallet/util/OpenAliasHelper.java new file mode 100644 index 00000000..13b82d30 --- /dev/null +++ b/app/src/main/java/com/m2049r/xmrwallet/util/OpenAliasHelper.java @@ -0,0 +1,245 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Specs from https://openalias.org/ + +package com.m2049r.xmrwallet.util; + +import android.os.AsyncTask; + +import com.m2049r.xmrwallet.data.BarcodeData; + +import org.jitsi.dnssec.validator.ValidatingResolver; +import org.xbill.DNS.DClass; +import org.xbill.DNS.Flags; +import org.xbill.DNS.Message; +import org.xbill.DNS.Name; +import org.xbill.DNS.RRset; +import org.xbill.DNS.Rcode; +import org.xbill.DNS.Record; +import org.xbill.DNS.Section; +import org.xbill.DNS.SimpleResolver; +import org.xbill.DNS.TXTRecord; +import org.xbill.DNS.Type; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import timber.log.Timber; + +public class OpenAliasHelper { + public static final String OA1_SCHEME = "oa1:"; + public static final String OA1_ASSET = "asset"; + public static final String OA1_ADDRESS = "recipient_address"; + public static final String OA1_NAME = "recipient_name"; + public static final String OA1_DESCRIPTION = "tx_description"; + public static final String OA1_AMOUNT = "tx_amount"; + public static final String OA1_PAYMENTID = "tx_payment_id"; + + public static final int DNS_LOOKUP_TIMEOUT = 2500; // ms + + public static void resolve(String name, OnResolvedListener resolvedListener) { + new DnsTxtResolver(resolvedListener).execute(name); + } + + public static Map parse(String oaString) { + return new OpenAliasParser(oaString).parse(); + } + + public interface OnResolvedListener { + void onResolved(Map dataMap); + + void onFailure(); + } + + private static class DnsTxtResolver extends AsyncTask { + List txts = new ArrayList<>(); + boolean dnssec = false; + + private final OnResolvedListener resolvedListener; + + private DnsTxtResolver(OnResolvedListener resolvedListener) { + this.resolvedListener = resolvedListener; + } + + // trust anchor of the root zone + // http://data.iana.org/root-anchors/root-anchors.xml + final String ROOT = + ". IN DS 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5\n" + + ". IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + final String[] DNSSEC_SERVERS = { + "4.2.2.1", // Level3 + "4.2.2.2", // Level3 + "4.2.2.6", // Level3 + "1.1.1.1", // cloudflare + "9.9.9.9", // quad9 + "8.8.4.4", // google + "8.8.8.8" // google + }; + + @Override + protected Boolean doInBackground(String... args) { + //main(); + if (args.length != 1) return false; + String name = args[0]; + if ((name == null) || (name.isEmpty())) + return false; //pointless trying to lookup nothing + Timber.d("Resolving %s", name); + try { + SimpleResolver sr = new SimpleResolver(DNSSEC_SERVERS[new Random().nextInt(DNSSEC_SERVERS.length)]); + ValidatingResolver vr = new ValidatingResolver(sr); + vr.setTimeout(0, DNS_LOOKUP_TIMEOUT); + vr.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes("ASCII"))); + Record qr = Record.newRecord(Name.fromConstantString(name + "."), Type.TXT, DClass.IN); + Message response = vr.send(Message.newQuery(qr)); + final int rcode = response.getRcode(); + if (rcode != Rcode.NOERROR) { + Timber.i("Rcode: %s", Rcode.string(rcode)); + for (RRset set : response.getSectionRRsets(Section.ADDITIONAL)) { + if (set.getName().equals(Name.root) && set.getType() == Type.TXT + && set.getDClass() == ValidatingResolver.VALIDATION_REASON_QCLASS) { + Timber.i("Reason: %s", ((TXTRecord) set.first()).getStrings().get(0)); + } + } + return false; + } else { + dnssec = response.getHeader().getFlag(Flags.AD); + for (Record record : response.getSectionArray(Section.ANSWER)) { + if (record.getType() == Type.TXT) { + txts.addAll(((TXTRecord) record).getStrings()); + } + } + } + } catch (IOException | IllegalArgumentException ex) { + return false; + } + return true; + } + + @Override + public void onPostExecute(Boolean success) { + if (resolvedListener != null) + if (success) { + Map dataMap = new HashMap<>(); + for (String txt : txts) { + BarcodeData bc = BarcodeData.parseOpenAlias(txt); + if (bc != null) { + bc.isSecure(dnssec); + if (!dataMap.containsKey(bc.asset)) { + dataMap.put(bc.asset, bc); + } + } + } + resolvedListener.onResolved(dataMap); + } else { + resolvedListener.onFailure(); + } + } + } + + private static class OpenAliasParser { + int currentPos = 0; + final String oaString; + StringBuilder sb = new StringBuilder(); + + OpenAliasParser(String oaString) { + this.oaString = oaString; + } + + Map parse() { + if ((oaString == null) || !oaString.startsWith(OA1_SCHEME)) return null; + if (oaString.charAt(oaString.length() - 1) != ';') return null; + + Map oaAttributes = new HashMap<>(); + + final int assetEnd = oaString.indexOf(' '); + if (assetEnd > 20) return null; // random sanity check + String asset = oaString.substring(OA1_SCHEME.length(), assetEnd); + oaAttributes.put(OA1_ASSET, asset); + + boolean inQuote = false; + boolean inKey = true; + String key = null; + for (currentPos = assetEnd; currentPos < oaString.length() - 1; currentPos++) { + char c = currentChar(); + if (inKey) { + if ((sb.length() == 0) && Character.isWhitespace(c)) continue; + if ((c == '\\') || (c == ';')) return null; + if (c == '=') { + key = sb.toString(); + if (oaAttributes.containsKey(key)) return null; // no duplicate keys allowed + sb.setLength(0); + inKey = false; + } else { + sb.append(c); + } + continue; + } + + // now we are in the value + if ((sb.length() == 0) && (c == '"')) { + inQuote = true; + continue; + } + if ((!inQuote || ((sb.length() > 0) && (c == '"'))) && (nextChar() == ';')) { + if (!inQuote) appendCurrentEscapedChar(); + oaAttributes.put(key, sb.toString()); + sb.setLength(0); + currentPos++; // skip the next ; + inQuote = false; + inKey = true; + key = null; + continue; + } + appendCurrentEscapedChar(); + } + if (inQuote) return null; + + if (key != null) { + oaAttributes.put(key, sb.toString()); + } + + return oaAttributes; + } + + char currentChar() { + return oaString.charAt(currentPos); + } + + char nextChar() throws IndexOutOfBoundsException { + int pos = currentPos; + char c = oaString.charAt(pos); + if (c == '\\') { + pos++; + } + return oaString.charAt(pos + 1); + } + + void appendCurrentEscapedChar() throws IndexOutOfBoundsException { + char c = oaString.charAt(currentPos); + if (c == '\\') { + c = oaString.charAt(++currentPos); + } + sb.append(c); + } + + } +} \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index af267026..818762f0 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -222,7 +222,6 @@ %1$s (Anhaltswert) - XMR oder BTC Adresse des Empfängers Zahlungs-ID (optional) 0.00 Private Notizen (optional) @@ -338,6 +337,14 @@ %1$s angesteckt %1$s abgesteckt + Beschreibung (optional) + + OpenAlias nicht aufgelöst - Adresse nicht verfügbar + OpenAlias sicher ✔ + Löse OpenAlias auf… + OpenAlias ohne DNSSEC - Adresse kann gefälscht sein! + Empfänger XMR/BTC Adresse oder OpenAlias + Writing Tag Writing Tag failed! Writing Tag successful @@ -347,6 +354,4 @@ I don\'t know what you want! Reading Tag successful NFC Available! - - Beschreibung (optional) diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 6eee91fa..9120baa5 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -196,7 +196,6 @@ %1$s (ενδεικτικό) - Διεύθυνση XMR ή BTC παραλήπτη ID Πληρωμής(Payment ID)(προαιρετικό) 0.00 Προσωπικές σημειώσεις (προαιρετικό) @@ -348,4 +347,10 @@ NFC Available! Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index f377bd87..5138cd76 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -164,7 +164,6 @@ Introduce una dirección válida Introduce tu semilla de 25 palabras - Dirección XMR o BTC del Destinatario ID de Pago (opcional) 0.00 Notas Privadas (opcional) @@ -335,4 +334,10 @@ NFC Available! Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index f2562361..ddf75a3f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -224,7 +224,6 @@ %1$s (indicatif) - Adresse XMR ou BTC du Destinataire ID de Paiement (optionnel) 0.00 Notes Privées (optionnelles) @@ -351,4 +350,10 @@ Use System Language Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 04a75cfa..eb9c1661 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -222,7 +222,6 @@ %1$s (előrejelzés) - Kedvezményezett XMR vagy BTC címe Fizetési azonosító (opcionális) 0.00 Privát közlemény (opcionális) @@ -349,4 +348,10 @@ NFC elérhető! Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 614ffdc0..be865350 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -223,7 +223,6 @@ %1$s (indicativo) - Indirizzo XMR o BTC del ricevente ID pagamento (opzionale) 0,00 Note private (opzionali) @@ -350,4 +349,10 @@ NFC Available! Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 3fe6c995..91232873 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -221,7 +221,6 @@ %1$s (veiledende) - Mottakers BTC eller XMR adresse Betalings-ID (valgfritt) 0,00 Privat notat (valgfritt) @@ -348,4 +347,10 @@ NFC Available! Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 30519c6f..a872ba48 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -221,7 +221,6 @@ %1$s (indicativo) - Endereço de destino XMR ou BTC ID do pagamento (opcional) 0.00 Notas privadas (opcional) @@ -351,4 +350,10 @@ NFC Available! Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 242c35bf..19b73edf 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -196,7 +196,6 @@ %1$s (indicativ) - Adresa XMR sau BTC a destinatarului Payment ID (facultativ) 0.00 Notițe private (facultativ) @@ -348,4 +347,10 @@ NFC Available! Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cf24590d..d9cc8326 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -223,7 +223,6 @@ %1$s (приблизительно) - XMR или BTC адрес получателя ID платежа (необязательно) 0.00 Персональная заметка (необязательно) @@ -350,4 +349,10 @@ NFC Available! Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index e3c325ef..5b94759d 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -220,7 +220,6 @@ %1$s (indikatív) - XMR alebo BTC Adresa Príjemcu ID Platby (voliteľné) 0.00 Súkromné Poznámky (voliteľné) @@ -347,4 +346,10 @@ NFC je dostupné! Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 3a620310..4ed1c039 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -202,7 +202,6 @@ %1$s (ungefärlig) - Mottagarens XMR- eller BTC-adress Betalnings-ID (valfritt) 0,00 Privat anteckning (valfri) @@ -332,4 +331,10 @@ NFC Available! Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 35a79014..a163175c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -219,7 +219,6 @@ %1$s (参考价) - 收款者的 XMR 或 BTC 地址 付款ID (选填) 0.00 注记 (选填) @@ -346,4 +345,10 @@ NFC Available! Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 855b4351..444397ad 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -220,7 +220,6 @@ %1$s (參考價) - 收款者的 XMR 或 BTC 地址 付款 ID (選填) 0.00 註記 (選填) @@ -347,4 +346,10 @@ 可使用 NFC 標籤 Description (optional) + + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed + Receiver\'s XMR/BTC Address or OpenAlias diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 33b94174..098948cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -228,7 +228,7 @@ %1$s (indicative) - Receiver\'s XMR or BTC Address + Receiver\'s XMR/BTC Address or OpenAlias Payment ID (optional) 0.00 Private Notes (optional) @@ -239,6 +239,10 @@ Not a QR Code Not a valid payment QR code Not a valid address + OpenAlias not resolved - address not available + OpenAlias secure ✔ + Resolving OpenAlias… + OpenAlias without DNSSEC - address may be spoofed Send Balance: %1$s XMR Address diff --git a/app/src/test/java/com/m2049r/xmrwallet/util/OpenAliasHelperTest.java b/app/src/test/java/com/m2049r/xmrwallet/util/OpenAliasHelperTest.java new file mode 100644 index 00000000..2f7c5d20 --- /dev/null +++ b/app/src/test/java/com/m2049r/xmrwallet/util/OpenAliasHelperTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2018 m2049r + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.m2049r.xmrwallet.util; + +import com.m2049r.xmrwallet.data.BarcodeData; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.Map; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class OpenAliasHelperTest { + + private final static String MONERUJO = "oa1:xmr recipient_address=4AdkPJoxn7JCvAby9szgnt93MSEwdnxdhaASxbTBm6x5dCwmsDep2UYN4FhStDn5i11nsJbpU7oj59ahg8gXb1Mg3viqCuk; recipient_name=Monerujo Development; tx_description=Donation to Monerujo Core Team;"; + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void asset() { + Map attrs = OpenAliasHelper.parse(MONERUJO); + assertNotNull(attrs); + assertTrue(BarcodeData.OA_XMR_ASSET.equals(attrs.get(OpenAliasHelper.OA1_ASSET))); + } + + @Test + public void quotedSemicolon() { + Map attrs = OpenAliasHelper.parse("oa1:xmr abc=\";\";def=99;"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals(";")); + assertTrue(attrs.get("def").equals("99")); + } + + @Test + public void space() { + Map attrs = OpenAliasHelper.parse("oa1:xmr abc=\\ ;def=99;"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals(" ")); + assertTrue(attrs.get("def").equals("99")); + } + + @Test + public void quotaedSpace() { + Map attrs = OpenAliasHelper.parse("oa1:xmr abc=\" \";def=99;"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals(" ")); + assertTrue(attrs.get("def").equals("99")); + } + + @Test + public void quotes() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=\"def\";"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals("def")); + } + + @Test + public void simple() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=def;"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals("def")); + } + + @Test + public void duplex() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=def;ghi=jkl;"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals("def")); + assertTrue(attrs.get("ghi").equals("jkl")); + } + + @Test + public void duplexQ() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=def;ghi=jkl;"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals("def")); + assertTrue(attrs.get("ghi").equals("jkl")); + } + + @Test + public void simple_unterminated() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=def;ghi=jkl"); + assertNull(attrs); + } + + @Test + public void unterminatedQuotes() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=\"def;ghi=jkl;"); + assertNull(attrs); + } + + @Test + public void quoteEnd() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=def\";ghi=jkl;"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals("def\"")); + assertTrue(attrs.get("ghi").equals("jkl")); + } + + @Test + public void quoteMiddle() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=d\"ef;ghi=jkl;"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals("d\"ef")); + assertTrue(attrs.get("ghi").equals("jkl")); + } + + @Test + public void quoteMultiple() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=d\"ef\";ghi=jkl;"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals("d\"ef\"")); + assertTrue(attrs.get("ghi").equals("jkl")); + } + + @Test + public void quoteMalformedValue() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=d\"e;f\";ghi=jkl;"); + assertNull(attrs); + } + + @Test + public void quotedSemicolon2() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=\"d;ef\";ghi=jkl;"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals("d;ef")); + assertTrue(attrs.get("ghi").equals("jkl")); + } + + @Test + public void quotedQuote() { + Map attrs; + attrs = OpenAliasHelper.parse("oa1:xmr abc=\"d\"ef\";ghi=jkl;"); + assertNotNull(attrs); + assertTrue(attrs.get("abc").equals("d\"ef")); + assertTrue(attrs.get("ghi").equals("jkl")); + } +}