How to use Google Workspace OAuth2 with mbsync (from isync) and msmtp on NetBSD

Google will remove old OAuth out-of-band (OOB) support on 2022-10-04. And desktop clients for Google Workspace should use Loopback IP address flow instead. So I must change my script to support Loopback IP address flow. My previous setup using OOB is described in How to use Google G suite OAuth2 with mbsync (from isync) and msmtp on NetBSD post.

I have searched the web and found How to continue using msmtp OAuth 2.0 for Gmail in mutt after oob deprecation? on StackExchange. The answer is to use getmail-gmail-xoauth-tokens script from getmail6 to get refresh token and access tokens. The answer says "- which are what we've been looking for, but that 1h expiry is prohibitively awkward...". It is absolutely unacceptable for me. I am away from GUI or web browsers in most time. I have not read getmail-gmail-xoauth-tokens script. However I feel that it may be the answer's misunderstanding or misuse. Anyway I do not have enough time to investigate the potentially hopeless script.

As a result, I found that the access tokens will expire in 3600 seconds and the refresh token has much longer life time like before. I have create two scripts to confirm this result. And I can reuse the scripts for my mbsync from isync and msmtp setups.

Get refresh token and save it

At first, you must download your client secret file as client_secret_*.json. Just rename it as client_secret.json

Google provides Google Auth Python Library and related libraries for Python. For further changes, I should use common methods to get tokens. I am a pkgsrc user and I have installed required libraries as follows:

# cd /usr/pkgsrc/www/py-google-api-python-client
# make install
# cd /usr/pkgsrc/security/py-google-auth-oauthlib
# make install

To get refresh token, you can use google_auth_oauthlib.flow.InstalledAppFlow.from_client_secrets_file() with the client_secret.json file. My source code (google-oauth2-loopback-ip.py) is as follows:

#!/usr/pkg/bin/python3.10

# Get refresh token and save it as ~/.refresh_token.pickle.
# Saved information will be used to refresh access token hourly.

# Follow:
# https://googleapis.github.io/google-api-python-client/docs/oauth-installed.html

import os
import pickle
from google_auth_oauthlib.flow import InstalledAppFlow

# Constants
CLIENT_SECRETS_FILE = 'client_secret.json'
CREDENTIAL_CACHE_FILE = '.refresh_token.pickle'

## Configurations
### To identify scopes and services, See:
### https://developers.google.com/identity/protocols/oauth2/scopes#gmail
SCOPES=['https://mail.google.com/']
API_SERVICE_NAME = 'mail'
API_VERSION = 'v1'

## Get credentials
def get_credentials():
    ## Create client object
    flow = InstalledAppFlow.from_client_secrets_file(
        CLIENT_SECRETS_FILE,
        scopes=SCOPES)

    ## Open with local web browser, input credential manually
    flow.run_local_server(host='localhost',
        port=8091,
        authorization_prompt_message='Please visit this URI: {url}',
        success_message='The auth flow is complete; you may close this web browser window.',
        open_browser=True)

    return flow.credentials


if __name__ == '__main__':
    credentials = get_credentials()

    # Get home directory path and create a path for cache file
    homeDir = os.environ['HOME']
    if not homeDir:
        homeDir = '.'
    credPath = os.path.join(homeDir, CREDENTIAL_CACHE_FILE)

    # Save credentials as file
    with open(credPath, 'wb') as tokenCache:
        pickle.dump(credentials, tokenCache)

    # https://google-auth.readthedocs.io/en/stable/reference/google.oauth2.credentials.html#google.oauth2.credentials.Credentials
    print('Refresh Token:', credentials.refresh_token)

You will get your refresh token as Refresh Token: YOURREFRESHTOKENSTRING. This script get an access token simultaniously (See your ~/.refresh_token.pickle). However I will not use the access token in this script at all.

Get access token using the refresh token

The refresh token has longer life time than 3600 seconds (life time of the access token). And the refresh token is used to get refreshed access tokens without login via web browsers.

To get a refreshed access token, you should load saved credentials in ~/.refresh_token.pickle and invoke refresh() method. My source code (google-oauth2-refresh-access_token.py) is as follows:

#!/usr/pkg/bin/python3.10

# Get new access token using saved refresh token.

# Follow:
# https://googleapis.github.io/google-api-python-client/docs/oauth-installed.html

import os
import pickle
from google.auth.transport.requests import Request

# Constants
CREDENTIAL_CACHE_FILE = '.refresh_token.pickle'

## Configurations
### To identify scopes and services, See:
### https://developers.google.com/identity/protocols/oauth2/scopes#gmail
SCOPES=['https://mail.google.com/']
API_SERVICE_NAME = 'mail'
API_VERSION = 'v1'

