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に記事を作ってもらっています。
動作環境、テスト方法、観測方法、記録内容などが常に正しいことを保証するものではありません。