소곤소곤 ad

2014년 2월 18일 화요일

구글플레이 안드로이드 인앱결제 보안 이야기 #1 (프리덤을 막자)

(1) public key를 숨겨라

※ 서버검증할거라면 클라이언트(앱)에서 public_key는 필요가 없다.  => 자세한내용보기


/* base64EncodedPublicKey should be YOUR APPLICATION'S PUBLIC KEY
 * (that you got from the Google Play developer console). This is not your
 * developer public key, it's the *app-specific* public key.
 *
 * Instead of just storing the entire literal string here embedded in the
 * program,  construct the key at runtime from pieces or
 * use bit manipulation (for example, XOR with some other string) to hide
 * the actual key.  The key itself is not secret information, but we don't
 * want to make it easy for an attacker to replace the public key with one
 * of their own and then fake messages from the server.
 */
String base64EncodedPublicKey = "CONSTRUCT_YOUR_KEY_AND_PLACE_IT_HERE";

인앱기능을 구현하기위해 샘플 코드들을 가져다 놓고서 열어보면 이런 부분이 있다.

위의 /* 코멘트 */ 부분은 눈에 잘 들어오지 않고 (영어라서!) "Your key PLACE_IT_HERE"라는 문구가 보인다. 얼른 복사해서 붙여넣고 넘어가려는게 일단 문제다.

그냥 붙여넣지 말자. Construct를 해서 넣어야 한다. 한번쯤은 꼬아 놓자.

String piece1 = "SDFGJKGB4UIH234WE/FRT23RS#trashdata#DF/3DFUISDFVWE";
String piece2 = "SDFGJKGB4UIHUI#trashdata#SDFVWE";
String piece3 = "BDY#trashdata#ASGBDNAWGRET24IYE23das4saGBENWKD";
String piece4 = "#trashdata#432423SDF23R/+SDDS";

mHelper = new IabHelper(this, piece1.substring(...) + piece2.substring(...) + piece3.substring(...) + piece4.substring(...));
이런식으로만 해 두어도 난이도가 올라간다. 크래커입장에서는 이 키를 자기자신의 키로 변경하고자 시도할텐데, 일반 문자열로 들어가 있으면 식은죽 먹기일테지.

권장하는 방법은 실시간으로 서버로부터 이 키를 암호화된채로 받아서 복호화해서 IabHelper를 초기화하는 것.


(2) 결제과정에서 구글지갑을 꼭 체크하자.


유저가 결제한 직후에,  launchPurchaseFlow -> QueryInventory -> Consume -> onFinishedConsume 의 절차를 거치도록 하자.

*) 결제후에, 인벤토리에 그 아이템이 있는지 검사하고, 컨슘(사용)한다음에 최종적으로 OK가 나왔을때에 서버측에 전달하여 유료아이템 구매를 처리하는 식.

구글에서 제공해주는 샘플소스를 잘 살펴보자.


(3) 구매완료후 영수증을 확인한다. (Clinet Side) 

※ 영수증을 서버에서 검증한다면 굳이 검증할 필요는 없다.



public void onConsumeFinished(Purchase purchase, IabResult result) {
    if (result.isSuccess()) {
       if(purchase != null){
           // provision the in-app purchase to the user
           verifyBilling(purchase.getSignature(), purchase.getOriginalJson());
        } else {
           LogToServer("onConsumeFinished:purchase=null");
        }
   } else {
        LogToServer("onConsumeFinished:consume=failed");
   }
}

your_publickey, json_data, signature로 검증을 할 수 있다.  (Security.Java에 잘 구현되어 있다)


(4) 서버측에서 검증한다 (Server Side)


영수증데이터를 서버로 보내 서버에서 검증하는 방법이다.

인터넷을 뒤져서 c++로 구현된 서버측 검증소스를 하나 찾아냈는데, 잘 동작하지 않는다. 게다가 이렇게 뭐가뭔지 모를 소스코드는 기겁하게 만든다.

int InappBillingVerify(const char* data, const char* signature, const char* pub_key_id)
{
    std::shared_ptr<EVP_MD_CTX> mdctx =
    std::shared_ptr<EVP_MD_CTX>(EVP_MD_CTX_create(), EVP_MD_CTX_destroy);
    const EVP_MD* md = EVP_get_digestbyname("SHA1");

    EVP_VerifyInit_ex(mdctx.get(), md, NULL);

    EVP_VerifyUpdate(mdctx.get(), (void*)data, strlen(data));

    std::shared_ptr<BIO> b64 = std::shared_ptr<BIO>(BIO_new(BIO_f_base64()), BIO_free);
    BIO_set_flags(b64.get(),BIO_FLAGS_BASE64_NO_NL);

    std::shared_ptr<BIO> bPubKey = std::shared_ptr<BIO>(BIO_new(BIO_s_mem()), BIO_free);
    BIO_puts(bPubKey.get(),pub_key_id);
    BIO_push(b64.get(), bPubKey.get());
    std::shared_ptr<EVP_PKEY> pubkey = 
             std::shared_ptr<EVP_PKEY>(d2i_PUBKEY_bio(b64.get(), NULL), EVP_PKEY_free);
    std::string decoded_signature = Base64Decode(std::string(signature));

    return EVP_VerifyFinal(mdctx.get(), (unsigned char*)decoded_signature.c_str(), 

             decoded_signature.length(), pubkey.get());
}

