Skip to content

Commit 039d2d4

Browse files
committed
add rust binary for windows/linux
1 parent a2a5563 commit 039d2d4

26 files changed

Lines changed: 3616 additions & 60 deletions

File tree

.github/workflows/binary-release.yaml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,15 @@ jobs:
4949
secrets:
5050
OP_CI_TOKEN: ${{ secrets.OP_CI_TOKEN }}
5151

52+
# Build Rust native binaries for Linux and Windows
53+
build-native-rust:
54+
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'varlock@')
55+
uses: ./.github/workflows/build-native-rust.yaml
56+
with:
57+
artifact-name: native-bin-rust
58+
5259
release-binaries:
53-
needs: notarize-native-macos
60+
needs: [notarize-native-macos, build-native-rust]
5461
# was using github.ref.tag_name, but it seems that when publishing multiple tags at once, it was behaving weirdly
5562
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref_name, 'varlock@')
5663
runs-on: ubuntu-latest
@@ -100,6 +107,27 @@ jobs:
100107
- name: Restore native binary execute permission
101108
run: chmod +x packages/varlock/native-bins/darwin/VarlockEnclave.app/Contents/MacOS/varlock-local-encrypt
102109

110+
# Download Rust native binaries for Linux and Windows
111+
- name: Download Linux x64 native binary
112+
uses: actions/download-artifact@v8
113+
with:
114+
name: native-bin-rust-linux-x64
115+
path: packages/varlock/native-bins/linux-x64
116+
- name: Download Linux arm64 native binary
117+
uses: actions/download-artifact@v8
118+
with:
119+
name: native-bin-rust-linux-arm64
120+
path: packages/varlock/native-bins/linux-arm64
121+
- name: Download Windows x64 native binary
122+
uses: actions/download-artifact@v8
123+
with:
124+
name: native-bin-rust-win32-x64
125+
path: packages/varlock/native-bins/win32-x64
126+
- name: Restore Rust binary execute permissions
127+
run: |
128+
chmod +x packages/varlock/native-bins/linux-x64/varlock-local-encrypt
129+
chmod +x packages/varlock/native-bins/linux-arm64/varlock-local-encrypt
130+
103131
- name: build libs
104132
run: bun run build:libs
105133
env:

