com.mycelium.wallet.external.cashila.api.CashilaService.java Source code

Java tutorial

Introduction

Here is the source code for com.mycelium.wallet.external.cashila.api.CashilaService.java

Source

/*
 * Copyright 2013, 2014 Megion Research and Development GmbH
 *
 * Licensed under the Microsoft Reference Source License (MS-RSL)
 *
 * This license governs use of the accompanying software. If you use the software, you accept this license.
 * If you do not accept the license, do not use the software.
 *
 * 1. Definitions
 * The terms "reproduce," "reproduction," and "distribution" have the same meaning here as under U.S. copyright law.
 * "You" means the licensee of the software.
 * "Your company" means the company you worked for when you downloaded the software.
 * "Reference use" means use of the software within your company as a reference, in read only form, for the sole purposes
 * of debugging your products, maintaining your products, or enhancing the interoperability of your products with the
 * software, and specifically excludes the right to distribute the software outside of your company.
 * "Licensed patents" means any Licensor patent claims which read directly on the software as distributed by the Licensor
 * under this license.
 *
 * 2. Grant of Rights
 * (A) Copyright Grant- Subject to the terms of this license, the Licensor grants you a non-transferable, non-exclusive,
 * worldwide, royalty-free copyright license to reproduce the software for reference use.
 * (B) Patent Grant- Subject to the terms of this license, the Licensor grants you a non-transferable, non-exclusive,
 * worldwide, royalty-free patent license under licensed patents for reference use.
 *
 * 3. Limitations
 * (A) No Trademark License- This license does not grant you any rights to use the Licensors name, logo, or trademarks.
 * (B) If you begin patent litigation against the Licensor over patents that you think may apply to the software
 * (including a cross-claim or counterclaim in a lawsuit), your license to the software ends automatically.
 * (C) The software is licensed "as-is." You bear the risk of using it. The Licensor gives no express warranties,
 * guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot
 * change. To the extent permitted under your local laws, the Licensor excludes the implied warranties of merchantability,
 * fitness for a particular purpose and non-infringement.
 */

package com.mycelium.wallet.external.cashila.api;

import android.net.Uri;
import android.util.Base64;
import android.util.Log;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.google.common.base.Optional;
import com.mrd.bitlib.crypto.Hmac;
import com.mrd.bitlib.crypto.InMemoryPrivateKey;
import com.mrd.bitlib.crypto.SignedMessage;
import com.mrd.bitlib.model.Address;
import com.mrd.bitlib.util.ByteWriter;
import com.mrd.bitlib.util.HashUtils;
import com.mrd.bitlib.util.Sha256Hash;
import com.mycelium.wallet.MbwManager;
import com.mycelium.wallet.api.retrofit.JacksonConverter;
import com.mycelium.wallet.bitid.BitIDSignRequest;
import com.mycelium.wallet.bitid.BitIdAuthenticator;
import com.mycelium.wallet.bitid.BitIdResponse;
import com.mycelium.wallet.external.cashila.ApiException;
import com.mycelium.wallet.external.cashila.ApiExceptionAuth;
import com.mycelium.wallet.external.cashila.api.request.CreateBillPay;
import com.mycelium.wallet.external.cashila.api.request.GetDeepLink;
import com.mycelium.wallet.external.cashila.api.response.*;
import com.mycelium.wapi.api.WapiJsonModule;
import com.squareup.okhttp.*;
import com.squareup.otto.Bus;
import okio.Buffer;
import retrofit.RequestInterceptor;
import retrofit.RestAdapter;
import retrofit.client.OkClient;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import rx.functions.Func1;
import rx.schedulers.Schedulers;

import java.io.IOException;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

public class CashilaService {
    private final static String API_CLIENT_ID = "61d9b5fd-9018-4654-bc7a-d3c7b87d9ada";
    private static final String HEADER_API_CLIENT = "API-Client";
    private static final String HEADER_API_USER = "API-User";
    private static final String HEADER_API_NONCE = "API-Nonce";
    private static final String HEADER_API_SIGN = "API-Sign";