출처 : http://kukuta.tistory.com/166

이 분의 소스를 가지고 구현해보았지만 에러가 나면서 실패. 소스코드를 보고 있으면 머리가 지끈지끈하다.

20년을 프로그래밍을 해왔는데, 이 소스는 대체 뭐란 말이냐.... 이유없이 Segmentation Fault 가 나오거나 에러코드 -1이 떨어지는데, 왜 안되는지 이유조차 알 수가 없고, 직접좀 만들어볼까 하니 외계인이 만들어 놓은것 같은 openssl 함수들과 구조체들을 보면 게보린 한통은 필요해 보인다.

결국 포기 T.T

불행한 것은 전 세계를 다 뒤졌음에도, 아직 잘 동작하는 c++소스를 못 찾았았다는 것. 결국 임시방편으로 내가 쓴 방법은 이렇다.

1) Security.Java의 verify를 호출하여 검증하는 Java프로그램을 만든후에, 실행가능한 jar로 만든다.
2) 게임서버에서 java를 호출하여 실행시킨후에 출력값을 얻어온다. (리눅스 pipe를 이용한다)

*. 이 과정에서 jar가 정상 실행되지 않거나 ClassNotFound등의 오류에 시달려야 했는데, 패키지이름을 부여하지 않아서라든지 jdk 버전이 이클립스개발용과 서버용이 달라서 생기는 문제들.


(4-1) 구글 개발자 API를 이용.

구글은 "판매자의 지갑"을 들여다 볼 수 있는 API를 제공한다.  하지만 구현이 만만치 않다. 게다가 c++ 버전의 샘플이나 라이브러리는 제공하지도 않는다.

느리고, 하루 20만회 호출로 제한되어 있으며, 가끔 응답없음이 나오기도 한다그래서 일단 패쓰.


(*) 프리덤!

구글 인앱결제를 우회하게 해주는 프리덤이라는 앱이 있는데,  (프리덤, freedom, ㅍㄹㄷ 으로 검색) 가짜 카드로 무한 결제가 가능한 무시무시한 앱이다.
4단계의 서버검증에서 걸러낼 수 있다. 그러니까, 인앱결제를 넣을거라면 서버검증은 필수라는 것.


2014년 1월 27일 월요일

한글이 포함된 문자열의 실제 크기는 얼마일까?

한글이 포함된 문자열의 길이를 구해보자.

String s = "소곤소곤abcd";
System.out.println(s.length()); // 8

String.length()는 한글이건, 영문이건 상관없이 우리가 인지하는 글자 갯수를 돌려준다.

String s = "소곤소곤abcd";

byte[] buf = s.getBytes(); // using DefaultCharset

System.out.println(buf.length);  // 16
이 경우는, 메모리상에 위치할 실제 바이트수를 알아낼 수 있다.  잠깐! 한글은 2바이트니깐 결과값이 12일텐데 16이라고??? 12도 맞고 16도 맞다. 내 시스템의 eclipse는 UTF-8로 세팅되어 있고, 여기서 한글 한글자는 3바이트를 차지한다. 즉, 결과값은 이 코드가 동작하는 시스템의 기본 인코딩이 뭐냐에 달려있다.

문제는 실행파일(apk 혹은 jar)을 배포했는데, 그게 돌아가는 환경에 따라 이 값이 다르게 나온다는 것이다. 그래서, 시스템 비의존적인 실제 바이트수를 알아낼 필요가 있다.

이때 쓸 함수가 getBytes(String EncodingCharsetName)이다.
String s = "소곤소곤abcd";
System.out.println(s.getBytes("UTF-8").length);
하지만 그냥은 런타임에러가 난다. 엔코딩실패를 대비해서 try catch로 감싸주어야 한다. 함수로 만들어 보았다.
public int getRealSize(String text)
{
    int size = -1;
    try {
        size = text.getBytes("UTF-8").length;
    } catch (UnsupportedEncodingException e1){
    
    }

    return size;
}

이종시스템간 데이터교환 (특히 클라이언트-서버방식의 게임)에서는 항상 주의해야 한다.

2013년 1월 30일 수요일

eclipse에서 프로젝트 복사하여 새프로젝트 시작하기

eclipse에서 프로젝트 복사하여 새프로젝트 시작하기 


 1. 파일시스템에서 workspace를 찾아 일단 폴더째로 복사한다.

 2. eclipse에서 import... from existing projects
이렇게 하면 복사한 프로젝트를 불러오는데 성공. 그러나 이대로는 컴파일이 안된다.

Search (Ctrl+H)를 이용하여 기존 패키지이름을 모두 찾아내서 새로운 패키지이름으로 모두 수정해야한다.

특히 xml 파일들에 주의. 잘못된 곳이 하나라도 있으면 "R.layout.main" 라인이 이유없이 당신을 괴롭힐 것이다.