This article show a way to get private key for Pinterest android app. This article is inspired from this one http://blog.will3942.com/reverse-engineering-instagram. This method was tested on Pinterest app version 2.4.2 and was done from MacOSX.
Preparation
- Download Android SDK from http://developer.android.com/sdk/index.html
Unzip the file we just downloaded. We will see 2 folder: eclipse
, sdk
In sdk
folders, there are build-tools
, extras
, platform-tools
, system-images
, tools
Modify $PATH environment variable
$> echo "export PATH=$PATH:/DEVELOPMENT/ANDROID/sdk/tools/:/DEVELOPMENT/ANDROID/sdk/platform-tools/:/DEVELOPMENT/ANDROID/sdk/build-tools/android-4.4.2/" >> ~/.bash_profile
- Download apktools.
In this article, we tested with Mac OSX 10.8, so we use apktool.1.5.2
- Set up new android virtual device
$> android avd
Start new virtual device we just created
- Download pinterest.2.4.2.apk from http://apkandroid.blogspot.com/2014/02/pinterest-242-apk.html
Hacking
- Using apktool to decompile pinterest.2.4.2.apk to smali
$> java -jar /DEVELOPMENT/ANDROID/apktool1.5.2/apktool.jar d pinterest.2.4.2.apk
All the code will located in smali
folder
The code to get private key is located in smali/com/pinterest/base/Application.smali
- There are 2 function we will need to hack
The first one is getClientID
at smali/com/pinterest/base/Application.smali:78
.method public static final getClientID()Ljava/lang/String;
.locals 1
.prologue
.line 268
sget-object v0, Lcom/pinterest/api/a;->d:Ljava/lang/String;
return-object v0
.end method
We modify it to
.method public static final getClientID()Ljava/lang/String;
.locals 2
.prologue
.line 268
sget-object v0, Lcom/pinterest/api/a;->d:Ljava/lang/String;
const-string v1, "LOGGING: CLIENT ID"
invoke-static {v1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
return-object v0
.end method
NOTE: because we use one more variable v1, so we will need to modify .locals 1
to .locals 2
The second function is getClientSecret
at smali/com/pinterest/base/Application.smali:89
We also add a logging to print out the value. Modify .locals 6
to .locals 7
and modify end of function like this
.line 305
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
const-string v6, "LOGGING: CLIENT SECRET"
invoke-static {v6, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
goto :goto_0
.end method
- Using apktool to recompile this code
$> java -jar /DEVELOPMENT/ANDROID/apktool1.5.2/apktool.jar b pinterest.2.4.2
The new apk file is located at pinterest.2.4.2/dist/pinterest.2.4.2
- Using keytool to generate an key
$> cd pinterest.2.4.2/dist
$> keytool -genkey -v -keystore android.keystore -alias android -keyalg RSA -keysize 2048 -validity 10000
NOTE, we set keystore password and key password is android
(https://developer.android.com/tools/publishing/app-signing.html#setup). I am not sure this one is required
- Sign new apk app
$> jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore android.keystore pinterest.2.4.2.apk android
- Verify apk
$> zipalign -f -v 4 pinterest.2.4.2.apk pinterest.apk
- Install apk app on emulator
$> adb install pinterest.apk
- Check log of emulator
$> adb logcat | grep LOGGING
- Now in emulator, we only need to start pinterest app
The client ID and client secret will be show in log like this
D/LOGGING: CLIENT ID( 1549): 1431602
D/LOGGING: CLIENT SECRET( 1549): 492124fd20e80e0f678f7a03344875f9b6234e2b
Get pinterest algorithm to generate signature
After get client id and client secret, I want to know about how Pinterest generate their signature
- Firstly, find out where they code signature generation function
$> cd pinterest.2.4.2/smali/com/pinterest
$> grep -r "oauth_signature" .
The file is api/a/i.smali
- Let look at how pinterest implement there algorithm
.method private static a(Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;)Ljava/lang/String;
.locals 9
.parameter
.parameter
.parameter
.prologue
const/4 v8, 0x1
const/4 v7, 0x0
.line 315
.line 317
:try_start_0
const-string v0, "\\?"
invoke-virtual {p1, v0}, Ljava/lang/String;->split(Ljava/lang/String;)[Ljava/lang/String;
move-result-object v0
const/4 v1, 0x0
aget-object v0, v0, v1
const-string v1, "UTF-8"
invoke-static {v0, v1}, Ljava/net/URLEncoder;->encode(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
:try_end_0
.catch Ljava/io/UnsupportedEncodingException; {:try_start_0 .. :try_end_0} :catch_1
move-result-object v0
.line 322
:goto_0
new-instance v1, Ljava/lang/StringBuilder;
invoke-direct {v1}, Ljava/lang/StringBuilder;-><init>()V
.line 323
invoke-virtual {v1, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v2
const-string v3, "&"
invoke-virtual {v2, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
.line 324
invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
const-string v2, "&"
invoke-virtual {v0, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
.line 325
invoke-interface {p2}, Ljava/util/Map;->keySet()Ljava/util/Set;
move-result-object v0
invoke-interface {v0}, Ljava/util/Set;->iterator()Ljava/util/Iterator;
move-result-object v2
:cond_0
:goto_1
invoke-interface {v2}, Ljava/util/Iterator;->hasNext()Z
move-result v0
if-eqz v0, :cond_2
invoke-interface {v2}, Ljava/util/Iterator;->next()Ljava/lang/Object;
move-result-object v3
.line 327
:try_start_1
invoke-interface {p2, v3}, Ljava/util/Map;->get(Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v0
.line 328
instance-of v4, v0, Ljava/util/List;
if-eqz v4, :cond_1
.line 329
check-cast v0, Ljava/util/List;
.line 330
invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator;
move-result-object v4
:goto_2
invoke-interface {v4}, Ljava/util/Iterator;->hasNext()Z
move-result v0
if-eqz v0, :cond_0
invoke-interface {v4}, Ljava/util/Iterator;->next()Ljava/lang/Object;
move-result-object v0
check-cast v0, Ljava/lang/String;
.line 331
invoke-virtual {v1, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder;
move-result-object v5
const-string v6, "="
invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v5
invoke-static {v0}, Lcom/pinterest/api/a/i;->c(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
invoke-virtual {v5, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
const-string v5, "&"
invoke-virtual {v0, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
goto :goto_2
.line 338
:catch_0
move-exception v0
goto :goto_1
:catch_1
move-exception v0
move-object v0, p1
goto :goto_0
.line 334
:cond_1
invoke-virtual {v1, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/Object;)Ljava/lang/StringBuilder;
move-result-object v3
const-string v4, "="
invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v3
check-cast v0, Ljava/lang/String;
invoke-static {v0}, Lcom/pinterest/api/a/i;->c(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
invoke-virtual {v3, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
move-result-object v0
const-string v3, "&"
invoke-virtual {v0, v3}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
:try_end_1
.catch Ljava/io/UnsupportedEncodingException; {:try_start_1 .. :try_end_1} :catch_0
goto :goto_1
.line 342
:cond_2
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-virtual {v1}, Ljava/lang/StringBuilder;->length()I
move-result v1
add-int/lit8 v1, v1, -0x1
invoke-virtual {v0, v7, v1}, Ljava/lang/String;->substring(II)Ljava/lang/String;
move-result-object v1
.line 343
const-string v0, ""
.line 346
:try_start_2
new-instance v2, Ljavax/crypto/spec/SecretKeySpec;
sget-object v3, Lcom/pinterest/api/a;->f:Ljava/lang/String;
const-string v4, "UTF-8"
invoke-virtual {v3, v4}, Ljava/lang/String;->getBytes(Ljava/lang/String;)[B
move-result-object v3
const-string v4, "HMACSHA256"
invoke-direct {v2, v3, v4}, Ljavax/crypto/spec/SecretKeySpec;-><init>([BLjava/lang/String;)V
.line 348
const-string v3, "HMACSHA256"
invoke-static {v3}, Ljavax/crypto/Mac;->getInstance(Ljava/lang/String;)Ljavax/crypto/Mac;
move-result-object v3
.line 349
invoke-virtual {v3, v2}, Ljavax/crypto/Mac;->init(Ljava/security/Key;)V
.line 350
const-string v4, "LOGGING: MESSAGE"
invoke-static {v4, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
const-string v2, "UTF-8"
invoke-virtual {v1, v2}, Ljava/lang/String;->getBytes(Ljava/lang/String;)[B
move-result-object v1
invoke-virtual {v3, v1}, Ljavax/crypto/Mac;->doFinal([B)[B
move-result-object v1
.line 351
new-instance v2, Ljava/lang/String;
invoke-static {v1}, Lorg/apache/commons/codec/binary/Hex;->encodeHex([B)[C
move-result-object v1
invoke-direct {v2, v1}, Ljava/lang/String;-><init>([C)V
const-string v1, " "
const-string v3, ""
invoke-virtual {v2, v1, v3}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v1
const-string v2, "<"
const-string v3, ""
invoke-virtual {v1, v2, v3}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v1
const-string v2, ">"
const-string v3, ""
invoke-virtual {v1, v2, v3}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
:try_end_2
.catch Ljava/lang/Exception; {:try_start_2 .. :try_end_2} :catch_2
move-result-object v0
.line 357
:goto_3
const-string v1, "oauth_signature=%s"
new-array v2, v8, [Ljava/lang/Object;
aput-object v0, v2, v7
invoke-static {v1, v2}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
move-result-object v0
.line 358
const-string v1, "%s&%s"
const/4 v2, 0x2
new-array v2, v2, [Ljava/lang/Object;
aput-object p1, v2, v7
aput-object v0, v2, v8
invoke-static {v1, v2}, Ljava/lang/String;->format(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;
move-result-object v0
return-object v0
.line 353
:catch_2
move-exception v1
invoke-virtual {v1}, Ljava/lang/Exception;->printStackTrace()V
goto :goto_3
.end method
From this code, I can only guess they use HMACSHA256 for generate their signature.
They will use CLIENT_SECRET as key for the sha algorithm. We only need to know what they use as message
Search .line 350
, this is the message will be pass to sha1 algorithm. Again, we add these two line
const-string v4, "LOGGING: MESSAGE"
invoke-static {v4, v1}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
This will print for us the format of message Pinterest use.
Now run the pinterest app again, we will see the format of that message
POST&https%3A%2F%2Fapi.pinterest.com%2Fv3%2Flogin%2F&client_id=1431602&password=k×tamp=1395914520&username_or_email=trungkien2288%40gmail.com
Here is the code I use to simulate request in python
import unittest
import urllib
import hashlib
import hmac
client_secret = '492124fd20e80e0f678f7a03344875f9b6234e2b'
client_id = '1431602'
def generate_signature(method, url, data):
data['client_id'] = client_id
sorted_keys = sorted(data.keys())
message = '&'.join(["%s=%s" % (k, urllib.quote_plus(data[k])) for k in sorted_keys])
message = "%s&%s&%s" % (method.upper(), urllib.quote_plus(url), message)
signature = hmac.new(client_secret, message.encode('utf-8'), hashlib.sha256).hexdigest()
return signature
class TestCase(unittest.TestCase):
def test_generate_signature_1(self):
data = {
'username_or_email': '[email protected]',
'password': 'k',
'timestamp': '1395914520'
}
url = 'https://api.pinterest.com/v3/login/'
signature = generate_signature("POST", url, data)
self.assertEquals(signature, '6ee35c775b5f92668530d9cc2b91d9380c4bf01f1b17ccfa73ecfd2867b7b562')
def test_generate_signature_2(self):
data = {
'timestamp': '1395914294'
}
url = 'https://api.pinterest.com/v3/callback/post_install/'
signature = generate_signature("GET", url, data)
self.assertEquals(signature, '0783e2deb355326bb998876387445f2d20bafefc930762cb216e4bc6a2ed748e')