Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Release configuration for `make local-dmg` and `make dmg`.
#
# Copy to .env.local (gitignored) and fill in the blanks:
#
# cp .env.example .env.local
#
# A variable already set in your shell wins over this file, so CI and one-off
# command lines keep working exactly as before.

# Sparkle appcast feed. Matches the value already in the Xcode project.
SPARKLE_FEED_URL=https://jonaskahn.github.io/EasyKey/appcast.xml

# Sparkle EdDSA public key, baked into the app as SUPublicEDKey.
# Deliberately NOT committed: project.pbxproj must keep "$(SPARKLE_PUBLIC_ED_KEY)"
# (see Scripts/test-release-config.sh). Read it out of an installed build with
# /usr/libexec/PlistBuddy -c 'Print :SUPublicEDKey' EasyKey.app/Contents/Info.plist
# or copy it from the sparkle-public.key that `make update-secrets` writes locally.
SPARKLE_PUBLIC_ED_KEY=

# Shown as the support link in the app.
EASYKEY_SUPPORT_URL=https://github.com/jonaskahn/EasyKey

# Shown as the privacy link in the app. No privacy page exists yet, so this is
# a stand-in — set a real HTTPS URL before a public release.
EASYKEY_PRIVACY_POLICY_URL=

# --- Required only by `make dmg` (Developer ID signing + notarization) ---
# The signed path needs these; `make local-dmg` does not. `make update-secrets`
# stages the same names as GitHub secrets for CI.
# DEVELOPER_ID_APPLICATION=
# DEVELOPMENT_TEAM=
# APPLE_ID=
# APPLE_TEAM_ID=
# APPLE_APP_SPECIFIC_PASSWORD=
39 changes: 35 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,51 @@ jobs:
with:
xcode-version: latest-stable

