0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Chrome拡張 LINE 3.7.2のログイン維持問題を追った記録

0
Posted at

0. 発端

Xubuntuでlineのトークルームを見たい。
なので公式LINE Chrome拡張 3.7.2を使用した。

LINEウィンドウを閉じて再度開くだけならログイン状態は維持される。
しかし、Chromeを完全終了して起動し直すと、LINEがログイン画面に戻ることに気付いた。

メールアドレス+パスワードでログインしても結果は同じだった。

ここから、

LINEはログイン情報をどこに保存していて、Chrome終了時に何が失われているのか?

を追ってみることにした。


1. まずChromeプロセスの生存とログイン状態を比較

最初に分かったのは、LINEウィンドウを閉じただけの場合と、Chromeを完全終了した場合で挙動が違うことだった。

Chromeプロセスが生きたままの場合

LINEを×で閉じる
↓
再びLINEを起動
↓
ログイン状態を維持

Chromeを完全終了した場合

Chrome完全終了
↓
Chrome起動
↓
LINE起動
↓
ログイン画面

Chromeが完全に終了したかは、次のコマンドで確認した。

pgrep -a chrome

完全終了後は何も表示されなかった。

Chromeの

Google Chrome を閉じた際にバックグラウンド アプリの処理を続行する

はすでにONだったが、今回の環境では全ウィンドウを終了するとChromeプロセスは残らなかった。

ここで分かったこと

LINEウィンドウを閉じること自体ではログアウトしない。
Chromeプロセスの終了が境界になっている。


2. LINEは認証情報を保存していないのでは?

次に疑ったのは、

Chromeを終了すると、LINEの認証情報そのものが消えているのでは?

ということ。

main.js を調べると、LINEにはsecure storageらしき実装が存在した。

概略は次のようなものだった。

localStorage.setItem(this.localStorageKey, encryptedData)

キー名は、

lcs_secure_...

となっていた。

保存されるデータ構造には、次のような項目が含まれていた。

accessToken
refreshToken
durationUntilRefreshInSec
loginSessionId
tokenIssueTimeEpochSec
tokenIssuedVersion

つまりLINEは少なくとも設計上、accessToken と refreshToken を暗号化して localStorage へ保存している。


3. Chrome終了で lcs_secure_* が消えるのか確認

ログイン中にConsoleで次を実行した。

Object.entries(localStorage)
  .filter(([k]) => k.startsWith("lcs_secure_"))
  .map(([k,v]) => ({ count: 1, length: v.length }))

結果:

count: 1
length: 2528

Chromeを完全終了 → 再起動 → LINEがログアウト状態になったあとにも確認した。

結果は同じだった。

count: 1
length: 2528

ここで最初の仮説が否定された

Chrome終了後もsecure storage自体は消えていない。


4. 中身が同じかSHA-256で比較

長さが同じでも、内容が変わっている可能性がある。

そこで値そのものを表示せず、SHA-256だけ保存して比較した。

再起動前のhashを保存し、再起動後に比較すると、

再起動前と同じ? false

になった。

つまりLINEがログアウト状態になった時点では、lcs_secure_* の暗号文も書き換えられている。

ここで新しい疑問が出た。

Chrome終了時にChrome自身が書き換えているのか?

それとも、

LINE起動時に書き換えているのか?


5. LINEを起動する「前」にsecure storageを確認

Chromeを完全終了する前にhashを記録。

次に、

Chrome完全終了
↓
Chrome再起動
↓
LINEは起動しない

という状態にした。

LINEを起動せず、拡張の静的リソースを直接開き、同じextension originから localStorage を確認した。

結果:

LINE起動前でも同じ? true

だった。

ここはかなり重要

Chromeの終了・再起動そのものではsecure storageは変更されていない。

そして、

LINEアプリをcold startした後に初めて書き換えられている。

ことが分かった。


6. 暗号化データを復号できなくなっているのでは?

次に疑ったのは、

暗号文は残っているが、Chrome再起動後に復号できなくなっているのでは?

という可能性。

コードを追うと、

