Posts for: #Security

evernote から upnote への移行

6時に寝て8時半に起きた。オフィスで3時前ぐらいまで調べものして、帰ってからも眠れなくてネットしてたら朝になった。

今日の筋トレは腹筋:15x2,腕立て:10x1,スクワット10x1をした。

サンライズ出雲の予約

以前 やまさきさんに教えていただいた夜行列車 を予約してみた。列車は瀬戸と出雲の2つがある。岡山〜東京間は瀬戸と出雲が連結して運行するそうなのでどちらを選択しても同じだと思うけれど、出雲で予約してみた。乗り込む車両が異なるのかな?

三ノ宮(00:11発) => 東京 (07:08着)

  • 特急サンライズ出雲(シングル)
    • 料金券部分 10,460円 (三ノ宮 - 東京)
    • 乗車券部分 9,460円 (神戸市内 - 東京都区内 2月6日~2月9日まで有効)

前回に 専用の予約画面 があることを調査済みだったので迷わず予約を取れた。唯一、面倒なところがオンラインで予約した後に紙の切符を発行しないといけないところ。切符を発行するには緑の窓口・券売機へ行かないといけない。三ノ宮駅にはあるので問題はない。発行には次の3つが必要になる。初めてだから予約後、すぐに駅へ行って発行してみた。迷わず簡単にできたのでこれなら乗車する直前でもそう困らないように思える。

  • 予約したクレジットカード
  • 電話番号の下四桁
  • 予約番号

upnote への移行

数年前から evernote のビジネスが安定していないのは既知になっていたが、ここ1-2年でフリープランをどんどん縮小して投資回収的な値上げを強行している。

値上げするだけならまだしも、アプリを開くたびに有料プランにアップグレードするためのダイアログを表示して、意図的にユーザー体験を悪化させて、フリープランのユーザーを退会させたいようにみえる。ここまで露骨にやって既存の課金ユーザーだけで十分にビジネスはペイするという目論見なのか、経営陣が短期的にV字回復させたという実績作りなのか。いずれにしても、私のようなちょっと使いのフリープランユーザーはターゲットではないらしい。どうやら2011年6月から使っていたらしいが、件数にすると216件のノートしかない。週に1-2回は使うのだけど、なくなったら別アプリに移行しても困らない程度の用途にしか使っていない。

ずっと移行したいと思いつつ、なおざりにしていた理由として移行先のアプリを選定していなかった。私の用途としては次のような使い方をしている。

  • 買いものの備忘録として思いついたときにパソコンから記録し、スマホでチェックしながら買いものする
  • 法人番号、住所、家族の誕生日など、オンラインで手入力しないといけないときなどのコピペ元
  • 郵便受けのダイヤルロックを忘れたときのメモ
  • パソコンやデバイスを購入したときのスペックを記録しておく
  • オンラインの記事やサイトを保管しておくときの web クリップ

正直メモ帳アプリでなくても、他のアプリへ移行することもできるが、惰性で使っているのでできればそのまま移行したい。私の周りでは obsidian を使っている人も多く評判もいいからこれでもよかったんだけど、いくつか記事を読んでいると upnote の評判もよさそうにみえた。また upnote はサブスクリプションモデルではなく、パッケージライセンスなのでコストが安い。app store 経由でプレミアムのライセンスを購入する。為替によって値段が変わると思うが、いま買ったら3,500円だった。この値段で未来永劫 upnote のインフラコストを賄えるのかは懐疑的だが、3,500円ぐらいならそうなったときにまた別のメモ帳アプリへ移行するのでもよいかなというぐらいの気持ちで upnote を使ってみることにした。

web 版はなくて、それぞれのデバイスごとにインストールする。linux は snap パッケージが用意されているので ubuntu なら次のようにインストールする。

$ sudo snap install upnote

evernote から移行をするにあたり、.enex という xml ファイル形式でデータをエクスポートできる。これは macos または windows アプリでないとエクスポートできない。仕方ないから macos に evernote のアプリをインストールしてエクスポートする。ノートブック単位でエクスポートできる。

