IT_Programming/Kotlin

[펌][안드로이드] SMS Retriever API를 사용하여 SMS의 내용을 검색하기

JJun ™ 2019. 3. 7. 11:14



 * 출처

 : https://qiita.com/mii-chang/items/769589a1379aacff009e



시작하기

어느 날 GooglePlayConsole에 이런 경고가 나오고있었습니다.


이 응용 프로그램은 android.permission.RECEIVE_SMS권한을 부여했습니다. android.permission.RECEIVE_SMS권한을 사용한다면 그 권한을 무슨 목적으로 사용 하는지를 명확하게 신청해 달라는 것이 이번 경고의 내용입니다.

이 응용 프로그램에서 android.permission.RECEIVE_SMS의 사용은 계정 인증시 SMS 인증을하고 인증 코드를 자동 입력시키기 위해 수신 된 SMS의 내용을 얻을 않았다. 이 용도로 android.permission.RECEIVE_SMS를 사용해 버리면 자신의 응용 프로그램에 필요한 SMS 이외의 모든 SMS 앱에서 읽을 수 버립니다. 그것은 보안으로 어떨 것인가 : thinking :라고하는 것으로, Google에 꾸중을 받았습니다. 그리고 '대안으로 SMS Retriever API 를 사용 해주세요.'라고했습니다.

SMS Retriever API는

android.permission.RECEIVE_SMS를 사용하지 않고 자신의 응용 프로그램에 필요한 SMS를 수신 할 수있는 구조가 SMS Retriever API 입니다. https://developers.google.com/identity/sms-retriever/overview

서버에서 보내 오는 SMS 메시지에 접두사와 자신의 응용 프로그램임을 식별 해시를 제공합니다. 
https://developers.google.com/identity/sms-retriever/verify


 
  예시)

  keytool -alias MyAndroidKey -exportcert -keystore MyProduction.keystore | xxd -p | tr -d "[:space:]"



응용 프로그램은 SMS Retriever API를 사용하고 자신의 앱 해시가 포함 된 SMS 내용만 가져올 수 있습니다. 
https://developers.google.com/identity/sms-retriever/request

이번에는 위의 이미지 ③⑤ 부분의 구현에 대해서 쓰고 있습니다.

사전 준비

app 부하의 build.gradle두 com.google.android.gms:play-services-auth를 추가합니다. 
버전은 여기 를 참고하십시오.

dependencies  { 
    implementation  "com.google.android.gms : play-services-auth : 16.0.1" 
}

구현 방법

대부분 레퍼런스 거리인데 샘플을 올립니다

MainActivity.kt
class  MainActivity  :  AppCompatActivity ()  GoogleApiClient . ConnectionCallbacks ,  GoogleApiClient . OnConnectionFailedListener  {

    override  fun  onCreate ( savedInstanceState :  Bundle ?)  { 
        super . onCreate ( savedInstanceState ) 
        setContentView ( R . layout . activity_main ) 
        createSmsRetrieverClient ()

        // Hash 만들기 
// val helper = AppSignatureHelper (applicationContext) 
// Log.d ( "hash ="helper.appSignatures.toString ()) //이 해시를 SMS로 포함 
    }

    private  fun  createSmsRetrieverClient ()  { 
        val  receiver  =  SMSBroadcastReceiver () 
        val  intentFilter  =  IntentFilter () 
        intentFilter . addAction ( SmsRetriever . SMS_RETRIEVED_ACTION ) 
        registerReceiver ( receiver ,  intentFilter )

        val  client  =  SmsRetriever . getClient ( this ) 
        val  task  =  client . startSmsRetriever ()

        task . addOnSuccessListener  { 
            // SMSRetriever 추가 성공시 청취자 
            Log . d ( "Listener" ,  "SUCCESS" ) 
        }

        task . addOnFailureListener  { 
            // SMSRetriever 추가 실패 청취자 
            Log . d ( "Listener" ,  "FAILURE" ) 
        } 
    }

    inner  class  SMSBroadcastReceiver  :  BroadcastReceiver ()  { 
        override  fun  onReceive ( context :  Context ,  intent :  Intent )  { 
            if  ( SmsRetriever . SMS_RETRIEVED_ACTION  ==  intent . action )  { 
                val  extras  =  intent . extras 
                val  status  =  extras ! . get ( SmsRetriever . EXTRA_STATUS)  as  Status 
                var  message  =  ""

                when  ( status . statusCode )  { 
                    CommonStatusCodes . SUCCESS  ->  { 
                        // 자신의 응용 프로그램에 대한 해시 값이 포함 된 SMS를 수신 할 때 여기에 와서 
                        // 대상 SMS의 내용 만 취득 가능 
                        message  =  extras . get ( SmsRetriever . EXTRA_SMS_MESSAGE )  as  String 
                        Log . d ( "SMSMessage =" ,  message ) 
                    }

                    CommonStatusCodes . TIMEOUT  ->  { 
                        // 수신기를 시작하고 5 분 SMS를 수신하지 못하면 만료 될 
                        Log . d ( "SMSMessage =" ,  "TIMEOUT" ) 
                    } 
                } 
            } 
        } 
    }

