• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacklok / toolhive-studio / 22192519146

19 Feb 2026 05:25PM UTC coverage: 56.334% (+0.2%) from 56.125%
22192519146

Pull #1661

github

web-flow
Merge 8ea96e427 into 67271ad6d
Pull Request #1661: feat(db): add SQLite persistence layer with incremental read migration via feature flags

2659 of 4952 branches covered (53.7%)

270 of 439 new or added lines in 23 files covered. (61.5%)

15 existing lines in 3 files now uncovered.

4292 of 7387 relevant lines covered (58.1%)

114.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.0
/main/src/db/encryption.ts
1
import { app } from 'electron'
2
import crypto from 'node:crypto'
3
import fs from 'node:fs'
4
import path from 'node:path'
5
import log from '../logger'
6

7
const ALGORITHM = 'aes-256-gcm'
1✔
8
const KEY_LENGTH = 32
1✔
9
const IV_LENGTH = 12
1✔
10
const AUTH_TAG_LENGTH = 16
1✔
11
const KEY_FILE_NAME = '.toolhive-key'
1✔
12

13
let cachedKey: Buffer | null = null
1✔
14

15
function getOrCreateKey(): Buffer {
16
  if (cachedKey) return cachedKey
10✔
17

18
  const keyPath = path.join(app.getPath('userData'), KEY_FILE_NAME)
1✔
19

20
  try {
1✔
21
    if (fs.existsSync(keyPath)) {
1!
NEW
22
      cachedKey = fs.readFileSync(keyPath)
×
NEW
23
      if (cachedKey.length === KEY_LENGTH) return cachedKey
×
NEW
24
      log.warn('[DB] Encryption key file has wrong length, regenerating')
×
25
    }
26
  } catch {
NEW
27
    log.warn('[DB] Could not read encryption key, generating new one')
×
28
  }
29

30
  cachedKey = crypto.randomBytes(KEY_LENGTH)
1✔
31
  try {
1✔
32
    fs.writeFileSync(keyPath, cachedKey, { mode: 0o600 })
1✔
33
  } catch (err) {
NEW
34
    log.error('[DB] Failed to persist encryption key:', err)
×
35
  }
36
  return cachedKey
1✔
37
}
38

39
/**
40
 * Encrypts a plaintext string using AES-256-GCM.
41
 * Output format: [12-byte IV][16-byte auth tag][ciphertext]
42
 */
43
export function encryptSecret(plaintext: string): Buffer {
44
  try {
5✔
45
    const key = getOrCreateKey()
5✔
46
    const iv = crypto.randomBytes(IV_LENGTH)
5✔
47
    const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
5✔
48

49
    const encrypted = Buffer.concat([
5✔
50
      cipher.update(plaintext, 'utf-8'),
51
      cipher.final(),
52
    ])
53
    const authTag = cipher.getAuthTag()
5✔
54

55
    return Buffer.concat([iv, authTag, encrypted])
5✔
56
  } catch (err) {
NEW
57
    log.error('[DB] Encryption failed, storing as plaintext fallback:', err)
×
NEW
58
    return Buffer.from(plaintext, 'utf-8')
×
59
  }
60
}
61

62
/**
63
 * Decrypts a buffer produced by encryptSecret.
64
 * Expected format: [12-byte IV][16-byte auth tag][ciphertext]
65
 */
66
export function decryptSecret(data: Buffer): string {
67
  // If the buffer is too short to contain IV + auth tag, treat it as plaintext
68
  if (data.length < IV_LENGTH + AUTH_TAG_LENGTH) {
6✔
69
    return data.toString('utf-8')
1✔
70
  }
71

72
  try {
5✔
73
    const key = getOrCreateKey()
5✔
74
    const iv = data.subarray(0, IV_LENGTH)
5✔
75
    const authTag = data.subarray(IV_LENGTH, IV_LENGTH + AUTH_TAG_LENGTH)
5✔
76
    const ciphertext = data.subarray(IV_LENGTH + AUTH_TAG_LENGTH)
5✔
77

78
    const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
5✔
79
    decipher.setAuthTag(authTag)
5✔
80

81
    return Buffer.concat([
5✔
82
      decipher.update(ciphertext),
83
      decipher.final(),
84
    ]).toString('utf-8')
85
  } catch (err) {
NEW
86
    log.error('[DB] Decryption failed, attempting plaintext fallback:', err)
×
NEW
87
    return data.toString('utf-8')
×
88
  }
89
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc