[Android] SMS Retriever API - SMS 권한 없이 인증번호 읽어오기! |
https://developers.google.com/identity/sms-retriever/
-
SMS Retriever API 를 사용하면 SMS Permission 이 없어도 인증번호를 읽어올 수 있다.
덧붙여 SMS Retriever API 는 SMS Verification 에 대한 녀석으로 권한 없이 “PhoneNumber” 까지도 얻어올 수 있다.
그러나 예상했겠지만, 아무것도 하지 않고 그냥 읽어올 수 있는 것은 아니다.
우선 SMS 정보 읽어오기의 Key Idea 는..
Server 에서 인증코드를 내려줄 때 특정 Hash 값을 함께 내려주고, ( app 의 package name 과 sign key 조합 )
Client 에서는 Server 에 SMS 를 요청한 후 SMS Retriever API 를 사용하면, 내부에서 자체적으로 hash 값을 구한다.
그럼 SMS Retriever 에서는 해당 Hash 값이 있는 문자에 대해서만 Reading 을 가능하게 하는 그런 것이다.
PhoneNumber 읽어오기의 Key Idea 는..
우리가 맘대로 읽어오고 끝이 아니라, User 에게 Selection 을 제공하는 형태이다.
-
가입동선에서 SMS Verification 을 하는 경우가 많은데, 이 목적을 위해서만 READ_PHONE_STATE 나 READ_SMS Permission 을 요청하는 것은 부담스러울 수 있다.
그렇다고 User 가 전화번호를 직접 Typing 하거나, 문자를 수신한 후 문자를 보고 직접 Typing 하도록 하는 것도 꽤 별로인 상황인지라 이 녀석을 이용하면 좋을 것 같다.
-
이 녀석의 Prerequisites 는 눈여겨볼만하다.
Android 단말에 Google Play Service Version 10.2 이상이 설치되어 있어야 한다.
https://developers.google.com/android/guides/releases 에 따르면 2017년 2월에 10.2 버전이 Release 되었다.
그러나 모든 단말에서 10.2 이상이 깔려 있을것이란 보장을 할 수 없고,
AOSP ( 중국 샤오미 단말 등 ) 단말들에는 Google 관련된 기능이 설치가 되어 있지 않을 확률이 아주 높다.
-
Phone Number 얻어오기 -> User 가 PhoneNumber 를 선택하는 Popup 이 뜬다.
이를 통해 직접 입력하는 수고가 덜어진다. (직접 입력하며 typo 가 발생하는 상황도 방지한다.)
private void requestHint() { HintRequest hintRequest = new HintRequest.Builder() .setPhoneNumberIdentifierSupported(true) .build(); PendingIntent intent = Auth.CredentialsApi.getHintPickerIntent(apiClient, hintRequest); startIntentSenderForResult(intent.getIntentSender(), RESOLVE_HINT, null, 0, 0, 0); } // Obtain the phone number from the result @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == RESOLVE_HINT) { if (resultCode == RESULT_OK) { Credential credential = data.getParcelableExtra(Credential.EXTRA_KEY); String phoneNumber = credential.getId(); // string } } }
-
SMS 읽어오는 Callback 등록
private void startSmsRetriever(){ // Get an instance of SmsRetrieverClient, used to start listening for a matching SMS message. SmsRetrieverClient client = SmsRetriever.getClient(this); // this = context // Starts SmsRetriever, which waits for ONE matching SMS message until timeout // (5 minutes). The matching SMS message will be sent via a Broadcast Intent with // action SmsRetriever#SMS_RETRIEVED_ACTION. Task<Void> task = client.startSmsRetriever(); // Listen for success/failure of the start Task. If in a background thread, this // can be made blocking using Tasks.await(task, [timeout]); task.addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { // Successfully started retriever, expect broadcast intent registerSmsBroadcastReceiver(); } }); task.addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Failed to start retriever, inspect Exception for more details // ... } }); } /** * BroadcastReceiver to wait for SMS messages. This can be registered either * in the AndroidManifest or at runtime. Should filter Intents on * SmsRetriever.SMS_RETRIEVED_ACTION. */ public class MySMSBroadcastReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (SmsRetriever.SMS_RETRIEVED_ACTION.equals(intent.getAction())) { Bundle extras = intent.getExtras(); Status status = (Status) extras.get(SmsRetriever.EXTRA_STATUS); switch(status.getStatusCode()) { case CommonStatusCodes.SUCCESS: // Get SMS message contents String message = (String) extras.get(SmsRetriever.EXTRA_SMS_MESSAGE); // Extract one-time code from the message and complete verification // by sending the code back to your server. break; case CommonStatusCodes.TIMEOUT: // Waiting for SMS timed out (5 minutes) // Handle the error ... break; } } } } private registerSmsBroadcastReceiver(){ // in real impl, for unregister, MySMSBroadcastReceiver should be member variable assigned registerBroadcastReceiver(new MySMSBroadcastReceiver(), new IntentFilter(SmsRetriever.SMS_RETRIEVED_ACTION)); }
-
전화번호를 받아오는 절차를 수행한 후 user 에게 SmartLock 에 저장할 지 prompt 를 띄울 수 있다.
1회에 한해 agree 를 받으면 이후에 동의 없이 해당 단말은 물론 다른 단말에서도 phoneNumber 를 쉽게 구해올 수 있다.
동의 없이 구해 오는 것은 앱 재설치는 물론, 다른 단말에 설치시에도 유효하다.
단 주의할 점은.. 받아온 전화번호를 다시 쓰는 것이기 때문에 USIM 이 다른 케이스(실제 전화번호가 바뀐 케이스)에도 유효한지는 테스트가 필요해 보인다.
// Save Credential credential = new Credential.Builder(phoneNumberString) .setAccountType("https://signin.example.com") // a URL specific to the app .setName(displayName) // optional: a display name if available .build(); Auth.CredentialsApi.save(apiClient, credential).setResultCallback( new ResultCallback() { public void onResult(Result result) { Status status = result.getStatus(); if (status.isSuccess()) { Log.d(TAG, "SAVE: OK"); // already saved } else if (status.hasResolution()) { // Prompt the user to save status.startResolutionForResult(this, RC_SAVE); } } }); // On the next install, retrieve the phone number mCredentialRequest = new CredentialRequest.Builder() .setAccountTypes("https://signin.example.com") // the URL specific to the developer .build(); Auth.CredentialsApi.request(apiClient, mCredentialRequest).setResultCallback( new ResultCallback<CredentialRequestResult>() { public void onResult(CredentialRequestResult credentialRequestResult) { if (credentialRequestResult.getStatus().isSuccess()) { credentialRequestResult.getCredential().getId(); // this is the phone number } } }); // Then, initiate verification and sign the user in (same as original verification logic)
-
Server 사이드에서 할 것은 인증 메시지에 Hash 를 넣는 것이다.
그 메시지에는 조건이 있는데...
1. 140 bytes 이하
2. string 의 시작이 <#>(11.2 이상) 또는 zero-width space char(U+200B) 2개
3. one time code(인증코드) 를 포함할 것
4. 11자리의 hash string이 마지막에 붙어있어야 한다.
위의 조건이 만족되어야만 SMS Retriever 에서 제대로 파싱 할 수 있다.
ex)
<#> Your ExampleApp code is: 123ABC78
FA+9qCX9VSu
-
Hash String 은 package name 과 sign key ( public key cert ) 조합으로 생성된다.
1. 우선 app 의 public key cert 를 구해오는 것은 아래와 같이 한다
$ keytool -alias MyAndroidKey -exportcert -keystore MyProduction.keystore | xxd -p | tr -d "[:space:]”
2. packageName + singleSpace + publicKeyCert string 을 만든다.
3. 위의 조합을 SHA-256 sum 으로 계산한다.
4. SHA-256 sum 을 Base64 로 encoding 한다. 이 결과물이 11자리의 최종 hash 이다.
위의 과정은 아래 명령어 하나로도 생성 가능하다
$ keytool -exportcert -alias MyAndroidKey -keystore MyProductionKeys.keystore | xxd -p | tr -d "[:space:]" | echo -n com.example.myapp `cat` | sha256sum | tr -d "[:space:]-" | xxd -r -p | base64 | cut -c1-11
-
위의 과정을 거치지 않고 SMS Retriever 의 sample app 에 있는 AppSignatureHelper 를 통해서도 hash 값을 구할 수 있다.
-
끝!
'프로그래밍 놀이터 > 안드로이드, Java' 카테고리의 다른 글
[android] Transition animations (0) | 2019.01.21 |
---|---|
[android] Snackbar 를 써보자 (0) | 2019.01.20 |
연락처 계정 생성시 OEM 에서 연락처에서 추가/편집 가능한 Field 정의 (0) | 2019.01.17 |
[Map] HashMap vs. TreeMap vs. LinkedHashMap (0) | 2019.01.11 |
[android] SQLiteDatabase 와 SQLiteOpenHelper Test (0) | 2019.01.10 |
댓글