その後、upnote の macos アプリでインポートしようと試みるも macos 版にはインポート機能が見当たらない。linux 版にはインポート機能があったので .enex ファイルを linux にコピーしてきて linux 版でインポートしたら移行を完了した。移行の制約事項は次の通り。216件のノートしかないので10分もあれば完了した。同期もかなり速くて、すぐに iphone, macos, linux でノートが同期された。

最適なパフォーマンスを得るには、次の点に注意してください。

  • 多数のノートをインポートすると、アプリのパフォーマンスに影響を与える可能性があります。
  • 20MBを超えるファイルはインポートされません。
  • 300,000文字を超えるノートはインポートされません。

インストールしてから、このアプリの開発元はどこなんだろう?と調べてみるとベトナムの会社らしい。ややセキュリティは大丈夫なのかな?と不安に思ってしまう。Frequently Asked Questions から引用する。firebase を使っているならストレージは強固だし、同期も firebase に委譲すればおかしなことも起こらないだろう。

アップノートはどのように私のデータを保存するのですか?

UpNoteは、アカウントにサインアップしなくても確実に動作するように設計されています。サインインしない場合、メモはあなたのデバイスにローカルに残ります。デバイス間でメモを同期したい場合は、アカウントにサインアップすると、UpNoteが中央サーバーにメモを保存し、他のデバイスと同期できるようになります。

お客様はご自身のデータを完全に管理することができ、アップノートがお客様のデータにアクセスしたり、お客様のデータを第三者に販売したりすることは決してありません。プライバシーに関する詳細は、プライバシーポリシーをご覧ください。

アップノートのサーバーはどこにありますか?安全ですか?

UpNoteはFirebaseサーバー(Googleが提供するサービス)にデータを保存します。Firebaseプラットフォームは、主要なプライバシーおよびセキュリティ基準で認定されており、EU一般データ保護規則(GDPR)およびカリフォルニア州消費者プライバシー法(CCPA)を完全にサポートしています。Firebaseは、HTTPSを使用して転送中のデータを暗号化し、静止時のデータを暗号化します。詳しくは https://firebase.google.com/support/privacy をご覧ください。また、お客様のデータが安全に保護され、お客様だけがアクセスできるように細心の注意を払っています。

アップノートはエンドツーエンド暗号化に対応していますか?

エンドツーエンド暗号化(E2EE)は、データを暗号化・復号化するための高度なセキュリティ手法で、機密性の高い情報を保護するために設計されています。実装が複雑なため、アップノートでは現在E2EEをサポートする予定はありません。パスワードやクレジットカード番号などの機密情報を保存する場合は、機密情報を暗号化するために特別に設計されたパスワードマネージャーアプリケーションを使用することをお勧めします。

e2ee に対応していないところは残念なところ。未対応のセキュリティリスクの懸念としては、upnote 社の社員が私のノートの内容を閲覧できる可能性があったり、攻撃者が upnote 社のインフラに侵入してしまった場合、メモ帳の内容が外部に漏洩してしまう可能性がある。とはいえ、値段も安いのだからそこは割り切って機密情報はすべてパスワードマネージャに移行せよという戦略は理解できる。私の用途だと、メモ帳に機密情報はほとんどないけれど、賃貸の部屋のインターネット接続のアカウント情報や wifi パスワードなどを書いていたりする。こういった内容もこの機会に整理して 1password へ移行していくのがよいように思う。

RemoteAddr はほとんど役に立たない

0時に寝て何度か起きて7時に起きた。寒くて朝起きれなくなってきた。

ローカルネットワークからのリクエストのみを受け付けるミドルウェア

Request 構造体の中にある RemoteAddr を参照すればすぐできるだろ思って、すぐにできた。すぐにできたんだけど、これは実際の運用で使えるものではない。

type LocalNetworkConfig struct {
	Skipper middleware.Skipper
}

func localNetworkWithConfig(cfg LocalNetworkConfig) echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			if cfg.Skipper(c) {
				return next(c)
			}

			req := c.Request()
			addr, _, err := net.SplitHostPort(req.RemoteAddr)
			if err != nil {
				slog.Error("failed to get remote address",
					"err", err, "addr", addr)
				return echo.ErrForbidden
			}
			ip := net.ParseIP(addr)
			if ip.IsPrivate() || ip.IsLoopback() {
				return next(c)
			}

			slog.Error("requested from an external network", "ip", ip.String())
			return echo.ErrForbidden
		}
	}
}

func NewLocalNetwork() echo.MiddlewareFunc {
	return localNetworkWithConfig(LocalNetworkConfig{
		Skipper: func(c echo.Context) bool { return false },
	})
}

過去にも同じようなことをやらかしたような既視感がある。

golangのnet.Request構造体内にはRemoteAddr属性があり、当然のことながら要求元のリモートアドレスが含まれています。これで仕事は終わりですね?しかし、リバースプロキシやロードバランサーをアプリケーションに使っている場合はそうではありません。これはgoサーバには常に、すべてのリクエストがロードバランサから来ているかのように見えます。これは、これを何らかのスロットリングの指標として使いたいのであれば、最悪なことです。ですから、何らかの形のリバースプロキシの後ろでなく、goサーバーが1つだけ動いているのでない限り、私たちの目的には役に立たないとして、すぐにこれを捨てることができます。

https://husobee.github.io/golang/ip-address/2015/12/17/remote-ip-go.html

途中のロードバランサーやアプリケーションゲートウェイ、リバースプロキシなど、中継するサーバーの ip アドレスに置き換わってしまうため、ネットワークのインフラを管理していないとどこからリクエストされたかを追跡することはできない。X-Real-IPX-Forwarded-For という http ヘッダーに中継したサーバーの ip アドレスを記録するという慣習があるようだけど、これはクライアント側で書き換えできるのでこれ単体でアクセス制御のようなものに使うことはできない。

次に同じ失敗をしないようにここに書いておく。

パスワードリセットのテストのためのガイドライン

0時に寝て何度か起きて4時に起きて仮眠して6時に起きた。あまり寝た気がしない。

OWASP のパスワードリセットのガイドライン

パスワードリセットの仕組みをメンバーに開発してもらっている。そのコードレビューが先週から白熱している。セキュリティが関連するので堅牢に作る必要があるのでここはあまり妥協せきない。お手伝い先のシニアエンジニアから独自設計で作るのではなく、最低限、世の中の一般的なガイドラインに従っているかを確認するために OWASP のガイドラインを紹介してくれた。これはパスワードリセットの仕組みをテストするための要項をあげている。

これをメンバーに読んでもらって理解して実装しろと言いたいところでもあるが、私自身、読んだことがないとレビューできないことに気付いて、これは私が読んだ上で既存の設計や実装を見直すべきだと判断して deepl を駆使しながらほとんどを読んでみた。たしかに読んでみていくつか抜け・漏れに気付いたり、うちのセキュリティポリシーとして意図的に緩和しているところも認識できたりして、結論から言って読んでよかったと思う。当初はパスワードリセットのために一時トークンを1つだけ使っていたのだけど、それも2つを別々の経路に送って、割符のように組み合わせて認証する仕組みに変更した。よりセキュアにするという意図では一時トークンも1つよりも2つの方がよいというのは概ね正しいと思う。

ステートレスな認証という概念

0時に寝て4時ぐらいに起きてだらだらして7時半に起きた。やっぱりあまり眠れない。

ステートレスな認証という概念

次の開発フェーズが始まっていて、ちょっと時間が経ってしまったが、前開発フェーズのお披露目的な製品紹介をお手伝い先の全社向けに行った。主には直近の開発フェーズで追加した機能などを紹介した。その過程で新たに認証の仕組みを追加して jwt で認証するといった話しをしたところ、それはステートレスなのかどうかといった質問が出た。セキュリティを考慮して、アーキテクチャ的にフロントエンドの認証と api サーバーの認証は分けて実装しているのと、そのために仕組みも複雑になっているのだけど、ステートレスという言葉が指す意図を私がよくわかっていなくて、うまく説明できなかった。説明を終えた後にアーキテクチャのイメージ図と一緒に補足をしながらやり取りして次の記事を教えてもらった。