    public static final String DEEP_LINK_DASHBOARD = "dashboard";
    public static final String DEEP_LINK_ADD_RECIPIENT = "recipients/add";
    public static final String CASHILA_CERT = "sha1/cl8wPAZF71fZyBWNmh5tvVV5UYM=";

    private final String baseUrl;
    private final Bus eventBus;
    private final ObjectMapper objectMapper;
    private long lastNonce;
    private Object nonceSynce = new Object();
    private ApiSecretToken securityToken;
    private Cashila cashila;

    public CashilaService(String baseUrl, String apiVersion, Bus eventBus) {
        this.baseUrl = baseUrl;
        this.eventBus = eventBus;

        OkHttpClient client = new OkHttpClient();
        client.setConnectTimeout(5000, TimeUnit.MILLISECONDS);
        client.setReadTimeout(5000, TimeUnit.MILLISECONDS);
        client.networkInterceptors().add(hmacInterceptor);
        CertificatePinner certPinner = new CertificatePinner.Builder().add("cashila.com", CASHILA_CERT)
                .add("cashila-staging.com", CASHILA_CERT).build();
        client.setCertificatePinner(certPinner);

        // use jackson as json mapper, as we already use it in the rest of the project
        objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
        objectMapper.registerModule(new WapiJsonModule());

        RestAdapter adapter = new RestAdapter.Builder().setEndpoint(baseUrl + apiVersion + "/")
                .setLogLevel(RestAdapter.LogLevel.BASIC)
                //.setLogLevel(RestAdapter.LogLevel.FULL)
                .setConverter(new JacksonConverter(objectMapper)).setClient(new OkClient(client))
                .setRequestInterceptor(apiIdInterceptor).build();

        cashila = adapter.create(Cashila.class);

        // initialise nonce with current time and increase by one on each call
        lastNonce = System.currentTimeMillis();
    }

    private volatile Observable<ApiSecretToken> requestToken = null;

