3 Commits
Author SHA1 Message Date
nmemmertandClaude Sonnet 4.6 7351f9048a fix: replace X25519 with ECDH P-256 for Electron compatibility
Build & Release / Docker / backend (push) Has been cancelled
Build & Release / Docker / frontend (push) Has been cancelled
Build & Release / Desktop / Linux & Windows (push) Has been cancelled
Build & Release / Deploy to server (push) Has been cancelled
Build & Release / Create release (push) Has been cancelled
X25519 Web Crypto support is not available in Electron's Chromium build
despite being in Chrome 113+. P-256 ECDH is universally supported across
all browsers and Electron versions with equivalent security.

- Replace { name: 'X25519' } with { name: 'ECDH', namedCurve: 'P-256' }
  throughout the keypair generation, sealing, and opening functions
- Wire format updated: ephemeral pub key grows from 32 to 65 bytes
  (uncompressed P-256 point: 0x04 || x || y)
- Existing accounts with X25519 keys must re-register

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-28 11:41:11 -04:00
nmemmertandClaude Sonnet 4.6 84ff218a30 fix: support both docker compose (v2) and docker-compose (v1) in deploy
Build & Release / Docker / backend (push) Successful in 1m7s
Build & Release / Docker / frontend (push) Successful in 1m14s
Build & Release / Desktop / Linux & Windows (push) Successful in 18m15s
Build & Release / Deploy to server (push) Successful in 18s
Build & Release / Create release (push) Has been cancelled
The target server has Docker Compose V1 (standalone binary) not the V2
plugin, so 'docker compose' fails. Detect which is available at runtime
and use the appropriate command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-28 11:27:03 -04:00
nmemmertandClaude Sonnet 4.6 3e89775e60 fix: downgrade upload/download-artifact to v3 for Gitea Actions compatibility
Build & Release / Docker / backend (push) Has been cancelled
Build & Release / Docker / frontend (push) Has been cancelled
Build & Release / Desktop / Linux & Windows (push) Has been cancelled
Build & Release / Deploy to server (push) Has been cancelled
Build & Release / Create release (push) Has been cancelled
actions/upload-artifact@v4 uses a new artifact API that requires
specific runner-side configuration not present in all act_runner setups.
v3 uses the simpler API that works reliably with self-hosted Gitea runners.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-28 11:24:55 -04:00
3 changed files with 32 additions and 42 deletions

No files matched your search