jwt は暗号化の技術で認証する仕組みなので有効期限が切れるまでは有効なアクセストークンとなる。そのため、jwt のみだとログアウトという概念はないため、そこをどうしているのか?という質問だった。フロントエンド/api サーバーともに session をオンメモリで保持して、ログインしたユーザーを管理しているため、ログアウトしたら session からレコードを削除することで有効な jwt のアクセストークンが来ても認証エラーにしてしまうことでステートをもった認証方式を実現している。とくまる先生が次のように説明しているところ。

「セッションIDをJWTに内包する」 という考え方です。

うちはこれをセッション ID ではなくユーザー名でやっている。とくに難しいことをやっているわけではなく、普通に実装したらそんな感じかな?と考えていたが、jwt = ステートレス認証だと思い込んでいる人たちがいるから ストートレスな認証 というキーワードが出てきたんだなと理解できた。最近のトレンドとしてはログアウトで jwt のアクセストークンを無効にできないと脆弱性と指摘される可能性がありそうとも書いてある。

ログアウト時にJWTを無効化できない実装は今後脆弱性診断で「OWASP Top 10 2021違反」と指摘されるようになりそう(今も個別にされてるかもしれないけど)

私はアーキテクチャ的にブラウザに api サーバーのアクセストークンをみせないというところに注力して認証機能の開発をサポートしていた。それ自体も間違っていないとは思うけど、今回の質問はその工夫とは異なるところの質問だった。認証は難しい。

縁の下のマネージャー

20時にホテルに戻ってきてのんびりしながら気付いたら22時ぐらいになって、少しテレビをみて0時に寝て4時ぐらいから起きてその後はあまり眠れなかった。それでも7時過ぎまでだらだらしていた。

7月後半に実装予定の新機能の設計

9月までに実装する新機能のうち、唯一、私の頭の中で設計の見通しをもっていなかった機能の設計を行うことにした。

ざっくりした機能概要から私がふわっと想定していたものはずっと複雑なものだったのだけど、プロダクトオーナーに要件をヒアリングしているうちにそんな高度なものは求められていないことに気付いた。逆にその高度な機能の仕組みを提供しても、実際に運用の現場で使うにあたって手間暇だけかかってそんなものを求めていないと言われそうな気がした。そこで私が作りたいなと思っていた設計のアイディアは封印することにした。既存の先行プロダクトがもっている機能とほぼ同様のものを、うちらの開発しているプロダクトで実現するだけでよさそうにみえた。そのシンプルな機能の設計を軽くやっておいた。詳細を詰めるのは次のマイルストーンで私ではないメンバーに実装してもらうことになるけれど、なんとなく当初の想定よりも早くできそうに思えた。

ログ出力のリファクタリング

id 連携の処理で複雑なリソースを map 型で扱うときデバッグ用途でリソースを丸ごと dump したい。しかし、パスワードのような機密情報が含まれる場合はそれらはログに出力したくない。この処理をいまは連携種別ごとに実装していて、本質じゃないところで個別実装の手間があるのと機密情報の出力というセキュリティに関するところを毎回プログラマーが手で実装するのもどうかな?という気がして汎用のログユーティリティとしてロガーのライブラリ側で提供することにした。インフラやプラットフォーム的な機能に私は積極的に開発に介入している。

やり方の1つとしてオリジナルのリソースをコピーして機密情報だけ削除した一時的なリソースコピーを dump してログ出力する。go 1.21 で標準ライブラリに追加される maps パッケージを使うと map の操作が簡単にできる。コピー関数もある。しかし、この機能は shallow copy なので map の値にネストした map が含まれる場合はオリジナルの値を書き換えてしまう。ネストした map を調べてそれらもクローンしていく処理を実装した。excludeKeys に除外したい任意のキーを渡し、map の値を再帰的にチェックして取り除く。最終的には次のようなコピーユーティリティになった。