# TODO: re-enable Developer ID signing + notarization once Apple cert is available.
# Was: "Import Developer ID certificate" step + `make dmg` (signed/notarized) below.
- name: Build ${{ matrix.target.name }} DMG
- name: Import Developer ID certificate
env:
APPLE_SIGN_P12_BASE64: ${{ secrets.APPLE_SIGN_P12_BASE64 }}
APPLE_SIGN_P12_PASSWORD: ${{ secrets.APPLE_SIGN_P12_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
if [ -z "$APPLE_SIGN_P12_BASE64" ]; then
echo "APPLE_SIGN_P12_BASE64 is not set; run 'make update-secrets' first" >&2
exit 1
fi
keychain="$RUNNER_TEMP/signing.keychain-db"
security create-keychain -p actions "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p actions "$keychain"
printf '%s' "$APPLE_SIGN_P12_BASE64" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$APPLE_SIGN_P12_PASSWORD" -T /usr/bin/codesign
rm -f "$RUNNER_TEMP/cert.p12"
security set-key-partition-list -S apple-tool:,apple:,codesign: -k actions "$keychain"
security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"')
identity=$(security find-identity -v -p codesigning "$keychain" | grep "Developer ID Application" | head -n 1 | sed 's/.*) \([A-F0-9]*\) .*/\1/')
[ -n "$identity" ] || { echo "no Developer ID identity in the imported certificate" >&2; exit 1; }
{
echo "DEVELOPER_ID_APPLICATION=$identity"
echo "DEVELOPMENT_TEAM=$APPLE_TEAM_ID"
} >> "$GITHUB_ENV"
echo "imported Developer ID identity $identity"

- name: Build, sign, and notarize ${{ matrix.target.name }} DMG
env:
SPARKLE_FEED_URL: ${{ vars.SPARKLE_FEED_URL }}
SPARKLE_PUBLIC_ED_KEY: ${{ secrets.SPARKLE_PUBLIC_ED_KEY }}
EASYKEY_SUPPORT_URL: ${{ vars.EASYKEY_SUPPORT_URL }}
EASYKEY_PRIVACY_POLICY_URL: ${{ vars.EASYKEY_PRIVACY_POLICY_URL }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
# REQUIRED_ARCHS is read from the environment by Scripts/verify-arch.sh
# inside `make dmg`; it is not passed on the make command line. Do not
# drop it from this block.
ARCHS: ${{ matrix.target.archs }}
REQUIRED_ARCHS: ${{ matrix.target.required_archs }}
TARGET_NAME: ${{ matrix.target.name }}
TAG_NAME: ${{ github.ref_name }}
run: |
make local-dmg
make dmg
built_version=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' build/export/EasyKey.app/Contents/Info.plist)
[[ "$TAG_NAME" == "v$built_version" ]] || {
printf 'Tag %s does not match built version %s.\n' "$TAG_NAME" "$built_version" >&2
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ node_modules/
dSYM/
*.dSYM/

# Local secrets (Sparkle signing keys, etc.)
# Local secrets (Sparkle signing keys, Developer ID exports, etc.)
keys/
*.key
*.pem
*.p12
*.p12.b64
.env.local
.nodeterm/
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,26 @@
# Changelog

_Last reviewed: 2026-08-27_
_Last reviewed: 2026-10-16_

Notable changes to EasyKey. Format follows Keep a Changelog; versioning follows Semantic Versioning. EasyKey requires macOS 14.0 or later; tested OS and Mac minimums are owned by [platform compatibility](docs/reference.md#platform-compatibility) in the compact reference. Supported report scope for a given version is in [SECURITY.md](SECURITY.md). How a version is built and published is in [CONTRIBUTING.md](CONTRIBUTING.md) and [engineering](docs/engineering.md#release).

## [Unreleased]

## [0.0.15] - 2026-10-16

**Security**

- Release DMGs are now signed with a Developer ID certificate and notarized by Apple, so Gatekeeper opens them without the Control-click workaround. The archived app, its login helper and Sparkle are all signed with the hardened runtime, which notarization requires and which `make verify-release` now asserts.

**Fixed**

- Vietnamese text typed into Chromium/Electron web-page fields and Safari is now precomposed (NFC) again: these apps had been forced to combining-diacritic output, which rendered correctly but broke website search and find-in-page because sites compare precomposed text. All Unicode-class typing output is now canonically composed at the emission point (legacy TCVN3/VNI Windows/CP1258 output is unchanged), and the user's typing-encoding setting is honored in every app.
- The Chromium zero-width-space (and VSCode word-joiner) workaround is now transient — inserted and immediately deleted — instead of being left in the field until the next edit. A leftover invisible character used to travel inside search queries (live filtering while typing, and submits after "query + space + Enter") so site search never matched.

**Added**

- `make update-secrets` stages the signing, notarization and Sparkle credentials in GitHub for the Release workflow. The Developer ID `.p12` is exported from the keychain rather than pasted, and the Sparkle keypair is read from it instead of regenerated, so a published update-trust key is never rotated by accident.

## [0.0.14] - 2026-08-27

**Fixed**
Expand Down
32 changes: 16 additions & 16 deletions EasyKey.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -2080,7 +2080,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 14;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
Expand All @@ -2093,7 +2093,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.0.14;
MARKETING_VERSION = 0.0.15;
PRODUCT_BUNDLE_IDENTIFIER = one.ifelse.easykeyKit;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -2138,7 +2138,7 @@
CODE_SIGN_ENTITLEMENTS = EasyKeyApp/EasyKeyApp.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 14;
EASYKEY_PRIVACY_POLICY_URL = "";
EASYKEY_SUPPORT_URL = "https://github.com/jonaskahn/EasyKey";
GENERATE_INFOPLIST_FILE = NO;
Expand All @@ -2147,7 +2147,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.0.14;
MARKETING_VERSION = 0.0.15;
PRODUCT_BUNDLE_IDENTIFIER = one.ifelse.easykey;
PRODUCT_NAME = EasyKey;
SPARKLE_FEED_URL = "https://jonaskahn.github.io/EasyKey/appcast.xml";
Expand Down Expand Up @@ -2177,14 +2177,14 @@
CODE_SIGN_ENTITLEMENTS = EasyKeyLoginHelper/EasyKeyLoginHelper.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 14;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = EasyKeyLoginHelper/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.0.14;
MARKETING_VERSION = 0.0.15;
PRODUCT_BUNDLE_IDENTIFIER = one.ifelse.easykey.LoginHelper;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down Expand Up @@ -2212,7 +2212,7 @@
CODE_SIGN_ENTITLEMENTS = EasyKeyApp/EasyKeyApp.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 14;
EASYKEY_PRIVACY_POLICY_URL = "";
EASYKEY_SUPPORT_URL = "https://github.com/jonaskahn/EasyKey";
GENERATE_INFOPLIST_FILE = NO;
Expand All @@ -2221,7 +2221,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.0.14;
MARKETING_VERSION = 0.0.15;
PRODUCT_BUNDLE_IDENTIFIER = one.ifelse.easykey;
PRODUCT_NAME = EasyKey;
SPARKLE_FEED_URL = "https://jonaskahn.github.io/EasyKey/appcast.xml";
Expand All @@ -2235,7 +2235,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 14;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
Expand All @@ -2248,7 +2248,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.0.14;
MARKETING_VERSION = 0.0.15;
PRODUCT_BUNDLE_IDENTIFIER = com.easykey.EasyEngineCore;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand All @@ -2264,14 +2264,14 @@
CODE_SIGN_ENTITLEMENTS = EasyKeyLoginHelper/EasyKeyLoginHelper.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 14;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = EasyKeyLoginHelper/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.0.14;
MARKETING_VERSION = 0.0.15;
PRODUCT_BUNDLE_IDENTIFIER = one.ifelse.easykey.LoginHelper;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand All @@ -2283,7 +2283,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 14;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
Expand All @@ -2296,7 +2296,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.0.14;
MARKETING_VERSION = 0.0.15;
PRODUCT_BUNDLE_IDENTIFIER = one.ifelse.easykeyKit;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down Expand Up @@ -2324,7 +2324,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 13;
CURRENT_PROJECT_VERSION = 14;
DEFINES_MODULE = YES;
DYLIB_COMPATIBILITY_VERSION = 1;
DYLIB_CURRENT_VERSION = 1;
Expand All @@ -2337,7 +2337,7 @@
"@executable_path/../Frameworks",
"@loader_path/Frameworks",
);
MARKETING_VERSION = 0.0.14;
MARKETING_VERSION = 0.0.15;
PRODUCT_BUNDLE_IDENTIFIER = com.easykey.EasyEngineCore;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
Expand Down
23 changes: 18 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ XCODEBUILD = xcodebuild -project "$(PROJECT)" -scheme "$(SCHEME)" -destination "

.DEFAULT_GOAL := help

.PHONY: all build release run test test-parallel coverage coverage-parallel coverage-gate build-for-testing lint format clean clean-local clean-derived clean-all qa archive export verify-arch verify-compatibility verify-release dmg local-dmg help
.PHONY: all build release run test test-parallel coverage coverage-parallel coverage-gate build-for-testing lint format clean clean-local clean-derived clean-all qa archive export verify-arch verify-compatibility verify-release dmg local-dmg update-secrets update-secret help

# `make` with no args prints grouped help. `make all` still builds.
all: build
Expand Down Expand Up @@ -134,11 +134,10 @@ archive:
export:
Scripts/export.sh

# Fail early with an actionable message when release configuration is missing.
# Values come from the environment or .env.local (cp .env.example .env.local).
release-config-check:
@if [ -z "$$SPARKLE_PUBLIC_ED_KEY" ]; then \
echo "Error: SPARKLE_PUBLIC_ED_KEY environment variable is not set." >&2; \
exit 1; \
fi
@Scripts/check-release-config.sh

verify-arch:
Scripts/verify-arch.sh
Expand Down Expand Up @@ -173,6 +172,19 @@ local-dmg: release-config-check
RELEASE_LOCAL=1 Scripts/verify-release.sh
Scripts/create-dmg.sh "$(BUILD_DIR)/export/EasyKey.app"

# Prompt for release secrets and store them with gh. The Developer ID .p12 is
# exported from the keychain and the Sparkle keypair is read from it; nothing
# is pasted by hand. Empty input at any prompt keeps the existing value.
# ALL=1 re-ask everything
# ARGS=... e.g. ARGS="--list" to audit names without writing anything
update-secrets:
@if [ "$(ALL)" = "1" ]; then Scripts/update-secrets.sh --all $(ARGS); else Scripts/update-secrets.sh $(ARGS); fi

# Alias — `update-secrets` (plural) is the canonical name, matching the
# reference implementation; the singular spelling is kept because it is the
# one people type first.
update-secret: update-secrets

# ---------------------------------------------------------------------------
# Help — grouped by usage. Run `make` (no args) or `make help`.
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -206,6 +218,7 @@ help:
@echo " make local-dmg Universal ad-hoc DMG (no Developer ID, no notarization)"
@echo ""
@echo " Release (signed / distributed)"
@echo " make update-secrets Upload signing/notarization/Sparkle secrets to GitHub (gh)"
@echo " make archive Create signed archive (needs Developer ID env)"
@echo " make export Export .app from archive"
@echo " make verify-arch Confirm arm64 + x86_64 in exported app"
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<strong>EasyKey</strong><br><br>
<i>Fast, private Vietnamese typing for macOS</i><br><br>
<a href="https://github.com/jonaskahn/EasyKey/releases/latest"><img src="https://img.shields.io/badge/download-Releases-111111?style=flat-square" alt="Download EasyKey"></a>
<a href="https://github.com/jonaskahn/EasyKey/releases/latest"><img src="https://img.shields.io/badge/version-0.0.14-6e3dbc9?style=flat-square" alt="Latest version"></a>
<a href="https://github.com/jonaskahn/EasyKey/releases/latest"><img src="https://img.shields.io/badge/version-0.0.15-6e3dbc9?style=flat-square" alt="Latest version"></a>
<a href="https://github.com/jonaskahn/EasyKey/actions/workflows/ci.yml"><img src="https://github.com/jonaskahn/EasyKey/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
<a href="https://github.com/jonaskahn/EasyKey/actions/workflows/ci.yml"><img src="https://img.shields.io/badge/coverage-90%25-brightgreen?style=flat-square" alt="90% coverage gate"></a>
</p>
Expand All @@ -22,7 +22,7 @@ vieejt nam → việt nam
2. Open the DMG, drag **EasyKey** into **Applications**, and launch it.
3. Grant **Accessibility** access when prompted (required for system-wide typing).

> If macOS blocks the app: Control-click **EasyKey** → **Open**, or use **System Settings → Privacy & Security → Open Anyway**.
Release DMGs are signed with a Developer ID certificate and notarized by Apple, so macOS opens them normally. If you are still running a DMG downloaded before notarization was enabled, re-download the latest release — or Control-click **EasyKey** → **Open**, or use **System Settings → Privacy & Security → Open Anyway**.

## 🖊️ Usage

Expand Down
10 changes: 5 additions & 5 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
# Security policy

_Last reviewed: 2026-08-27_
_Last reviewed: 2026-10-16_

**0.0.14** is the only version in scope for security reports. Report through this repository’s issue tracker, marked as a security report. There is no published private contact, no `security.txt`, and no published safe-harbor commitment.
**0.0.15** is the only version in scope for security reports. Report through this repository’s issue tracker, marked as a security report. There is no published private contact, no `security.txt`, and no published safe-harbor commitment.

Technical posture (threat model, data classes, permissions) is owned by compact [security](docs/security.md). Sparkle HTTPS appcast and EdDSA-signed update archives are owned by compact [engineering](docs/engineering.md).

## Supported versions

| Version | Supported |
|---|---|
| 0.0.14 | yes — current marketing version and the README version badge |
| Older tagged releases | no documented support window; treat only 0.0.14 as in scope |
| 0.0.15 | yes — current marketing version and the README version badge |
| Older tagged releases | no documented support window; treat only 0.0.15 as in scope |

In scope for reports:

Expand All @@ -33,7 +33,7 @@ This repository publishes no issue template and no dedicated private reporting a

What to include:

- Reproduction steps, app version (for example 0.0.14), and macOS version
- Reproduction steps, app version (for example 0.0.15), and macOS version
- Believed impact, and which data or capability is affected
- Configuration that matters (permissions granted, providers configured, persistence enabled)

Expand Down
Loading
Loading