Skip to content

Security & Compliance

SkylerX targets dev / test / prod alike, with a baked-in end-to-end security model from connection credentials through result rendering, SQL submission, and bulk export. This page maps every line of defense actually implemented in code: what it does, what it doesn't, and what evidence it produces for ops and audit.

1. Overview

SkylerX's security model breaks down along the "data flow", with code-level enforcement at each stage:

StageModule / fileResponsibility
Credential storageapps/desktop/src/main/db/connectionStore.tsPasswords / SSH keys encrypted via OS keychain (Electron safeStorage)
Environment taggingpackages/ui/src/connEnv.tsdev / test / prod tri-color + read-only connections + read-only statement allowlist
Statement gatingpackages/ui/src/sqlLint.ts7 heuristic rules (UPDATE/DELETE without WHERE, DROP/TRUNCATE on prod, etc.)
Display layerpackages/ui/src/masking.ts + DataMaskingViewDialogColumn-name pattern matching → render-time masking + persistent masking views
Governance / auditcompliance.ts / PiiScannerDialog / DataContractDialog / export-encrypt.tsMLPS compliance checks, PII scanning, data contracts, encrypted export

Each section below is anchored to the code.

2. Connection-password encryption (OS keychain)

Code: apps/desktop/src/main/db/connectionStore.ts

When you create/edit a connection, the password doesn't land in SQLite plaintext — it goes through Electron safeStorage (macOS = Keychain, Windows = DPAPI, Linux = libsecret / kwallet):

ts
function encryptPassword(plain?: string): string | null {
  if (!plain) return null
  if (safeStorage.isEncryptionAvailable()) {
    return `enc:${safeStorage.encryptString(plain).toString('base64')}`
  }
  return `plain:${Buffer.from(plain, 'utf8').toString('base64')}`
}

Storage always carries a prefix so versions can be told apart:

PrefixMeaningWhen seen
enc:OS-keychain ciphertextNormal path on macOS / Windows / most Linux
plain:base64 fallback (dev only)When safeStorage.isEncryptionAvailable() returns false — common on bare Linux containers without libsecret / kwallet
OtherLegacy unprefixed valuesHistorical data

Important: when you see plain:, SkylerX still works, but it's plaintext-equivalent. On Linux, install gnome-keyring or kwallet and then re-edit each connection (any change followed by save triggers re-encryption).

SSH tunnel keys

SSH config has password / privateKey / passphrase — all go through the same encryption pipeline. The list call (listConnections) strips key material before returning, avoiding redundant in-memory copies:

ts
function decryptSsh(stored, withSecrets) {
  const ssh = JSON.parse(decryptPassword(stored)) as SshConfig
  return withSecrets
    ? ssh
    : { ...ssh, password: undefined, privateKey: undefined, passphrase: undefined }
}

Full key material is restored only when actually connecting or repopulating the edit form (getConnection).

3. Environment tags dev / test / prod + production safeguards

Code: packages/ui/src/connEnv.ts

Connection config field extra.env holds a tri-state:

ValueUI labelColor (ENV_META.color)Default strictness
devDevelopment#4caf50 greenStandard
testTesting#e0a020 orangeStandard
prodProduction#e04050 redExtra SQL rules + pre-execution confirmation

Whole-connection read-only (extra.readOnly)

connReadOnly() marks read-only connections. SkylerX gates this in two places:

  1. Whole-connection level: isReadOnlyStatement(sql) uses a first-keyword allowlist (select / with / show / explain / desc(ribe) / pragma) to block write statements at the wire.
  2. Commit mode: read-only connections are forced to auto commit (manual tx is meaningless for read-only); see initialCommitMode().

Production watermark

Settings → Production watermark lets you customize text / angle / opacity / color. On prod connections, all views (SQL editor, result grid, export preview) overlay an SVG watermark to deter screenshot leaks.

4. SQL Linter — 7 built-in rules

Code: packages/ui/src/sqlLint.ts

Heuristic string scanning — not a full parser, just hits on "obviously dangerous" patterns. Severities:

SeverityUI feedbackStill runs?
errorModal confirmationOnly after explicit confirm
warnToastYes (warning only)
infoUp to caller (e.g. badge in editor margin)Yes

Full rule table:

Rule IDSeverityTriggerMessage
no-where-updateerrorUPDATE starts + no WHEREUPDATE without WHERE — will update the entire table
no-where-deleteerrorDELETE FROM + no WHEREDELETE without WHERE — will empty the entire table
prod-droperrorenv=prod + DROP TABLE/DATABASE/SCHEMA/INDEX/VIEWDROP XXX on production
prod-truncatewarnenv=prod + TRUNCATETRUNCATE on production
cross-joinwarnSELECT + FROM a, b (comma join) or JOIN without ON/USINGMulti-table query without join condition (suspected Cartesian product)
select-starinfoSELECT *SELECT * — list columns explicitly
forgotten-limitinfoSELECT without LIMIT / FETCH FIRST / TOP n / COUNT()SELECT without LIMIT may return a lot of data

The Linter is "cheap"

Comments are stripped with two regexes (/\/\*[\s\S]*?\*\//g and /--[^\n]*/g) so -- WHERE 1=1 can't fool the linter. All rules are O(n) string scans — fast enough to run on the execution hot path.

Multi-statement merge

lintStatements(stmts, ctx) keeps a finding by max severity across same-id hits — useful when you copy a whole SQL file and select-all to execute.

5. Data contracts (notNull / range / regex)

Code: packages/ui/src/components/DataContractDialog.vue

A data contract pre-declares "values that shouldn't appear" for business fields. Four parts per contract:

FieldTypeDescription
namestringContract name
tablestringThe schema.table it applies to
notNullstring[]Columns that must not be NULL
rangeRecord<string, [min, max]>Numeric range; null = unbounded
regexRecord<string, string>Regex the column value must match
enabledbooleanToggle

Stored in localStorage.skylerx.dataContracts as a JSON array.

Typical usage

json
{
  "name": "users completeness",
  "table": "public.users",
  "notNull": ["phone", "email"],
  "range": { "age": [0, 150] },
  "regex": { "email": "^[^@]+@[^@]+$", "phone": "^1\\d{10}$" },
  "enabled": true
}

Import / export

  • 📋 Export → copies JSON to clipboard, paste into team docs / git repo
  • 📥 Import → paste JSON to replace the current list

DBAs author contracts, then distribute to developers, where they automatically take effect in SkylerX.

6. Sensitive-field scanner (PII Scanner)

Code: packages/ui/src/components/PiiScannerDialog.vue

Two-stage heuristic: column-name match → sample verification.

Column-name match

Uses columnPattern regexes from DEFAULT_MASK_RULES (next section). E.g. user_phone hits (phone|mobile|tel|手机|电话), classified as phone.

Sample verification (optional)

For matched columns, pulls the first N rows (default 50, 10-1000) and re-checks with regex:

kindSample regex
phone/^\+?[\d\s\-()]{7,20}$/
email/^[^\s@]+@[^\s@]+\.[^\s@]+$/
idCard/^\d{15}$|^\d{17}[\dxX]$/
bankCard/^\d{12,19}$/
name / address / defaultNone — column-name only

Hit rate < 30% is treated as "name coincidence, not actually PII" and dropped from the report.

Report and next steps

The report groups by table sorted by hit count, with 📋 Export CSV (columns: schema/table/column/data_type/rule/kind/sample). The CSV is ready for audit; you can also right-click a DB → "Generate masking view" picking these columns.

7. Data masking views (DataMaskingViewDialog)

Code: packages/ui/src/masking.ts + packages/ui/src/components/DataMaskingViewDialog.vue

7.1 Built-in mask rules

DEFAULT_MASK_RULES is the baseline; you can edit / add / remove under Settings → Data masking.

NamecolumnPatternkindDefault onAlgorithm
Phone(phone|mobile|tel|手机|电话)phoneFirst 3 + **** + last 4
Email(email|mail|邮箱)emailFirst letter + ***@domain
ID card(id_?card|身份证|idno)idCardFirst 6 + *… + last 4
Bank card(bank_?card|card_?no|账号|账户)bankCardFirst 4 **** **** last 4
Name(real_?name|user_?name|full_?name|姓名)nameFirst char + * (rest hidden)
Address(address|addr|地址)addressFirst 6 chars + ***
Password / Token(password|passwd|secret|pwd|token|api_?key|密码)defaultFirst 2 + **** + last 2

7.2 Render-time masking vs database-level masking views

SkylerX offers two independent masking paths:

  • Render-time masking: Settings → Data masking → Enable. The frontend masks by column-name → rule → algorithm in real time. Doesn't touch the database; export dialog lets you pick "raw / masked".
  • Database masking views (DataMaskingViewDialog): generate CREATE OR REPLACE VIEW ... AS SELECT mask_expr(c) ... SQL and land it in the DB. Apps then read through the view, not the raw table. Six strategies:
StrategyGenerated SQL expression (MySQL example)
raw`c` AS `c`
md5md5(CAST(`c` AS char(4000))) AS `c`
partialCONCAT(LEFT(`c`,N), '***', RIGHT(`c`,M)) AS `c`
fixed replace'***' AS \c``
truncateLEFT(`c`, max) AS `c`
nullNULL AS `c`

The dialog suggests a strategy per column via recommendStrategy(colName); the user can override per column. The generated SQL is editable before execution (▶ Create view).

8. MLPS 2.0 compliance check

Code: packages/ui/src/compliance.ts + packages/ui/src/components/ComplianceDialog.vue

Checks limited to "verifiable through a database connection" — doesn't cover OS-level concerns like firewall / disk encryption. Four states:

SeverityMeaning
passCompliant
warn ⚠️Non-compliant but low risk (audit disabled, SSL off, etc.)
failSerious violation (remote root, empty-password user)
unknownCan't determine (insufficient privileges, enterprise-only feature)

MySQL family (MySQL / MariaDB / OceanBase / TiDB) — 7 checks

IDCategoryTitleDetection
mysql.auth.password-policyAuthenticationEnforce strong-password policySHOW VARIABLES LIKE 'validate_password%', policy ≥ MEDIUM and length ≥ 8
mysql.audit.enabledAuditAudit logging enabledaudit_log_* (enterprise) or server_audit_* (MariaDB)
mysql.auth.root-remoteAccess controlroot not allowed to log in remotelySELECT user, host FROM mysql.user WHERE user='root'
mysql.auth.anonymousAccess controlNo anonymous usersmysql.user WHERE user=''
mysql.transport.sslIntegrityEnforce SSLrequire_secure_transport=ON
mysql.audit.slowlogAuditSlow query log enabledslow_query_log=ON
mysql.integrity.binlogIntegritybinlog enabledlog_bin=ON (required for PITR / replication)

PostgreSQL family (PG / KingbaseES / openGauss / Greenplum / CockroachDB) — 6 checks

IDCategoryTitleDetection
pg.auth.password-encryptionAuthenticationPassword encryption uses SCRAM-SHA-256SHOW password_encryption
pg.audit.pgauditAuditpgaudit extension installedpg_extension WHERE extname='pgaudit'
pg.transport.sslIntegritySSL enabledSHOW ssl
pg.access.superuser-countAccess controlSuperuser count limited (≤ 2)SELECT rolname FROM pg_roles WHERE rolsuper
pg.audit.log-statementAuditlog_statement configuredSHOW log_statement ≠ none
pg.auth.empty-passwordAuthenticationNo login-able users with empty passwordpg_authid WHERE rolpassword IS NULL AND rolcanlogin

Markdown report export

Click Export MarkdownrenderReport() groups by category and adds "Summary: ✅ N · ⚠️ N · ❌ N · — N" plus each rule's description / conclusion / raw evidence. File name auto-formats with connection name + timestamp: compliance-<safeName>-<YYYY-MM-DDTHH-MM-SS>.md.

Parallel execution

"Start check" runs all rules in Promise.all; failures don't affect others (try/catch falls back to unknown), drivers handle their own pooling.

Other dialects

Non-MySQL/PG family fall through to a placeholder:

This dialect has no compliance checks yet — please verify manually

Oracle / SQL Server / SQLite / DM rules to follow.

9. Chinese SM2 / SM3 / SM4 (planned)

The compliance rules already flag "password_encryption=md5 is weak under Chinese / MLPS norms" (see the pg.auth.password-encryption description). The auxiliary API for SM2 / SM3 / SM4 (for app-layer SM signing / encryption before write) is not yet released, planned for v0.6 as a standalone cryptoCn.ts module:

  • SM2 elliptic-curve sign / encrypt (based on sm-crypto)
  • SM3 hash
  • SM4 symmetric block cipher (CBC / ECB)

We'll add a "Chinese-crypto helper API" section once the API stabilizes.

10. Encrypted export .skbk

Code: packages/ui/src/export-encrypt.ts

Encrypts arbitrary text (typically a SQL dump or connection config) with a password into a single-line JSON file with extension .sql.enc / .skbk.

Algorithm stack

StageAlgorithmParameters
Key derivationPBKDF2-HMAC-SHA-256iter = DEFAULT_ITER = 200,000 (tunable, recorded in header)
EncryptionAES-GCM 256salt 16 bytes + iv 16 bytes, regenerated each time
IntegrityAES-GCM built-in 128-bit auth tagWrong password / tampered file → decrypt throws WRONG_PASSWORD
File headermagic: 'SKYLERX-ENC-v1'Identifies version on algorithm/param upgrades

PBKDF2 iter = 200,000 trade-off: OWASP 2023 recommends SHA-256 ≥ 600,000, but desktop has to account for older hardware (Atom CPUs sit at 1+ second at 600k). If your contents are extremely sensitive, raise the iter at call time in encryptText.

On-disk format

json
{
  "magic": "SKYLERX-ENC-v1",
  "salt": "<base64 16B>",
  "iv":   "<base64 16B>",
  "iter": 200000,
  "data": "<base64 ciphertext + tag>"
}

Field order is fixed for clean git diffs; single-line JSON for streaming I/O.

Error codes

ErrorWhen thrownUI feedback
INVALID_BLOBParsing missing fields / wrong types / magic mismatch"File is corrupted"
WRONG_PASSWORDAES-GCM auth tag mismatch (wrong password / tampered)"Wrong password" (no distinction — avoids leaking raw errors)

Web Crypto dependency

Uses globalThis.crypto.subtle — no third-party deps. Electron renderer + modern browsers work directly; Node 18+ also works (for tests). Ancient environments throw Web Crypto API unavailable: upgrade to Node 18+ or a modern browser.

11. AI privacy boundary

The AI assistant (Anthropic / OpenAI / DeepSeek / Codex / Grok) is a key SkylerX feature, but what gets sent to third-party APIs is only what the context demands:

DataSent?Notes
Current SQL textRequired for the conversation / completion the user triggered
Current schema hint (DB / table / column names)Metadata only — no row data
Error message + error codeFor "Ask AI" diagnosis; see AI section 4
Connection metadata (dialect / connection name / DB name)So the AI picks the right dialect
Result-set rowsEven with AI inline completion on, we only send schema hints — not the rows SELECT returned
Connection password / SSH private keyThe keychain ciphertext is never decrypted into a prompt
Entire local connection configOnly the dialect / database of the selected connection

To fully isolate AI:

  1. Settings → AI Provider → clear API Key → disables inline completion / chat / Ask AI entry
  2. Or use a local endpoint (Ollama / vLLM / private deploy) by setting endpoint to http://localhost:...

AI webhook notifications follow the same rule: the body contains "title + summary + trigger time" by default — no SQL row data. Edit templates under Settings → Notifications.

12. Security shortcuts

ActionEntry
MLPS compliance check⌘K → "MLPS 2.0 compliance check · connection" / right-click connection → Compliance
PII scanRight-click DB → PII Scanner
Generate masking viewRight-click DB / table → Generate masking view
Data contracts⌘K → "Data contracts" / Tools → Data contracts
Encrypted exportResult grid / SQL editor → Export → pick .skbk
Security policies for all connectionsSettings → Data masking / Settings → Production watermark
Customize shortcuts (avoid mishits)Settings → Key bindings

13. Known limitations

What DBAs need to know about real-world edges:

  • SQL Linter is heuristic: no full SQL parser, string scanning may miss in rare cases (e.g. nested /* ... */ comments combined with string literals containing the where keyword). For high-risk operations enable the prod confirmation prompt (type connection name) as well.
  • Compliance checks need read permission: mysql.user requires SELECT, pg_authid requires superuser. Lacking permission yields unknown, not faildon't treat unknown as pass.
  • Render masking is UI-only: data in the DB is still raw. To stop apps reading raw values, go through database masking views + tighter DB-account privileges.
  • Encrypted export doesn't defeat "offline brute force": 200k PBKDF2 rounds is ~10^7 cost — weak passwords are still crackable offline. Use strong passwords or distribute via KMS / public-key.
  • Environment tag is a soft constraint: extra.env = 'prod' is filled by the user. If they slip and pick dev, prod rules don't fire. Standardize this via team-wide "export config → coworker imports" practice.

Apache License 2.0 · Wuhan Skyler Network Technology Co., Ltd.