func copyWithoutExcludeKeys(
	fields map[string]any, excludeKeys []string,
) map[string]any {
	cloned := maps.Clone(fields)
	for k, v := range cloned {
		switch t := v.(type) {
		case map[string]string:
			strMapCloned := maps.Clone(t)
			for _, sk := range maps.Keys(strMapCloned) {
				if slices.Contains(excludeKeys, sk) {
					delete(strMapCloned, sk)
				}
			}
			cloned[k] = strMapCloned
		case map[string]any:
			cloned[k] = copyWithoutExcludeKeys(t, excludeKeys)
		case []map[string]any:
			for i, v := range t {
				t[i] = copyWithoutExcludeKeys(v, excludeKeys)
			}
		default:
			if slices.Contains(excludeKeys, k) {
				delete(cloned, k)
			}
		}
	}
	return cloned
}

yubikey bio を完全にマスターした

2時に寝て遅くまで飲んでいたせいか、やや吐き気もしながら朝起きてだらだらして11時ぐらいに起きた。

1password のロック解除を yubikey で行う

先日購入して触っていた yubikey bio の設定 の続き。1password のロック解除を指紋認証で行いたかったが、1password の 8.10.4 のアプリではロック解除をシステム認証で行おうとするとエラーが発生していた (バグってた) 。5月の頭に 8.10.6 がリリースされていてシステム認証のバグが直っていることに気付いた。1password のアプリケーションがどうやって linux のシステム認証を使っているかは次の1文に書いてある。

システム認証は、Linux のユーザーアカウントに組み込まれたアクセス制御機構を使用します。これは polkit と PAM (Pluggable Authentication Modules) という2つの Linux 標準に依存しています。この2つを組み合わせることで、安全な認証サービスを提供します

https://support.1password.com/system-authentication-linux/#about-system-authentication-security

