Commit 17fa84aebe711b4f27b4944d2182225e731166c5

Authored by Matti Virkkunen
1 parent d9734609

Added a tool for getting a mobile auth key from an installed LINE app.

line-android.md
... ... @@ -59,19 +59,25 @@ The mobile version of LINE does not require the user to manually register an acc
59 59 automatically generated when the app is first installed. Therefore the user authentication method is
60 60 also different.
61 61  
62   -The mobile client stores a 15 byte unique key which is used to authenticate it to the LINE servers.
  62 +The Android client stores a 15 byte auth key which is used to authenticate it to the LINE servers.
63 63 The key is stored in an encrypted settings database, but obviously since the app needs to be able to
64 64 read it, this is easy to circumvent (it turns out they essentially use an 8-bit encryption key,
65   -which is generated from the phone's ANDROID_ID value).
  65 +which is generated from the phone's ANDROID_ID value). The auth key is likely generated when the app
  66 +is first installed and it does not seem to ever change.
66 67  
67   -The unique key along with the user ID is used to generate a hash that is used for authentication.
  68 +You can use the tools/view-android-settings.py script to view the auth key for your account. It
  69 +needs the LINE settings SQLite database and optionally the ANDROID_ID (it can also bruteforce it
  70 +which takes about a millisecond on a modern computer). The database on my phone is located at:
  71 +
  72 +/data/data/jp.naver.line.android/databases/naver_line
  73 +
  74 +The displayed user MID and auth key are used to generate an authentication token whenever the client
  75 +connects to the server.
68 76  
69 77 (TODO)
70 78  
71 79 (TODO add example code: mobile authentication token generation)
72 80  
73   -(TODO add example code: obtaining the mobile auth key from the LINE settings SQLite database)
74   -
75 81 Custom Thrift protocol
76 82 ----------------------
77 83  
... ...
tools/view-android-settings.py 0 → 100755
  1 +#!/usr/bin/env python3
  2 +
  3 +# Requires: PyCrypto
  4 +
  5 +import sys
  6 +import base64
  7 +import re
  8 +import sqlite3
  9 +from Crypto.Cipher import AES
  10 +
  11 +def get_setting(path, name):
  12 + """Gets a setting value from the settings database."""
  13 +
  14 + db = sqlite3.connect(path)
  15 +
  16 + r = db.execute("SELECT value FROM setting WHERE key = ?", (name,)).fetchone()
  17 +
  18 + return r[0] if r else None
  19 +
  20 +def crazy_operation(key, constant):
  21 + """Derives an AES key from two values using an unknown algorithm."""
  22 +
  23 + def byte(n):
  24 + return n & 0xff
  25 +
  26 + arr = [0] * 16
  27 + arr[0] = byte(key)
  28 + arr[1] = byte(key - 71)
  29 + arr[2] = byte(key - 142)
  30 +
  31 + for i in range(3, 16):
  32 + arr[i] = byte(i ^ (0xffffffb9 ^ (arr[i - 3] ^ arr[i - 2])))
  33 +
  34 + if constant < 2 and constant > -2:
  35 + constant = 0xfffffffffffb389d + 0xd2dfaf * constant;
  36 +
  37 + i = 0
  38 + k = -7
  39 +
  40 + larr = len(arr)
  41 + for _ in range(0, larr):
  42 + k1 = (i + 1) & (larr - 1)
  43 + l1 = constant * arr[k1] + k
  44 + k = byte(l1 >> 32)
  45 + i2 = l1 + k
  46 +
  47 + if i2 < k:
  48 + i2 += 1
  49 + k += 1
  50 +
  51 + arr[k1] = byte(-2 - i2)
  52 + i = k1
  53 +
  54 + return bytes(arr)
  55 +
  56 +def decrypt_setting(value, key):
  57 + """Decrypts an encrypted setting using the supplied 8-bit integer key."""
  58 +
  59 + ciphertext = base64.b64decode(value)
  60 +
  61 + # generate AES key from 8-bit key
  62 + aes_key = crazy_operation(key, 0xec4ba7)
  63 +
  64 + # decrypt setting with AES
  65 + aes = AES.new(aes_key, AES.MODE_ECB)
  66 + plaintext = aes.decrypt(ciphertext)
  67 +
  68 + if len(plaintext) == 0:
  69 + return plaintext
  70 +
  71 + # remove PKCS#7 padding
  72 + plaintext = plaintext[0:-plaintext[-1]]
  73 +
  74 + return plaintext
  75 +
  76 +def java_string_hash(string):
  77 + """Equivalent of java.lang.String.hashCode()."""
  78 +
  79 + r = 0
  80 + for c in string:
  81 + r = (31 * r + ord(c)) & 0xffffffff
  82 +
  83 + return r
  84 +
  85 +def is_profile_auth_key(value):
  86 + """Checks if a value looks like a PROFILE_AUTH_KEY."""
  87 +
  88 + return re.match(b"^u[a-z0-9]{32}:[a-zA-Z0-9+/]+$", value) != None
  89 +
  90 +def bruteforce_key(auth_key_value):
  91 + """Brute forces the key for a PROFILE_AUTH_KEY."""
  92 +
  93 + for key in range(0x00, 0xff):
  94 + plaintext = decrypt_setting(auth_key_value, key)
  95 +
  96 + if is_profile_auth_key(plaintext):
  97 + return key
  98 +
  99 + raise Exception("Couldn't brute force key.")
  100 +
  101 +def get_encrypted_setting(path, key, name):
  102 + """Shorthand for getting the value of an encrypted settings."""
  103 +
  104 + value = get_setting(path, name)
  105 + if value is None:
  106 + return None
  107 +
  108 + return decrypt_setting(value, key).decode("utf-8")
  109 +
  110 +if __name__ == "__main__":
  111 + if len(sys.argv) == 1:
  112 + print("USAGE: {0} SQLITE_DB [ANDROID_ID]".format(sys.argv[0]))
  113 + print()
  114 + sys.exit(1)
  115 +
  116 + path = sys.argv[1]
  117 +
  118 + auth_key_value = get_setting(path, "PROFILE_AUTH_KEY")
  119 +
  120 + if len(sys.argv) == 2:
  121 + print("No ANDROID_ID given, bruteforcing.")
  122 + key = bruteforce_key(auth_key_value)
  123 + else:
  124 + key = java_string_hash(sys.argv[2])
  125 +
  126 + auth_key_plaintext = decrypt_setting(auth_key_value, key)
  127 + if not is_profile_auth_key(auth_key_plaintext):
  128 + print("Key seems to be wrong.")
  129 + sys.exit(2)
  130 +
  131 + mid, auth_key = get_encrypted_setting(path, key, "PROFILE_AUTH_KEY").split(":")
  132 +
  133 + print("User MID: " + mid)
  134 + print("Auth key: " + auth_key)
  135 + print("Name: " + get_encrypted_setting(path, key, "PROFILE_NAME"))
  136 + print("LINE ID: " + get_encrypted_setting(path, key, "PROFILE_ID"))
  137 + print("Region: " + get_encrypted_setting(path, key, "PROFILE_REGION"))
  138 + print("Phone: " + get_encrypted_setting(path, key, "PROFILE_NORMALIZED_PHONE"))
... ...