.github/workflows/build-native-macos.yaml

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,83 @@ jobs:
117117
echo "=== Info.plist ==="
118118
cat "$APP_PATH/Contents/Info.plist"
119119
120+
# Test the binary (using --no-auth since CI has no biometric)
121+
# Keys are still Secure Enclave-backed, just without user presence requirement
122+
- name: Test binary - status
123+
run: |
124+
BIN="packages/varlock/native-bins/darwin/VarlockEnclave.app/Contents/MacOS/varlock-local-encrypt"
125+
echo "=== status ==="
126+
$BIN status
127+
$BIN status | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['ok'], 'status not ok'"
128+
129+
- name: Test binary - SE key lifecycle + encrypt/decrypt roundtrip
130+
run: |
131+
BIN="packages/varlock/native-bins/darwin/VarlockEnclave.app/Contents/MacOS/varlock-local-encrypt"
132+
133+
echo "=== generate-key (--no-auth for CI) ==="
134+
$BIN generate-key --key-id ci-test --no-auth
135+
136+
echo "=== key-exists ==="
137+
$BIN key-exists --key-id ci-test | python3 -c "import sys,json; d=json.load(sys.stdin); assert d['exists']"
138+
139+
echo "=== encrypt ==="
140+
PLAINTEXT=$(printf 'hello from macOS CI' | base64)
141+
CIPHERTEXT=$($BIN encrypt --key-id ci-test --data "$PLAINTEXT" | python3 -c "import sys,json; print(json.load(sys.stdin)['ciphertext'])")
142+
echo "Ciphertext: ${CIPHERTEXT:0:40}..."
143+
144+
echo "=== decrypt (one-shot, no auth needed) ==="
145+
DECRYPTED=$($BIN decrypt --key-id ci-test --data "$CIPHERTEXT" | python3 -c "import sys,json; print(json.load(sys.stdin)['plaintext'])")
146+
echo "Decrypted: $DECRYPTED"
147+
148+
if [ "$DECRYPTED" != "hello from macOS CI" ]; then
149+
echo "::error::Roundtrip failed! Expected 'hello from macOS CI', got '$DECRYPTED'"
150+
exit 1
151+
fi
152+
153+
echo "=== delete-key ==="
154+
$BIN delete-key --key-id ci-test
155+
156+
echo "All macOS binary tests passed"
157+
158+
- name: Test JS→Swift interop
159+
run: |
160+
BIN="packages/varlock/native-bins/darwin/VarlockEnclave.app/Contents/MacOS/varlock-local-encrypt"
161+
162+
# Generate SE key (no auth for CI)
163+
$BIN generate-key --key-id interop-test --no-auth
164+
165+
# Get the public key from the SE binary
166+
PUBLIC_KEY=$($BIN generate-key --key-id interop-tmp --no-auth > /dev/null 2>&1; echo "skip")
167+
# Actually, get public key by generating and reading the output
168+
GEN_OUTPUT=$($BIN key-exists --key-id interop-test)
169+
170+
# Use the SE binary's encrypt to get the public key indirectly:
171+
# generate-key already printed it — let's re-generate to capture it
172+
$BIN delete-key --key-id interop-test > /dev/null
173+
PUBLIC_KEY=$($BIN generate-key --key-id interop-test --no-auth | python3 -c "import sys,json; print(json.load(sys.stdin)['publicKey'])")
174+
echo "SE Public Key: ${PUBLIC_KEY:0:20}..."
175+
176+
# Encrypt with JS using the SE public key
177+
CIPHERTEXT=$(bun -e "
178+
const { encrypt } = await import('./packages/varlock/src/lib/local-encrypt/crypto.ts');
179+
const result = await encrypt('$PUBLIC_KEY', 'javascript to secure enclave');
180+
process.stdout.write(result);
181+
")
182+
echo "JS Ciphertext: ${CIPHERTEXT:0:40}..."
183+
184+
# Decrypt with Swift SE binary (proves JS wire format is SE-compatible)
185+
DECRYPTED=$($BIN decrypt --key-id interop-test --data "$CIPHERTEXT" | python3 -c "import sys,json; print(json.load(sys.stdin)['plaintext'])")
186+
187+
if [ "$DECRYPTED" != "javascript to secure enclave" ]; then
188+
echo "::error::JS→Swift interop failed! Got: $DECRYPTED"
189+
exit 1
190+
fi
191+
echo "✓ JS→Swift SE: '$DECRYPTED'"
192+
193+
# Cleanup
194+
$BIN delete-key --key-id interop-test
195+
echo "All macOS interop tests passed"
196+
120197
- name: Upload native binary artifact
121198
uses: actions/upload-artifact@v7
122199
with:
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
name: Build Rust native binaries
2+
3+
# Reusable workflow that compiles the varlock-local-encrypt Rust binary
4+
# for Linux and Windows targets.
5+
#
6+
# Builds are cached by Cargo.lock + source hash. Each platform builds
7+
# natively on its own runner for maximum compatibility.
8+
#
9+
# Output: native binaries uploaded as artifacts, ready to be bundled
10+
# into the varlock npm package and CLI release archives.
11+
12+
permissions:
13+
contents: read
14+
15+
on:
16+
workflow_call:
17+
inputs:
18+
artifact-name:
19+
description: 'Base name for uploaded artifacts (suffixed with platform)'
20+
type: string
21+
default: 'native-bin-rust'
22+
23+
jobs:
24+
build:
25+
strategy:
26+
matrix:
27+
include:
28+
- os: ubuntu-latest
29+
target: x86_64-unknown-linux-gnu
30+
native-bin-subdir: linux-x64
31+
binary-name: varlock-local-encrypt
32+
test-interop: true # only need interop on one platform — crypto is identical
33+
- os: ubuntu-24.04-arm
34+
target: aarch64-unknown-linux-gnu
35+
native-bin-subdir: linux-arm64
36+
binary-name: varlock-local-encrypt
37+
test-interop: false
38+
- os: windows-latest
39+
target: x86_64-pc-windows-msvc
40+
native-bin-subdir: win32-x64
41+
binary-name: varlock-local-encrypt.exe
42+
test-interop: false
43+
44+
runs-on: ${{ matrix.os }}
45+
name: Build ${{ matrix.native-bin-subdir }}
46+
47+
steps:
48+
- uses: actions/checkout@v6
49+
50+
- name: Install Rust toolchain
51+
uses: dtolnay/rust-toolchain@stable
52+
with:
53+
targets: ${{ matrix.target }}
54+
55+
# Cache Cargo registry + build artifacts by lockfile hash
56+
- name: Cache Cargo
57+
uses: actions/cache@v5
58+
with:
59+
path: |
60+
~/.cargo/registry
61+
~/.cargo/git
62+
packages/encryption-binary-rust/target
63+
key: rust-${{ matrix.target }}-${{ hashFiles('packages/encryption-binary-rust/Cargo.lock') }}-${{ hashFiles('packages/encryption-binary-rust/src/**') }}
64+
restore-keys: |
65+
rust-${{ matrix.target }}-${{ hashFiles('packages/encryption-binary-rust/Cargo.lock') }}-
66+
rust-${{ matrix.target }}-
67+
68+
- name: Install UPX
69+
shell: bash
70+
run: |
71+
if [[ "${{ matrix.os }}" == *"ubuntu"* ]]; then
72+
sudo apt-get update && sudo apt-get install -y upx-ucl
73+
elif [[ "${{ matrix.os }}" == *"windows"* ]]; then
74+
choco install upx --yes
75+
fi
76+
77+
- name: Build release binary
78+
working-directory: packages/encryption-binary-rust
79+
run: cargo build --release --target ${{ matrix.target }}
80+
81+
- name: Prepare artifact
82+
shell: bash
83+
run: |
84+
ARTIFACT_DIR="native-bins/${{ matrix.native-bin-subdir }}"
85+
mkdir -p "$ARTIFACT_DIR"
86+
cp "packages/encryption-binary-rust/target/${{ matrix.target }}/release/${{ matrix.binary-name }}" "$ARTIFACT_DIR/"
87+
echo "=== Binary info (before UPX) ==="
88+
ls -la "$ARTIFACT_DIR/${{ matrix.binary-name }}"
89+
file "$ARTIFACT_DIR/${{ matrix.binary-name }}" || true
90+
echo "=== UPX compress ==="
91+
upx --best "$ARTIFACT_DIR/${{ matrix.binary-name }}"
92+
echo "=== Binary info (after UPX) ==="
93+
ls -la "$ARTIFACT_DIR/${{ matrix.binary-name }}"
94+
95+
# Run Rust unit tests
96+
- name: Run unit tests
97+
working-directory: packages/encryption-binary-rust
98+
run: cargo test --release --target ${{ matrix.target }}
99+
100+
# Test the built binary end-to-end (one-shot commands, no biometric)
101+
# Uses python (not python3) for Windows compat; printf for reliable base64 input
102+
- name: Test binary - status
103+
shell: bash
104+
run: |
105+
BIN="native-bins/${{ matrix.native-bin-subdir }}/${{ matrix.binary-name }}"
106+
PY=$(command -v python3 || command -v python)
107+
echo "=== status ==="
108+
$BIN status
109+
$BIN status | $PY -c "import sys,json; d=json.load(sys.stdin); assert d['ok'], 'status not ok'"
110+
111+
- name: Test binary - key lifecycle + encrypt/decrypt roundtrip
112+
shell: bash
113+
run: |
114+
BIN="native-bins/${{ matrix.native-bin-subdir }}/${{ matrix.binary-name }}"
115+
PY=$(command -v python3 || command -v python)
116+
117+
echo "=== generate-key ==="
118+
$BIN generate-key --key-id ci-test
119+
$BIN key-exists --key-id ci-test | $PY -c "import sys,json; d=json.load(sys.stdin); assert d['exists']"
120+
121+
echo "=== encrypt ==="
122+
PLAINTEXT=$($PY -c "import base64; print(base64.b64encode(b'hello from CI').decode())")
123+
CIPHERTEXT=$($BIN encrypt --key-id ci-test --data "$PLAINTEXT" | $PY -c "import sys,json; print(json.load(sys.stdin)['ciphertext'])")
124+
echo "Ciphertext: ${CIPHERTEXT:0:40}..."
125+
126+
echo "=== decrypt ==="
127+
DECRYPTED=$($BIN decrypt --key-id ci-test --data "$CIPHERTEXT" | $PY -c "import sys,json; print(json.load(sys.stdin)['plaintext'])")
128+
echo "Decrypted: $DECRYPTED"
129+
130+
if [ "$DECRYPTED" != "hello from CI" ]; then
131+
echo "::error::Roundtrip failed! Expected 'hello from CI', got '$DECRYPTED'"
132+
exit 1
133+
fi
134+
135+
echo "=== delete-key ==="
136+
$BIN delete-key --key-id ci-test
137+
138+
echo "All binary tests passed"
139+
140+
# Cross-platform interop: encrypt with Rust, decrypt with JS, and vice versa
141+
# Only on platforms where Bun is available and keys aren't DPAPI-protected
142+
- name: Setup Bun (for interop test)
143+
if: matrix.test-interop
144+
uses: oven-sh/setup-bun@v2
145+
146+
- name: Install JS deps (for interop test)
147+
if: matrix.test-interop
148+
run: bun install
149+
150+
# Bidirectional Rust↔JS interop test (Linux x64 only — crypto is platform-independent)
151+
- name: Setup Bun (for interop test)
152+
if: matrix.test-interop
153+
uses: oven-sh/setup-bun@v2
154+
155+
- name: Install JS deps (for interop test)
156+
if: matrix.test-interop
157+
run: bun install
158+
159+
- name: Test Rust↔JS interop
160+
if: matrix.test-interop
161+
shell: bash
162+
run: |
163+
BIN="native-bins/${{ matrix.native-bin-subdir }}/${{ matrix.binary-name }}"
164+
165+
# Generate key with Rust (no DPAPI on Linux = plaintext PKCS8)
166+
$BIN generate-key --key-id interop-test
167+
KEY_FILE="$HOME/.config/varlock/local-encrypt/keys/interop-test.json"
168+
PUBLIC_KEY=$(python3 -c "import json; print(json.load(open('$KEY_FILE'))['publicKey'])")
169+
PRIVATE_KEY=$(python3 -c "import json; print(json.load(open('$KEY_FILE'))['protectedPrivateKey'])")
170+
171+
# Rust→JS: encrypt with Rust, decrypt with JS
172+
PLAINTEXT_B64=$(python3 -c "import base64; print(base64.b64encode(b'rust to javascript').decode())")
173+
CIPHERTEXT=$($BIN encrypt --key-id interop-test --data "$PLAINTEXT_B64" | python3 -c "import sys,json; print(json.load(sys.stdin)['ciphertext'])")
174+
RESULT=$(bun -e "
175+
const { decrypt } = await import('./packages/varlock/src/lib/local-encrypt/crypto.ts');
176+
const result = await decrypt('$PRIVATE_KEY', '$PUBLIC_KEY', '$CIPHERTEXT');
177+
process.stdout.write(result);
178+
")
179+
[ "$RESULT" = "rust to javascript" ] || { echo "::error::Rust→JS failed! Got: $RESULT"; exit 1; }
180+
echo "Rust→JS: '$RESULT'"
181+
182+
# JS→Rust: encrypt with JS, decrypt with Rust
183+
CIPHERTEXT=$(bun -e "
184+
const { encrypt } = await import('./packages/varlock/src/lib/local-encrypt/crypto.ts');
185+
const result = await encrypt('$PUBLIC_KEY', 'javascript to rust');
186+
process.stdout.write(result);
187+
")
188+
RESULT=$($BIN decrypt --key-id interop-test --data "$CIPHERTEXT" | python3 -c "import sys,json; print(json.load(sys.stdin)['plaintext'])")
189+
[ "$RESULT" = "javascript to rust" ] || { echo "::error::JS→Rust failed! Got: $RESULT"; exit 1; }
190+
echo "JS→Rust: '$RESULT'"
191+
192+
$BIN delete-key --key-id interop-test
193+
echo "All interop tests passed"
194+
195+
- name: Upload artifact
196+
uses: actions/upload-artifact@v7
197+
with:
198+
name: ${{ inputs.artifact-name }}-${{ matrix.native-bin-subdir }}
199+
path: native-bins/${{ matrix.native-bin-subdir }}/
200+
retention-days: 7
201+
202+
# Cache the built binary so preview releases on other runners can restore it
203+
- name: Cache built binary
204+
uses: actions/cache/save@v5
205+
with:
206+
path: native-bins/${{ matrix.native-bin-subdir }}/
207+
key: native-bin-rust-${{ matrix.native-bin-subdir }}-${{ hashFiles('packages/encryption-binary-rust/Cargo.lock', 'packages/encryption-binary-rust/src/**') }}

.github/workflows/release.yaml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,15 @@ jobs:
3030
secrets:
3131
OP_CI_TOKEN: ${{ secrets.OP_CI_TOKEN }}
3232

33+
# Build Rust native binaries for Linux and Windows
34+
build-native-rust:
35+
uses: ./.github/workflows/build-native-rust.yaml
36+
with:
37+
artifact-name: native-bin-rust
38+
3339
release:
3440
name: Release
35-
needs: notarize-native-macos
41+
needs: [notarize-native-macos, build-native-rust]
3642
runs-on: ubuntu-latest
3743
permissions:
3844
id-token: write # Required for OIDC
@@ -77,6 +83,27 @@ jobs:
7783
- name: Restore native binary execute permission
7884
run: chmod +x packages/varlock/native-bins/darwin/VarlockEnclave.app/Contents/MacOS/varlock-local-encrypt
7985

86+
# Download Rust native binaries for Linux and Windows
87+
- name: Download Linux x64 native binary
88+
uses: actions/download-artifact@v8
89+
with:
90+
name: native-bin-rust-linux-x64
91+
path: packages/varlock/native-bins/linux-x64
92+
- name: Download Linux arm64 native binary
93+
uses: actions/download-artifact@v8
94+
with:
95+
name: native-bin-rust-linux-arm64
96+
path: packages/varlock/native-bins/linux-arm64
97+
- name: Download Windows x64 native binary
98+
uses: actions/download-artifact@v8
99+
with:
100+
name: native-bin-rust-win32-x64
101+
path: packages/varlock/native-bins/win32-x64
102+
- name: Restore Rust binary execute permissions
103+
run: |
104+
chmod +x packages/varlock/native-bins/linux-x64/varlock-local-encrypt
105+
chmod +x packages/varlock/native-bins/linux-arm64/varlock-local-encrypt
106+
80107
# ------------------------------------------------------------
81108
- name: Create Release Pull Request or Publish to npm
82109
id: changesets

0 commit comments

Comments
 (0)