私は polkit を使ったことがなくて初見でよく分かっていないが、どうやら polkit から pam を介して認証しているようにみえる。pam.d 配下の設定を調べてみると /etc/pam.d/polkit-1 がある。前回の設定で pam.d の設定とテストの方法を理解していた。ここまでくれば Module Arguments のドキュメントをみながらオプションを設定するだけ。1Password のロック解除をYubiKeyでやる の記事で origin/appid にホスト名を指定しているが、最新のモジュールだと指定しないときはデフォルトでホスト名が使われるようにみえる。

  • origin: FIDO 認証手順の party ID を設定する、値が指定されない場合は識別子 pam://$HOSTNAME が使用される
  • appid: FIDO 認証手順の application ID を設定する、値を指定しない場合は origin で使用されたものと同じ値が使用される (origin も指定しない場合は pam://$HOSTNAME)
  • nouserok: 認証しようとするユーザーが authfile 内に存在しない場合や authfile が欠落/不正確な場合でも、認証の試行を成功させるように設定する
  • cue: タッチすることを促すメッセージを表示するように設定する

これらを踏まえて u2f で認証が成功したらそれ以降の処理をスキップするように次の設定を先頭に追加する。

$ sudo vi /etc/pam.d/polkit-1
auth   sufficient   pam_u2f.so nouserok cue
...

cue を指定したことでパスワードプロンプトを表示せず、デバイスをタッチするように促される。よい感じ。

$ pamtester polkit-1 t2y authenticate
Please touch the device.
pamtester: successfully authenticated

これで pam の polkit の設定が正しいことを検証できる。この後に 1password のアプリケーションでロック解除するときに u2f を使って指紋認証できた。めっちゃ便利。

yubikey bio を触ってみた

3時半に寝て7時に起きた。リリース終えたので晩ご飯を食べてから自分の会社のお仕事をしていたら遅くなった。

ストレッチ

今週もリリース明けでそれほど座っている時間が長かったわけでもないため、腰の張りはあまりなく、負荷は低くなっているのではないかと推測する。今日の開脚幅は開始前156cmで、ストレッチ後158cmだった。右股関節周りの硬さは相変わらず大きく変化はないが、他がよくなった分、右ふともも前の筋を伸ばすと張りがきつくてしんどいように感じた。しばらく余裕のある生活ができるので歩いたりストレッチしたりする余裕が出てくるかもしれない。

yubikey bio を触ってみた

先日 yubikey bio をオンラインストアで購入 した。会社の経費で業務で使うことを目的に購入したのでこの場合は商用利用の輸入にあたる。また別途、輸入についての会計処理を書く。yubico はスウェーデンの会社でどうやら日本向けはスウェーデンの首都ストックホルムから発送されてきたらしい。安いエコノミープランで注文していた。発送メールを受け取ったのが 4/17 でオフィスの郵便受けに入っていることに気付いたのが 4/29 になる。12日で届いたことになる。船便にしては早いからなにかしら航空便の安いプランで届いたのだと推測する。

Economy - 10-20 Working Days - No tracking available

早速、接続していろいろ触ってみた。結論から言って ubuntu 22.04 で fido2 pin を設定して u2f を使って2要素認証のメソッドとして pam や web アプリケーションから問題なく使えた。ubuntu 向けのアプリケーションも ppa のリポジトリを追加して apt からインストールできる。

このドキュメントには次の4つのアプリケーションのインストールが書いてある。

  • YubiKey Manager (CLI): sudo apt install yubikey-manager
  • YubiKey Personalization Tool: sudo apt install yubikey-personalization-gui
  • libpam-yubico: sudo apt install libpam-yubico
  • libpam-u2f: sudo apt install libpam-u2f

yubikey は他にもいろいろな認証の用途に使えるらしい。今回は fido2 の設定のみを紹介する。fido2 pin を登録するには YubiKey Manager の GUI を使った方が簡単なので次のアプリケーションも一緒にインストールするとよい。

$ sudo apt install -y yubikey-manager-qt
$ ykman-gui

おそらく ykman の cli でも登録できると思うけど、yubikey や認証設定に慣れていない初心者は gui のナビゲーションに従って作業して結果を確認した方が安心だと思う。fido2 pin は ssh でいうところのパスフレーズのようなものなのかな?デバイスが盗まれるのを防ぐために pin があるという。一方で web アプリケーションが fido2 で認証するときに pin を要求するかどうかは任意になるらしい。よくあるパターンは初めて使うときは pin を要求して2回目以降はスキップするといった用途が多いのではないかと推測する。

この fido2 pin を使って Universal 2nd Factor (u2f) の設定を行う。

$ mkdir -p ~/.config/Yubico
$ pamu2fcfg > ~/.config/Yubico/u2f_keys
Enter PIN for /dev/hidraw3: ***  <= このときに yubikey manager で設定した fido2 pin を入力

この後 yubikey デバイスが点滅するので丸いセンサー部分を指でタッチすると完了する。このときに指紋登録しているのか、物理的にタッチすることを要求しているだけなのか、よくわかっていない。この前に chrome で指紋登録 をしていたのでそれが使われているのかもしれない。いま ykman で設定をみたら Fingerprints registered とあるのでどこかしらに指紋登録されているらしい。おそらく chrome じゃないかと推測する。

$ ykman fido info
WARNING: PC/SC not available. Smart card (CCID) protocols will not function.
PIN is set, with 8 attempt(s) remaining.
Fingerprints registered, with 3 attempt(s) remaining.
Always Require User Verification is turned on.

linux で認証時に u2f を使いたいときは pam_u2f.so を使って pam の設定をするとよい。ubuntu だと /etc/pam.d/ 配下にいろんな認証設定がある。例えば login 時に2要素認証をしたい場合は次のようにフックする。パスワード入力した後に yubikey のセンサーを物理的にタッチして指紋認証を行える。セキュリティに厳しい会社はこういった運用をしているのかもしれない。

$ sudo vi /etc/pam.d/login
...
# Standard Un*x authentication.
@include common-auth
auth       required    pam_u2f.so

パスワード認証の代替として使いたい場合は通常の認証の前に sufficient として呼び出せば指紋認証が成功したときにそれ以降の処理がスキップされる。

auth       sufficient    pam_u2f.so
@include common-auth

これらの設定を確認にするには pamtester というツールを使うと簡単にできる。認証の設定を誤るとログインできなくなってしまうので慎重にテストして振る舞いを確認した上で実際の運用を行うとよい。

$ pamtester login t2y authenticate  <= このときに yubikey デバイスが点滅するので指紋認証する
pamtester: successfully authenticated

せっかく購入したので 1password アカウントや github の2要素認証に設定してみた。ワンタイムパスワードの otp の代替として使える。ワンタイムパスワードを入力するより yubikey のセンサーをタッチする方が日々の運用としては少しお手軽と言える。設定していて otp と yubikey でお互いに2要素認証のバックアップを兼ねることに気付いた。認証方法が増えるのでセキュリティ的には脆弱になるけれど、サービスとセキュリティのトレードオフの考え方から、yubikey の分だけ脆弱になっても物理的なセキュリティを担保できるのであれば利便性を考慮してよいように私には思えた。

rsync に daemon モードがあるらしい

23時に寝て2時半に起きて4時や5時に起きて7時に起きた。泊まっているホテルの低反発枕の寝心地がよい。

rsync daemon over ssh

外部向けのドキュメントを公開するための gitlab ci/cd を構築した。web サーバにドキュメントをアップロードする手段として rsync を使っている。rsync over ssh でデータを転送するときにさらに daemon モード (rsyncd) という仕組みがあって、権限や書き込み先の acl なども細かく制御できる。手順や設定は古の古臭い雰囲気はするけれど、実用的には ssh の秘密鍵を使ってちょっと高機能なアップロードを実現できる。ssh agent で鍵登録できていれば次のような cli でセキュアに rsync できる。全然知らない方法だったので学びの1つになった。

$ rsync \
    --verbose \
    --rsh ssh \
    --stats \
    --compress-choice=zstd \
    --compress-level=10 \
    --itemize-changes \
    --recursive \
    --checksum \
    --delete \
    local/ ${USER}@${HOST}::${RSYNC_DIR}

LLMを使ってみる会

LLMを使ってみる会 に参加した。私も chatgpt に調べものやちょっとしたことを聞くようになったりしているが、他の人たちがどんな用途に使っているのかも知りたくて参加してみた。fin-py のイベントだったのでみんな金融系のドキュメントの要約に使っているのが多そうにみえた。あとは研究テーマとして gpt/llm を取り上げている人たちも何人かいた。

暗号化と復号化を実装してみる

不定期に寝て7時半に起きた。起きてからシャワー浴びたらしゃんとした。昨日と同様に昼間働いて、ホテルに戻ってきて2-3時間寝てから夜に2-3時間かけて次のコードを書いた。

go での暗号化/復号化

ある機密情報を扱うので暗号化して普通には「みえない」ようにしたい。まったくわからないのでググったら次の記事がみつかった。

まずはこの記事にあるサンプルコードを動かしてみながら内容を理解する。暗号技術そのものは理解できないが、コードの振る舞いそのものは動かしながら理解できる。その後、いろいろ調べているとこのサンプルコードの大半は crypto/cipher パッケージにあるサンプルコードと同じであることに気づく。標準ライブラリのドキュメントでは iv (initialization vector) を暗号化対象の文字列の一部を切り取って生成している。iv は一意である必要はあるが、セキュアでなくてもよいとある。よくあるやり方とあるのでサンプルコードをみながら同じように実装した。

// The IV needs to be unique, but not secure. Therefore it's common to
// include it at the beginning of the ciphertext.

最終的な暗号化と復号化のコードは次になった。標準ライブラリにあるサンプルコードと基本的には同じ。

func Encrypt(secret string, plainText []byte) ([]byte, error) {
	block, err := aes.NewCipher([]byte(secret))
	if err != nil {
		return nil, fmt.Errorf("failed to create chiper: %w", err)
	}
	cipherText := make([]byte, aes.BlockSize+len(plainText))
	iv := cipherText[:aes.BlockSize]
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return nil, fmt.Errorf("failed to read for iv: %w", err)
	}
	stream := cipher.NewCFBEncrypter(block, iv)
	stream.XORKeyStream(cipherText[aes.BlockSize:], plainText)

	buf := make([]byte, base64.StdEncoding.EncodedLen(len(cipherText)))
	base64.StdEncoding.Encode(buf, cipherText)
	return buf, nil
}
func Decrypt(secret string, text []byte) ([]byte, error) {
	block, err := aes.NewCipher([]byte(secret))
	if err != nil {
		return nil, fmt.Errorf("failed to create chiper: %w", err)
	}

	dbuf := make([]byte, len(text))
	n, err := base64.StdEncoding.Decode(dbuf, text)
	if err != nil {
		return nil, fmt.Errorf("failed to create chiper: %w", err)
	}
	cipherText := dbuf[:n]

	if len(cipherText) < aes.BlockSize {
		return nil, fmt.Errorf("ciphertext is too short: %d", len(cipherText))
	}
	iv := cipherText[:aes.BlockSize]
	cipherText = cipherText[aes.BlockSize:]
	stream := cipher.NewCFBDecrypter(block, iv)
	// XORKeyStream can work in-place if the two arguments are the same.
	stream.XORKeyStream(cipherText, cipherText)
	return cipherText, nil
}