    override  fun  onConnectionFailed ( p0 :  ConnectionResult )  { 
    }

    override  fun  onConnectionSuspended ( p0 :  Int )  { 
    }

    override  fun  onConnected ( p0 :  Bundle ?)  { 
    } 
}

SMS를 수신하고 싶을 때 SmsRetrieverClient의 인스턴스를 생성하고 SMS를받을 준비를합니다.
 

서버에서

<#> 당신의 인증 코드 : 123456입니다
해시

라고하는 내용의 SMS가 보낸 해시가 자신의 앱 해시와 일치하지 않으면

extras . get ( SmsRetriever . EXTRA_SMS_MESSAGE )

그리고, SMS의 내용을 검색 할 수 있습니다.

해시 앱의 패키지 이름 인증서 정보에서 생성됩니다. 
해시 생성을위한 
샘플 코드 가 제공되고 있기 때문에, 이제 자신의 환경에 맞는 해시를 생성합니다.

더보기
package com.google.samples.smartlock.sms_verify;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.util.Base64;
import android.util.Log;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
/**
 * This is a helper class to generate your message hash to be included in your SMS message.
 *
 * Without the correct hash, your app won't recieve the message callback. This only needs to be
 * generated once per app and stored. Then you can remove this helper class from your code.
 */
public class AppSignatureHelper extends ContextWrapper {
    public static final String TAG = AppSignatureHelper.class.getSimpleName();
    private static final String HASH_TYPE = "SHA-256";
    public static final int NUM_HASHED_BYTES = 9;
    public static final int NUM_BASE64_CHAR = 11;
    public AppSignatureHelper(Context context) {
        super(context);
    }
    /**
     * Get all the app signatures for the current package
     * @return
     */
    public ArrayList<String> getAppSignatures() {
        ArrayList<String> appCodes = new ArrayList<>();
        try {
            // Get all package signatures for the current package
            String packageName = getPackageName();
            PackageManager packageManager = getPackageManager();
            Signature[] signatures = packageManager.getPackageInfo(packageName,
                    PackageManager.GET_SIGNATURES).signatures;
            // For each signature create a compatible hash
            for (Signature signature : signatures) {
                String hash = hash(packageName, signature.toCharsString());
                if (hash != null) {
                    appCodes.add(String.format("%s", hash));
                }
            }
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Unable to find package to obtain hash.", e);
        }
        return appCodes;
    }
    private static String hash(String packageName, String signature) {
        String appInfo = packageName + " " + signature;
        try {
            MessageDigest messageDigest = MessageDigest.getInstance(HASH_TYPE);
            messageDigest.update(appInfo.getBytes(StandardCharsets.UTF_8));
            byte[] hashSignature = messageDigest.digest();
            // truncated into NUM_HASHED_BYTES
            hashSignature = Arrays.copyOfRange(hashSignature, 0, NUM_HASHED_BYTES);
            // encode into Base64
            String base64Hash = Base64.encodeToString(hashSignature, Base64.NO_PADDING | Base64.NO_WRAP);
            base64Hash = base64Hash.substring(0, NUM_BASE64_CHAR);
            Log.d(TAG, String.format("pkg: %s -- hash: %s", packageName, base64Hash));
            return base64Hash;
        } catch (NoSuchAlgorithmException e) {
            Log.e(TAG, "hash:NoSuchAlgorithm", e);
        }
        return null;
    } 

}

여담? 하지만, 참조 에 써있는 명령은 올바른 해시를 만들 수 없습니다 ... (이제 몇 시간 빠진 ...) 명령에 적용 항목을 잘못한 것이라고 생각 합니다만, 무엇을 잘못했는지 확인한 후에 추가하겠습니다. .

수신 확인

서버에서 SMS를 보내주는 것이 실제 사용 용도라고 생각 합니다만, 확인만한다면 SMSBroadcastReceiver가 준비 되어있는 검증 단말기에 다른 단말에서 <#>의 접두사를 붙여 생성된 해시를 포함한 SMS를 보내도 수신 확인이 가능합니다: ok_woman :

정리

상기와 같이 구현하면 android.permission.RECEIVE_SMS권한없이 자신의 응용 프로그램에 관련된 SMS의 내용만 응용 프로그램에서 읽을 수 있습니다. SMS 인증 코드 자동 입력 등에 활용하십시오!

내가 조사한 시점에서는 일본어의 정보가 매우 적었기 때문에, 글을 써보았습니다. 
참고 주시면 감사하겠습니다!

샘플 코드

샘플 코드 전부는 GitHub 에 싣고 있습니다


SMSRetrieverSample-master.zip




SMSRetrieverSample-master.zip
0.14MB