## Refresh credentials and get new ones
def get_refreshed_credentials():
    # Get home directory path and create a path for cache file
    homeDir = os.environ['HOME']
    if not homeDir:
        homeDir = '.'
    credPath = os.path.join(homeDir, CREDENTIAL_CACHE_FILE)

    if os.path.exists(credPath):
        with open(credPath, 'rb') as tokenCache:
            credentials = pickle.load(tokenCache)

        if credentials:
            # Refresh access token with refresh token
            credentials.refresh(Request())

    return credentials

if __name__ == '__main__':
    credentials = get_refreshed_credentials()

    # https://google-auth.readthedocs.io/en/stable/reference/google.oauth2.credentials.html#google.oauth2.credentials.Credentials
    print('Access Token:', credentials.token)
    #print('Expiry:', credentials.expiry) # in UTC

You will get your access token as Access Token: YOURREFRESHEDACCESSTOKENSTRING.

For mbsync and msmtp

This part is almost identical to my previous post. I will include my current script and configuration files.

$ cat /opt/bin/get_teteraorg_token.sh
#!/bin/sh

python3.10 /opt/bin/google-oauth2-refresh-access_token.py | awk -F" " '{if(NR==1)print $3}'
$ cat ~/.mbsync
(snip)
IMAPAccount gmail
Host imap.gmail.com
User username@tetera.org
AuthMechs XOAUTH2
PassCmd "/opt/bin/get_teteraorg_token.sh"
SSLType IMAPS
CertificateFile /etc/openssl/certs/ca-certificates.crt
(snip)
$ cat ~/.msmtprc
(snip)
account teteraorg
tls on
tls_certcheck off
tls_starttls off
host smtp.gmail.com
port 465
protocol smtp
auth xoauth2
from username@tetera.org
user username@tetera.org
passwordeval "/opt/bin/get_teteraorg_token.sh"
(snip)

Windows 10で、スタートアップディレクトリーを開く

Windows 10にはスタートアップディレクトリーがあって、ログインすると、ここに入っているショートカット等が自動的に開かれる。 このフォルダーを開く方法を忘れないように書いておく。

スタートアップが自分のユーザー用と、全ユーザーに適用されるものの2つがある。 自分のユーザーだけに適用されるフォルダーを開くには、Windowsキー+Rで表示される「ファイル名を指定して実行」よりshell:startupを実行すれば良い。 全ユーザーに適用されるフォルダーを開くには、shell:common startupを実行すれば良い。

これは、「ファイル名を指定して実行」から実行する必要があり、コマンドプロンプトからは実行できない。

エレコム製のハードウェア暗号化USBメモリーMF-ENU3A32GBKのファームウェアを最新化する

エレコム製の ハードウェア暗号化USBメモリーMF-ENU3A32GBKと言うのがあって、 Windows 10 Pro 21H2 64ビット版の環境で利用できる。 このUSBメモリーは、AESで暗号化する機能をハードウェアとして実装しており、USB CD-ROMとして認識される読み取り専用のパーティションに格納されたソフトウェアを 使ってパスワードを入力することで、USBリムーバブルディスクがインサートされた状態になり、読み書きができるようになる。 他にも、内容を全て削除してパスワードも初期化をする機能や、USBリムーバブルディスクを読み取り専用にする機能もある。 試してはいないが、macOS用のソフトウェアも、同じUSB CD-ROMのパーティション内に配置されており、macOSからでも利用できるはずである。

このMF-ENU3A32GBKが3台あって、いずれもProductVersion: 400というバージョンになっていた。そして、これはメニューの操作ではエラーになりファームウェアをアップデートできなかった。 これを最新版であるProductVersion: 520にする方法を記載しておく

Startup.exeを実行して、ファームウェアのアップデートをしようとすると、下図のようなエラーになってしまう。 どうやら、違う製品のファームウェアをインストールしようとして失敗しているように見える。 良く見ると、問題なくファームウェアをアップデートできたMF-ENU3A04GBKやMF-ENU3A08GBKとは違い、MF-ENU3 ソフトウェア更新内容ではなく、ソフトウェア更新内容/Software UpdateというURIにPLM30という文字列の入った変更履歴ページへリンクされている。

1つ目の図にあるように、エレコムのブランドで販売されていたが、実際には子会社になったハギワラソリューションズの製品のようだ。 そこで、ハギワラソリューションズのウェブサイトを見ていると、セキュリティUSBのページ というウェブページがあり、修復ソフトのダウンロード先が案内されている。エレコムの製品Q&A 5152番 が案内される。 しかし、実際にはそこから更に新しいファームウェアへのアップデーターエレコムの製品Q&A 7117番 へのリンクがあり、そこでProductVersion: 520にするためのアップデーターENU3A_Update_v520.zipがダウンロードできる。