cookie の secure 属性と localhost

0時に寝て6時半に起きて7時半に起きて9時過ぎに起きた。普通は明け方に2度寝して起きるのになぜか3度寝して寝坊した。慌てて準備してオフィス行って業務開始が9時半をまわってしまった。年に1回ぐらい意味なく寝坊することもある。こういうことがあるから他人がなにか意図しない失敗しても寛容になれる。

以前たまたま Meety脆弱性 2022-11 をみたときに認証 cookie に secure 属性が使われていないという指摘をみかけた。http でログインするときに cookie にセッション情報を保存してしまうと、平文でセッション情報が流れてしまうのでセキュリティ的によくない。具体的な攻撃方法としては wifi の通信をパケットキャプチャするとか、ルーター (カフェの無料 wifi とか) で中間者攻撃 (man in the middle) などのセキュリティ上の懸念がある。その対策としては cookie の secure 属性を付けておくと、http のときはセッション情報をブラウザに保存しなくなるのでログインできなくなる代わりにそういった攻撃から守れるようになる。フロントエンドのセキュリティのお作法みたいなものにみえたので覚えていた。

ちょうど管理画面のログイン機能をメンバーに実装してもらったところなのでその対応をしているかどうかをメンバーに確認していた。メンバーもその認識はもっていて環境変数の設定で切り替えられるように実装していた。テスト環境にデプロイするときにその設定を有効にしてもらって、私が意図した振る舞いをしているかどうか、テスト環境にアクセスして検証していた。しかし、secure 属性が付いていることは確認したものの、http でもセッション情報がブラウザに保存されていて、あれー?って感じで検証していた。メンバーは保存されないという。