    public synchronized Observable<ApiSecretToken> getSecurityToken() {
        if (requestToken == null) {
            // Call getRequestToken, this should return a BitID-URI
            // We authenticate against it and get back a random security token - with this we HMAC all
            // our future API calls
            requestToken = cashila.getRequestToken().observeOn(Schedulers.newThread())
                    .map(new Func1<CashilaResponse<RequestToken>, RequestToken>() {
                        @Override
                        public RequestToken call(CashilaResponse<RequestToken> requestTokenCashilaResponse) {
                            if (requestTokenCashilaResponse.isError()) {
                                throw new ApiExceptionAuth(requestTokenCashilaResponse);
                            }
                            return requestTokenCashilaResponse.result;
                        }
                    }).map(new Func1<RequestToken, ApiSecretToken>() {
                        @Override
                        public ApiSecretToken call(RequestToken requestToken) {
                            // execute the BitID Login scheme on the returned uri
                            Optional<BitIDSignRequest> request = BitIDSignRequest
                                    .parse(Uri.parse(requestToken.uri));
                            if (!request.isPresent()) {
                                new ApiExceptionAuth("Unable to parse the BitID request");
                            }

                            MbwManager manager = MbwManager.getInstance(null);
                            String websiteId = baseUrl + "v1/bitid/pair";
                            InMemoryPrivateKey key = manager.getBitIdKeyForWebsite(websiteId);
                            Address address = key.getPublicKey().toAddress(manager.getNetwork());

                            // use the BitID authenticator to get the temporary cashila security token for all next api calls
                            BitIdAuthenticator authenticator = new BitIdAuthenticator(request.get(), true, key,
                                    address) {
                                @Override
                                protected Request getRequest(SignedMessage signature) {
                                    Request request = super.getRequest(signature);
                                    return request.newBuilder().addHeader("API-Client", API_CLIENT_ID).build();
                                }
                            };
                            BitIdResponse bitIdResponse = authenticator.queryServer();

                            if (bitIdResponse.status == BitIdResponse.ResponseStatus.SUCCESS) {

                                WrappedApiSecretToken apiSecurityToken = null;
                                try {
                                    apiSecurityToken = objectMapper.readValue(bitIdResponse.message,
                                            WrappedApiSecretToken.class);
                                } catch (IOException e) {
                                    Log.e("cashila", "error while deserialize bitid response", e);
                                    apiSecurityToken = new WrappedApiSecretToken(e);
                                }

                                if (apiSecurityToken.isError()) {
                                    throw new ApiExceptionAuth(apiSecurityToken.error.message);
                                }
                                return apiSecurityToken.result;
                            } else {
                                throw new ApiException("BitID query failed, " + bitIdResponse.toString());
                            }
                        }
                    }).doOnError(new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            // reset everything to retry later
                            securityToken = null;
                            requestToken = null;
                        }
                    }).observeOn(AndroidSchedulers.mainThread()).filter(new Func1<ApiSecretToken, Boolean>() {
                        @Override
                        public Boolean call(ApiSecretToken apiSecretToken) {
                            securityToken = apiSecretToken;
                            return true;
                        }
                    }).cache();
            //.retry(2);

        }

        return requestToken;
    }

    public Observable<DeepLink> getDeepLink(final String resource) {
        return new ApiCaller<DeepLink>() {
            @Override
            Observable<CashilaResponse<DeepLink>> apiCall() {
                Observable<CashilaResponse<DeepLink>> deepLink = getApi().getDeepLink(new GetDeepLink(resource));
                return deepLink;
            }
        }.call();
    }

    // used for json de-serialisation
    private static class WrappedApiSecretToken extends CashilaResponse<ApiSecretToken> {
        private WrappedApiSecretToken() {
        }

        private WrappedApiSecretToken(Throwable e) {
            this.error = new Error();
            this.error.message = e.getMessage();
        }
    }

    Observable<CashilaResponse<List<BillPayRecentRecipient>>> billPaysRecentCache;

    // Call ApiMethods with a new security Token ensured
    public Observable<List<BillPayRecentRecipient>> getBillPaysRecent(final boolean fromCache) {
        return new ApiCaller<List<BillPayRecentRecipient>>() {
            @Override
            Observable<CashilaResponse<List<BillPayRecentRecipient>>> apiCall() {
                if (fromCache && billPaysRecentCache != null) {
                    return billPaysRecentCache;
                } else {
                    Observable<CashilaResponse<List<BillPayRecentRecipient>>> billPaysRecent = getApi()
                            .getBillPaysRecent();
                    billPaysRecentCache = billPaysRecent.cache();
                    return billPaysRecent;
                }
            }
        }.call();
    }

    public Observable<BillPay> createBillPay(final UUID newPaymentId, final CreateBillPay createBillPayRequest) {
        return new ApiCaller<BillPay>() {
            @Override
            Observable<CashilaResponse<BillPay>> apiCall() {
                return getApi().createBillPay(newPaymentId, createBillPayRequest);
            }
        }.call();
    }

    public Observable<BillPay> reviveBillPay(final UUID paymentId) {
        return new ApiCaller<BillPay>() {
            @Override
            Observable<CashilaResponse<BillPay>> apiCall() {
                return getApi().reviveBillPay(paymentId);
            }
        }.call();
    }

    public Observable<List<BillPay>> getBillPays() {
        return new ApiCaller<List<BillPay>>() {
            @Override
            Observable<CashilaResponse<List<BillPay>>> apiCall() {
                return getApi().getBillPays("pending,expired,transcribed");
            }
        }.call();
    }

    public Observable<List<BillPay>> getAllBillPays() {
        return new ApiCaller<List<BillPay>>() {
            @Override
            Observable<CashilaResponse<List<BillPay>>> apiCall() {
                return getApi().getBillPays();
            }
        }.call();
    }

    public Observable<List<Void>> deleteBillPay(final UUID paymentId) {
        return new ApiCaller<List<Void>>() {
            @Override
            Observable<CashilaResponse<List<Void>>> apiCall() {
                return getApi().deleteBillPay(paymentId);
            }
        }.call();
    }

    private abstract class ApiCaller<T> {
        public Observable<T> call() {
            Observable<CashilaResponse<T>> ret;
            // if no security token is available, first get it and call the requested api afterwards ...
            if (securityToken == null) {
                ret = getSecurityToken().flatMap(new Func1<ApiSecretToken, Observable<CashilaResponse<T>>>() {
                    @Override
                    public Observable<CashilaResponse<T>> call(ApiSecretToken apiSecretToken) {
                        return apiCall().observeOn(Schedulers.newThread());
                    }
                });
            } else {
                // ... otherwise, call the api directly
                ret = apiCall().doOnError(new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        // On Api error, drop the security token and request a new one on the next call
                        if (throwable instanceof ApiException) {
                            // todo: make it more granular, when we want to drop the security token
                            securityToken = null;
                        }
                    }
                }).observeOn(Schedulers.newThread());
            }

            return ret.map(new CashilaResponseUnwrapper<T>()).observeOn(AndroidSchedulers.mainThread())
                    .doOnError(new Action1<Throwable>() {
                        @Override
                        public void call(Throwable throwable) {
                            broadcastErrorHandler.call(throwable);
                        }
                    });
        }

        abstract Observable<CashilaResponse<T>> apiCall();

        private class CashilaResponseUnwrapper<T> implements Func1<CashilaResponse<T>, T> {
            @Override
            public T call(CashilaResponse<T> tCashilaResponse) {
                if (tCashilaResponse.isError()) {
                    throw new ApiException(tCashilaResponse);
                } else {
                    return tCashilaResponse.result;
                }
            }
        }
    }

    public Cashila getApi() {
        return cashila;
    }

    private String getApiUriPathSegment(final String url) {
        if (url.startsWith(baseUrl)) {
            // only return everything after the base path
            return url.substring(baseUrl.length() - 1);
        } else {
            return url;
        }
    }

    // Retrofit interceptor
    private final RequestInterceptor apiIdInterceptor = new RequestInterceptor() {
        @Override
        public void intercept(RequestFacade request) {
            request.addHeader(HEADER_API_CLIENT, API_CLIENT_ID);
        }
    };

    // OkHttp Client interceptor for the hmac-auth
    private final Interceptor hmacInterceptor = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = chain.request();

            if (securityToken != null) {
                byte[] decodedSecret = Base64.decode(securityToken.secret, Base64.DEFAULT);
                Request.Builder requestBuilder = request.newBuilder();

                String uriPath = getApiUriPathSegment(request.urlString());
                String method = request.method().toUpperCase();
                synchronized (nonceSynce) {
                    String nonce = String.valueOf(getNextNonce());

                    ByteWriter hashBytes = new ByteWriter(1024);
                    hashBytes.putRawStringUtf8(nonce);

                    if (request.body() != null && request.body().contentLength() > 0) {
                        Buffer bodySink = new Buffer();
                        request.body().writeTo(bodySink);
                        hashBytes.putBytes(bodySink.readByteArray());
                    }

                    Sha256Hash contentHash = HashUtils.sha256(hashBytes.toBytes());

                    ByteWriter hmacBytes = new ByteWriter(1024);

                    // put method + uripath into hash
                    hmacBytes.putRawStringUtf8(method + uriPath);

                    // append the content hash
                    hmacBytes.putBytes(contentHash.getBytes());

                    byte[] hmac = Hmac.hmacSha512(decodedSecret, hmacBytes.toBytes());

                    String hmacString = Base64.encodeToString(hmac, Base64.NO_WRAP);
                    request = requestBuilder.header(HEADER_API_USER, securityToken.token)
                            .header(HEADER_API_NONCE, nonce).header(HEADER_API_SIGN, hmacString).build();

                    Response response = chain.proceed(request);
                    return response;
                }
            } else {
                Response response = chain.proceed(request);
                return response;
            }
        }
    };

    private synchronized long getNextNonce() {
        return lastNonce++;
    }

    private final Action1<Throwable> broadcastErrorHandler = new Action1<Throwable>() {
        @Override
        public void call(Throwable throwable) {
            if (throwable instanceof ApiException) {
                eventBus.post(throwable);
            } else {
                //throw new RuntimeException(throwable);
                Log.e("cashila", "Error", throwable);
            }
        }
    };
}