+9 -4
View File
@@ -103,7 +103,7 @@ jobs:
working-directory: desktop working-directory: desktop
run: npx electron-builder --win nsis --x64 --publish never run: npx electron-builder --win nsis --x64 --publish never
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v3
with: with:
name: desktop-linux-windows-${{ github.sha }} name: desktop-linux-windows-${{ github.sha }}
path: | path: |
@@ -128,8 +128,13 @@ jobs:
script: | script: |
cd ${{ secrets.DEPLOY_PATH }} cd ${{ secrets.DEPLOY_PATH }}
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.necloud.us -u "${{ secrets.REGISTRY_USER }}" --password-stdin echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.necloud.us -u "${{ secrets.REGISTRY_USER }}" --password-stdin
docker compose pull backend frontend if docker compose version > /dev/null 2>&1; then
docker compose up -d --no-build backend frontend docker compose pull backend frontend
docker compose up -d --no-build backend frontend
else
docker-compose pull backend frontend
docker-compose up -d --no-build backend frontend
fi
docker logout git.necloud.us docker logout git.necloud.us
docker image prune -f docker image prune -f
@@ -141,7 +146,7 @@ jobs:
if: startsWith(github.ref, 'refs/tags/v') if: startsWith(github.ref, 'refs/tags/v')
steps: steps:
- name: Download desktop artifacts - name: Download desktop artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v3
with: with:
name: desktop-linux-windows-${{ github.sha }} name: desktop-linux-windows-${{ github.sha }}
path: ./release-assets path: ./release-assets
+22 -37
View File
@@ -50,24 +50,23 @@ export async function decryptMasterKey(encB64: string, derived: CryptoKey): Prom
]); ]);
} }
// ── X25519 ECDH keypair ────────────────────────────────────────────────────── // ── ECDH P-256 keypair ───────────────────────────────────────────────────────
// P-256 is universally supported across all browsers and Electron versions.
// Raw public key is 65 bytes (uncompressed point: 0x04 || x || y).
const ECDH_ALGO = { name: 'ECDH', namedCurve: 'P-256' } as const;
const PUB_KEY_BYTES = 65; // uncompressed P-256 point
export async function generateX25519Keypair(): Promise<{ export async function generateX25519Keypair(): Promise<{
publicKeyB64: string; publicKeyB64: string;
privateKey: CryptoKey; privateKey: CryptoKey;
pkcs8Bytes: Uint8Array<ArrayBuffer>; pkcs8Bytes: Uint8Array<ArrayBuffer>;
}> { }> {
const kp = await crypto.subtle.generateKey({ name: 'X25519' } as any, true, ['deriveBits']); const kp = await crypto.subtle.generateKey(ECDH_ALGO, true, ['deriveBits']);
const publicRaw = await crypto.subtle.exportKey('raw', (kp as CryptoKeyPair).publicKey); const publicRaw = await crypto.subtle.exportKey('raw', (kp as CryptoKeyPair).publicKey);
const pkcs8 = await crypto.subtle.exportKey('pkcs8', (kp as CryptoKeyPair).privateKey); const pkcs8 = await crypto.subtle.exportKey('pkcs8', (kp as CryptoKeyPair).privateKey);
const privateKey = await crypto.subtle.importKey( const privateKey = await crypto.subtle.importKey('pkcs8', pkcs8, ECDH_ALGO, false, ['deriveBits']);
'pkcs8',
pkcs8,
{ name: 'X25519' } as any,
false,
['deriveBits']
);
return { return {
publicKeyB64: bufToB64(publicRaw), publicKeyB64: bufToB64(publicRaw),
@@ -85,12 +84,12 @@ export async function decryptPrivateKeyToCryptoKey(
master: CryptoKey master: CryptoKey
): Promise<CryptoKey> { ): Promise<CryptoKey> {
const pkcs8 = await aesDecryptRaw(encB64, master); const pkcs8 = await aesDecryptRaw(encB64, master);
return crypto.subtle.importKey('pkcs8', pkcs8, { name: 'X25519' } as any, false, ['deriveBits']); return crypto.subtle.importKey('pkcs8', pkcs8, ECDH_ALGO, false, ['deriveBits']);
} }
// ── Conversation key exchange (ephemeral X25519 ECDH sealed box) ───────────── // ── Conversation key exchange (ephemeral ECDH P-256 sealed box) ───────────────
// //
// Wire format: ephemeral_pub_key(32) | nonce(12) | ciphertext // Wire format: ephemeral_pub_key(65) | nonce(12) | ciphertext
export function generateConvKeyRaw(): Uint8Array<ArrayBuffer> { export function generateConvKeyRaw(): Uint8Array<ArrayBuffer> {
return crypto.getRandomValues(new Uint8Array(32)); return crypto.getRandomValues(new Uint8Array(32));
@@ -100,23 +99,15 @@ export async function sealConvKey(
keyRaw: Uint8Array<ArrayBuffer>, keyRaw: Uint8Array<ArrayBuffer>,
recipientPublicKeyB64: string recipientPublicKeyB64: string
): Promise<string> { ): Promise<string> {
const recipientPubKey = await crypto.subtle.importKey( const recipientPubKey = await crypto.subtle.importKey('raw', b64ToBuf(recipientPublicKeyB64), ECDH_ALGO, false, []);
'raw',
b64ToBuf(recipientPublicKeyB64),
{ name: 'X25519' } as any,
false,
[]
);
const ephemeral = await crypto.subtle.generateKey({ name: 'X25519' } as any, true, [ const ephemeral = await crypto.subtle.generateKey(ECDH_ALGO, true, ['deriveBits']);
'deriveBits',
]);
const ephemeralPubRaw = new Uint8Array( const ephemeralPubRaw = new Uint8Array(
await crypto.subtle.exportKey('raw', (ephemeral as CryptoKeyPair).publicKey) await crypto.subtle.exportKey('raw', (ephemeral as CryptoKeyPair).publicKey)
); );
const sharedBits = await crypto.subtle.deriveBits( const sharedBits = await crypto.subtle.deriveBits(
{ name: 'X25519', public: recipientPubKey } as any, { name: 'ECDH', public: recipientPubKey },
(ephemeral as CryptoKeyPair).privateKey, (ephemeral as CryptoKeyPair).privateKey,
256 256
); );
@@ -126,10 +117,10 @@ export async function sealConvKey(
const nonce = crypto.getRandomValues(new Uint8Array(12)); const nonce = crypto.getRandomValues(new Uint8Array(12));
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, aesKey, keyRaw); const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, aesKey, keyRaw);
const out = new Uint8Array(32 + 12 + ct.byteLength); const out = new Uint8Array(PUB_KEY_BYTES + 12 + ct.byteLength);
out.set(ephemeralPubRaw, 0); out.set(ephemeralPubRaw, 0);
out.set(nonce, 32); out.set(nonce, PUB_KEY_BYTES);
out.set(new Uint8Array(ct), 44); out.set(new Uint8Array(ct), PUB_KEY_BYTES + 12);
return bufToB64(out); return bufToB64(out);
} }
@@ -138,20 +129,14 @@ export async function openSealedConvKey(
myPrivateKey: CryptoKey myPrivateKey: CryptoKey
): Promise<Uint8Array<ArrayBuffer>> { ): Promise<Uint8Array<ArrayBuffer>> {
const data = b64ToBuf(sealedB64); const data = b64ToBuf(sealedB64);
const ephemeralPubRaw = u8(data, 0, 32); const ephemeralPubRaw = u8(data, 0, PUB_KEY_BYTES);
const nonce = u8(data, 32, 44); const nonce = u8(data, PUB_KEY_BYTES, PUB_KEY_BYTES + 12);
const ct = u8(data, 44); const ct = u8(data, PUB_KEY_BYTES + 12);
const ephemeralPubKey = await crypto.subtle.importKey( const ephemeralPubKey = await crypto.subtle.importKey('raw', ephemeralPubRaw, ECDH_ALGO, false, []);
'raw',
ephemeralPubRaw,
{ name: 'X25519' } as any,
false,
[]
);
const sharedBits = await crypto.subtle.deriveBits( const sharedBits = await crypto.subtle.deriveBits(
{ name: 'X25519', public: ephemeralPubKey } as any, { name: 'ECDH', public: ephemeralPubKey },
myPrivateKey, myPrivateKey,
256 256
); );
+1 -1
View File
@@ -60,7 +60,7 @@ export default function RegisterPage() {
socket.connect(accessToken); socket.connect(accessToken);
navigate('/messages'); navigate('/messages');
} catch (err: any) { } catch (err: any) {
setError(err.response?.data?.error || 'Registration failed'); setError(err.response?.data?.error || err.message || 'Registration failed');
} finally { } finally {
setLoading(false); setLoading(false);
} }