localhost は例外

ブラウザによっては localhost だと httpsの要件が無視されるとのことでした。

CookieのSameSite属性とSecure属性について

この記事によると localhost だと適用外になるブラウザがあるんだと気付いた。たまたま私が ssh の port-forwarding 経由でテスト環境にアクセスしていたので localhost 経由のアクセスになっていた。これは開発時は http でも動くようにすることで環境変数で切り替えるといった、それ自体がセキュリティインシデントになり得る設定をもたないようにするための、ブラウザベンダーの配慮だろう。chrome はそういう振る舞いをしていることを私は確認できた。

v-html は使わなくてもよい

0時に寝て7時に起きた。また日曜日は寝てた。

任意のカラムの書き換え

v-data-table の、あるセルが複雑なデータをもっていて、単純にその値を表示するのではなく、一定の構造化やレイアウトを調整した状態で表示したい。セル内の構造を書き換える方法を私は知らなかったので v-html という api を使って書き換えればよいのだと思った。しかし、これは間違いだった。間違いの訂正は翌日にやるとして仮に v-html を使うとしても xss の懸念があるのでスクリプトをエスケープしてあげないといけない。Sanitize v-html #6333 でも議論されていて vue3 はデフォルトでエスケープする仕組みが入るのかな?vue2 だと sanitize-html を使って次のようにラップすればいいと書いてあった。実際に動かしてみるとスクリプトを実行できたので v-html は危険だというのはわかった。

<div v-html="$sanitize(value)" />

この仕組みを作って pr でレビューしてもらっていたら、カラムの構造を書き換えたいだけなら slots を使えば普通にできると教えてもらった。また明日へ。