このZIPアーカイブファイル中のSoftwareWriter.exeを実行すると、ファームウェアを最新化し、読み取り専用パーティションの内容もSecurityUSB.iso の内容に更新された。

グループポリシーでWindows Updateを管理されているPCで、Microsoftのサーバーに接続してWindows Updateする

Active Directoryで管理しているWindows 10 ProのPCで、自前で用意しているWindows Server Update Services (WSUS)のサーバーにのみ接続させ、 適用させたい更新のみを配布したいというのは理解できるが、 Windows Updateで配布されているデバイスドライバーにアップデートしたい場合がある。 ちゃんと理由は調べてはいないが、IntelのWi-Fiアダプターを搭載したラップトップで、新しいiPhoneのWi-Fiホットスポットにつなごうとするとき、古いデバイスドライバーだと SSIDが見つからない場合があった。 Intelは最新のデバイスドライバーを自社ウェブサイトで配布するのは止めてしまっているし、ラップトップのメーカーも最新のものを配布していないことが多い。 そうすると、Microsoftの提供するWindows Updateのサーバーに接続しないといけない。 まあ、本当は管理しているマシンに必要な最新のデバイスドライバーはWSUSで配信できるのだと思うので、ちゃんと管理して欲しいのだが。

いずれにしても、一時的にMicrosoftの提供するWindows Updateのサーバーに接続しないといけない。 その際には、勝手に最新のWindows 10のFeature Updateにアップグレードしないようにして、WSUSではなくMicrosoftのWindows Updateサーバーに接続するようにしないといけない。

指定したFeature Updateに留まるには、以下のように管理者権限のコマンドプロンプトで実行し、レジストリーに設定すれば良い。 ここでは、21H1のFeature Updateに留まるように設定している。

> REG ADD HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate /v TargetReleaseVersion /t REG_DWORD /d 1 /f
> REG ADD HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate /v TargetReleaseVersionInfo /t REG_SZ /d 21H1 /f

その上で、HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU\UseWUServer1になっているのを 0にしてやれば良い。デバイスドライバーが最新になったら、REG ADDした2つの値は削除し、UseWUServerの値は0に 戻しておけば良い。

neomuttで、未読の電子メールのみ表示させる

とあるドメイン宛ての電子メールをPostfixを使ってユーザー名によらず受信し、Maildir形式で保存するようにしているサーバーがある。 ほぼ全てがspamである訳だが、中には内容を確認しないといけない電子メールも存在している。 そのためにIMAP4サーバーを立ち上げるというようなことはしたくない。 そこで、sshでログインして、netmuttで受信した電子メールを読むことにした。 netmuttで、未読の電子メールのみを表示させるには、l~Uと入力すれば良かった。 現在この環境以外ではnetmuttを利用しておらず、すぐに忘れてしまうので、書いておく。

Clojure 1.11.1からJava 2Dを利用する

ClojureからJava 2Dを利用するようにしたいが良く分からなかった。 探してみるとCX's Hello, World!のHello, Java 2D(Clojure) World!に 例が掲載されていた。 ただ、そのままではOpenJDK 17.0.4で動かしたClojure 1.11.1では動かなかった。 その部分を動くようにして、フォントの設定をしたかったのでそれを追加してみた。

; From http://cx20.main.jp/blog/hello/2012/12/05/hello-java2d-clojure-world/
; Modified for Clojure 1.11.1 by Ryo ONODERA <ryo@tetera.org>
; Run: clj -M java2d.clj

(import
  (javax.swing JFrame)
  (javax.swing JPanel)
  (java.awt Graphics)
  (java.awt Graphics2D)
  (java.awt Font)
)

(defn create-panel []
  (proxy [JPanel] []
    (paintComponent [g]
      (.setFont ^Graphics2D g (Font. "Droid Sans" Font/PLAIN 72))
      (.drawString ^Graphics2D g "Hello, Java 2D World!" 0 100)
)))

(def panel
  (create-panel))

(def frame
  (JFrame.))

(doto frame
  (.setDefaultCloseOperation
    javax.swing.WindowConstants/EXIT_ON_CLOSE)
  (.add panel)
  (.setSize 640 480)
  (.setTitle "Hello, World")
  (.setLocation 100 100)
  (.setVisible true))

Windows 10で保存されたWi-FiアクセスポイントのSSIDとパスフレーズを一覧する

Windows 10で保存されたWi-FiアクセスポイントのSSIDとパスフレーズを知りたいのだが、標準のGUIでは今接続しているSSIDに対応するパスフレーズを 表示させることはできるものの、接続中ではないもののパスフレーズを表示することはできないようだ。 しかし、Windows 10の保存している全てのSSIDとパスフレーズの組み合わせを表示させたい場合がある。

