diff --git a/src/server/gkill/api/gkill_server_api/handle_browse_zip_contents.go b/src/server/gkill/api/gkill_server_api/handle_browse_zip_contents.go index 16491f5e5..ec927163c 100644 --- a/src/server/gkill/api/gkill_server_api/handle_browse_zip_contents.go +++ b/src/server/gkill/api/gkill_server_api/handle_browse_zip_contents.go @@ -231,16 +231,9 @@ func extractZip(zipFilePath string, cacheDir string) error { // ZIP仕様: Flags bit 11 (0x800) が立っていればUTF-8、そうでなければレガシーエンコーディング entryName := decodeZipEntryName(f) - // パストラバーサル防止 - name := filepath.FromSlash(entryName) - name = filepath.Clean(name) - if strings.HasPrefix(name, "..") || filepath.IsAbs(name) { - continue - } - - destPath := filepath.Join(tmpDir, name) - // destPathがtmpDir配下であることを確認 - if !strings.HasPrefix(filepath.Clean(destPath), filepath.Clean(tmpDir)+string(os.PathSeparator)) && filepath.Clean(destPath) != filepath.Clean(tmpDir) { + // パストラバーサル防止: tmpDir配下に収まるエントリのみ展開する + destPath, ok := reps.SecureJoin(tmpDir, entryName) + if !ok || filepath.IsAbs(filepath.FromSlash(entryName)) { continue } diff --git a/src/server/gkill/api/gkill_server_api/handle_get_kyous_mcp.go b/src/server/gkill/api/gkill_server_api/handle_get_kyous_mcp.go index 8aad9c36c..4eb6aa17f 100644 --- a/src/server/gkill/api/gkill_server_api/handle_get_kyous_mcp.go +++ b/src/server/gkill/api/gkill_server_api/handle_get_kyous_mcp.go @@ -144,10 +144,8 @@ func (g *GkillServerAPI) HandleGetKyousMCP(w http.ResponseWriter, r *http.Reques } // 候補IDを収集 - candidateCount := request.Limit - if candidateCount > len(batch) { - candidateCount = len(batch) - } + // request.Limitは冒頭でクランプ済みだが、割り当てサイズの上限を明示するためここでも定数で制限する + candidateCount := min(request.Limit, len(batch), maxLimit) candidateIDs := make([]string, 0) for i := range candidateCount { candidateIDs = append(candidateIDs, batch[i].ID) diff --git a/src/server/gkill/api/gkill_server_api/utils.go b/src/server/gkill/api/gkill_server_api/utils.go index ab688290f..434f99da3 100644 --- a/src/server/gkill/api/gkill_server_api/utils.go +++ b/src/server/gkill/api/gkill_server_api/utils.go @@ -14,11 +14,13 @@ import ( "log/slog" "net" "net/http" + "net/url" "os" "path/filepath" "regexp" "strconv" "strings" + "syscall" "time" "github.com/google/uuid" @@ -59,12 +61,11 @@ func sanitizeFilename(name string) string { func (g *GkillServerAPI) resolveFileName(repDir string, filename string, behavior req_res.FileUploadConflictBehavior) (string, error) { // OS不正文字・制御文字を除去する (クライアント側 sanitize_filename と同じ処理) filename = sanitizeFilename(filename) - // パストラバーサル対策: ファイル名をサニタイズしてrepDir外へのアクセスを禁止する - cleanFilename := filepath.Clean(filename) - if filepath.IsAbs(cleanFilename) || cleanFilename == ".." || strings.HasPrefix(cleanFilename, ".."+string(os.PathSeparator)) { + // パストラバーサル対策: repDir外へのアクセスを禁止する + fullFilename, ok := reps.SecureJoin(repDir, filename) + if !ok { return "", fmt.Errorf("invalid filename: path traversal detected") } - fullFilename := filepath.Join(repDir, cleanFilename) _, err := os.Stat(fullFilename) if err != nil { return fullFilename, nil @@ -306,17 +307,66 @@ func publicKey(priv any) any { } } -func httpGetBase64Data(url string) (string, error) { - req, err := http.NewRequest("GET", url, nil) +// maxHTTPGetBodyBytes は httpGetBase64Data が取得するレスポンスボディの上限サイズです。 +const maxHTTPGetBodyBytes = 10 * 1024 * 1024 + +// isDisallowedFetchIP はSSRF対策として、ユーザ指定URLの取得先にできないIPか判定します。 +// loopback・プライベート・リンクローカル・マルチキャスト・未指定アドレスを拒否します。 +func isDisallowedFetchIP(ip net.IP) bool { + if ip == nil { + return true + } + return ip.IsLoopback() || + ip.IsPrivate() || + ip.IsUnspecified() || + ip.IsLinkLocalUnicast() || + ip.IsLinkLocalMulticast() || + ip.IsMulticast() +} + +// ssrfSafeHTTPClient はユーザ指定URLの取得に使うHTTPクライアントです。 +// Dialer.Controlで実際の接続先IPを検証するため、DNSリバインディングやリダイレクトで +// 内部アドレスへ誘導されても接続段階で拒否されます。 +var ssrfSafeHTTPClient = &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + Control: func(network, address string, c syscall.RawConn) error { + host, _, err := net.SplitHostPort(address) + if err != nil { + return err + } + if isDisallowedFetchIP(net.ParseIP(host)) { + return fmt.Errorf("blocked request to disallowed address: %s", address) + } + return nil + }, + }).DialContext, + }, +} + +func httpGetBase64Data(urlString string) (string, error) { + parsedURL, err := url.Parse(urlString) + if err != nil { + err = fmt.Errorf("error at parse url %s: %w", urlString, err) + return "", err + } + if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + err = fmt.Errorf("unsupported url scheme %q at http get %s", parsedURL.Scheme, urlString) + return "", err + } + + req, err := http.NewRequest("GET", urlString, nil) if err != nil { err = fmt.Errorf("error at new http get request: %w", err) return "", err } - req.Header.Set("Referer", url) + req.Header.Set("Referer", urlString) - res, err := http.DefaultClient.Do(req) + res, err := ssrfSafeHTTPClient.Do(req) if err != nil { - err = fmt.Errorf("error at http get %s: %w", url, err) + err = fmt.Errorf("error at http get %s: %w", urlString, err) return "", err } defer func() { @@ -326,9 +376,13 @@ func httpGetBase64Data(url string) (string, error) { } }() - b, err := io.ReadAll(res.Body) + b, err := io.ReadAll(io.LimitReader(res.Body, maxHTTPGetBodyBytes+1)) if err != nil { - err = fmt.Errorf("error at read all body %s: %w", url, err) + err = fmt.Errorf("error at read all body %s: %w", urlString, err) + return "", err + } + if len(b) > maxHTTPGetBodyBytes { + err = fmt.Errorf("response body too large at http get %s", urlString) return "", err } diff --git a/src/server/gkill/api/gkill_server_api/utils_ssrf_test.go b/src/server/gkill/api/gkill_server_api/utils_ssrf_test.go new file mode 100644 index 000000000..6adeef2fc --- /dev/null +++ b/src/server/gkill/api/gkill_server_api/utils_ssrf_test.go @@ -0,0 +1,68 @@ +package gkill_server_api + +import ( + "net" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestIsDisallowedFetchIP(t *testing.T) { + disallowed := []string{ + "127.0.0.1", + "10.0.0.1", + "172.16.0.1", + "192.168.1.1", + "169.254.169.254", + "0.0.0.0", + "::1", + "fe80::1", + "fc00::1", + } + for _, s := range disallowed { + if !isDisallowedFetchIP(net.ParseIP(s)) { + t.Errorf("isDisallowedFetchIP(%s) = false, want true", s) + } + } + + allowed := []string{ + "93.184.216.34", + "8.8.8.8", + "2001:4860:4860::8888", + } + for _, s := range allowed { + if isDisallowedFetchIP(net.ParseIP(s)) { + t.Errorf("isDisallowedFetchIP(%s) = true, want false", s) + } + } + + if !isDisallowedFetchIP(nil) { + t.Error("isDisallowedFetchIP(nil) = false, want true") + } +} + +func TestHttpGetBase64Data_BlocksLoopback(t *testing.T) { + // loopbackで実際にサーバを立てても、接続段階で拒否されることを確認する + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Error("request to loopback server should have been blocked") + })) + defer server.Close() + + _, err := httpGetBase64Data(server.URL) + if err == nil { + t.Fatal("httpGetBase64Data to loopback should fail") + } + if !strings.Contains(err.Error(), "blocked") { + t.Errorf("error should mention blocked address, got: %v", err) + } +} + +func TestHttpGetBase64Data_RejectsScheme(t *testing.T) { + for _, u := range []string{"file:///etc/passwd", "ftp://example.com/a", "gopher://example.com"} { + _, err := httpGetBase64Data(u) + if err == nil || !strings.Contains(err.Error(), "unsupported url scheme") { + t.Errorf("httpGetBase64Data(%s) should fail with scheme error, got: %v", u, err) + } + } +} diff --git a/src/server/gkill/dao/account/account_dao_sqlite3_impl.go b/src/server/gkill/dao/account/account_dao_sqlite3_impl.go index dae93b56a..94018534d 100644 --- a/src/server/gkill/dao/account/account_dao_sqlite3_impl.go +++ b/src/server/gkill/dao/account/account_dao_sqlite3_impl.go @@ -292,7 +292,15 @@ VALUES ( account.IsEnable, account.PasswordResetToken, } - slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgs) + // パスワードハッシュ・リセットトークンはログに出さない + queryArgsForLog := []any{ + account.UserID, + "***", + account.IsAdmin, + account.IsEnable, + "***", + } + slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgsForLog) _, err = stmt.ExecContext(ctx, queryArgs...) if err != nil { @@ -334,7 +342,16 @@ WHERE USER_ID = ? account.PasswordResetToken, account.UserID, } - slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgs) + // パスワードハッシュ・リセットトークンはログに出さない + queryArgsForLog := []any{ + account.UserID, + "***", + account.IsAdmin, + account.IsEnable, + "***", + account.UserID, + } + slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgsForLog) _, err = stmt.ExecContext(ctx, queryArgs...) if err != nil { diff --git a/src/server/gkill/dao/plugin_manager.go b/src/server/gkill/dao/plugin_manager.go index b2d8b419b..089a47f2d 100644 --- a/src/server/gkill/dao/plugin_manager.go +++ b/src/server/gkill/dao/plugin_manager.go @@ -7,6 +7,7 @@ import ( "log/slog" "os" "path/filepath" + "strings" "github.com/mt3hr/gkill/src/server/gkill/api/gkill_plugin" "github.com/mt3hr/gkill/src/server/gkill/dao/reps" @@ -22,8 +23,21 @@ type PluginManager struct { plugins []reps.PluginRepository } +// isSingleSafePathElement は値を単一のパス要素として使ってよいか検証する。 +// 区切り文字・親ディレクトリ参照・空文字を含むものを拒否する。 +func isSingleSafePathElement(element string) bool { + if element == "" || element == "." || element == ".." { + return false + } + if strings.ContainsAny(element, `/\`) { + return false + } + return filepath.Clean(element) == element +} + // newPluginManager はユーザ別の PluginManager を生成する。 // まだプラグインの発見は行わない。 +// userID がパス要素として不正な場合はプラグイン無しとして扱う(pluginsDirを空にする)。 func newPluginManager(userID string) *PluginManager { // GKILL_HOME は InitGkillOptions() で設定される確定済みパスを使う。 // gkill_options.GkillHomeDir は "$HOME/gkill" のような未展開文字列のため、 @@ -32,7 +46,12 @@ func newPluginManager(userID string) *PluginManager { if pluginsBaseDir == "" || pluginsBaseDir == "$GKILL_HOME" { pluginsBaseDir = filepath.Clean(os.ExpandEnv(gkill_options.GkillHomeDir)) } - pluginsDir := filepath.Join(pluginsBaseDir, "plugins", userID) + pluginsDir := "" + if isSingleSafePathElement(userID) { + pluginsDir = filepath.Join(pluginsBaseDir, "plugins", userID) + } else { + slog.Warn(fmt.Sprintf("invalid user id for plugin dir, plugins disabled for user %q", userID)) + } return &PluginManager{ userID: userID, pluginsDir: pluginsDir, @@ -45,6 +64,10 @@ func newPluginManager(userID string) *PluginManager { // すでに登録済みのプラグインはスキップする(重複防止)。 // 発見失敗は警告ログに記録し、gkill本体の起動を止めない。 func (pm *PluginManager) DiscoverPlugins(ctx context.Context) error { + // pluginsDirが空 = userIDが不正でプラグイン無効 + if pm.pluginsDir == "" { + return nil + } if err := os.MkdirAll(pm.pluginsDir, os.ModePerm); err != nil { // ディレクトリ作成失敗はプラグイン無しとして扱う(警告のみ) slog.Warn(fmt.Sprintf("plugin dir create failed for user %s: %v", pm.userID, err)) diff --git a/src/server/gkill/dao/reps/idf_thumb_file_server.go b/src/server/gkill/dao/reps/idf_thumb_file_server.go index 1b7ee9b1a..52f5f860b 100644 --- a/src/server/gkill/dao/reps/idf_thumb_file_server.go +++ b/src/server/gkill/dao/reps/idf_thumb_file_server.go @@ -133,7 +133,7 @@ func (t *thumbFileServer) GenerateThumbCache(ctx context.Context, queryURL strin return nil } - abs, ok := secureJoin(t.rootDir, rel) + abs, ok := SecureJoin(t.rootDir, rel) if !ok { err := fmt.Errorf("bad path %s", queryURL) return err @@ -216,7 +216,7 @@ func (t *thumbFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - abs, ok := secureJoin(t.rootDir, rel) + abs, ok := SecureJoin(t.rootDir, rel) if !ok { http.Error(w, "bad path", http.StatusBadRequest) return @@ -305,8 +305,9 @@ func cleanRelURLPath(p string) (string, bool) { return cp, true } -// rootDir から外へ出ないように join -func secureJoin(rootDir, rel string) (string, bool) { +// SecureJoin は rootDir から外へ出ないように join する。 +// 結果が rootDir 配下でなければ ok=false を返す。 +func SecureJoin(rootDir, rel string) (string, bool) { root := filepath.Clean(rootDir) full := filepath.Join(root, filepath.FromSlash(rel)) full = filepath.Clean(full) diff --git a/src/server/gkill/dao/reps/idf_video_file_server.go b/src/server/gkill/dao/reps/idf_video_file_server.go index ec7583a62..1d9c05fde 100644 --- a/src/server/gkill/dao/reps/idf_video_file_server.go +++ b/src/server/gkill/dao/reps/idf_video_file_server.go @@ -82,7 +82,7 @@ func (v *IDFVideoFileServer) ensureServePathForURL(ctx context.Context, u *url.U return ensuredVideo{}, false, nil } - abs, ok := secureJoin(v.rootDir, rel) + abs, ok := SecureJoin(v.rootDir, rel) if !ok { return ensuredVideo{}, false, nil } diff --git a/src/server/gkill/dao/server_config/server_config_dao_sqlite3_impl.go b/src/server/gkill/dao/server_config/server_config_dao_sqlite3_impl.go index 634ee1c0e..1d106a01c 100644 --- a/src/server/gkill/dao/server_config/server_config_dao_sqlite3_impl.go +++ b/src/server/gkill/dao/server_config/server_config_dao_sqlite3_impl.go @@ -824,7 +824,12 @@ INSERT INTO SERVER_CONFIG ( key, value, } - slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgs) + queryArgsForLog := []any{ + serverConfig.Device, + key, + sqlite3impl.MaskSensitiveValueForLog(key, value), + } + slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgsForLog) _, err = stmt.ExecContext(ctx, queryArgs...) if err != nil { @@ -965,7 +970,12 @@ INSERT INTO SERVER_CONFIG ( key, value, } - slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", insertSQL, queryArgs) + queryArgsForLog := []any{ + serverConfig.Device, + key, + sqlite3impl.MaskSensitiveValueForLog(key, value), + } + slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", insertSQL, queryArgsForLog) _, err = countStmt.ExecContext(ctx, queryArgs...) if err != nil { @@ -984,7 +994,12 @@ INSERT INTO SERVER_CONFIG ( serverConfig.Device, key, } - slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgs) + queryArgsForLog := []any{ + sqlite3impl.MaskSensitiveValueForLog(key, value), + serverConfig.Device, + key, + } + slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgsForLog) _, err = updateStmt.ExecContext(ctx, queryArgs...) if err != nil { @@ -1180,7 +1195,12 @@ INSERT INTO SERVER_CONFIG ( key, value, } - slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", insertSQL, queryArgs) + queryArgsForLog := []any{ + serverConfig.Device, + key, + sqlite3impl.MaskSensitiveValueForLog(key, value), + } + slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", insertSQL, queryArgsForLog) _, err = countStmt.ExecContext(ctx, queryArgs...) if err != nil { @@ -1210,7 +1230,12 @@ INSERT INTO SERVER_CONFIG ( serverConfig.Device, key, } - slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgs) + queryArgsForLog := []any{ + sqlite3impl.MaskSensitiveValueForLog(key, value), + serverConfig.Device, + key, + } + slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgsForLog) _, err = updateStmt.ExecContext(ctx, queryArgs...) if err != nil { @@ -1420,8 +1445,13 @@ INSERT INTO SERVER_CONFIG ( key, value, } + queryArgsForLog := []any{ + serverConfig.Device, + key, + sqlite3impl.MaskSensitiveValueForLog(key, value), + } slog.Log(ctx, gkill_log.TraceSQL, "sql", "sql", insertSQL) - slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", insertSQL, queryArgs) + slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", insertSQL, queryArgsForLog) _, err = insertStmt.ExecContext(ctx, queryArgs...) if err != nil { err = fmt.Errorf("error at query :%w", err) diff --git a/src/server/gkill/dao/sqlite3impl/sqlite3impl_util.go b/src/server/gkill/dao/sqlite3impl/sqlite3impl_util.go index 5747749f0..f5c46e31d 100644 --- a/src/server/gkill/dao/sqlite3impl/sqlite3impl_util.go +++ b/src/server/gkill/dao/sqlite3impl/sqlite3impl_util.go @@ -18,6 +18,23 @@ import ( const TimeLayout = "2006-01-02T15:04:05-07:00" +// sensitiveLogColumns はTraceSQLログで値をマスクする機密カラム名です。 +var sensitiveLogColumns = map[string]struct{}{ + "GOOGLE_MAP_API_KEY": {}, + "GKILL_NOTIFICATION_PRIVATE_KEY": {}, + "PASSWORD_SHA256": {}, + "PASSWORD_RESET_TOKEN": {}, +} + +// MaskSensitiveValueForLog は機密カラムの値をログ用に "***" へ置き換えます。 +// SQLの実行引数には使わず、TraceSQLログ出力にのみ使うこと。 +func MaskSensitiveValueForLog(key string, value any) any { + if _, ok := sensitiveLogColumns[key]; ok { + return "***" + } + return value +} + func EscapeSQLite(str string) string { return strings.ReplaceAll(str, "'", "''") } diff --git a/src/server/gkill/dao/user_config/application_config_dao_sqlite3_impl.go b/src/server/gkill/dao/user_config/application_config_dao_sqlite3_impl.go index f475c4e60..f71f7e39a 100644 --- a/src/server/gkill/dao/user_config/application_config_dao_sqlite3_impl.go +++ b/src/server/gkill/dao/user_config/application_config_dao_sqlite3_impl.go @@ -1083,7 +1083,13 @@ INSERT INTO APPLICATION_CONFIG ( key, value, } - slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgs) + queryArgsForLog := []any{ + applicationConfig.UserID, + device, + key, + sqlite3impl.MaskSensitiveValueForLog(key, value), + } + slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgsForLog) _, err = insertStmt.ExecContext(ctx, queryArgs...) if err != nil { err = fmt.Errorf("error at add application config sql: %w", err) @@ -1364,7 +1370,13 @@ INSERT INTO APPLICATION_CONFIG ( key, value, } - slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", insertSQL, queryArgs) + queryArgsForLog := []any{ + applicationConfig.UserID, + device, + key, + sqlite3impl.MaskSensitiveValueForLog(key, value), + } + slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", insertSQL, queryArgsForLog) _, err = insertStmt.ExecContext(ctx, queryArgs...) if err != nil { @@ -1395,7 +1407,13 @@ INSERT INTO APPLICATION_CONFIG ( device, key, } - slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgs) + queryArgsForLog := []any{ + sqlite3impl.MaskSensitiveValueForLog(key, value), + applicationConfig.UserID, + device, + key, + } + slog.Log(ctx, gkill_log.TraceSQL, "sql: %s query: %#v", sql, queryArgsForLog) _, err = updateStmt.ExecContext(ctx, queryArgs...) if err != nil { err = fmt.Errorf("error at query :%w", err) diff --git a/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillApiClient.kt b/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillApiClient.kt index 5a56d352c..66b687492 100644 --- a/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillApiClient.kt +++ b/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillApiClient.kt @@ -23,31 +23,38 @@ import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager -/** Minimal gkill API client using OkHttp (blocking, call from coroutine/thread). */ -class GkillApiClient(private val serverUrl: String) { +/** + * Minimal gkill API client using OkHttp (blocking, call from coroutine/thread). + * + * @param allowSelfSignedCert When true, certificate and hostname verification are + * disabled so that a gkill server running on localhost with a self-signed + * certificate can be reached. Defaults to false (standard platform validation); + * enable only via the explicit user setting. + */ +class GkillApiClient( + private val serverUrl: String, + private val allowSelfSignedCert: Boolean = false +) { private val client = buildOkHttpClient() - /** - * Build an OkHttpClient that trusts all certificates. - * gkill typically runs on localhost with a self-signed certificate, - * so standard certificate validation would always fail. - */ private fun buildOkHttpClient(): OkHttpClient { - val trustAll = object : X509TrustManager { - override fun checkClientTrusted(chain: Array, authType: String) {} - override fun checkServerTrusted(chain: Array, authType: String) {} - override fun getAcceptedIssuers(): Array = arrayOf() - } - val sslContext = SSLContext.getInstance("TLS").apply { - init(null, arrayOf(trustAll), SecureRandom()) - } - return OkHttpClient.Builder() - .sslSocketFactory(sslContext.socketFactory, trustAll) - .hostnameVerifier { _, _ -> true } + val builder = OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) - .build() + if (allowSelfSignedCert) { + val trustAll = object : X509TrustManager { + override fun checkClientTrusted(chain: Array, authType: String) {} + override fun checkServerTrusted(chain: Array, authType: String) {} + override fun getAcceptedIssuers(): Array = arrayOf() + } + val sslContext = SSLContext.getInstance("TLS").apply { + init(null, arrayOf(trustAll), SecureRandom()) + } + builder.sslSocketFactory(sslContext.socketFactory, trustAll) + .hostnameVerifier { _, _ -> true } + } + return builder.build() } private val json = Json { ignoreUnknownKeys = true } diff --git a/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillCredentialStore.kt b/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillCredentialStore.kt index 80c6158b5..1687b26e2 100644 --- a/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillCredentialStore.kt +++ b/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillCredentialStore.kt @@ -27,6 +27,12 @@ class GkillCredentialStore(context: Context) { prefs.edit().putString("password_sha256", hash).apply() } + fun getAllowSelfSignedCert(): Boolean = prefs.getBoolean("allow_self_signed_cert", false) + + fun setAllowSelfSignedCert(allow: Boolean) { + prefs.edit().putBoolean("allow_self_signed_cert", allow).apply() + } + fun getSessionId(): String = prefs.getString("session_id", "") ?: "" fun setSessionId(id: String) { diff --git a/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillWearableListenerService.kt b/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillWearableListenerService.kt index fbaddf505..714cd7c3f 100644 --- a/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillWearableListenerService.kt +++ b/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/GkillWearableListenerService.kt @@ -36,7 +36,7 @@ class GkillWearableListenerService : WearableListenerService() { override fun onMessageReceived(event: MessageEvent) { Log.d(TAG, "onMessageReceived path=${event.path} sourceNode=${event.sourceNodeId}") val store = GkillCredentialStore(this) - val apiClient = GkillApiClient(store.getServerUrl()) + val apiClient = GkillApiClient(store.getServerUrl(), store.getAllowSelfSignedCert()) scope.launch { when (event.path) { diff --git a/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/MainActivity.kt b/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/MainActivity.kt index 7febc2d00..4ac91f281 100644 --- a/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/MainActivity.kt +++ b/src/wear_os/phone_companion/src/main/java/com/gkill_android/mobile_app/src/gkill/mt3hr/gkill/wear/companion/MainActivity.kt @@ -60,6 +60,11 @@ class MainActivity : AppCompatActivity() { android.text.InputType.TYPE_TEXT_VARIATION_PASSWORD layoutParams = lp } + val cbAllowSelfSigned = android.widget.CheckBox(this).apply { + text = "自己署名証明書を許可 (localhost等の自己署名HTTPSサーバー向け。証明書検証が無効になります)" + isChecked = store.getAllowSelfSignedCert() + layoutParams = lp + } val btnSave = Button(this).apply { text = "保存 & 接続テスト" layoutParams = lp @@ -73,6 +78,7 @@ class MainActivity : AppCompatActivity() { layout.addView(etServerUrl) layout.addView(etUserId) layout.addView(etPassword) + layout.addView(cbAllowSelfSigned) layout.addView(btnSave) layout.addView(tvStatus) @@ -92,15 +98,17 @@ class MainActivity : AppCompatActivity() { } val passwordSha256 = sha256(password) + val allowSelfSigned = cbAllowSelfSigned.isChecked store.setServerUrl(serverUrl) store.setUserId(userId) store.setPasswordSha256(passwordSha256) + store.setAllowSelfSignedCert(allowSelfSigned) store.clearSession() tvStatus.text = "接続テスト中..." CoroutineScope(Dispatchers.IO).launch { - val client = GkillApiClient(serverUrl) + val client = GkillApiClient(serverUrl, allowSelfSigned) val (sessionId, errorMsg) = client.loginWithError(userId, passwordSha256) withContext(Dispatchers.Main) { if (sessionId != null) {