My nix-darwin and NixOS config
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: script rewrite

+68 -22
+68 -22
scripts/migrate-gts-to-sharkey.sh
··· 4 4 # 5 5 # Preserves @ewan@ewancroft.uk by: 6 6 # 1. Extracting the RSA keypair from GTS SQLite 7 - # 2. Stopping GTS, switching to Sharkey via nixos-rebuild 8 - # 3. Injecting the old RSA keypair into Sharkey's PostgreSQL 7 + # 2. Converting JSON-encoded Go big.Int keys → PEM (via nix + cryptography) 8 + # 3. Stopping GTS, switching to Sharkey via nixos-rebuild 9 + # 4. Injecting the old RSA keypair into Sharkey's PostgreSQL 9 10 # 10 11 # GTS schema (SQLite): 11 12 # table: accounts 12 - # columns: private_key, public_key (PEM strings, local account has domain IS NULL) 13 + # columns: private_key, public_key 14 + # format: JSON-encoded Go *rsa.PrivateKey / *rsa.PublicKey (big.Int as 15 + # decimal JSON numbers, e.g. {"N":12345...,"E":65537,"D":...}) 16 + # note: GTS holds a WAL lock — must use immutable=1 URI to read 13 17 # 14 18 # Sharkey schema (PostgreSQL, 2025.4.6): 15 19 # table: user_keypair 16 - # columns: "userId" (PK, FK → user.id), "publicKey", "privateKey" (varchar 4096) 20 + # columns: "userId" (PK), "publicKey", "privateKey" (varchar 4096, PEM) 17 21 # 18 22 # Run as root on the NixOS server. 19 - # Prereq: sharkey.nix written + secrets/sharkey.env sops-encrypted + nixos-rebuild pending. 23 + # Prereq: sharkey.nix written + secrets/sharkey.env sops-encrypted + rebuilt. 20 24 # ============================================================================= 21 25 set -euo pipefail 22 26 ··· 43 47 [[ "${r,,}" == "y" ]] 44 48 } 45 49 50 + # ── Inline Python converter: Go JSON RSA key → PEM ──────────────────────────── 51 + # Called as: json_to_pem <private|public> <<< "$JSON" 52 + # Uses nix shell to provide python3 + cryptography without a global install. 53 + JSON_TO_PEM_PY=' 54 + import sys, json 55 + from cryptography.hazmat.primitives.asymmetric.rsa import ( 56 + RSAPrivateNumbers, RSAPublicNumbers, rsa_crt_iqmp, rsa_crt_dmp1, rsa_crt_dmq1 57 + ) 58 + from cryptography.hazmat.primitives.serialization import ( 59 + Encoding, PrivateFormat, PublicFormat, NoEncryption 60 + ) 61 + 62 + # Go marshals big.Int via MarshalText() — plain decimal, no quotes, as a JSON number. 63 + def bigint(v): 64 + return int(v) 65 + 66 + k = json.loads(sys.stdin.read()) 67 + N = bigint(k["N"]) 68 + E = int(k["E"]) 69 + D = bigint(k["D"]) 70 + P = bigint(k["Primes"][0]) 71 + Q = bigint(k["Primes"][1]) 72 + 73 + pub = RSAPublicNumbers(E, N) 74 + priv = RSAPrivateNumbers(P, Q, D, rsa_crt_dmp1(D, P), rsa_crt_dmq1(D, Q), rsa_crt_iqmp(P, Q), pub) 75 + key = priv.private_key() 76 + 77 + if sys.argv[1] == "private": 78 + sys.stdout.write(key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()).decode()) 79 + else: 80 + sys.stdout.write(key.public_key().public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo).decode()) 81 + ' 82 + 83 + json_to_pem() { 84 + local mode="$1" # private | public 85 + local json_input 86 + json_input="$(cat)" 87 + echo "$json_input" | nix shell nixpkgs#python3Packages.cryptography --command \ 88 + python3 -c "$JSON_TO_PEM_PY" "$mode" 89 + } 90 + 46 91 # ── Pre-flight ──────────────────────────────────────────────────────────────── 47 92 [[ "$(id -u)" -eq 0 ]] || error "Run as root." 48 93 [[ -f "$GTS_DB" ]] || error "GTS DB not found: $GTS_DB" 49 - for cmd in sqlite3 psql systemctl curl jq; do 94 + for cmd in sqlite3 psql systemctl curl jq nix; do 50 95 command -v "$cmd" &>/dev/null || error "Missing required command: $cmd" 51 96 done 52 97 53 98 # ── Step 1: Extract RSA keys from GTS SQLite ───────────────────────────────── 54 - # GTS bun ORM maps PrivateKey/PublicKey (*rsa.PrivateKey/*rsa.PublicKey) to 55 - # snake_case columns private_key/public_key, stored as PEM strings. 99 + # GTS holds a WAL lock on the DB even when stopped (until it flushes). 100 + # immutable=1 bypasses the lock for read-only access. 56 101 info "Extracting RSA keypair for @${GTS_USERNAME} from SQLite..." 57 102 58 - PRIVATE_KEY=$(sqlite3 "$GTS_DB" \ 103 + GTS_DB_URI="file://${GTS_DB}?mode=ro&immutable=1" 104 + 105 + PRIVATE_KEY_JSON=$(sqlite3 "$GTS_DB_URI" \ 59 106 "SELECT private_key FROM accounts WHERE username='${GTS_USERNAME}' AND domain IS NULL LIMIT 1;") 60 - PUBLIC_KEY=$(sqlite3 "$GTS_DB" \ 107 + PUBLIC_KEY_JSON=$(sqlite3 "$GTS_DB_URI" \ 61 108 "SELECT public_key FROM accounts WHERE username='${GTS_USERNAME}' AND domain IS NULL LIMIT 1;") 62 109 63 - [[ -n "$PRIVATE_KEY" ]] || error "private_key is empty — check GTS_USERNAME and that the account is local." 64 - [[ -n "$PUBLIC_KEY" ]] || error "public_key is empty." 110 + [[ -n "$PRIVATE_KEY_JSON" ]] || error "private_key is empty — check GTS_USERNAME." 111 + [[ -n "$PUBLIC_KEY_JSON" ]] || error "public_key is empty." 65 112 66 - # Sanity-check PEM headers 67 - [[ "$PRIVATE_KEY" == *"BEGIN RSA PRIVATE KEY"* || "$PRIVATE_KEY" == *"BEGIN PRIVATE KEY"* ]] || 68 - error "private_key does not look like a PEM block." 69 - [[ "$PUBLIC_KEY" == *"BEGIN PUBLIC KEY"* ]] || 70 - error "public_key does not look like a PEM block." 113 + info "Converting JSON-encoded RSA keys to PEM (fetching cryptography via nix)..." 114 + PRIVATE_KEY=$(echo "$PRIVATE_KEY_JSON" | json_to_pem private) 115 + PUBLIC_KEY=$(echo "$PUBLIC_KEY_JSON" | json_to_pem public) 116 + 117 + [[ "$PRIVATE_KEY" == *"BEGIN"* ]] || error "PEM conversion failed for private key." 118 + [[ "$PUBLIC_KEY" == *"BEGIN"* ]] || error "PEM conversion failed for public key." 119 + info "Conversion OK." 71 120 72 121 { 73 122 echo "PRIVATE_KEY<<EOF" ··· 99 148 info "Sharkey + PostgreSQL are up." 100 149 101 150 # ── Step 3: Create the local account in Sharkey ─────────────────────────────── 102 - warn "Create @${GTS_USERNAME} in the Sharkey setup wizard or admin panel:" 151 + warn "Create @${GTS_USERNAME} in the Sharkey setup wizard:" 103 152 warn " https://${AP_HOSTNAME} (first run triggers the setup wizard)" 104 153 warn " Username must be exactly: ${GTS_USERNAME}" 105 154 read -r -p "$(echo -e "${YELLOW}Press Enter once the account exists...${NC}")" 106 155 107 156 SHARKEY_USER_ID=$(sudo -u postgres psql -d "$SHARKEY_DB" -tA \ 108 157 -c "SELECT id FROM \"user\" WHERE username='${GTS_USERNAME}' AND host IS NULL LIMIT 1;" 2>/dev/null || true) 109 - SHARKEY_USER_ID="${SHARKEY_USER_ID// /}" # trim whitespace psql may add 158 + SHARKEY_USER_ID="${SHARKEY_USER_ID// /}" 110 159 111 160 [[ -n "$SHARKEY_USER_ID" ]] || error "@${GTS_USERNAME} not found in Sharkey DB — create the account first." 112 161 info "Sharkey user ID: ${SHARKEY_USER_ID}" 113 162 114 163 # ── Step 4: Inject old RSA keypair into user_keypair ───────────────────────── 115 - # Sharkey 2025.4.6 always has the user_keypair table (UserKeypair.ts entity). 116 - # Columns: "userId" (PK), "publicKey" varchar(4096), "privateKey" varchar(4096). 117 - # Dollar-quoting ($pem$...$pem$) handles PEM newlines safely without escaping. 118 164 info "Injecting GTS RSA keypair into Sharkey's user_keypair table..." 119 165 120 166 sudo -u postgres psql -d "$SHARKEY_DB" -c \