まず、保存されたWi-FiのSSIDの一覧は、以下のようにすれば表示できる。

> netsh wlan show profiles

インターフェイス Wi-Fi のプロファイル:

グループ ポリシー プロファイル (読み取り専用)
---------------------------------------------
    ≤なし>

ユーザー プロファイル
---------------------
    すべてのユーザー プロファイル     : SSIDSTRING

そこでSSIDとしてSSIDSTRINGが分かるので、そのパスフレーズは以下のようにすれば表示できる。 「セキュリティの設定」の「主要なコンテンツ」の部分の値がパスフレーズである。

> netsh wlan show profile name=SSIDSTRING key=clear

インターフェイス Wi-Fi のプロファイル SSIDSTRING:
=======================================================================

適用先: すべてのユーザー プロファイル

プロファイル情報
-------------------
    バージョン             : 1
    種類                   : ワイヤレス LAN
    名前                   : SSIDSTRING
    コントロール オプション        :
        接続モード    : 手動接続
        ネットワーク ブロードキャスト : このネットワークがブロードキャスト配信している場合に限り接続
        AutoSwitch         : 他のネットワークに切り替えません
        MAC ランダム化  : 無効

接続の設定
---------------------
    SSID の数        : 1
    SSID 名             : "SSIDSTRING"
    ネットワークの種類           : インフラストラクチャ
    無線の種類          : [ 任意の無線の種類 ]
    ベンダー拡張          : 存在しません

セキュリティの設定
-----------------
    認証                : WPA2-パーソナル
    暗号                : CCMP
    認証                : WPA2-パーソナル
    暗号                : GCMP
    セキュリティ キー      : あり
    主要なコンテンツ       : SSIDPASSPHRASE

コスト設定
-------------
    コスト                   : 制限なし
    混雑                   : いいえ
    データ制限間近         : いいえ
    データ制限超過         : いいえ
    ローミング             : いいえ
    コスト ソース          : 既定

これをWindows 10上で組み合わせてやれば、SSIDとパスフレーズを一覧できるはずである。 ウェブを検索してみると、stackoverflow.comに How can I see all Wifi Passwords using netsh with 1 command?という記事があった。 PowerShellを使い、以下のようにすれば良い。

> type show-all-saved-wifis.ps1
netsh wlan show profiles | Select-String ":.*[a-zA-Z0-0-_]+" | % {"$_".split(":")[1].trim()} | % {netsh wlan show profile name=$_ key=clear}

Windows 10で秘密鍵がエクスポート禁止になっている電子証明書をエクスポートする

どういう仕組みで設定されているのかは把握していないが、Windows 10では秘密鍵がエクスポートできない電子証明書を指定することができるようだ。 だが、その秘密鍵は例えばHTTPSで電子証明書によるクライアント認証をする際には送られているはずなので、 単に秘密鍵をエクスポートを禁止するユーザーインターフェイスの機能があるだけで、秘密鍵を得ることはできるはずである。

その秘密鍵を受け取るようなサーバーを用意すれば良いのだとは思うが、同じようなことをしようと考えている人はいるはずである。 検索すると、https://github.com/iSECPartners/jailbreakというのが見付かる。 今回は、Windows 10 21H2 64-bitで試してみた。

https://github.com/iSECPartners/jailbreak/binariesに ビルド済みのバイナリーが含まれている。リポジトリーをcloneするか.zipファイルとしてダウンロードし、 管理者権限のコマンドプロンプトで以下のように実行すればcertmgr.mscが開き、そこでは秘密鍵がエクスポートできないという制限はなくなった。

binaries> jailbreak64 c:\windows\system32\mmc.exe c:\windows\system32\certmgr.msc -64

もし、管理者権限のないコマンドプロンプトで実行すると、以下のようなエラーになってしまう。

binaries>jailbreak64 c:\windows\system32\mmc.exe c:\windows\system32\certmgr.msc -64
CreateProces failed with error code = 740
Command line = c:\windows\system32\mmc.exe c:\windows\system32\certmgr.msc -64

また、アンチウイルスソフトウェアによっては、hacking toolとして認識されてしまうので、アンチウイルスソフトウェアは 停止させておかないといけない。Microsoft Defenderは検出はしないようである。

plgarc/wip/llama.cppでpkgsrcのBLASサポートを探る

この記事は、 NetBSD Advent Calendar 2024 の13日目の記事です。 llama.cppを使ってみる 以前に、 NetBSD/amd64でllama.cppを使ってみる という記事で llama.cpp を使ってみていました。 あれか...