decryptWithStorageKey(...)

によって復号し、失敗すると空の初期データへフォールバックする実装だった。

また、storage keyの初期化では、

getEncryptedIdentityV3

が使われていた。

Networkで確認すると、

getEncryptedIdentityV3 → 200 OK

だった。

さらに復号後のコードへBreakpointを置き、this.data を確認した。

this.data === undefined
// false

this.data?.tokenIssuedVersion
// "3.7.2"

!!this.data?.tokenV3IssueResult?.accessToken
// true

!!this.data?.tokenV3IssueResult?.refreshToken
// true

これで復号失敗説も否定

Chrome再起動後にも、

  • secure storageは存在する
  • 正常に復号できる
  • バージョンも3.7.2
  • accessToken あり
  • refreshToken あり

だった。


7. TokenManagerにtokenが渡っていないのでは?

次にTokenManagerの初期化処理を追った。

コードでは概ね、

const e = getTokenV3IssueResult();

if (!e.accessToken)
    throw ACCESS_TOKEN_NOT_EXISTS;

...

this.setTokenV3IssueResult(e);

となっていた。

Breakpointで確認すると、

!!e.accessToken
// true

さらに setTokenV3IssueResult(e) 後も、

!!this.tokenV3IssueResult?.accessToken
// true

だった。

deinit() にBreakpointを置いても、初期化直後には呼ばれなかった。

これでTokenManagerへのロード失敗説もほぼ否定

Chrome再起動後にも、保存されたaccess tokenはTokenManagerまで正常に到達している。


8. それでも getProfile が認証失敗する

cold start時のNetworkを追うと、

getEncryptedIdentityV3 → 200
getProfile → 400

となった。

getProfile のレスポンスは次の通り。

{
  "code": 10051,
  "message": "RESPONSE_ERROR",
  "data": {
    "name": "TalkException",
    "code": 1,
    "reason": "Authentication Failed."
  }
}

つまり、

accessToken / refreshToken は復元できているのに、LINEのプロフィール取得では認証に失敗している。

そしてこの失敗後、

_T
↓
xT
↓
CT
↓
deinitStore

というリセット・ログアウト系処理に進むこともBreakpointで確認した。

途中で発生した、

SECURE_STORAGE_NOT_READY

は原因ではなく、失敗後のcleanup中に発生する二次的な例外だった。


9. lct Cookieを発見

コードを追うとログアウト処理に、

chrome.cookies.remove({
    ...
    name: "lct"
})

が存在した。

そこで lct を調べた。

ログイン中:

name: lct
session: true
expirationDate: undefined

つまりセッションCookieだった。

Chromeを完全終了 → Chromeだけ起動 → LINE起動前に確認すると、

lct count = 0

だった。

ここで分かったこと

Chrome終了時に lct は消える。


10. 「lct が消えるのが原因では?」という仮説

ここで一度、

Chrome終了
↓
lct消失
↓
LINE cold start
↓
getProfile Authentication Failed

という仮説を立てた。

ところがさらに調べると、ログイン画面の段階ですでに lct が存在することが分かった。

Chrome起動
↓
lctなし

LINE起動
↓
まだ未ログインなのにlctあり

つまり、単純に

lct が存在しないから認証失敗する

という話ではなかった。


11. ログイン前後で lct の中身は同じか

値そのものを表示せず、SHA-256で比較した。

ログイン前の lct のhashを保存し、ログイン後と比較。

結果:

ログイン前と同じ? false

つまり、

lct は未ログイン時にも存在するが、ログインすると内容が変更される。

ことが分かった。

これにより、lct が何らかのログインセッション状態に関与している可能性は依然として高い。


12. loginV2 に keepLoggedIn:false を発見

ログイン時のNetworkを見ると、

/api/talk/thrift/Talk/AuthService/loginV2

のPayloadに、

"keepLoggedIn": false

があった。

さらに main.js を検索。

巨大なminified JSだったので、Pythonで全出現箇所を抽出した。

from pathlib import Path

