/* * Copyright (c) 2015-2016 Spotify AB * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.spotify.sdk.android.authentication; import android.annotation.SuppressLint; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.Signature; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class SpotifyNativeAuthUtil { /* * This is used to pass information activity_about the protocol version * to the AuthorizationActivity. * DO NOT CHANGE THIS. */ private static final String EXTRA_VERSION = "VERSION"; /* * Constants below have their counterparts in Spotify app where * they're used to parse messages received from SDK. * If anything changes in the structure of those messages this version * should be bumped and new protocol version should be implemented on the other side. */ private static final int PROTOCOL_VERSION = 1; // PROTOCOL BEGIN static final String EXTRA_REPLY = "REPLY"; static final String EXTRA_ERROR = "ERROR"; static final String KEY_CLIENT_ID = "CLIENT_ID"; static final String KEY_REQUESTED_SCOPES = "SCOPES"; static final String KEY_REDIRECT_URI = "REDIRECT_URI"; static final String KEY_RESPONSE_TYPE = "RESPONSE_TYPE"; static final String KEY_ACCESS_TOKEN = "ACCESS_TOKEN"; static final String KEY_AUTHORIZATION_CODE = "AUTHORIZATION_CODE"; static final String KEY_EXPIRES_IN = "EXPIRES_IN"; static final String RESPONSE_TYPE_TOKEN = "token"; static final String RESPONSE_TYPE_CODE = "code"; // PROTOCOL END private static final String SPOTIFY_AUTH_ACTIVITY_ACTION = "com.spotify.sso.action.START_AUTH_FLOW"; private static final String SPOTIFY_PACKAGE_NAME = "com.spotify.music"; private static final String[] SPOTIFY_PACKAGE_SUFFIXES = new String[]{ ".debug", ".canary", ".partners", "" }; private static final String[] SPOTIFY_SIGNATURE_HASH = new String[] { "80abdd17dcc4cb3a33815d354355bf87c9378624", "88df4d670ed5e01fc7b3eff13b63258628ff5a00", "d834ae340d1e854c5f4092722f9788216d9221e5", "1cbedd9e7345f64649bad2b493a20d9eea955352", "4b3d76a2de89033ea830f476a1f815692938e33b", }; private Activity mContextActivity; private AuthenticationRequest mRequest; public SpotifyNativeAuthUtil(Activity contextActivity, AuthenticationRequest request) { mContextActivity = contextActivity; mRequest = request; } public boolean startAuthActivity() { Intent intent = createAuthActivityIntent(); if (intent == null) { return false; } intent.putExtra(EXTRA_VERSION, PROTOCOL_VERSION); intent.putExtra(KEY_CLIENT_ID, mRequest.getClientId()); intent.putExtra(KEY_REDIRECT_URI, mRequest.getRedirectUri()); intent.putExtra(KEY_RESPONSE_TYPE, mRequest.getResponseType()); intent.putExtra(KEY_REQUESTED_SCOPES, mRequest.getScopes()); try { mContextActivity.startActivityForResult(intent, LoginActivity.REQUEST_CODE); } catch (ActivityNotFoundException e) { return false; } return true; } private Intent createAuthActivityIntent() { Intent intent = null; for (String suffix : SPOTIFY_PACKAGE_SUFFIXES) { intent = tryResolveActivity(SPOTIFY_PACKAGE_NAME + suffix); if (intent != null) { break; } } return intent; } private Intent tryResolveActivity(String packageName) { Intent intent = new Intent(SPOTIFY_AUTH_ACTIVITY_ACTION); intent.setPackage(packageName); ComponentName componentName = intent.resolveActivity(mContextActivity.getPackageManager()); if (componentName == null) { return null; } if (!validateSignature(componentName.getPackageName())) { return null; } return intent; } @SuppressLint("PackageManagerGetSignatures") private boolean validateSignature(String spotifyPackageName) { try { final PackageInfo packageInfo = mContextActivity.getPackageManager().getPackageInfo(spotifyPackageName, PackageManager.GET_SIGNATURES); if (packageInfo.signatures == null) { return false; } for (Signature signature : packageInfo.signatures) { final String signatureString = signature.toCharsString(); final String sha1Signature = sha1Hash(signatureString); for (String s : SPOTIFY_SIGNATURE_HASH) { if (s.equals(sha1Signature)) { return true; } } } } catch (PackageManager.NameNotFoundException ignored) { } return false; } public void stopAuthActivity() { mContextActivity.finishActivity(LoginActivity.REQUEST_CODE); } private static String sha1Hash(String toHash) { String hash = null; try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); byte[] bytes = toHash.getBytes("UTF-8"); digest.update(bytes, 0, bytes.length); bytes = digest.digest(); hash = bytesToHex(bytes); } catch (NoSuchAlgorithmException ignored) { } catch (UnsupportedEncodingException ignored) { } return hash; } private final static char[] HEX_ARRAY = "0123456789abcdef".toCharArray(); private static String bytesToHex(byte[] bytes) { char[] hexChars = new char[bytes.length * 2]; for (int j = 0; j < bytes.length; j++) { int v = bytes[j] & 0xFF; hexChars[j * 2] = HEX_ARRAY[v >>> 4]; hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; } return new String(hexChars); } }