p = Path("static/js/main.js")
s = p.read_text(errors="replace")

word = "keepLoggedIn"
pos = 0
n = 0

while (i := s.find(word, pos)) != -1:
    n += 1
    print(f"\n===== occurrence {n} / offset {i} =====")
    print(s[max(0, i-300):i+500])
    pos = i + len(word)

print(f"\nTOTAL: {n}")

結果:

TOTAL: 1

唯一のコードは、

{
    type: FU.ID_CREDENTIAL,
    identityProvider: AU.LINE,
    identifier: "",
    password: "",
    keepLoggedIn: !1,
    ...
    ...t
}

だった。

!1 は false。

実際のNetwork Payloadも false だったので、

LINE Chrome拡張3.7.2では、通常のログイン時に keepLoggedIn:false が送られている。

ことが確認できた。


13. keepLoggedIn:true にすれば直るのでは?

これも実際に検証した。

loginV2 の送信直前でBreakpointを置いた。

実際のPayloadは、

r[0]

に入っていたので、

r[0].keepLoggedIn = true

としてResume。

NetworkのPayloadを確認すると、

"keepLoggedIn": true

になっていた。

つまり、本当に true がサーバーへ送信された。

その状態でもログイン自体は正常に成功した。


14. keepLoggedIn:true でChromeを完全終了

これで直るか確認した。

loginV2
keepLoggedIn:true
↓
ログイン成功
↓
Chrome完全終了
↓
Chrome再起動
↓
LINE起動

結果:

ログインできなかった

ここで分かったこと

keepLoggedIn:false が今回の問題の単独原因ではない。

ことが実験で確認できた。


15. true にすると lct が永続Cookieになるのでは?

これも比較した。

通常の keepLoggedIn:false:

name: lct
session: true
expirationDate: undefined

keepLoggedIn:true:

name: lct
session: true
expirationDate: undefined

同じだった。

したがって、

keepLoggedIn:true にしても lct は永続Cookieにならない。

ことも確認できた。


現時点で確実に分かっていること

まとめると、現在確認できている流れは次の通り。

LINE Chrome 3.7.2

Chromeを完全終了
        ↓
lct(session cookie)は消える
lcs_secure_* は残る
        ↓
Chrome再起動
        ↓
LINE cold start
        ↓
secure storageの復号成功
accessTokenあり
refreshTokenあり
TokenManagerへのロードも成功
        ↓
しかし getProfile
        ↓
10051 Authentication Failed
        ↓
ログアウト / リセット処理
        ↓
secure storageも書き換えられる
        ↓
ログイン画面

さらに、

lctはLINE起動時の未ログイン状態でも生成される
↓
ログインするとlctの内容は変化する

loginV2は通常 keepLoggedIn:false
↓
trueに改変して送信することもできる
↓
ログインは成功する
↓
しかしChrome再起動後のログイン維持には効果なし
↓
lctもsession cookieのまま

というところまで確認できた。


現時点では分かっていないこと

まだ確定していないのは、

なぜ保存済み accessToken / refreshToken を正常に復元できているのに、cold start後の getProfile が Authentication Failed になるのか

という点。

特に未調査なのは次のあたり。

  • tokenRefresh がどのタイミングで使われるのか
  • tokenRefresh が lct を更新するのか
  • getProfile が具体的に何を認証材料としているのか
  • ログイン後の lct がTalkセッションでどんな役割を持つのか

結論

原因特定には至らなかった。

ただし、少なくとも次の単純な原因ではないことまでは実験で確認できた。

  • Chromeが保存tokenを消している
  • secure storageの復号に失敗している
  • keepLoggedIn が false だからログイン維持できない

つまり、

「保存済みtokenは残っていて正常に復号もできる。それでもcold start後の認証に失敗する」

というところまでは絞り込めた。


注意

この記事で扱っている内容は、手元の環境で観測した結果をもとにしたものです。
LLMと一緒と一緒に調査し、LLMに記事を作ってもらっています。
動作環境、テスト方法、観測方法、記録内容などが常に正しいことを保証するものではありません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?