Core library: model, envelope, generator !7

merged merged by cmc on 2026-09-17 15:53 UTC · krz/keycask:core into main

25 files changed, +12524 −0

Layout: unified · split

.gitbay/ci.yml added +52
@@ -0,0 +1,52 @@
1# The runner is Debian 13, root, no docker. apt state does not survive between
2# builds; $HOME does, so the toolchain tarball is fetched and verified once.
3# swift-keys.asc is https://www.swift.org/keys/all-keys.asc, committed because the
4# runner cannot fetch it intact.
5# Each step runs in its own `sh -c`, so the install block is repeated per job.
6jobs:
7 build:
8 steps:
9 - |
10 set -eu
11 export DEBIAN_FRONTEND=noninteractive
12 apt-get -qq update >/dev/null
13 apt-get -qq install -y binutils-gold curl gpg libicu-dev libcurl4-openssl-dev libedit-dev libsqlite3-dev libncurses-dev libpython3-dev libxml2-dev pkg-config uuid-dev tzdata git gcc libstdc++-14-dev >/dev/null
14 SWIFT="$HOME/swift-6.4.0-RELEASE-debian13"
15 if [ ! -x "$SWIFT/usr/bin/swift" ]; then
16 base="https://download.swift.org/swift-6.4.0-release/debian13/swift-6.4.0-RELEASE/swift-6.4.0-RELEASE-debian13.tar.gz"
17 work="$(mktemp -d)"
18 curl -fsSL "$base" -o "$work/swift.tar.gz" "$base.sig" -o "$work/swift.tar.gz.sig"
19 export GNUPGHOME="$work/gnupg"; mkdir -m 700 "$GNUPGHOME"
20 gpg --batch --quiet --import .gitbay/swift-keys.asc
21 gpg --batch --verify "$work/swift.tar.gz.sig" "$work/swift.tar.gz"
22 tar -xzf "$work/swift.tar.gz" -C "$work"
23 mv "$work/swift-6.4.0-RELEASE-debian13" "$SWIFT" || true
24 rm -rf "$work"
25 fi
26 export PATH="$SWIFT/usr/bin:$PATH"
27 swift --version
28 swift format lint --strict --recursive Sources Tests Package.swift
29 swift build
30 test:
31 steps:
32 - |
33 set -eu
34 export DEBIAN_FRONTEND=noninteractive
35 apt-get -qq update >/dev/null
36 apt-get -qq install -y binutils-gold curl gpg libicu-dev libcurl4-openssl-dev libedit-dev libsqlite3-dev libncurses-dev libpython3-dev libxml2-dev pkg-config uuid-dev tzdata git gcc libstdc++-14-dev >/dev/null
37 SWIFT="$HOME/swift-6.4.0-RELEASE-debian13"
38 if [ ! -x "$SWIFT/usr/bin/swift" ]; then
39 base="https://download.swift.org/swift-6.4.0-release/debian13/swift-6.4.0-RELEASE/swift-6.4.0-RELEASE-debian13.tar.gz"
40 work="$(mktemp -d)"
41 curl -fsSL "$base" -o "$work/swift.tar.gz" "$base.sig" -o "$work/swift.tar.gz.sig"
42 export GNUPGHOME="$work/gnupg"; mkdir -m 700 "$GNUPGHOME"
43 gpg --batch --quiet --import .gitbay/swift-keys.asc
44 gpg --batch --verify "$work/swift.tar.gz.sig" "$work/swift.tar.gz"
45 tar -xzf "$work/swift.tar.gz" -C "$work"
46 mv "$work/swift-6.4.0-RELEASE-debian13" "$SWIFT" || true
47 rm -rf "$work"
48 fi
49 export PATH="$SWIFT/usr/bin:$PATH"
50 swift test
51 paths-ignore:
52 - docs/**
.gitbay/swift-keys.asc added +390
@@ -0,0 +1,390 @@
1-----BEGIN PGP PUBLIC KEY BLOCK-----
2Version: GnuPG v2.0.14 (GNU/Linux)
3
4mQINBFZNNIwBEADNFSwCfUUyXZujWzQCqYLlvRwZkuMUcJQH4NRwC/an9KwilFrz
5pf/+atZFSciMx5R9vOG2odWbwBYU7t8XEd/NYCgyTwMNxAFkuaN1E0b/3mOWwbT5
691HziQNy0jDUlM28370pyv0rBTHvPbBQlo7BxJbwQXL3rs3KObAnhTG51aLztGds
7OTxZDdpNY07dPGKJevP61l35yZI40tUmCKn/z4sLmX+DoUUplA35SMRnvQjd/pGF
89BTAFzLfsbN0caBixy++0ZIyK9ep1dezhqisUndE/rNk8KPNAM5zeFA1onzEEGFc
9gk4iZ0N/S7dVb7N4Uhn8C/87E9yUkrzO0Kl+p2zumORiqIvkv9vtHMaKa6rBP/vI
10iG02eiLmaWdDkg7afpJjnC6WrrJoBe/lbDAYrbD5eR4KGHdxMtAdh0sYZrZR8T4m
11gI6alC8PjYY22wiNLeekATobDu5JVQna0525L57gpbLOhSjXk/69HHw/l7d/e6br
1235rmMYGJd/9Q1w/4gMo8tX5m1WaoQxac4tZs8bBRKNP7YJkHmwfVG21h0KA91yFA
13l1BMe9U+gomfqRoBCRXGDl1mHmyGqu59JjpYEP04Xu2dLCu8yt/HHYirMdsKIvHT
14WKm6EvuQ4vFTQbPcdrMltL9yf8xG8EL0oDIbaSAnHI+sY+C30i0S557G3wARAQAB
15tD9Td2lmdCBBdXRvbWF0aWMgU2lnbmluZyBLZXkgIzEgPHN3aWZ0LWluZnJhc3Ry
16dWN0dXJlQHN3aWZ0Lm9yZz6JAj0EEwEKACcFAlZNNIwCGwMFCQPCZwAFCwkIBwMF
17FQoJCAsFFgIDAQACHgECF4AACgkQ1EHJd0ErN63tFA/+N3ANSFFc2FcdimzisPOv
18wBL8xSxQxUdsdELsht6WLh3buTunlIO36Ubo980hXyf6oxsfvMrDsn1eve28cpks
19SXZxLpfQm96753mWICz1WJcoUv1EXpFKuqxb3OJXu6di11NQEtFNd9rM4x7MJQja
20ICr5WXxzX0ObEUxY6xTjMlUyHkmR9ekEEPZUdrwpRFC847bV35vdCtMRztghecBH
21tHMhTEbuYxpjBZ0hqIkLzDAiWenqca2P9YD2sgntTJFfZ3V/oi/REFxj5v5BmNQ2
22wCyppoWDyD+oWJyh5dwuOc+xlYboN5EtIkuhS7/ZuvQipZpeJD7w4pkPO7l2Twal
23aHulJJV5f40gpIniullTC/I6P9HIp94wZ+UPm3/9M4AiO0oB0x4uk+buA7nCZ5Oi
24BaZKwO8nMFQBEaNuqKz+xLeokum3Alcwx3drwi0TlBX+GafrFFHwhdwDGuQsWpGT
25vNl6LEBNduPvs57382W13aiYT/p470M/RuTt6qpLyLVsWmGO5oh8mm/WWGDzza6Q
262NuUBcs2ZCECvRlEt//689elx7VDbJwE6Mw3V8bosGx8y4XcQYATFqxm7WflbBpo
275HR9AST2Noc6g4VPepZSgi0T4lRbIsyLml7kIvOQBre4i36br0VLIUairez1mqpm
28sG3P11076R+g/BcL4hg9jXE=
29=ONEK
30-----END PGP PUBLIC KEY BLOCK-----
31-----BEGIN PGP PUBLIC KEY BLOCK-----
32Version: GnuPG v2
33
34mQINBFZZS3oBEACbRmmzVwF/Zf/zGEZTGXWzeCjwmYcvCx/aqklIWRFC/PPRwZ53
35rNgeVpEHNu31duF3JCEX/7fL9rmjnW0C2mQCzkvKF+rZNO+jPL/0/fsjVfnSXx+g
36m9yVyPmR0XvR0Kk5PqjR8XH/syNblnCo16fr7YK/JMrGM9E/d+UomomXKALbgebX
37oO0c8eA2Vij4b2BAFr+L54MTzquKqyEru9Ty2PTcIBxcEYqko8i3Ttej6WVMKphW
38J9QNaeVF9mfHuA7zpFlwtfFOrUnA2Qh6IwT+w7o941q4TpzYNVlcR3IV2P51CZrG
39zQAGRvWWmg5yswhyTEJpt0i4eLoqeJrcNZRT8FQjmcXgtdPvSFkJPi39e5LrT23M
40/W5tHvnldFDd4RYQmoO3XcIJzyEe6yXNrcaTT386m2e+cjA0SwZ7l6kHVhMikaS3
41FWccQ9VgWqLHZPd0f9oKdXbMMoIZs0D3F4hoz4oE6xkntEhw3Z3cVrKGpYuhJOKQ
42yPoCaNEpnT9qaTU8BmG/ToUWXZDuhm3FOAOEOOtFFChfy/0+9HnS6Q2aogYuD++o
436XNSFCwONIRnoHyIRjCA8ce/GNsxpBp7bjUDGLoQJzmki4O/lyxMxS5k0Z4mjLS+
44xZo6OLt8LW5/w3YJUEU6PM88GhA1c3dm+U4mV3oW0QL0WG0b1tSxhLHf+wARAQAB
45tD5Td2lmdCAyLjIgUmVsZWFzZSBTaWduaW5nIEtleSA8c3dpZnQtaW5mcmFzdHJ1
46Y3R1cmVAc3dpZnQub3JnPokCPQQTAQoAJwUCVllLegIbAwUJA8JnAAULCQgHAwUV
47CgkICwUWAgMBAAIeAQIXgAAKCRCfWX9NIaVtX/L3D/9NJVFh9tBYgzwtbhgHTVxq
48U3VKeKUIvvWxETtaYmKsL8BPBwVM2IHDPyiOGczxe7RbpNszoIY/EBKNsI7yOnon
49qrMebQVpzV0wK2WwfHKnXYXXFeWADapwBGEcST7WNwX+la/oJOQtBZpMbBHDia4C
50g7EENIdeWDRHxzl785PSlAOERvaKsPlFk3bolQl/Vhy5YZIe4Mt9KN5LZ2E+xz3Q
51DDplOTi3I2df9Maf9UOxrS7zk2NvHw+5YeqNs7cqp++mjIRQ7/M+PGPx7PZMttBk
52Y7sIn1m3uSZkK2nEWZ91b3pZdr3Sff2Ig1H+ConUqy9w0CqGSqCmY/6QP05J4kpv
53ga1uNmKRJdv/pGHfTaoZ+dubm4+UzGpfzHdEhvmm4gC/+kU+nq5BuhvzO7iWKk0o
54kZOKT642WSsp7TYKp96sMh2xeUbYl/eAHi8nQfGuYeo6n1NaUMtwb8jYusTuISuW
55yWRSKaUanjjKGkl9LTpODWLkXHmqGJhp9l61AQ3fKDQgXuKuglWBfsZOhDRl+oYr
56Q22LzpSd4ISG/gXN3X4ADeuWJpe8d+nvpc6KtuShgRT+KeqrE9BcXfDdEk56I0Xx
57+eGb1v4f4nibyiCvw/mBDCRMIqOsc/jyqA9lydLMxv/Us7KNYqh/Dzx7obfRUE6L
582S6Hfx8PaYq16WXdxd/PBg==
59=5r2z
60-----END PGP PUBLIC KEY BLOCK-----
61-----BEGIN PGP PUBLIC KEY BLOCK-----
62Version: GnuPG v2
63
64mQINBFdM2VsBEAC63xLZsLc7HBbJ1V7ak8dGNSDs2ibcaMKAuWU916UWjFX+KlZE
658ruOVTol1KvA0cac6CX+Fd5SMirpxAaEUbYMwVEMzxOG7NhBp0wAJiO3LWsVoL4z
66G7wIpdPzjxP32qUABpF0Z8uo5zhQL6uaWtmKIhlBlzUmhbxYFzGUuCO8Y94hCH5A
679l5Oedin3D3/v2T60/B2rygmQ1fC372Peg6m0dR8PCp42Jm+VXjgF4yfGyP4MJPO
68c1gq8AWNQ4Ex1qhQ9E/Mhbn88LvUQ+OO5u3iNw7AflBuvLaLy7VbD+pGpoGv3EdJ
69LsDJg3dU4F9Ii2hLAD5Sushtjx6DwwFJvKFsm7QzG7OTQB0rC9Gtw/MpopafFI0e
70Wo/6xmXnw9GQQ6uS8RoVri6Lfp4IpXdLZ3CMxBeJulmUpBdIFK+/SnyMfYW88tzx
71DbEFpXa6SaksSXjAXuXhUu+h37YdIfWcbE0KUXH848RcRPnTUOBwsqE2UzLQjF3A
72aPV7YIQxNNp0mUsRE+9R6yGhSog77jsu5XusswYEOfSU9p/bkQd5kJ5acTyHDU8A
73+q/+O2T6aZqIFOhgXFiRj2TDGlqj4K+LUycpMWzPgn+KWKNnWgbJH1u3xXPnOZjT
740i1Vm2umWd7OxzThSvlPGlvRLwPKWhdeFL37M3Bg3og1P5TgP7j4HT6Q2QARAQAB
75tD5Td2lmdCAzLnggUmVsZWFzZSBTaWduaW5nIEtleSA8c3dpZnQtaW5mcmFzdHJ1
76Y3R1cmVAc3dpZnQub3JnPokCPQQTAQoAJwUCV0zZWwIbAwUJA8JnAAULCQgHAwUV
77CgkICwUWAgMBAAIeAQIXgAAKCRBjvBz+kdMGxjBhD/9RUFlNP0cG6RJPl51mPCtO
78YMW/y5ssYyWi6u8q44Ccqit4EKjXUaeha4KSWQS9c+7q8bbfxbtl6AEsI+fxx3RA
79mYMjXfPnpjxqJLvwXOzAmG8bZzycdKiC1XBINR3e/Jw4Zu/yj/GtWSG2+OogFfNH
80n5Gg43B8D/fdqQkvlFXVuM0kMKPmAc+S1ikBvprpyjya47EKx5rWSf7pl/gm4jBE
81dBrWdpaBicOQmuv+Vr59rj2/UCWdD/dZRtwB66qn/AIZIgEOLyW2s/Mu9VK5zoNs
82oUZhRpe9t3eH59KbKvU+JdADMhBnUe3r4/HkSbHmPoML56g8RQV4T3LZa2sMt9Z3
83UC7WFr3VfPxXHpI9BhY8HJvaB4ML6PvYuXHP7TYkrSQ9FMoxjHcwEE7cga8BzAHu
84MxCuGrPryHRyApdFOh6oTts8/bjn+sb0VVvnAjFsAjDcBF18s5aI58eGLzLBgRnZ
85jyGvZ20h+I0JE3vDydi3rVEBm/fgkUwTtBuEG/mIwIea9QEcMkv5dq1aYheav1NZ
86Amqr2BsSl8PufUdfGgMKjMIR6Sv40mhTCUFSL1TG8OsaOecK7XXEjcpaQjCg1lqj
87hL04ayrZehJzuiP30htRdbrFraDr/WQOPW3VqE0DtjVfaH7AARKLp/HRDOd5hiUU
88RBrjB1unJxVpnKg4t0El0g==
89=ssEv
90-----END PGP PUBLIC KEY BLOCK-----
91-----BEGIN PGP PUBLIC KEY BLOCK-----
92Version: GnuPG v2
93
94mQINBFlAigEBEACbGsfWe5nCOFrYSpz6oPh9ihq0RZQlktSz7E3Ou0cwSNqOnGRO
95s6z5pyPYlJgpvvaxsKcBS6R4DFq4/30Aut4Jj4T2qa0BXZqqrHrXyM8ucmpPpF0T
96jcRJAQZg/Rj8AhelYtcNKml/j8b9n8p14K+u+Zp0SudaNxLdxIbr94eEEDd/ecH/
97caGspqPuJjBanQNTzV9ws8ft7NLCdWnSvh5aiZrsWIrFfFxQm8zh9kCim2e3QX+p
98wufMDIKH/uEaOlinVCmFJNXGqpeyRjrile+r6Bd8fJiFLFtUeIiCdyxD4nrV+iLx
99ZAc6boVeSXdawPpX/eyXeH7NhpEofFz02W1EAoNFKgzdoEi6CBaA9P9Jrk/wRZuH
100vR+Mzbuu5aXDwDCjhtFu2Mm3bmN5pt3xuYCYoCgOp1wEld+szMfSjQNNJfUkFwzU
101eaHuaofXHQxuQ/uIEh8cr8/GEYtqGOU82T1raj3Sv1GcUfj5RO9k2rI7hWro8Hzc
102oXjLTDfABSZJrsuDLrzMUDOSKb+R3rf7uDYf/bTWkpLwSvrJRQaMXkQm70WWBFF0
103A75HxFBi8/9f35azYlFfCi0cEHAAHm0wdxU/D9b6TV/9zDedbgFl+/8xMBQiOX1r
104WfQrXA7zONhZpc+to5tyzrV57+Q4buKQUibyYut/7Gl1FbVaD4LvAUpbqwARAQAB
105tD5Td2lmdCA0LnggUmVsZWFzZSBTaWduaW5nIEtleSA8c3dpZnQtaW5mcmFzdHJ1
106Y3R1cmVAc3dpZnQub3JnPokCPQQTAQoAJwUCWUCKAQIbAwUJA8JnAAULCQgHAwUV
107CgkICwUWAgMBAAIeAQIXgAAKCRDvVDDwceGyNQE4D/4jyvkwjMzp3KSZKTMvcalT
108JPmc4rkpqc1b3VJCTpG/QRK9faJAbUVBz+iJBRIA7OjsjW9nS+oEkschjs7mjdiA
109U3XxtJMsZSJiO6TRdstJg0i2jT1yhQ7zuf8xeFEpx0Ekya05mYHiz3LdS2TZJcZZ
110D713M1GFsb+bDRGk18nVvlUIDB+9jIoqLgXf3muzHUmF0VF1HHfjB5zHfd9h6P+n
111DG5e/qA1MA6jaoGcHK31qoqvrLvoDMYlo6Nirq5Gsjf8rNTwij2GM1tNP8BIn2Y2
1125rgQ+UbZ3dUE+2KgUg+cqNtZPe0szHOInUew/Kq4ogRWgSWw/NbR+W5J5Ro9pCFJ
113pJaLBhCDQ35CIfcp5Mn2qSr0UW+7U4wfsoI4Oui5/dRx4iXFLD2LRlQuLTe0vRDK
114N6PcSeybtCyxeXXopWZvdNvO9oMoJ6vrorZciF+tcG9qaqaaf/Y1h7pfwiYkLvwp
1154RC/FFURZyjeyk8gfxaE1NfcnXM3rByW8br/gtL1VR/s3igKXk/rVeywlMeXyeXP
116CxS1uflC2dxGfzFiNFJoQlS+0XYV4nYNTVALe4/css3LLFtLt9rBKe3U7JmXl52k
117zyUq5nMsltYp3kg3dRYHejwzyrlVGt0Twos/Zqf8NztD9LPiVsZP/f84AobFsmkN
118ex1agHRY0JDH1IrwmfVwrw==
119=nP+X
120-----END PGP PUBLIC KEY BLOCK-----
121-----BEGIN PGP PUBLIC KEY BLOCK-----
122Version: GnuPG v2
123
124mQINBFoBAhsBEACtaQbqQhwMedruhTrierPCHRy6ZmO4HuRdrd9LnOBSraQnHFb9
125kc2IfuHwB6YytZg6VFy4iaFx6vOkZQk7bFiAMB5RwIdPm7OmHGAYWkKeDOCB3lJB
126gJ0YdP7bMOGJL21RRZfUmkTMaT4zP3J0NeUL/mt4ezdJQyyWxdYigN2AgglAxZve
12727K+IjlQVvuAe5MWqPn4pTktjWxuDkIN2BvdWXz5Ycbewul7Hi9dZzaLIVDeTq2F
128MUjR9FBJoNpJKjPTAeOyXj2jvHR753yAeRG/79sfeId9568XNxTi/2OXqdkvwg/q
129huGbx0fgkqnkMw/0nvqDVsbnHXMdE0WNVluemt6I9hJvoHfOEBYYAZi4+spoVCJW
130xwwgVt8tNTgbynejzr09d1RgVJH/OKcKCCOTtbbjUdtMt3MMbaBaN10AKKFWWBbR
131Tom8J6wwM8LsKkFj0F5uzkG928C/vx5hFv3ZltlFUu9j8u4WU2kF1D1nzhw4E9+S
132epq4jrWMa5Dvr9UhnkJCW0g/0vPnynbOcJaXyWQtqxdgAT9LxMBeMSd26O9UklaH
133INivaw8l2SpdauJrYon0uhK8RTTp2rSt+SLHqgxsbmGmr5jTm1Z37EzffgoIFDOB
134a+3zHaLJvkeZQXzbLAqzfHYSV9lUe7In+F9tqZ5l7702UqJmIWoGb6IGqwARAQAB
135tD9Td2lmdCBBdXRvbWF0aWMgU2lnbmluZyBLZXkgIzIgPHN3aWZ0LWluZnJhc3Ry
136dWN0dXJlQHN3aWZ0Lm9yZz6JAj0EEwEKACcFAloBAhsCGwMFCQPCZwAFCwkIBwMF
137FQoJCAsFFgIDAQACHgECF4AACgkQdjjx+ysrCMSihxAApw7qKGfiN8a4bFVwKRdY
1387BLYu9HLetKHUJPerypKEKfUshU0X8Ns3lBxjSqNY2TvLyun1kbtUsxy+qi/X+HM
139nb93ONYOAeny1DFxUdiehhVttTQQB3RXdmhHGytoxzHX62N/uo9tYW9xQ+gaK+QC
140Rt6nZN0Prm6X+4vpQGD7H7O9cN8pw4YuoJRDwn6bFPl7WgqrYWX8/jvbXH1NjbJi
141j6kXvDSzwtPQNsj7vTC5K4TBMgfBFw5kndu+1sEHAfPIbwK8w/EGr32eSRT6l3xk
142TYFquL0S5NGCi9/pIqT90knsDqXyNer4xmwQ5FJPUTpdUbFz0o/DQvIFrbJ465Q4
143feibfhpxVqLCpX8goosAS5nd0jRGicSYmoxdtUBUp4gn3ee3yI6JbA0TN8haczNC
144e0FfQD9s9O+m6ft2/CzIx7YFeiIRhfcjfmGJ0+eAdpEsDqC5Hyc60hwHhPg222Ah
145ASkhwDD9NejskxvdMUPALk2C41ZlR1dYqfXwUJJDPwNY210QQgBZ0+GG72ErYssh
1464sG5U9XRv4IZhFlgBVmMlfbSi71BawOy+IN7ii49m9AY7p6U+m7yoeZV2H8YJbTk
147KhfnS3rZqsSU+ZYJYkGOxnQetkKUDGaVKdwJE49ll5R1/4om0h8aOOU8VyzrXPc9
148cZMQ1BcfMNkeu81HaxE0Lpc=
149=l0K5
150-----END PGP PUBLIC KEY BLOCK-----
151-----BEGIN PGP PUBLIC KEY BLOCK-----
152Version: GnuPG v2
153
154mQINBFyUJhwBEADDBN6jVDScsgzfQDxSQga0Og4O3N/nIquwi/DGOgsAsTMWcVA1
155gmRJb792vWa9CmnHILl3ap8zObDSbcXg90nx+eCzWJjG+Ud5c7xuam96NR8ntKmU
1568+BbH/dp8zc9WJB37TiWcaLsddrn58zfm7Ml6P9M48WAeRJX8nxVBTw1SjXJurW0
157Ab8LOgfb60I09Skq72Ud7HaYJOG03iNTf6qLlF977OQsHCb21BnKSAmJqapUS96/
158VOngz7TBzYz4rntfetb8hg28WtUl1s5BQzWaFunkP03b8mPh3PL1SZxutwViVWBu
159hf9kJtx/MLb79fnuJEOus1FvDqJdpd83H+XmXMIDYWgcBIBVrLT5HDtRerjF178H
160okb1F+gboGIqhnx25xTPIYctSHPgJRScZp4WKrqQLKswAmSL4YJXnkXSff05l4gE
161WXQpEMLBZa46qmu8lj8HfoZSbP9lfvEtZ6A+Q3sfh3gjYPv7e6n37x22tSgvyzCb
162jHU1pA2rv10AHK7EIeEQElN+zCyAbmKuhPBiCyxDFg5Dx3xNkYwg9szBJ0KleVD4
163+Y3PZJ4N+u+SSATYnHGtmzvkhiNtqJCCwuaqY+jjVObzzqvLLtGjtjxNUWi2X4He
164q+r2fubjCOW14UnQ06qfr3mVUSmuLSKs8BD8qTGuqlgcenGsY0bk0qUPcwARAQAB
165tD5Td2lmdCA1LnggUmVsZWFzZSBTaWduaW5nIEtleSA8c3dpZnQtaW5mcmFzdHJ1
166Y3R1cmVAc3dpZnQub3JnPokCPQQTAQoAJwUCXJQmHAIbAwUJA8JnAAULCQgHAwUV
167CgkICwUWAgMBAAIeAQIXgAAKCRCSXMHM7T0VYSm3EACUtuxMfTmGexA2xBvGhKQ+
168bi3kRMBj+9BoSkVV11gDvumurldXRoKsIIxEdrD4xjagSNiVd7zj3L9wANu6+YgP
169HgBq8sUMXwsyXdL16jNC28qVqKF4jZxwmI9t6nVpQOSxGfBxdXmIhTxDDXFo4JMT
170uKx67BaP1GzZ+BKvxm09xChIJ5xh7x8/bsZ7HlXqc++ARn0Lh9d5CISUKE1PPnMN
171KyRUNt4X/qj7zJHDoAihufPKodvvXPsVAIpfCJZm7XB5WvP63IvQaFW/cX5wineR
172FZY/BKS+tsJt45jJ9c/Ofzqrr//stCz4a44fYvpqaxIqTLr4CpNH3WBIM0RBQqJj
17335jJvdrNIuueby7GN3afRcvZIQBkbMkwZrW1iT8eah3EQDiXszbi7GkbXuOxQOn/
174n8KJJvTJU7wAyH4WLvvWLFUm0oyHjIW4K9vSUM+x74WRuKxVikM6FAJ7uXsYtZ3Z
175lgL2hNlbMwpB+wxvA9isT3dSyzAtZmXpomOLuQTETtm4tSpt8XnF3eCsAKHSWqrA
176/zknqizKeBTPprqibixfO3dwUzFhUu3tkhxznIN3eEUrDMPxNrxObqJf1I4ySUYe
177vc6fqBG86SJ2yjN3IMJm5Y/z58QQUmSOweL9xQSr1hdcCxBtmLSjAlVzJMkpQ/TW
178xXPfHp4mZnRMLsGyO3eYrA==
179=l2j3
180-----END PGP PUBLIC KEY BLOCK-----
181-----BEGIN PGP PUBLIC KEY BLOCK-----
182Version: GnuPG v2
183
184mQINBF3DedcBEADmr11PU7BBVSEjSx3z2bqrktMURD2AnrXJgL+sAeZfMaZpDZCU
185pmLKtI32Yp5X3Mv3mlj2Mz3VdtRh7ni83E5cqTLXs4Yjsujgu6nWS5qWqmSYw0Ax
186RszPPJISf2MAbZyBQJpaoDK9t2eIh3yG6J9XhIMu/ZpDND8afNYGNRhXKetv6+l7
187MHTYd5rlU94eIXcxLA/yV340BVTMoaiIfAwUF2uFubDYfro4Ckj+xzjGIvn3ueu0
188u+Yfmc40YIuCsA+zgf24Tie/rIwifEzWxFCka2jNPU+q8XIrbPmTKGvxnpwjBKIz
189B5HtB+yyV5mhnaVg1kkNZH/rZxJQm/FdFS9IXmOmV5CFn+R8grTxsYP/A4jDHnmz
190wx/gJuUvt/ADt3cuU7sGLtT1cvH1UlS/fso2bDDHBhuEa/9hjD0uIYpLuR4eTUQs
191WNU9a2WFxprNfM7nbUIWqfTr6tdnQORPnDi8lSGePlJNWQHzg36WyxPzq9EG0S5r
19235Gb5NXeok6CN9x0EUiRt/Fl0POQRJ2Y6TWmOzaZ+8B8HExhA02d86Wnzfss8kAB
193eLSHE6aOguwujep9LFrTxUeMZTIkk/kxc0ibHuq0r1jwRWnCx9TwXCFeAW1gs7eo
194xybF/HVuDqSLaRIgpIl2KFxvXRY6g37ggtcAxV5DlH6oK90ja+72Wre8+wARAQAB
195tD9Td2lmdCBBdXRvbWF0aWMgU2lnbmluZyBLZXkgIzMgPHN3aWZ0LWluZnJhc3Ry
196dWN0dXJlQHN3aWZ0Lm9yZz6JAj0EEwEKACcFAl3DedcCGwMFCQPCZwAFCwkIBwMF
197FQoJCAsFFgIDAQACHgECF4AACgkQ+vaYnhvBb+oGHQ//ViB09Rk7W4BxyuvwDZx8
198w7JR/WfCFDYAz210EILu2o3tqHWpfShynA2V/oOFT+VHykr2TK7wT2n/o9axfIq6
199rF8W+X2VnXzwzN3a04iVtakcSeCuH4KIiT7vIjfOtmEwqJlpAIe6LF0NXb5wV9/I
200mxc7xLKbTCBSjrNqQsCOZ6VbLqwQuOjI0afRjnUsI07u/8bo/8WeZS1roKy0ntVI
201wyw05P5cV64pjHwNXH5CN8nB/G4pjCRwehI+4hAm0SOUfIDyLdQ4IThAJY+FVzIQ
202dXSKMyVNQMe/PuwLB3CBNMPOtxsEwOdgrLwClm2jOOS/bjrBI5jerUgCSBDPerEP
2033rb6hjehuvhYudYZ+awmWG2NRxdWqUBO3twudVci4keQjjaBM+XiaWpEn9jD3XkI
204ZZC++bXx0o6ugDGFNTNwpFZ9hCC0x8djDrJDz1OdSYA4Mn4bB1n/8FI3+wSk8AP2
205YzfjXbdXQMe44FUWPJgcCJ8EEsF9eKtw5019288ECIlfaHIzw+tgeg7iTShhB7wi
2068T6PFJvRp8L4hssDfzMS9ZG93InUGL+6eyasIVXJtPDmCCP4LHMN3XYH0TgqBriD
207A6MXJzpykPpl51JkXNRtCWtUVBJpDN3L6Ue4Ouxqs0amMBJCNXM/6t+aJfrgwqiq
208bDzlXsGoKE1i/038Ag41vE0=
209=kzvP
210-----END PGP PUBLIC KEY BLOCK-----
211-----BEGIN PGP PUBLIC KEY BLOCK-----
212Version: GnuPG v2
213
214mQINBFyUJhwBEADDBN6jVDScsgzfQDxSQga0Og4O3N/nIquwi/DGOgsAsTMWcVA1
215gmRJb792vWa9CmnHILl3ap8zObDSbcXg90nx+eCzWJjG+Ud5c7xuam96NR8ntKmU
2168+BbH/dp8zc9WJB37TiWcaLsddrn58zfm7Ml6P9M48WAeRJX8nxVBTw1SjXJurW0
217Ab8LOgfb60I09Skq72Ud7HaYJOG03iNTf6qLlF977OQsHCb21BnKSAmJqapUS96/
218VOngz7TBzYz4rntfetb8hg28WtUl1s5BQzWaFunkP03b8mPh3PL1SZxutwViVWBu
219hf9kJtx/MLb79fnuJEOus1FvDqJdpd83H+XmXMIDYWgcBIBVrLT5HDtRerjF178H
220okb1F+gboGIqhnx25xTPIYctSHPgJRScZp4WKrqQLKswAmSL4YJXnkXSff05l4gE
221WXQpEMLBZa46qmu8lj8HfoZSbP9lfvEtZ6A+Q3sfh3gjYPv7e6n37x22tSgvyzCb
222jHU1pA2rv10AHK7EIeEQElN+zCyAbmKuhPBiCyxDFg5Dx3xNkYwg9szBJ0KleVD4
223+Y3PZJ4N+u+SSATYnHGtmzvkhiNtqJCCwuaqY+jjVObzzqvLLtGjtjxNUWi2X4He
224q+r2fubjCOW14UnQ06qfr3mVUSmuLSKs8BD8qTGuqlgcenGsY0bk0qUPcwARAQAB
225tD5Td2lmdCA1LnggUmVsZWFzZSBTaWduaW5nIEtleSA8c3dpZnQtaW5mcmFzdHJ1
226Y3R1cmVAc3dpZnQub3JnPokCPQQTAQoAJwIbAwULCQgHAwUVCgkICwUWAgMBAAIe
227AQIXgAUCYFlw2wUJB4exvwAKCRCSXMHM7T0VYcDjD/9gX5EXlPJU1QgFw4PwRUDj
2289GxJ7DQ8ocgZT3mtt03gpRj7yTOL0iBDsJ4GtDiPcq6xCm7WEmNYBs/cJWVGwBC2
229jeQCUCkxCXR310V3/RRFMhKZZPBq9A1UqqooO0WYkpR6JFP2a5cf2kounuyBoVGX
230wV7Cn4ZqX/fLOt156EiWUtOxC5L/nJqn6Ea57arwVUwZ3Q2tcKthxFrmMD9H13oW
231Y4coWiQhVx0xCMbc4cPh7TeRIdUu7D2JJPg/ypqhE2fIPPYrCAJSWp9A/m5oaLM+
2326/ABV+JYw3sgmnCSMxX+EV5iDsRamWMJwO4Yep8qMBD61jID5zn0N+VSLdEVfX+5
233O4tdHMvC/YenJ0SFlWmZ7VGD99497IlsTMBFuOzBwXsfwdbSmG5KB6uKormLB6KB
234INvASgi7HbcYgM6vh82mvpswp5UmrT8f2trnSaxHUz5gtUiYFKdBrLgbLBhMlUTZ
235giRHJqm7DnYJVUAOOj26uu06gcXu77MzEiBT3PGVNPMgfYhc5I6hnx4nrXDYT5IT
236bP9Gt1BpsaG8J2jZL2OyOdkd7FkAuWzJVmozDIhMcsf5AjxrZaSYKc2BwgevdIB/
237nvrD/IHRzS34EDnqv2J2LsQqWI3PM7JC5fW9L69NaSWdWpIoHl4gPcm55wbF+tHP
238SnioHPOvgHUi1wc55I6QNA==
239=z7kI
240-----END PGP PUBLIC KEY BLOCK-----
241-----BEGIN PGP PUBLIC KEY BLOCK-----
242Version: GnuPG v2
243
244mQINBGGJWe0BEADGVbVhCEWnCMdAoxCaUaCPNomG5wdzw+sgx8FvqCFiGqYq/qzN
245ckaydk+XWfiDT/Vo/NjMYnUJ0+gdSua1X0IUGqQlATYhKSGTHqgab4nwDCym0nFl
246SBHPDgmMTNO57dHSGYduPjuisuhq7i0pybyn/oXuHAW5SIlz7bfpaz2jzFDY4xEP
247TXImllHOmTxBsDdwv2J+H158w1NSHJoHeXpPIse3VHlcwJVNeGYMiDx4Qw9TfaL5
248fI7GgjbcELCXw/pGjbiz/JFX7Fzm4snesnqwIG3TN4kH3BdKRNMFnyN3zNZxQTaZ
249LNCr3NCzhmmWTjeX5VSu6C+W8wuYRAThqJ42HpPR78zgv6pMnHgdhqquqAfL/Oid
250XlISbQ4bSdc1y8xkLb86pzwzrLV3LmHjJ3NAi9U1QXygW0ikTgbB9LPzaTSGVU+y
2510PMQ6JrRfSvyt5G7RmVzqm1ro8fkmeZBjNUf7GXCldn9linbhKzh/IvvPTYeNhIK
252T99giWSChvZyR950vE5a4WEYKV3taJRLCqXHH1d7FKC080pvPR9VsqWB/thQ48Vr
253dPleEa6O9IFMDudpOfsXTnHtnO7wKssN9RtxpotRukIhLGWxjHED3Rrt93T+IMQz
254kpKz+Tcz/koM8SuAcYX15N/jeW8BzgA6Y6ODI/OSAMBzMQUYME5S7F7xOwARAQAB
255tEZTd2lmdCBBdXRvbWF0aWMgU2lnbmluZyBLZXkgIzQgPHN3aWZ0LWluZnJhc3Ry
256dWN0dXJlQGZvcnVtcy5zd2lmdC5vcmc+iQI9BBMBCgAnBQJhiVntAhsDBQkDwmcA
257BQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEPFn3xrPnOBpZQQP/31gfgRaSdBM
258Y0ob4SX+WgcPO9Ks7LVHAojH/jDdra6EjCdM60YH3VnSEogt3oRSx7TyZhQxy6E8
259cD6j1pJwDIo7sSdsOnFfFQSCRVYITTGd6CfDDlVzw45qVQTD/kEXM5GLCjCERuwL
260bFa3Idjy/YleAizAqJhj/05WGQuKBVYLifvoSkOLEfUyrEyAUjmPM8fyWSb81ujV
2615BxMONy3ahN4zb7Gp0KktJQZmkCrs6Y8HzzUTtB0P72MewyYUmoQkoj+DcujuvfB
262xeOUgTZzmgRQA5bBBjW+WgcjvDamS6/DTTvrQo5K1zSP53vm/LZXwB2V9gJL6ytq
263njwJwnZxC13iyqoMDTgO7OAd60yXUhQDJqARj4pjAlfuZhO2ITnVKfTqXpRPSXOO
264rvsTqCfg2sTvl8hytrqxzEW0LEU9tYPbfw0gZ0CG874jAtpvhMf0Iuz6feXDBgZS
265sT9UHW6sp11oefDbxiaMkdqsRLzrQDGM7ghpUGk1FtduxobC8edfMsUkiKNTBcDw
266wdgPP+YBRxMYfhsEmuiHlqW9vY3ytuhm+GRVvI+Rkd2XjoDcYCUX/RF2JSr181j3
267qJ5VoE//0imfk4FdhL2CYY+bww+1BiEvUnb2JInaaaT4oDvjOF6SDPqziDMuLSEc
268l1Y4x1Ig0njPuZ23erJGFRgt2R42gBy6
269=kqeX
270-----END PGP PUBLIC KEY BLOCK-----
271-----BEGIN PGP PUBLIC KEY BLOCK-----
272Version: GnuPG v2
273
274mQINBFyUJhwBEADDBN6jVDScsgzfQDxSQga0Og4O3N/nIquwi/DGOgsAsTMWcVA1
275gmRJb792vWa9CmnHILl3ap8zObDSbcXg90nx+eCzWJjG+Ud5c7xuam96NR8ntKmU
2768+BbH/dp8zc9WJB37TiWcaLsddrn58zfm7Ml6P9M48WAeRJX8nxVBTw1SjXJurW0
277Ab8LOgfb60I09Skq72Ud7HaYJOG03iNTf6qLlF977OQsHCb21BnKSAmJqapUS96/
278VOngz7TBzYz4rntfetb8hg28WtUl1s5BQzWaFunkP03b8mPh3PL1SZxutwViVWBu
279hf9kJtx/MLb79fnuJEOus1FvDqJdpd83H+XmXMIDYWgcBIBVrLT5HDtRerjF178H
280okb1F+gboGIqhnx25xTPIYctSHPgJRScZp4WKrqQLKswAmSL4YJXnkXSff05l4gE
281WXQpEMLBZa46qmu8lj8HfoZSbP9lfvEtZ6A+Q3sfh3gjYPv7e6n37x22tSgvyzCb
282jHU1pA2rv10AHK7EIeEQElN+zCyAbmKuhPBiCyxDFg5Dx3xNkYwg9szBJ0KleVD4
283+Y3PZJ4N+u+SSATYnHGtmzvkhiNtqJCCwuaqY+jjVObzzqvLLtGjtjxNUWi2X4He
284q+r2fubjCOW14UnQ06qfr3mVUSmuLSKs8BD8qTGuqlgcenGsY0bk0qUPcwARAQAB
285tD5Td2lmdCA1LnggUmVsZWFzZSBTaWduaW5nIEtleSA8c3dpZnQtaW5mcmFzdHJ1
286Y3R1cmVAc3dpZnQub3JnPokCPAQTAQoAJwIbAwULCQgHAwUVCgkICwUWAgMBAAIe
287AQIXgAUCZBiLlAUJC0bMeAAKCRCSXMHM7T0VYSogD/i38g/nf1JSDrWMWVKLzTWA
288MO9V9JR1wcxx3YNQmGPzUv+gdXgOv8kWSKSJ5zopTrWJg/AJ6etShX7HpjXeHULp
289aJGLohlMQuPCqD2+HvxpY67iA9i49owkzQI4gSIC6UwtCluWQCgcz2qE5zBWB1nK
290ImegSYn9mHIooJ9em4UwHdEDztWPYeB192M+Eq1+HK3BlEFDrMxWdzVuEQ+oGslm
2914Z5apV+r65fQa9T4rRxOpau8T9tKpXfaw+IzUDUE4PRTOyOkR8AOFm9hIg5KlV9z
2921bhDRS4Xpb6i3H91OyGROoB6QsTCmVGnSw35bzYjlOc5I1bAtqJu3EC30gV97kCj
293btm/cQLb+su/DEwjp4/lRtQagmRmHUuvJyPA+WNNiPYY/Ldzfy3w/SK4eJqM3Ctv
294iEs+btFLg4yXCBmL6UFZ4rmZ++x11DoW9l63b/9PJYPASXBGXamqsrMvX+IozjQ1
295CzlI8Pi5XwZfa/yAMiNXTveKItZJFUl4L4xMz2Fk6q8vByb5P2TziooTNtpjJedg
296+W2ZNKatqZtvqowJlr9utWmogInrlv+YSLXN43kVt/X+Dbi1qzIE0FnApLPJEw+x
297nuahcoGN56UfxsdlmMDQ1QWs0Zp1DzXGOwAo9ciEIxrqlxR7N8rMluDv9vltVnZZ
2981p6hhnwu6nXqf83I5GJK
299=77jt
300-----END PGP PUBLIC KEY BLOCK-----
301-----BEGIN PGP PUBLIC KEY BLOCK-----
302Version: GnuPG v2
303
304mQINBGGJWe0BEADGVbVhCEWnCMdAoxCaUaCPNomG5wdzw+sgx8FvqCFiGqYq/qzN
305ckaydk+XWfiDT/Vo/NjMYnUJ0+gdSua1X0IUGqQlATYhKSGTHqgab4nwDCym0nFl
306SBHPDgmMTNO57dHSGYduPjuisuhq7i0pybyn/oXuHAW5SIlz7bfpaz2jzFDY4xEP
307TXImllHOmTxBsDdwv2J+H158w1NSHJoHeXpPIse3VHlcwJVNeGYMiDx4Qw9TfaL5
308fI7GgjbcELCXw/pGjbiz/JFX7Fzm4snesnqwIG3TN4kH3BdKRNMFnyN3zNZxQTaZ
309LNCr3NCzhmmWTjeX5VSu6C+W8wuYRAThqJ42HpPR78zgv6pMnHgdhqquqAfL/Oid
310XlISbQ4bSdc1y8xkLb86pzwzrLV3LmHjJ3NAi9U1QXygW0ikTgbB9LPzaTSGVU+y
3110PMQ6JrRfSvyt5G7RmVzqm1ro8fkmeZBjNUf7GXCldn9linbhKzh/IvvPTYeNhIK
312T99giWSChvZyR950vE5a4WEYKV3taJRLCqXHH1d7FKC080pvPR9VsqWB/thQ48Vr
313dPleEa6O9IFMDudpOfsXTnHtnO7wKssN9RtxpotRukIhLGWxjHED3Rrt93T+IMQz
314kpKz+Tcz/koM8SuAcYX15N/jeW8BzgA6Y6ODI/OSAMBzMQUYME5S7F7xOwARAQAB
315tEZTd2lmdCBBdXRvbWF0aWMgU2lnbmluZyBLZXkgIzQgPHN3aWZ0LWluZnJhc3Ry
316dWN0dXJlQGZvcnVtcy5zd2lmdC5vcmc+iQJUBBMBCgA+AhsDBQsJCAcDBRUKCQgL
317BRYCAwEAAh4BAheAFiEE6BPIkoIKb6E3VbJo8WffGs+c4GkFAmVN6ZMFCQeG9qYA
318CgkQ8WffGs+c4Glz3BAAl1MjB02h+TNnnmHg0RWSGUzIBpFEXQ8ojoQFhudloVlu
3192zkdTzznXXpmuBH4QQTI0igPQyVQkOEXPAP05geeqOvlGm4wnwtw486Q5vTGxgn1
320+WjS3OMHFFS4y0GiOciRtu6WhwFOv6c2eSLIOmoDDvsb4js+YqtZ3fFcim/6gmRX
321cKsEWzFyIcAxgm8iDks/sdqX2XQhkBuZ1bs3mOGGZnj8N2reNv0POJ/QTg9s5ydI
3222NylO66kDmsNu7z5jkLg/Ol7eGuYvzwKIvdasHU2kd9gCPX/6B7mmUSK8LIiD9UN
323D6wetgkT/ov9XhYWWKZxYM2sPJVhVRLkMZkNjxSS7GRWeYgkfca+xE2xm8KgbnpZ
324CC2F4b9g9tCLH5LW93SXL2pdurimE9pa26A0t8C7/CBwV2YkvUP3oBIVKH+vEB+m
325tS9H/VVaoflL/UPBPV7k1Nd8pgDxfUx+hDGW6wNrMyJSvykypWte5Xrk5q/TVTuZ
326wOZVyyH6mMxWPKyYiUU6rcFu9kMkRNodBB6E/IT/oQ2EFCBAbKAGeMWii4OJtwfu
3273W1lC4GFNCE5Ae6V5ew++5GjN+5fVJnjyY0rbKvLPHa+y0Rkojw+w6avXKmCF4JR
328iPwYZF0RD0fuidRNNDG8HIZZq9fTuC2RWpdAk403/j9ary56FYxsYqyZ5saMn4w=
329=RrPQ
330-----END PGP PUBLIC KEY BLOCK-----
331-----BEGIN PGP PUBLIC KEY BLOCK-----
332Version: GnuPG v2
333
334mQINBGbolYEBEADEvoijjZaq+5hLyiOHMns6+i/1mxczO5g9ZuXANYMI5uKXNgED
335dWoJRJV1DKwY+1f9oBdcEctD0um4LY6346p38SJOurk/zRqlEx25sAq0bbOn0epE
336BrOkHnmgBp+C5gWgrk+gKGjOXw63m2ipKp5joxP7QI7iplb7LRHnqOWqVFPF6c+A
337y5zq7/FFfECwHYdkS3IV8uYG0qPmKDYoJgqCbySGzbHTiawFt8OJS5xYqzhXrClh
338KMJf6orq8gNF4eBwa6FkyJFyPf/s8mbW3wREirL9DFinurMK68pUA7SDxLsegGgt
3392PVse5o+1TVcKTDTCV0v5gmiQyXIUzvB87GaJLVIEIwYb08jtSBMvVLdVIpWv6RJ
340bi7za2LvfaCBcpSAbqCtoq/lk9NYrXs8uiENxW0srIWoKoe6Gfvz0XvJEjJPIMNH
341cy/42Nt3kgn6V3lHTioTPhxyEJ8AZ+s7yIeVR5fYuxdlVwe6zhiwvGtjWLmjTZBY
342RbXayDV3ekNL3rEQ4qhXrvcmW0/v7n+vzf6a7Wog6R4pHr67BafM3v9M6jpu4Lqk
343mLyXU/lX1+VTzKz7vDCz8CXafYUP8wFYGGW73xzd6Kho8vya1E4WNg+UPnYcu2SV
344TkQe50I2Ik95QEAEdG3DJrLiGuLwH170KgeMYpWffNLtUNH1oCkh1lj8jQARAQAB
345tEVTd2lmdCA2LnggUmVsZWFzZSBTaWduaW5nIEtleSA8c3dpZnQtaW5mcmFzdHJ1
346Y3R1cmVAZm9ydW1zLnN3aWZ0Lm9yZz6JAj0EEwEKACcFAmbolYECGwMFCQPCZwAF
347CwkIBwMFFQoJCAsFFgIDAQACHgECF4AACgkQ74CoZrR6mB/DIhAAnSHnGfRpr/0A
34898dWa2uM2tt4oGT1+SN+va544/vu6CAc9jhvS871GvHe25B5HL5S6MDF3dim3oQe
349zIGaIx3DUhbb0/JLeayL3NfhbCed5DX0W1XIQtksjYuHnvBRD+zrhcTQKqumR+LU
350NVOh6l4o2Ko+aAYc8mIm+HufryRKJSWup1ZnExEdcZZEzyY4kT0H1fxAIWWU3NI1
351LDtq1O0Wl5CTUHU7mI/h2eWrxFDg4SmfV9dv4uBXyvxherHEhImJXceQ4ZkxvYGF
352amXjlNzIr8c6y/IE8Q775nkfG5+4Gt+bGIJqxeSzfc4wq+txMa85TtRpgSthgqFi
353+yQx5qghs3y8Pqj3kDatJB41oQZ221WbBRc2uvFDMWUaOtJ/pRymCN54FUwxUm6U
354aM4VTWbGw0es1QRO+Px25Thoh8a5a3Fu0s9fa1sDJOUYSc7vfyRUfqNOVv5g8rnS
355sPTmOGGjrjxWHA0oL7B5hxCR+/jBhw+6mLu18qwGI7YpgyZYSzowPY+LrMm/J973
356SyUtlCb2o42WhDR9FsX0AGvLhZpxy4Q7br7Pa2RXwmWEaxZik0iQ2Sg4pEvL5tVd
357aFIUvVsFlayfXSBCZVjH0uyh0Vkk1ERZpSmxkRWgzp5MRIfkP5eh9009uhEUmxHV
358Yih/un915S6ObH32x1IbJfi0cGK1NUA=
359=sWSN
360-----END PGP PUBLIC KEY BLOCK-----
361-----BEGIN PGP PUBLIC KEY BLOCK-----
362Version: GnuPG v2
363
364mQINBGGJWe0BEADGVbVhCEWnCMdAoxCaUaCPNomG5wdzw+sgx8FvqCFiGqYq/qzN
365ckaydk+XWfiDT/Vo/NjMYnUJ0+gdSua1X0IUGqQlATYhKSGTHqgab4nwDCym0nFl
366SBHPDgmMTNO57dHSGYduPjuisuhq7i0pybyn/oXuHAW5SIlz7bfpaz2jzFDY4xEP
367TXImllHOmTxBsDdwv2J+H158w1NSHJoHeXpPIse3VHlcwJVNeGYMiDx4Qw9TfaL5
368fI7GgjbcELCXw/pGjbiz/JFX7Fzm4snesnqwIG3TN4kH3BdKRNMFnyN3zNZxQTaZ
369LNCr3NCzhmmWTjeX5VSu6C+W8wuYRAThqJ42HpPR78zgv6pMnHgdhqquqAfL/Oid
370XlISbQ4bSdc1y8xkLb86pzwzrLV3LmHjJ3NAi9U1QXygW0ikTgbB9LPzaTSGVU+y
3710PMQ6JrRfSvyt5G7RmVzqm1ro8fkmeZBjNUf7GXCldn9linbhKzh/IvvPTYeNhIK
372T99giWSChvZyR950vE5a4WEYKV3taJRLCqXHH1d7FKC080pvPR9VsqWB/thQ48Vr
373dPleEa6O9IFMDudpOfsXTnHtnO7wKssN9RtxpotRukIhLGWxjHED3Rrt93T+IMQz
374kpKz+Tcz/koM8SuAcYX15N/jeW8BzgA6Y6ODI/OSAMBzMQUYME5S7F7xOwARAQAB
375tEZTd2lmdCBBdXRvbWF0aWMgU2lnbmluZyBLZXkgIzQgPHN3aWZ0LWluZnJhc3Ry
376dWN0dXJlQGZvcnVtcy5zd2lmdC5vcmc+iQJUBBMBCgA+AhsDBQsJCAcDBRUKCQgL
377BRYCAwEAAh4BAheAFiEE6BPIkoIKb6E3VbJo8WffGs+c4GkFAmkXkkAFCQtQn1MA
378CgkQ8WffGs+c4GlQpw/+LUkquzG+3wt6WzHThd8m99PcVygHR4YnI/GTRYsno7l/
379w4bB3WxhK8BFTr5GzsEDd2gPSIyKvqrcN1GP2lj96VyNfksh8LledNpzGz1HAgIt
380RY8NgzdQWqrOiyPK7T3cOkpOzwx4TNFTvEnKU/bPcdZM/muvLFManJr7bOvhuWGX
381KafvD6Q2EwofIIRaIocaVlsai1jJZcYCiB3UpTQL+pg1kdlYSVwnarEBvuAGEJan
382UlYuyS0hGFHpoU9VkxxTH0FzETN+03AtPXVaVJdjfmhrBUbWg7envpIUZ15z7PdD
383XgddnfoMIpfiVerr7DgbUdo3WervDtCo1PSMh8CeY14sdVsPlufCwP6zMLMllDBm
384VCEHN4vOCfabA/+GSev0Mtli60OUdFAVSKt0pGddONDWSrYezRD9lfofq5ZnRqFa
38550K6IrWoczkv1Toh1NBh5V0bmnVx0gwa4rmNrrY1casCs4KprySkdKvOha7IFrf0
386YHNUjRf1i0v8Ep2oE8nRgbVwbh0mxCyHchux7RIty85vyqQh/nff1ArChQxtrWsr
387Ok/0iS+TmMi+o/IxBR5KzaewYXHWKfuAYkGWPCbwxgc24Zd+hAHpnstZ33zcpL9K
3881dQz0ClfOCKYQYZJ2MID1Ry27bPh5fJdWuGcXFlkhIebvXjllbD44yeIdCvc0rg=
389=12qA
390-----END PGP PUBLIC KEY BLOCK-----
.gitignore +1
@@ -3,3 +3,4 @@
33DerivedData/
44.DS_Store
55*.kc
6.superpowers/
.swift-format added +12
@@ -0,0 +1,12 @@
1{
2 "version": 1,
3 "indentation": { "spaces": 4 },
4 "lineLength": 100,
5 "maximumBlankLines": 1,
6 "respectsExistingLineBreaks": true,
7 "rules": {
8 "AlwaysUseLowerCamelCase": true,
9 "NeverForceUnwrap": false,
10 "NeverUseImplicitlyUnwrappedOptionals": true
11 }
12}
NOTICE added +5
@@ -0,0 +1,5 @@
1The word list in Sources/KeycaskCore/Wordlist.swift is the EFF Long
2Wordlist by the Electronic Frontier Foundation, licensed under the
3Creative Commons Attribution 3.0 United States License.
4https://www.eff.org/dice
5https://creativecommons.org/licenses/by/3.0/us/
Package.resolved added +33
@@ -0,0 +1,33 @@
1{
2 "originHash" : "f4830ec40bb4ed7681d28a845578098d2aaa25c4b7867710f01723e5ec0c9aa9",
3 "pins" : [
4 {
5 "identity" : "swift-argument-parser",
6 "kind" : "remoteSourceControl",
7 "location" : "https://github.com/apple/swift-argument-parser",
8 "state" : {
9 "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382",
10 "version" : "1.8.2"
11 }
12 },
13 {
14 "identity" : "swift-asn1",
15 "kind" : "remoteSourceControl",
16 "location" : "https://github.com/apple/swift-asn1.git",
17 "state" : {
18 "revision" : "d9a5b37470adc940d22c3bcd5ca6953a516b727f",
19 "version" : "1.7.2"
20 }
21 },
22 {
23 "identity" : "swift-crypto",
24 "kind" : "remoteSourceControl",
25 "location" : "https://github.com/apple/swift-crypto",
26 "state" : {
27 "revision" : "95ba0316a9b733e92bb6b071255ff46263bbe7dc",
28 "version" : "3.15.1"
29 }
30 }
31 ],
32 "version" : 3
33}
Package.swift added +33
@@ -0,0 +1,33 @@
1// swift-tools-version:6.4
2import PackageDescription
3
4let package = Package(
5 name: "keycask",
6 platforms: [.macOS(.v14), .iOS(.v17)],
7 products: [
8 .library(name: "KeycaskCore", targets: ["KeycaskCore"]),
9 .executable(name: "keycask", targets: ["keycask"]),
10 ],
11 dependencies: [
12 .package(url: "https://github.com/apple/swift-crypto", from: "3.15.0"),
13 .package(url: "https://github.com/apple/swift-argument-parser", from: "1.8.0"),
14 ],
15 targets: [
16 .target(
17 name: "KeycaskCore",
18 dependencies: [
19 .product(name: "Crypto", package: "swift-crypto"),
20 .product(name: "_CryptoExtras", package: "swift-crypto"),
21 ]
22 ),
23 .executableTarget(
24 name: "keycask",
25 dependencies: [
26 "KeycaskCore",
27 .product(name: "ArgumentParser", package: "swift-argument-parser"),
28 ]
29 ),
30 .testTarget(name: "KeycaskCoreTests", dependencies: ["KeycaskCore"]),
31 .testTarget(name: "KeycaskCLITests", dependencies: ["keycask"]),
32 ]
33)
Sources/KeycaskCore/Entry.swift added +65
@@ -0,0 +1,65 @@
1import Foundation
2
3public struct Entry: Codable, Equatable, Sendable {
4 public let id: EntryID
5 public var name: String
6 public var username: String?
7 public var password: String
8 public var url: String?
9 public var notes: String?
10 public var tags: [String]
11 public let created: Date
12 public var updated: Date
13
14 public init(
15 id: EntryID = .random(),
16 name: String,
17 username: String? = nil,
18 password: String,
19 url: String? = nil,
20 notes: String? = nil,
21 tags: [String] = [],
22 now: Date = .now
23 ) {
24 self.id = id
25 self.name = name
26 self.username = username
27 self.password = password
28 self.url = url
29 self.notes = notes
30 self.tags = Self.normalize(tags: tags)
31 let stamp = Self.truncateToSeconds(now)
32 created = stamp
33 updated = stamp
34 }
35
36 public static func normalize(tags: [String]) -> [String] {
37 var seen: Set<String> = []
38 var out: [String] = []
39 for raw in tags {
40 let tag = raw.trimmingCharacters(in: .whitespaces)
41 guard !tag.isEmpty, seen.insert(tag.lowercased()).inserted else { continue }
42 out.append(tag)
43 }
44 return out.sorted { a, b in
45 let (la, lb) = (a.lowercased(), b.lowercased())
46 return la == lb ? a < b : la < lb
47 }
48 }
49
50 public static func truncateToSeconds(_ date: Date) -> Date {
51 Date(timeIntervalSince1970: date.timeIntervalSince1970.rounded(.down))
52 }
53
54 public func hasTag(_ tag: String) -> Bool {
55 let needle = tag.lowercased()
56 return tags.contains { $0.lowercased() == needle }
57 }
58
59 public func matches(_ query: String) -> Bool {
60 let needle = query.lowercased()
61 guard !needle.isEmpty else { return false }
62 let haystacks = [name, username ?? "", url ?? "", notes ?? ""] + tags
63 return haystacks.contains { $0.lowercased().contains(needle) }
64 }
65}
Sources/KeycaskCore/EntryID.swift added +45
@@ -0,0 +1,45 @@
1public struct EntryID: Hashable, Sendable, CustomStringConvertible {
2 public static let alphabet: [Character] = Array("abcdefghijkmnpqrstuvwxyz23456789")
3 public static let length = 8
4
5 public let rawValue: String
6
7 public init?(_ raw: String) {
8 guard raw.count == Self.length else { return nil }
9 let allowed = Set(Self.alphabet)
10 guard raw.allSatisfy({ allowed.contains($0) }) else { return nil }
11 rawValue = raw
12 }
13
14 public static func random() -> EntryID {
15 var rng = SystemRandomNumberGenerator()
16 return random(using: &rng)
17 }
18
19 public static func random(using rng: inout some RandomNumberGenerator) -> EntryID {
20 var chars: [Character] = []
21 chars.reserveCapacity(length)
22 for _ in 0..<length {
23 chars.append(alphabet[Int(rng.next(upperBound: UInt32(alphabet.count)))])
24 }
25 return EntryID(String(chars))!
26 }
27
28 public var description: String { rawValue }
29}
30
31extension EntryID: Codable {
32 public init(from decoder: any Decoder) throws {
33 let raw = try decoder.singleValueContainer().decode(String.self)
34 guard let id = EntryID(raw) else {
35 throw DecodingError.dataCorrupted(
36 .init(codingPath: decoder.codingPath, debugDescription: "invalid entry id \(raw)"))
37 }
38 self = id
39 }
40
41 public func encode(to encoder: any Encoder) throws {
42 var container = encoder.singleValueContainer()
43 try container.encode(rawValue)
44 }
45}
Sources/KeycaskCore/Envelope.swift added +109
@@ -0,0 +1,109 @@
1import Crypto
2import Foundation
3import _CryptoExtras
4
5public struct Envelope: Codable, Equatable, Sendable {
6 public struct KDFParams: Codable, Equatable, Sendable {
7 public var name: String
8 public var iterations: Int
9 public var salt: Data
10
11 public init(name: String, iterations: Int, salt: Data) {
12 self.name = name
13 self.iterations = iterations
14 self.salt = salt
15 }
16
17 public static func fresh(iterations: Int = Envelope.defaultIterations) -> KDFParams {
18 var rng = SystemRandomNumberGenerator()
19 let salt = Data(
20 (0..<Envelope.saltLength).map { _ in UInt8.random(in: .min ... .max, using: &rng) })
21 return KDFParams(name: Envelope.kdfName, iterations: iterations, salt: salt)
22 }
23 }
24
25 public static let currentFormat = 1
26 public static let defaultIterations = 600_000
27 public static let kdfName = "pbkdf2-hmac-sha256"
28 public static let saltLength = 16
29 static let keyLength = 32
30 static let minimumBoxLength = 12 + 16
31
32 public var format: Int
33 public var kdf: KDFParams
34 public var box: Data
35
36 public static func seal(_ plaintext: Data, passphrase: String, kdf: KDFParams) throws
37 -> Envelope
38 {
39 let key = try deriveKey(passphrase: passphrase, kdf: kdf)
40 do {
41 let sealed = try ChaChaPoly.seal(plaintext, using: key)
42 return Envelope(format: currentFormat, kdf: kdf, box: sealed.combined)
43 } catch {
44 throw KeycaskError.failure("encrypt: \(error)")
45 }
46 }
47
48 public func open(passphrase: String) throws -> Data {
49 guard format == Self.currentFormat else {
50 throw KeycaskError.corrupt("unsupported format \(format)")
51 }
52 guard kdf.name == Self.kdfName else {
53 throw KeycaskError.corrupt("unsupported kdf \(kdf.name)")
54 }
55 guard box.count >= Self.minimumBoxLength else {
56 throw KeycaskError.corrupt("box too short")
57 }
58 let key = try Self.deriveKey(passphrase: passphrase, kdf: kdf)
59 let sealed: ChaChaPoly.SealedBox
60 do {
61 sealed = try ChaChaPoly.SealedBox(combined: box)
62 } catch {
63 throw KeycaskError.corrupt("box is malformed")
64 }
65 do {
66 return try ChaChaPoly.open(sealed, using: key)
67 } catch {
68 throw KeycaskError.cannotDecrypt
69 }
70 }
71
72 public init(parsing data: Data) throws {
73 do {
74 self = try JSONDecoder().decode(Envelope.self, from: data)
75 } catch {
76 throw KeycaskError.corrupt("not a keycask vault: \(error)")
77 }
78 }
79
80 public func encoded() throws -> Data {
81 let encoder = JSONEncoder()
82 encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
83 do {
84 return try encoder.encode(self)
85 } catch {
86 throw KeycaskError.io("encode envelope: \(error)")
87 }
88 }
89
90 init(format: Int, kdf: KDFParams, box: Data) {
91 self.format = format
92 self.kdf = kdf
93 self.box = box
94 }
95
96 static func deriveKey(passphrase: String, kdf: KDFParams) throws -> SymmetricKey {
97 guard (1...Int(UInt32.max)).contains(kdf.iterations) else {
98 throw KeycaskError.corrupt("bad iteration count \(kdf.iterations)")
99 }
100 let normalized = Array(passphrase.precomposedStringWithCanonicalMapping.utf8)
101 do {
102 return try KDF.Insecure.PBKDF2.deriveKey(
103 from: normalized, salt: kdf.salt, using: .sha256,
104 outputByteCount: keyLength, unsafeUncheckedRounds: kdf.iterations)
105 } catch {
106 throw KeycaskError.failure("derive key: \(error)")
107 }
108 }
109}
Sources/KeycaskCore/Generator.swift added +37
@@ -0,0 +1,37 @@
1public enum Generator {
2 public static let alphabet: [Character] = Array(
3 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{};:,.<>?"
4 )
5 public static let defaultLength = 24
6 public static let wordSeparator = "-"
7
8 public static func password(length: Int) -> String {
9 var rng = SystemRandomNumberGenerator()
10 return password(length: length, using: &rng)
11 }
12
13 public static func password(length: Int, using rng: inout some RandomNumberGenerator) -> String
14 {
15 var chars: [Character] = []
16 chars.reserveCapacity(max(length, 0))
17 for _ in 0..<max(length, 0) {
18 chars.append(alphabet[Int(rng.next(upperBound: UInt32(alphabet.count)))])
19 }
20 return String(chars)
21 }
22
23 public static func passphrase(words: Int) -> String {
24 var rng = SystemRandomNumberGenerator()
25 return passphrase(words: words, using: &rng)
26 }
27
28 public static func passphrase(words: Int, using rng: inout some RandomNumberGenerator) -> String
29 {
30 let list = Wordlist.words
31 var picked: [String] = []
32 for _ in 0..<max(words, 0) {
33 picked.append(list[Int(rng.next(upperBound: UInt32(list.count)))])
34 }
35 return picked.joined(separator: wordSeparator)
36 }
37}
Sources/KeycaskCore/KeycaskError.swift added +40
@@ -0,0 +1,40 @@
1public enum KeycaskError: Error, Equatable, Sendable {
2 case notFound(String)
3 case ambiguous(name: String, candidates: [Entry])
4 case cannotDecrypt
5 case corrupt(String)
6 case vaultExists(String)
7 case noVault(String)
8 case duplicateID(EntryID)
9 case io(String)
10 case usage(String)
11 case failure(String)
12
13 public var exitCode: Int32 {
14 switch self {
15 case .failure, .io, .corrupt, .vaultExists, .duplicateID: 1
16 case .usage: 2
17 case .notFound, .noVault: 3
18 case .cannotDecrypt: 4
19 case .ambiguous: 5
20 }
21 }
22
23 public var message: String {
24 switch self {
25 case .notFound(let what): "\(what): not found"
26 case .ambiguous(let name, let candidates):
27 (["\(name): ambiguous, use an id:"]
28 + candidates.map { " \($0.id.rawValue) \($0.username ?? "") \($0.url ?? "")" })
29 .joined(separator: "\n")
30 case .cannotDecrypt: "cannot decrypt: wrong passphrase or damaged vault"
31 case .corrupt(let why): "vault is corrupt: \(why)"
32 case .vaultExists(let path): "vault \(path) already exists"
33 case .noVault(let path): "vault \(path) not found (run `keycask init`)"
34 case .duplicateID(let id): "duplicate id \(id.rawValue)"
35 case .io(let why): why
36 case .usage(let why): why
37 case .failure(let why): why
38 }
39 }
40}
Sources/KeycaskCore/Vault.swift added +65
@@ -0,0 +1,65 @@
1import Foundation
2
3public struct Vault: Codable, Equatable, Sendable {
4 public var entries: [Entry]
5
6 public init(entries: [Entry] = []) {
7 self.entries = entries
8 }
9
10 public func entry(id: EntryID) -> Entry? {
11 entries.first { $0.id == id }
12 }
13
14 public mutating func add(_ entry: Entry) throws {
15 guard self.entry(id: entry.id) == nil else { throw KeycaskError.duplicateID(entry.id) }
16 var normalized = entry
17 normalized.tags = Entry.normalize(tags: entry.tags)
18 entries.append(normalized)
19 }
20
21 public mutating func remove(id: EntryID) throws {
22 guard let index = entries.firstIndex(where: { $0.id == id }) else {
23 throw KeycaskError.notFound(id.rawValue)
24 }
25 entries.remove(at: index)
26 }
27
28 public mutating func update(
29 id: EntryID, now: Date = .now, _ change: (inout Entry) -> Void
30 ) throws {
31 guard let index = entries.firstIndex(where: { $0.id == id }) else {
32 throw KeycaskError.notFound(id.rawValue)
33 }
34 change(&entries[index])
35 entries[index].tags = Entry.normalize(tags: entries[index].tags)
36 entries[index].updated = Entry.truncateToSeconds(now)
37 }
38
39 public func resolve(_ ref: String) throws -> Entry {
40 if let id = EntryID(ref), let hit = entry(id: id) {
41 return hit
42 }
43 let byName = entries.filter { $0.name == ref }
44 switch byName.count {
45 case 0: throw KeycaskError.notFound(ref)
46 case 1: return byName[0]
47 default: throw KeycaskError.ambiguous(name: ref, candidates: byName)
48 }
49 }
50
51 public func filter(tag: String) -> [Entry] {
52 sortedEntries.filter { $0.hasTag(tag) }
53 }
54
55 public func search(_ query: String) -> [Entry] {
56 sortedEntries.filter { $0.matches(query) }
57 }
58
59 public var sortedEntries: [Entry] {
60 entries.sorted { a, b in
61 let (la, lb) = (a.name.lowercased(), b.name.lowercased())
62 return la == lb ? a.id.rawValue < b.id.rawValue : la < lb
63 }
64 }
65}
Sources/KeycaskCore/VaultCodec.swift added +32
@@ -0,0 +1,32 @@
1import Foundation
2
3public enum VaultCodec {
4 public static func makeEncoder() -> JSONEncoder {
5 let encoder = JSONEncoder()
6 encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
7 encoder.dateEncodingStrategy = .iso8601
8 return encoder
9 }
10
11 public static func makeDecoder() -> JSONDecoder {
12 let decoder = JSONDecoder()
13 decoder.dateDecodingStrategy = .iso8601
14 return decoder
15 }
16
17 public static func encode(_ vault: Vault) throws -> Data {
18 do {
19 return try makeEncoder().encode(vault)
20 } catch {
21 throw KeycaskError.io("encode vault: \(error)")
22 }
23 }
24
25 public static func decode(_ data: Data) throws -> Vault {
26 do {
27 return try makeDecoder().decode(Vault.self, from: data)
28 } catch {
29 throw KeycaskError.corrupt("\(error)")
30 }
31 }
32}
Sources/KeycaskCore/Wordlist.swift added +7784
@@ -0,0 +1,7784 @@
1// swift-format-ignore-file
2// EFF long word list, https://www.eff.org/dice. See NOTICE.
3let effLongWordlist = """
4abacus
5abdomen
6abdominal
7abide
8abiding
9ability
10ablaze
11able
12abnormal
13abrasion
14abrasive
15abreast
16abridge
17abroad
18abruptly
19absence
20absentee
21absently
22absinthe
23absolute
24absolve
25abstain
26abstract
27absurd
28accent
29acclaim
30acclimate
31accompany
32account
33accuracy
34accurate
35accustom
36acetone
37achiness
38aching
39acid
40acorn
41acquaint
42acquire
43acre
44acrobat
45acronym
46acting
47action
48activate
49activator
50active
51activism
52activist
53activity
54actress
55acts
56acutely
57acuteness
58aeration
59aerobics
60aerosol
61aerospace
62afar
63affair
64affected
65affecting
66affection
67affidavit
68affiliate
69affirm
70affix
71afflicted
72affluent
73afford
74affront
75aflame
76afloat
77aflutter
78afoot
79afraid
80afterglow
81afterlife
82aftermath
83aftermost
84afternoon
85aged
86ageless
87agency
88agenda
89agent
90aggregate
91aghast
92agile
93agility
94aging
95agnostic
96agonize
97agonizing
98agony
99agreeable
100agreeably
101agreed
102agreeing
103agreement
104aground
105ahead
106ahoy
107aide
108aids
109aim
110ajar
111alabaster
112alarm
113albatross
114album
115alfalfa
116algebra
117algorithm
118alias
119alibi
120alienable
121alienate
122aliens
123alike
124alive
125alkaline
126alkalize
127almanac
128almighty
129almost
130aloe
131aloft
132aloha
133alone
134alongside
135aloof
136alphabet
137alright
138although
139altitude
140alto
141aluminum
142alumni
143always
144amaretto
145amaze
146amazingly
147amber
148ambiance
149ambiguity
150ambiguous
151ambition
152ambitious
153ambulance
154ambush
155amendable
156amendment
157amends
158amenity
159amiable
160amicably
161amid
162amigo
163amino
164amiss
165ammonia
166ammonium
167amnesty
168amniotic
169among
170amount
171amperage
172ample
173amplifier
174amplify
175amply
176amuck
177amulet
178amusable
179amused
180amusement
181amuser
182amusing
183anaconda
184anaerobic
185anagram
186anatomist
187anatomy
188anchor
189anchovy
190ancient
191android
192anemia
193anemic
194aneurism
195anew
196angelfish
197angelic
198anger
199angled
200angler
201angles
202angling
203angrily
204angriness
205anguished
206angular
207animal
208animate
209animating
210animation
211animator
212anime
213animosity
214ankle
215annex
216annotate
217announcer
218annoying
219annually
220annuity
221anointer
222another
223answering
224antacid
225antarctic
226anteater
227antelope
228antennae
229anthem
230anthill
231anthology
232antibody
233antics
234antidote
235antihero
236antiquely
237antiques
238antiquity
239antirust
240antitoxic
241antitrust
242antiviral
243antivirus
244antler
245antonym
246antsy
247anvil
248anybody
249anyhow
250anymore
251anyone
252anyplace
253anything
254anytime
255anyway
256anywhere
257aorta
258apache
259apostle
260appealing
261appear
262appease
263appeasing
264appendage
265appendix
266appetite
267appetizer
268applaud
269applause
270apple
271appliance
272applicant
273applied
274apply
275appointee
276appraisal
277appraiser
278apprehend
279approach
280approval
281approve
282apricot
283april
284apron
285aptitude
286aptly
287aqua
288aqueduct
289arbitrary
290arbitrate
291ardently
292area
293arena
294arguable
295arguably
296argue
297arise
298armadillo
299armband
300armchair
301armed
302armful
303armhole
304arming
305armless
306armoire
307armored
308armory
309armrest
310army
311aroma
312arose
313around
314arousal
315arrange
316array
317arrest
318arrival
319arrive
320arrogance
321arrogant
322arson
323art
324ascend
325ascension
326ascent
327ascertain
328ashamed
329ashen
330ashes
331ashy
332aside
333askew
334asleep
335asparagus
336aspect
337aspirate
338aspire
339aspirin
340astonish
341astound
342astride
343astrology
344astronaut
345astronomy
346astute
347atlantic
348atlas
349atom
350atonable
351atop
352atrium
353atrocious
354atrophy
355attach
356attain
357attempt
358attendant
359attendee
360attention
361attentive
362attest
363attic
364attire
365attitude
366attractor
367attribute
368atypical
369auction
370audacious
371audacity
372audible
373audibly
374audience
375audio
376audition
377augmented
378august
379authentic
380author
381autism
382autistic
383autograph
384automaker
385automated
386automatic
387autopilot
388available
389avalanche
390avatar
391avenge
392avenging
393avenue
394average
395aversion
396avert
397aviation
398aviator
399avid
400avoid
401await
402awaken
403award
404aware
405awhile
406awkward
407awning
408awoke
409awry
410axis
411babble
412babbling
413babied
414baboon
415backache
416backboard
417backboned
418backdrop
419backed
420backer
421backfield
422backfire
423backhand
424backing
425backlands
426backlash
427backless
428backlight
429backlit
430backlog
431backpack
432backpedal
433backrest
434backroom
435backshift
436backside
437backslid
438backspace
439backspin
440backstab
441backstage
442backtalk
443backtrack
444backup
445backward
446backwash
447backwater
448backyard
449bacon
450bacteria
451bacterium
452badass
453badge
454badland
455badly
456badness
457baffle
458baffling
459bagel
460bagful
461baggage
462bagged
463baggie
464bagginess
465bagging
466baggy
467bagpipe
468baguette
469baked
470bakery
471bakeshop
472baking
473balance
474balancing
475balcony
476balmy
477balsamic
478bamboo
479banana
480banish
481banister
482banjo
483bankable
484bankbook
485banked
486banker
487banking
488banknote
489bankroll
490banner
491bannister
492banshee
493banter
494barbecue
495barbed
496barbell
497barber
498barcode
499barge
500bargraph
501barista
502baritone
503barley
504barmaid
505barman
506barn
507barometer
508barrack
509barracuda
510barrel
511barrette
512barricade
513barrier
514barstool
515bartender
516barterer
517bash
518basically
519basics
520basil
521basin
522basis
523basket
524batboy
525batch
526bath
527baton
528bats
529battalion
530battered
531battering
532battery
533batting
534battle
535bauble
536bazooka
537blabber
538bladder
539blade
540blah
541blame
542blaming
543blanching
544blandness
545blank
546blaspheme
547blasphemy
548blast
549blatancy
550blatantly
551blazer
552blazing
553bleach
554bleak
555bleep
556blemish
557blend
558bless
559blighted
560blimp
561bling
562blinked
563blinker
564blinking
565blinks
566blip
567blissful
568blitz
569blizzard
570bloated
571bloating
572blob
573blog
574bloomers
575blooming
576blooper
577blot
578blouse
579blubber
580bluff
581bluish
582blunderer
583blunt
584blurb
585blurred
586blurry
587blurt
588blush
589blustery
590boaster
591boastful
592boasting
593boat
594bobbed
595bobbing
596bobble
597bobcat
598bobsled
599bobtail
600bodacious
601body
602bogged
603boggle
604bogus
605boil
606bok
607bolster
608bolt
609bonanza
610bonded
611bonding
612bondless
613boned
614bonehead
615boneless
616bonelike
617boney
618bonfire
619bonnet
620bonsai
621bonus
622bony
623boogeyman
624boogieman
625book
626boondocks
627booted
628booth
629bootie
630booting
631bootlace
632bootleg
633boots
634boozy
635borax
636boring
637borough
638borrower
639borrowing
640boss
641botanical
642botanist
643botany
644botch
645both
646bottle
647bottling
648bottom
649bounce
650bouncing
651bouncy
652bounding
653boundless
654bountiful
655bovine
656boxcar
657boxer
658boxing
659boxlike
660boxy
661breach
662breath
663breeches
664breeching
665breeder
666breeding
667breeze
668breezy
669brethren
670brewery
671brewing
672briar
673bribe
674brick
675bride
676bridged
677brigade
678bright
679brilliant
680brim
681bring
682brink
683brisket
684briskly
685briskness
686bristle
687brittle
688broadband
689broadcast
690broaden
691broadly
692broadness
693broadside
694broadways
695broiler
696broiling
697broken
698broker
699bronchial
700bronco
701bronze
702bronzing
703brook
704broom
705brought
706browbeat
707brownnose
708browse
709browsing
710bruising
711brunch
712brunette
713brunt
714brush
715brussels
716brute
717brutishly
718bubble
719bubbling
720bubbly
721buccaneer
722bucked
723bucket
724buckle
725buckshot
726buckskin
727bucktooth
728buckwheat
729buddhism
730buddhist
731budding
732buddy
733budget
734buffalo
735buffed
736buffer
737buffing
738buffoon
739buggy
740bulb
741bulge
742bulginess
743bulgur
744bulk
745bulldog
746bulldozer
747bullfight
748bullfrog
749bullhorn
750bullion
751bullish
752bullpen
753bullring
754bullseye
755bullwhip
756bully
757bunch
758bundle
759bungee
760bunion
761bunkbed
762bunkhouse
763bunkmate
764bunny
765bunt
766busboy
767bush
768busily
769busload
770bust
771busybody
772buzz
773cabana
774cabbage
775cabbie
776cabdriver
777cable
778caboose
779cache
780cackle
781cacti
782cactus
783caddie
784caddy
785cadet
786cadillac
787cadmium
788cage
789cahoots
790cake
791calamari
792calamity
793calcium
794calculate
795calculus
796caliber
797calibrate
798calm
799caloric
800calorie
801calzone
802camcorder
803cameo
804camera
805camisole
806camper
807campfire
808camping
809campsite
810campus
811canal
812canary
813cancel
814candied
815candle
816candy
817cane
818canine
819canister
820cannabis
821canned
822canning
823cannon
824cannot
825canola
826canon
827canopener
828canopy
829canteen
830canyon
831capable
832capably
833capacity
834cape
835capillary
836capital
837capitol
838capped
839capricorn
840capsize
841capsule
842caption
843captivate
844captive
845captivity
846capture
847caramel
848carat
849caravan
850carbon
851cardboard
852carded
853cardiac
854cardigan
855cardinal
856cardstock
857carefully
858caregiver
859careless
860caress
861caretaker
862cargo
863caring
864carless
865carload
866carmaker
867carnage
868carnation
869carnival
870carnivore
871carol
872carpenter
873carpentry
874carpool
875carport
876carried
877carrot
878carrousel
879carry
880cartel
881cartload
882carton
883cartoon
884cartridge
885cartwheel
886carve
887carving
888carwash
889cascade
890case
891cash
892casing
893casino
894casket
895cassette
896casually
897casualty
898catacomb
899catalog
900catalyst
901catalyze
902catapult
903cataract
904catatonic
905catcall
906catchable
907catcher
908catching
909catchy
910caterer
911catering
912catfight
913catfish
914cathedral
915cathouse
916catlike
917catnap
918catnip
919catsup
920cattail
921cattishly
922cattle
923catty
924catwalk
925caucasian
926caucus
927causal
928causation
929cause
930causing
931cauterize
932caution
933cautious
934cavalier
935cavalry
936caviar
937cavity
938cedar
939celery
940celestial
941celibacy
942celibate
943celtic
944cement
945census
946ceramics
947ceremony
948certainly
949certainty
950certified
951certify
952cesarean
953cesspool
954chafe
955chaffing
956chain
957chair
958chalice
959challenge
960chamber
961chamomile
962champion
963chance
964change
965channel
966chant
967chaos
968chaperone
969chaplain
970chapped
971chaps
972chapter
973character
974charbroil
975charcoal
976charger
977charging
978chariot
979charity
980charm
981charred
982charter
983charting
984chase
985chasing
986chaste
987chastise
988chastity
989chatroom
990chatter
991chatting
992chatty
993cheating
994cheddar
995cheek
996cheer
997cheese
998cheesy
999chef
1000chemicals
1001chemist
1002chemo
1003cherisher
1004cherub
1005chess
1006chest
1007chevron
1008chevy
1009chewable
1010chewer
1011chewing
1012chewy
1013chief
1014chihuahua
1015childcare
1016childhood
1017childish
1018childless
1019childlike
1020chili
1021chill
1022chimp
1023chip
1024chirping
1025chirpy
1026chitchat
1027chivalry
1028chive
1029chloride
1030chlorine
1031choice
1032chokehold
1033choking
1034chomp
1035chooser
1036choosing
1037choosy
1038chop
1039chosen
1040chowder
1041chowtime
1042chrome
1043chubby
1044chuck
1045chug
1046chummy
1047chump
1048chunk
1049churn
1050chute
1051cider
1052cilantro
1053cinch
1054cinema
1055cinnamon
1056circle
1057circling
1058circular
1059circulate
1060circus
1061citable
1062citadel
1063citation
1064citizen
1065citric
1066citrus
1067city
1068civic
1069civil
1070clad
1071claim
1072clambake
1073clammy
1074clamor
1075clamp
1076clamshell
1077clang
1078clanking
1079clapped
1080clapper
1081clapping
1082clarify
1083clarinet
1084clarity
1085clash
1086clasp
1087class
1088clatter
1089clause
1090clavicle
1091claw
1092clay
1093clean
1094clear
1095cleat
1096cleaver
1097cleft
1098clench
1099clergyman
1100clerical
1101clerk
1102clever
1103clicker
1104client
1105climate
1106climatic
1107cling
1108clinic
1109clinking
1110clip
1111clique
1112cloak
1113clobber
1114clock
1115clone
1116cloning
1117closable
1118closure
1119clothes
1120clothing
1121cloud
1122clover
1123clubbed
1124clubbing
1125clubhouse
1126clump
1127clumsily
1128clumsy
1129clunky
1130clustered
1131clutch
1132clutter
1133coach
1134coagulant
1135coastal
1136coaster
1137coasting
1138coastland
1139coastline
1140coat
1141coauthor
1142cobalt
1143cobbler
1144cobweb
1145cocoa
1146coconut
1147cod
1148coeditor
1149coerce
1150coexist
1151coffee
1152cofounder
1153cognition
1154cognitive
1155cogwheel
1156coherence
1157coherent
1158cohesive
1159coil
1160coke
1161cola
1162cold
1163coleslaw
1164coliseum
1165collage
1166collapse
1167collar
1168collected
1169collector
1170collide
1171collie
1172collision
1173colonial
1174colonist
1175colonize
1176colony
1177colossal
1178colt
1179coma
1180come
1181comfort
1182comfy
1183comic
1184coming
1185comma
1186commence
1187commend
1188comment
1189commerce
1190commode
1191commodity
1192commodore
1193common
1194commotion
1195commute
1196commuting
1197compacted
1198compacter
1199compactly
1200compactor
1201companion
1202company
1203compare
1204compel
1205compile
1206comply
1207component
1208composed
1209composer
1210composite
1211compost
1212composure
1213compound
1214compress
1215comprised
1216computer
1217computing
1218comrade
1219concave
1220conceal
1221conceded
1222concept
1223concerned
1224concert
1225conch
1226concierge
1227concise
1228conclude
1229concrete
1230concur
1231condense
1232condiment
1233condition
1234condone
1235conducive
1236conductor
1237conduit
1238cone
1239confess
1240confetti
1241confidant
1242confident
1243confider
1244confiding
1245configure
1246confined
1247confining
1248confirm
1249conflict
1250conform
1251confound
1252confront
1253confused
1254confusing
1255confusion
1256congenial
1257congested
1258congrats
1259congress
1260conical
1261conjoined
1262conjure
1263conjuror
1264connected
1265connector
1266consensus
1267consent
1268console
1269consoling
1270consonant
1271constable
1272constant
1273constrain
1274constrict
1275construct
1276consult
1277consumer
1278consuming
1279contact
1280container
1281contempt
1282contend
1283contented
1284contently
1285contents
1286contest
1287context
1288contort
1289contour
1290contrite
1291control
1292contusion
1293convene
1294convent
1295copartner
1296cope
1297copied
1298copier
1299copilot
1300coping
1301copious
1302copper
1303copy
1304coral
1305cork
1306cornball
1307cornbread
1308corncob
1309cornea
1310corned
1311corner
1312cornfield
1313cornflake
1314cornhusk
1315cornmeal
1316cornstalk
1317corny
1318coronary
1319coroner
1320corporal
1321corporate
1322corral
1323correct
1324corridor
1325corrode
1326corroding
1327corrosive
1328corsage
1329corset
1330cortex
1331cosigner
1332cosmetics
1333cosmic
1334cosmos
1335cosponsor
1336cost
1337cottage
1338cotton
1339couch
1340cough
1341could
1342countable
1343countdown
1344counting
1345countless
1346country
1347county
1348courier
1349covenant
1350cover
1351coveted
1352coveting
1353coyness
1354cozily
1355coziness
1356cozy
1357crabbing
1358crabgrass
1359crablike
1360crabmeat
1361cradle
1362cradling
1363crafter
1364craftily
1365craftsman
1366craftwork
1367crafty
1368cramp
1369cranberry
1370crane
1371cranial
1372cranium
1373crank
1374crate
1375crave
1376craving
1377crawfish
1378crawlers
1379crawling
1380crayfish
1381crayon
1382crazed
1383crazily
1384craziness
1385crazy
1386creamed
1387creamer
1388creamlike
1389crease
1390creasing
1391creatable
1392create
1393creation
1394creative
1395creature
1396credible
1397credibly
1398credit
1399creed
1400creme
1401creole
1402crepe
1403crept
1404crescent
1405crested
1406cresting
1407crestless
1408crevice
1409crewless
1410crewman
1411crewmate
1412crib
1413cricket
1414cried
1415crier
1416crimp
1417crimson
1418cringe
1419cringing
1420crinkle
1421crinkly
1422crisped
1423crisping
1424crisply
1425crispness
1426crispy
1427criteria
1428critter
1429croak
1430crock
1431crook
1432croon
1433crop
1434cross
1435crouch
1436crouton
1437crowbar
1438crowd
1439crown
1440crucial
1441crudely
1442crudeness
1443cruelly
1444cruelness
1445cruelty
1446crumb
1447crummiest
1448crummy
1449crumpet
1450crumpled
1451cruncher
1452crunching
1453crunchy
1454crusader
1455crushable
1456crushed
1457crusher
1458crushing
1459crust
1460crux
1461crying
1462cryptic
1463crystal
1464cubbyhole
1465cube
1466cubical
1467cubicle
1468cucumber
1469cuddle
1470cuddly
1471cufflink
1472culinary
1473culminate
1474culpable
1475culprit
1476cultivate
1477cultural
1478culture
1479cupbearer
1480cupcake
1481cupid
1482cupped
1483cupping
1484curable
1485curator
1486curdle
1487cure
1488curfew
1489curing
1490curled
1491curler
1492curliness
1493curling
1494curly
1495curry
1496curse
1497cursive
1498cursor
1499curtain
1500curtly
1501curtsy
1502curvature
1503curve
1504curvy
1505cushy
1506cusp
1507cussed
1508custard
1509custodian
1510custody
1511customary
1512customer
1513customize
1514customs
1515cut
1516cycle
1517cyclic
1518cycling
1519cyclist
1520cylinder
1521cymbal
1522cytoplasm
1523cytoplast
1524dab
1525dad
1526daffodil
1527dagger
1528daily
1529daintily
1530dainty
1531dairy
1532daisy
1533dallying
1534dance
1535dancing
1536dandelion
1537dander
1538dandruff
1539dandy
1540danger
1541dangle
1542dangling
1543daredevil
1544dares
1545daringly
1546darkened
1547darkening
1548darkish
1549darkness
1550darkroom
1551darling
1552darn
1553dart
1554darwinism
1555dash
1556dastardly
1557data
1558datebook
1559dating
1560daughter
1561daunting
1562dawdler
1563dawn
1564daybed
1565daybreak
1566daycare
1567daydream
1568daylight
1569daylong
1570dayroom
1571daytime
1572dazzler
1573dazzling
1574deacon
1575deafening
1576deafness
1577dealer
1578dealing
1579dealmaker
1580dealt
1581dean
1582debatable
1583debate
1584debating
1585debit
1586debrief
1587debtless
1588debtor
1589debug
1590debunk
1591decade
1592decaf
1593decal
1594decathlon
1595decay
1596deceased
1597deceit
1598deceiver
1599deceiving
1600december
1601decency
1602decent
1603deception
1604deceptive
1605decibel
1606decidable
1607decimal
1608decimeter
1609decipher
1610deck
1611declared
1612decline
1613decode
1614decompose
1615decorated
1616decorator
1617decoy
1618decrease
1619decree
1620dedicate
1621dedicator
1622deduce
1623deduct
1624deed
1625deem
1626deepen
1627deeply
1628deepness
1629deface
1630defacing
1631defame
1632default
1633defeat
1634defection
1635defective
1636defendant
1637defender
1638defense
1639defensive
1640deferral
1641deferred
1642defiance
1643defiant
1644defile
1645defiling
1646define
1647definite
1648deflate
1649deflation
1650deflator
1651deflected
1652deflector
1653defog
1654deforest
1655defraud
1656defrost
1657deftly
1658defuse
1659defy
1660degraded
1661degrading
1662degrease
1663degree
1664dehydrate
1665deity
1666dejected
1667delay
1668delegate
1669delegator
1670delete
1671deletion
1672delicacy
1673delicate
1674delicious
1675delighted
1676delirious
1677delirium
1678deliverer
1679delivery
1680delouse
1681delta
1682deluge
1683delusion
1684deluxe
1685demanding
1686demeaning
1687demeanor
1688demise
1689democracy
1690democrat
1691demote
1692demotion
1693demystify
1694denatured
1695deniable
1696denial
1697denim
1698denote
1699dense
1700density
1701dental
1702dentist
1703denture
1704deny
1705deodorant
1706deodorize
1707departed
1708departure
1709depict
1710deplete
1711depletion
1712deplored
1713deploy
1714deport
1715depose
1716depraved
1717depravity
1718deprecate
1719depress
1720deprive
1721depth
1722deputize
1723deputy
1724derail
1725deranged
1726derby
1727derived
1728desecrate
1729deserve
1730deserving
1731designate
1732designed
1733designer
1734designing
1735deskbound
1736desktop
1737deskwork
1738desolate
1739despair
1740despise
1741despite
1742destiny
1743destitute
1744destruct
1745detached
1746detail
1747detection
1748detective
1749detector
1750detention
1751detergent
1752detest
1753detonate
1754detonator
1755detoxify
1756detract
1757deuce
1758devalue
1759deviancy
1760deviant
1761deviate
1762deviation
1763deviator
1764device
1765devious
1766devotedly
1767devotee
1768devotion
1769devourer
1770devouring
1771devoutly
1772dexterity
1773dexterous
1774diabetes
1775diabetic
1776diabolic
1777diagnoses
1778diagnosis
1779diagram
1780dial
1781diameter
1782diaper
1783diaphragm
1784diary
1785dice
1786dicing
1787dictate
1788dictation
1789dictator
1790difficult
1791diffused
1792diffuser
1793diffusion
1794diffusive
1795dig
1796dilation
1797diligence
1798diligent
1799dill
1800dilute
1801dime
1802diminish
1803dimly
1804dimmed
1805dimmer
1806dimness
1807dimple
1808diner
1809dingbat
1810dinghy
1811dinginess
1812dingo
1813dingy
1814dining
1815dinner
1816diocese
1817dioxide
1818diploma
1819dipped
1820dipper
1821dipping
1822directed
1823direction
1824directive
1825directly
1826directory
1827direness
1828dirtiness
1829disabled
1830disagree
1831disallow
1832disarm
1833disarray
1834disaster
1835disband
1836disbelief
1837disburse
1838discard
1839discern
1840discharge
1841disclose
1842discolor
1843discount
1844discourse
1845discover
1846discuss
1847disdain
1848disengage
1849disfigure
1850disgrace
1851dish
1852disinfect
1853disjoin
1854disk
1855dislike
1856disliking
1857dislocate
1858dislodge
1859disloyal
1860dismantle
1861dismay
1862dismiss
1863dismount
1864disobey
1865disorder
1866disown
1867disparate
1868disparity
1869dispatch
1870dispense
1871dispersal
1872dispersed
1873disperser
1874displace
1875display
1876displease
1877disposal
1878dispose
1879disprove
1880dispute
1881disregard
1882disrupt
1883dissuade
1884distance
1885distant
1886distaste
1887distill
1888distinct
1889distort
1890distract
1891distress
1892district
1893distrust
1894ditch
1895ditto
1896ditzy
1897dividable
1898divided
1899dividend
1900dividers
1901dividing
1902divinely
1903diving
1904divinity
1905divisible
1906divisibly
1907division
1908divisive
1909divorcee
1910dizziness
1911dizzy
1912doable
1913docile
1914dock
1915doctrine
1916document
1917dodge
1918dodgy
1919doily
1920doing
1921dole
1922dollar
1923dollhouse
1924dollop
1925dolly
1926dolphin
1927domain
1928domelike
1929domestic
1930dominion
1931dominoes
1932donated
1933donation
1934donator
1935donor
1936donut
1937doodle
1938doorbell
1939doorframe
1940doorknob
1941doorman
1942doormat
1943doornail
1944doorpost
1945doorstep
1946doorstop
1947doorway
1948doozy
1949dork
1950dormitory
1951dorsal
1952dosage
1953dose
1954dotted
1955doubling
1956douche
1957dove
1958down
1959dowry
1960doze
1961drab
1962dragging
1963dragonfly
1964dragonish
1965dragster
1966drainable
1967drainage
1968drained
1969drainer
1970drainpipe
1971dramatic
1972dramatize
1973drank
1974drapery
1975drastic
1976draw
1977dreaded
1978dreadful
1979dreadlock
1980dreamboat
1981dreamily
1982dreamland
1983dreamless
1984dreamlike
1985dreamt
1986dreamy
1987drearily
1988dreary
1989drench
1990dress
1991drew
1992dribble
1993dried
1994drier
1995drift
1996driller
1997drilling
1998drinkable
1999drinking
2000dripping
2001drippy
2002drivable
2003driven
2004driver
2005driveway
2006driving
2007drizzle
2008drizzly
2009drone
2010drool
2011droop
2012drop-down
2013dropbox
2014dropkick
2015droplet
2016dropout
2017dropper
2018drove
2019drown
2020drowsily
2021drudge
2022drum
2023dry
2024dubbed
2025dubiously
2026duchess
2027duckbill
2028ducking
2029duckling
2030ducktail
2031ducky
2032duct
2033dude
2034duffel
2035dugout
2036duh
2037duke
2038duller
2039dullness
2040duly
2041dumping
2042dumpling
2043dumpster
2044duo
2045dupe
2046duplex
2047duplicate
2048duplicity
2049durable
2050durably
2051duration
2052duress
2053during
2054dusk
2055dust
2056dutiful
2057duty
2058duvet
2059dwarf
2060dweeb
2061dwelled
2062dweller
2063dwelling
2064dwindle
2065dwindling
2066dynamic
2067dynamite
2068dynasty
2069dyslexia
2070dyslexic
2071each
2072eagle
2073earache
2074eardrum
2075earflap
2076earful
2077earlobe
2078early
2079earmark
2080earmuff
2081earphone
2082earpiece
2083earplugs
2084earring
2085earshot
2086earthen
2087earthlike
2088earthling
2089earthly
2090earthworm
2091earthy
2092earwig
2093easeful
2094easel
2095easiest
2096easily
2097easiness
2098easing
2099eastbound
2100eastcoast
2101easter
2102eastward
2103eatable
2104eaten
2105eatery
2106eating
2107eats
2108ebay
2109ebony
2110ebook
2111ecard
2112eccentric
2113echo
2114eclair
2115eclipse
2116ecologist
2117ecology
2118economic
2119economist
2120economy
2121ecosphere
2122ecosystem
2123edge
2124edginess
2125edging
2126edgy
2127edition
2128editor
2129educated
2130education
2131educator
2132eel
2133effective
2134effects
2135efficient
2136effort
2137eggbeater
2138egging
2139eggnog
2140eggplant
2141eggshell
2142egomaniac
2143egotism
2144egotistic
2145either
2146eject
2147elaborate
2148elastic
2149elated
2150elbow
2151eldercare
2152elderly
2153eldest
2154electable
2155election
2156elective
2157elephant
2158elevate
2159elevating
2160elevation
2161elevator
2162eleven
2163elf
2164eligible
2165eligibly
2166eliminate
2167elite
2168elitism
2169elixir
2170elk
2171ellipse
2172elliptic
2173elm
2174elongated
2175elope
2176eloquence
2177eloquent
2178elsewhere
2179elude
2180elusive
2181elves
2182email
2183embargo
2184embark
2185embassy
2186embattled
2187embellish
2188ember
2189embezzle
2190emblaze
2191emblem
2192embody
2193embolism
2194emboss
2195embroider
2196emcee
2197emerald
2198emergency
2199emission
2200emit
2201emote
2202emoticon
2203emotion
2204empathic
2205empathy
2206emperor
2207emphases
2208emphasis
2209emphasize
2210emphatic
2211empirical
2212employed
2213employee
2214employer
2215emporium
2216empower
2217emptier
2218emptiness
2219empty
2220emu
2221enable
2222enactment
2223enamel
2224enchanted
2225enchilada
2226encircle
2227enclose
2228enclosure
2229encode
2230encore
2231encounter
2232encourage
2233encroach
2234encrust
2235encrypt
2236endanger
2237endeared
2238endearing
2239ended
2240ending
2241endless
2242endnote
2243endocrine
2244endorphin
2245endorse
2246endowment
2247endpoint
2248endurable
2249endurance
2250enduring
2251energetic
2252energize
2253energy
2254enforced
2255enforcer
2256engaged
2257engaging
2258engine
2259engorge
2260engraved
2261engraver
2262engraving
2263engross
2264engulf
2265enhance
2266enigmatic
2267enjoyable
2268enjoyably
2269enjoyer
2270enjoying
2271enjoyment
2272enlarged
2273enlarging
2274enlighten
2275enlisted
2276enquirer
2277enrage
2278enrich
2279enroll
2280enslave
2281ensnare
2282ensure
2283entail
2284entangled
2285entering
2286entertain
2287enticing
2288entire
2289entitle
2290entity
2291entomb
2292entourage
2293entrap
2294entree
2295entrench
2296entrust
2297entryway
2298entwine
2299enunciate
2300envelope
2301enviable
2302enviably
2303envious
2304envision
2305envoy
2306envy
2307enzyme
2308epic
2309epidemic
2310epidermal
2311epidermis
2312epidural
2313epilepsy
2314epileptic
2315epilogue
2316epiphany
2317episode
2318equal
2319equate
2320equation
2321equator
2322equinox
2323equipment
2324equity
2325equivocal
2326eradicate
2327erasable
2328erased
2329eraser
2330erasure
2331ergonomic
2332errand
2333errant
2334erratic
2335error
2336erupt
2337escalate
2338escalator
2339escapable
2340escapade
2341escapist
2342escargot
2343eskimo
2344esophagus
2345espionage
2346espresso
2347esquire
2348essay
2349essence
2350essential
2351establish
2352estate
2353esteemed
2354estimate
2355estimator
2356estranged
2357estrogen
2358etching
2359eternal
2360eternity
2361ethanol
2362ether
2363ethically
2364ethics
2365euphemism
2366evacuate
2367evacuee
2368evade
2369evaluate
2370evaluator
2371evaporate
2372evasion
2373evasive
2374even
2375everglade
2376evergreen
2377everybody
2378everyday
2379everyone
2380evict
2381evidence
2382evident
2383evil
2384evoke
2385evolution
2386evolve
2387exact
2388exalted
2389example
2390excavate
2391excavator
2392exceeding
2393exception
2394excess
2395exchange
2396excitable
2397exciting
2398exclaim
2399exclude
2400excluding
2401exclusion
2402exclusive
2403excretion
2404excretory
2405excursion
2406excusable
2407excusably
2408excuse
2409exemplary
2410exemplify
2411exemption
2412exerciser
2413exert
2414exes
2415exfoliate
2416exhale
2417exhaust
2418exhume
2419exile
2420existing
2421exit
2422exodus
2423exonerate
2424exorcism
2425exorcist
2426expand
2427expanse
2428expansion
2429expansive
2430expectant
2431expedited
2432expediter
2433expel
2434expend
2435expenses
2436expensive
2437expert
2438expire
2439expiring
2440explain
2441expletive
2442explicit
2443explode
2444exploit
2445explore
2446exploring
2447exponent
2448exporter
2449exposable
2450expose
2451exposure
2452express
2453expulsion
2454exquisite
2455extended
2456extending
2457extent
2458extenuate
2459exterior
2460external
2461extinct
2462extortion
2463extradite
2464extras
2465extrovert
2466extrude
2467extruding
2468exuberant
2469fable
2470fabric
2471fabulous
2472facebook
2473facecloth
2474facedown
2475faceless
2476facelift
2477faceplate
2478faceted
2479facial
2480facility
2481facing
2482facsimile
2483faction
2484factoid
2485factor
2486factsheet
2487factual
2488faculty
2489fade
2490fading
2491failing
2492falcon
2493fall
2494false
2495falsify
2496fame
2497familiar
2498family
2499famine
2500famished
2501fanatic
2502fancied
2503fanciness
2504fancy
2505fanfare
2506fang
2507fanning
2508fantasize
2509fantastic
2510fantasy
2511fascism
2512fastball
2513faster
2514fasting
2515fastness
2516faucet
2517favorable
2518favorably
2519favored
2520favoring
2521favorite
2522fax
2523feast
2524federal
2525fedora
2526feeble
2527feed
2528feel
2529feisty
2530feline
2531felt-tip
2532feminine
2533feminism
2534feminist
2535feminize
2536femur
2537fence
2538fencing
2539fender
2540ferment
2541fernlike
2542ferocious
2543ferocity
2544ferret
2545ferris
2546ferry
2547fervor
2548fester
2549festival
2550festive
2551festivity
2552fetal
2553fetch
2554fever
2555fiber
2556fiction
2557fiddle
2558fiddling
2559fidelity
2560fidgeting
2561fidgety
2562fifteen
2563fifth
2564fiftieth
2565fifty
2566figment
2567figure
2568figurine
2569filing
2570filled
2571filler
2572filling
2573film
2574filter
2575filth
2576filtrate
2577finale
2578finalist
2579finalize
2580finally
2581finance
2582financial
2583finch
2584fineness
2585finer
2586finicky
2587finished
2588finisher
2589finishing
2590finite
2591finless
2592finlike
2593fiscally
2594fit
2595five
2596flaccid
2597flagman
2598flagpole
2599flagship
2600flagstick
2601flagstone
2602flail
2603flakily
2604flaky
2605flame
2606flammable
2607flanked
2608flanking
2609flannels
2610flap
2611flaring
2612flashback
2613flashbulb
2614flashcard
2615flashily
2616flashing
2617flashy
2618flask
2619flatbed
2620flatfoot
2621flatly
2622flatness
2623flatten
2624flattered
2625flatterer
2626flattery
2627flattop
2628flatware
2629flatworm
2630flavored
2631flavorful
2632flavoring
2633flaxseed
2634fled
2635fleshed
2636fleshy
2637flick
2638flier
2639flight
2640flinch
2641fling
2642flint
2643flip
2644flirt
2645float
2646flock
2647flogging
2648flop
2649floral
2650florist
2651floss
2652flounder
2653flyable
2654flyaway
2655flyer
2656flying
2657flyover
2658flypaper
2659foam
2660foe
2661fog
2662foil
2663folic
2664folk
2665follicle
2666follow
2667fondling
2668fondly
2669fondness
2670fondue
2671font
2672food
2673fool
2674footage
2675football
2676footbath
2677footboard
2678footer
2679footgear
2680foothill
2681foothold
2682footing
2683footless
2684footman
2685footnote
2686footpad
2687footpath
2688footprint
2689footrest
2690footsie
2691footsore
2692footwear
2693footwork
2694fossil
2695foster
2696founder
2697founding
2698fountain
2699fox
2700foyer
2701fraction
2702fracture
2703fragile
2704fragility
2705fragment
2706fragrance
2707fragrant
2708frail
2709frame
2710framing
2711frantic
2712fraternal
2713frayed
2714fraying
2715frays
2716freckled
2717freckles
2718freebase
2719freebee
2720freebie
2721freedom
2722freefall
2723freehand
2724freeing
2725freeload
2726freely
2727freemason
2728freeness
2729freestyle
2730freeware
2731freeway
2732freewill
2733freezable
2734freezing
2735freight
2736french
2737frenzied
2738frenzy
2739frequency
2740frequent
2741fresh
2742fretful
2743fretted
2744friction
2745friday
2746fridge
2747fried
2748friend
2749frighten
2750frightful
2751frigidity
2752frigidly
2753frill
2754fringe
2755frisbee
2756frisk
2757fritter
2758frivolous
2759frolic
2760from
2761front
2762frostbite
2763frosted
2764frostily
2765frosting
2766frostlike
2767frosty
2768froth
2769frown
2770frozen
2771fructose
2772frugality
2773frugally
2774fruit
2775frustrate
2776frying
2777gab
2778gaffe
2779gag
2780gainfully
2781gaining
2782gains
2783gala
2784gallantly
2785galleria
2786gallery
2787galley
2788gallon
2789gallows
2790gallstone
2791galore
2792galvanize
2793gambling
2794game
2795gaming
2796gamma
2797gander
2798gangly
2799gangrene
2800gangway
2801gap
2802garage
2803garbage
2804garden
2805gargle
2806garland
2807garlic
2808garment
2809garnet
2810garnish
2811garter
2812gas
2813gatherer
2814gathering
2815gating
2816gauging
2817gauntlet
2818gauze
2819gave
2820gawk
2821gazing
2822gear
2823gecko
2824geek
2825geiger
2826gem
2827gender
2828generic
2829generous
2830genetics
2831genre
2832gentile
2833gentleman
2834gently
2835gents
2836geography
2837geologic
2838geologist
2839geology
2840geometric
2841geometry
2842geranium
2843gerbil
2844geriatric
2845germicide
2846germinate
2847germless
2848germproof
2849gestate
2850gestation
2851gesture
2852getaway
2853getting
2854getup
2855giant
2856gibberish
2857giblet
2858giddily
2859giddiness
2860giddy
2861gift
2862gigabyte
2863gigahertz
2864gigantic
2865giggle
2866giggling
2867giggly
2868gigolo
2869gilled
2870gills
2871gimmick
2872girdle
2873giveaway
2874given
2875giver
2876giving
2877gizmo
2878gizzard
2879glacial
2880glacier
2881glade
2882gladiator
2883gladly
2884glamorous
2885glamour
2886glance
2887glancing
2888glandular
2889glare
2890glaring
2891glass
2892glaucoma
2893glazing
2894gleaming
2895gleeful
2896glider
2897gliding
2898glimmer
2899glimpse
2900glisten
2901glitch
2902glitter
2903glitzy
2904gloater
2905gloating
2906gloomily
2907gloomy
2908glorified
2909glorifier
2910glorify
2911glorious
2912glory
2913gloss
2914glove
2915glowing
2916glowworm
2917glucose
2918glue
2919gluten
2920glutinous
2921glutton
2922gnarly
2923gnat
2924goal
2925goatskin
2926goes
2927goggles
2928going
2929goldfish
2930goldmine
2931goldsmith
2932golf
2933goliath
2934gonad
2935gondola
2936gone
2937gong
2938good
2939gooey
2940goofball
2941goofiness
2942goofy
2943google
2944goon
2945gopher
2946gore
2947gorged
2948gorgeous
2949gory
2950gosling
2951gossip
2952gothic
2953gotten
2954gout
2955gown
2956grab
2957graceful
2958graceless
2959gracious
2960gradation
2961graded
2962grader
2963gradient
2964grading
2965gradually
2966graduate
2967graffiti
2968grafted
2969grafting
2970grain
2971granddad
2972grandkid
2973grandly
2974grandma
2975grandpa
2976grandson
2977granite
2978granny
2979granola
2980grant
2981granular
2982grape
2983graph
2984grapple
2985grappling
2986grasp
2987grass
2988gratified
2989gratify
2990grating
2991gratitude
2992gratuity
2993gravel
2994graveness
2995graves
2996graveyard
2997gravitate
2998gravity
2999gravy
3000gray
3001grazing
3002greasily
3003greedily
3004greedless
3005greedy
3006green
3007greeter
3008greeting
3009grew
3010greyhound
3011grid
3012grief
3013grievance
3014grieving
3015grievous
3016grill
3017grimace
3018grimacing
3019grime
3020griminess
3021grimy
3022grinch
3023grinning
3024grip
3025gristle
3026grit
3027groggily
3028groggy
3029groin
3030groom
3031groove
3032grooving
3033groovy
3034grope
3035ground
3036grouped
3037grout
3038grove
3039grower
3040growing
3041growl
3042grub
3043grudge
3044grudging
3045grueling
3046gruffly
3047grumble
3048grumbling
3049grumbly
3050grumpily
3051grunge
3052grunt
3053guacamole
3054guidable
3055guidance
3056guide
3057guiding
3058guileless
3059guise
3060gulf
3061gullible
3062gully
3063gulp
3064gumball
3065gumdrop
3066gumminess
3067gumming
3068gummy
3069gurgle
3070gurgling
3071guru
3072gush
3073gusto
3074gusty
3075gutless
3076guts
3077gutter
3078guy
3079guzzler
3080gyration
3081habitable
3082habitant
3083habitat
3084habitual
3085hacked
3086hacker
3087hacking
3088hacksaw
3089had
3090haggler
3091haiku
3092half
3093halogen
3094halt
3095halved
3096halves
3097hamburger
3098hamlet
3099hammock
3100hamper
3101hamster
3102hamstring
3103handbag
3104handball
3105handbook
3106handbrake
3107handcart
3108handclap
3109handclasp
3110handcraft
3111handcuff
3112handed
3113handful
3114handgrip
3115handgun
3116handheld
3117handiness
3118handiwork
3119handlebar
3120handled
3121handler
3122handling
3123handmade
3124handoff
3125handpick
3126handprint
3127handrail
3128handsaw
3129handset
3130handsfree
3131handshake
3132handstand
3133handwash
3134handwork
3135handwoven
3136handwrite
3137handyman
3138hangnail
3139hangout
3140hangover
3141hangup
3142hankering
3143hankie
3144hanky
3145haphazard
3146happening
3147happier
3148happiest
3149happily
3150happiness
3151happy
3152harbor
3153hardcopy
3154hardcore
3155hardcover
3156harddisk
3157hardened
3158hardener
3159hardening
3160hardhat
3161hardhead
3162hardiness
3163hardly
3164hardness
3165hardship
3166hardware
3167hardwired
3168hardwood
3169hardy
3170harmful
3171harmless
3172harmonica
3173harmonics
3174harmonize
3175harmony
3176harness
3177harpist
3178harsh
3179harvest
3180hash
3181hassle
3182haste
3183hastily
3184hastiness
3185hasty
3186hatbox
3187hatchback
3188hatchery
3189hatchet
3190hatching
3191hatchling
3192hate
3193hatless
3194hatred
3195haunt
3196haven
3197hazard
3198hazelnut
3199hazily
3200haziness
3201hazing
3202hazy
3203headache
3204headband
3205headboard
3206headcount
3207headdress
3208headed
3209header
3210headfirst
3211headgear
3212heading
3213headlamp
3214headless
3215headlock
3216headphone
3217headpiece
3218headrest
3219headroom
3220headscarf
3221headset
3222headsman
3223headstand
3224headstone
3225headway
3226headwear
3227heap
3228heat
3229heave
3230heavily
3231heaviness
3232heaving
3233hedge
3234hedging
3235heftiness
3236hefty
3237helium
3238helmet
3239helper
3240helpful
3241helping
3242helpless
3243helpline
3244hemlock
3245hemstitch
3246hence
3247henchman
3248henna
3249herald
3250herbal
3251herbicide
3252herbs
3253heritage
3254hermit
3255heroics
3256heroism
3257herring
3258herself
3259hertz
3260hesitancy
3261hesitant
3262hesitate
3263hexagon
3264hexagram
3265hubcap
3266huddle
3267huddling
3268huff
3269hug
3270hula
3271hulk
3272hull
3273human
3274humble
3275humbling
3276humbly
3277humid
3278humiliate
3279humility
3280humming
3281hummus
3282humongous
3283humorist
3284humorless
3285humorous
3286humpback
3287humped
3288humvee
3289hunchback
3290hundredth
3291hunger
3292hungrily
3293hungry
3294hunk
3295hunter
3296hunting
3297huntress
3298huntsman
3299hurdle
3300hurled
3301hurler
3302hurling
3303hurray
3304hurricane
3305hurried
3306hurry
3307hurt
3308husband
3309hush
3310husked
3311huskiness
3312hut
3313hybrid
3314hydrant
3315hydrated
3316hydration
3317hydrogen
3318hydroxide
3319hyperlink
3320hypertext
3321hyphen
3322hypnoses
3323hypnosis
3324hypnotic
3325hypnotism
3326hypnotist
3327hypnotize
3328hypocrisy
3329hypocrite
3330ibuprofen
3331ice
3332iciness
3333icing
3334icky
3335icon
3336icy
3337idealism
3338idealist
3339idealize
3340ideally
3341idealness
3342identical
3343identify
3344identity
3345ideology
3346idiocy
3347idiom
3348idly
3349igloo
3350ignition
3351ignore
3352iguana
3353illicitly
3354illusion
3355illusive
3356image
3357imaginary
3358imagines
3359imaging
3360imbecile
3361imitate
3362imitation
3363immature
3364immerse
3365immersion
3366imminent
3367immobile
3368immodest
3369immorally
3370immortal
3371immovable
3372immovably
3373immunity
3374immunize
3375impaired
3376impale
3377impart
3378impatient
3379impeach
3380impeding
3381impending
3382imperfect
3383imperial
3384impish
3385implant
3386implement
3387implicate
3388implicit
3389implode
3390implosion
3391implosive
3392imply
3393impolite
3394important
3395importer
3396impose
3397imposing
3398impotence
3399impotency
3400impotent
3401impound
3402imprecise
3403imprint
3404imprison
3405impromptu
3406improper
3407improve
3408improving
3409improvise
3410imprudent
3411impulse
3412impulsive
3413impure
3414impurity
3415iodine
3416iodize
3417ion
3418ipad
3419iphone
3420ipod
3421irate
3422irk
3423iron
3424irregular
3425irrigate
3426irritable
3427irritably
3428irritant
3429irritate
3430islamic
3431islamist
3432isolated
3433isolating
3434isolation
3435isotope
3436issue
3437issuing
3438italicize
3439italics
3440item
3441itinerary
3442itunes
3443ivory
3444ivy
3445jab
3446jackal
3447jacket
3448jackknife
3449jackpot
3450jailbird
3451jailbreak
3452jailer
3453jailhouse
3454jalapeno
3455jam
3456janitor
3457january
3458jargon
3459jarring
3460jasmine
3461jaundice
3462jaunt
3463java
3464jawed
3465jawless
3466jawline
3467jaws
3468jaybird
3469jaywalker
3470jazz
3471jeep
3472jeeringly
3473jellied
3474jelly
3475jersey
3476jester
3477jet
3478jiffy
3479jigsaw
3480jimmy
3481jingle
3482jingling
3483jinx
3484jitters
3485jittery
3486job
3487jockey
3488jockstrap
3489jogger
3490jogging
3491john
3492joining
3493jokester
3494jokingly
3495jolliness
3496jolly
3497jolt
3498jot
3499jovial
3500joyfully
3501joylessly
3502joyous
3503joyride
3504joystick
3505jubilance
3506jubilant
3507judge
3508judgingly
3509judicial
3510judiciary
3511judo
3512juggle
3513juggling
3514jugular
3515juice
3516juiciness
3517juicy
3518jujitsu
3519jukebox
3520july
3521jumble
3522jumbo
3523jump
3524junction
3525juncture
3526june
3527junior
3528juniper
3529junkie
3530junkman
3531junkyard
3532jurist
3533juror
3534jury
3535justice
3536justifier
3537justify
3538justly
3539justness
3540juvenile
3541kabob
3542kangaroo
3543karaoke
3544karate
3545karma
3546kebab
3547keenly
3548keenness
3549keep
3550keg
3551kelp
3552kennel
3553kept
3554kerchief
3555kerosene
3556kettle
3557kick
3558kiln
3559kilobyte
3560kilogram
3561kilometer
3562kilowatt
3563kilt
3564kimono
3565kindle
3566kindling
3567kindly
3568kindness
3569kindred
3570kinetic
3571kinfolk
3572king
3573kinship
3574kinsman
3575kinswoman
3576kissable
3577kisser
3578kissing
3579kitchen
3580kite
3581kitten
3582kitty
3583kiwi
3584kleenex
3585knapsack
3586knee
3587knelt
3588knickers
3589knoll
3590koala
3591kooky
3592kosher
3593krypton
3594kudos
3595kung
3596labored
3597laborer
3598laboring
3599laborious
3600labrador
3601ladder
3602ladies
3603ladle
3604ladybug
3605ladylike
3606lagged
3607lagging
3608lagoon
3609lair
3610lake
3611lance
3612landed
3613landfall
3614landfill
3615landing
3616landlady
3617landless
3618landline
3619landlord
3620landmark
3621landmass
3622landmine
3623landowner
3624landscape
3625landside
3626landslide
3627language
3628lankiness
3629lanky
3630lantern
3631lapdog
3632lapel
3633lapped
3634lapping
3635laptop
3636lard
3637large
3638lark
3639lash
3640lasso
3641last
3642latch
3643late
3644lather
3645latitude
3646latrine
3647latter
3648latticed
3649launch
3650launder
3651laundry
3652laurel
3653lavender
3654lavish
3655laxative
3656lazily
3657laziness
3658lazy
3659lecturer
3660left
3661legacy
3662legal
3663legend
3664legged
3665leggings
3666legible
3667legibly
3668legislate
3669lego
3670legroom
3671legume
3672legwarmer
3673legwork
3674lemon
3675lend
3676length
3677lens
3678lent
3679leotard
3680lesser
3681letdown
3682lethargic
3683lethargy
3684letter
3685lettuce
3686level
3687leverage
3688levers
3689levitate
3690levitator
3691liability
3692liable
3693liberty
3694librarian
3695library
3696licking
3697licorice
3698lid
3699life
3700lifter
3701lifting
3702liftoff
3703ligament
3704likely
3705likeness
3706likewise
3707liking
3708lilac
3709lilly
3710lily
3711limb
3712limeade
3713limelight
3714limes
3715limit
3716limping
3717limpness
3718line
3719lingo
3720linguini
3721linguist
3722lining
3723linked
3724linoleum
3725linseed
3726lint
3727lion
3728lip
3729liquefy
3730liqueur
3731liquid
3732lisp
3733list
3734litigate
3735litigator
3736litmus
3737litter
3738little
3739livable
3740lived
3741lively
3742liver
3743livestock
3744lividly
3745living
3746lizard
3747lubricant
3748lubricate
3749lucid
3750luckily
3751luckiness
3752luckless
3753lucrative
3754ludicrous
3755lugged
3756lukewarm
3757lullaby
3758lumber
3759luminance
3760luminous
3761lumpiness
3762lumping
3763lumpish
3764lunacy
3765lunar
3766lunchbox
3767luncheon
3768lunchroom
3769lunchtime
3770lung
3771lurch
3772lure
3773luridness
3774lurk
3775lushly
3776lushness
3777luster
3778lustfully
3779lustily
3780lustiness
3781lustrous
3782lusty
3783luxurious
3784luxury
3785lying
3786lyrically
3787lyricism
3788lyricist
3789lyrics
3790macarena
3791macaroni
3792macaw
3793mace
3794machine
3795machinist
3796magazine
3797magenta
3798maggot
3799magical
3800magician
3801magma
3802magnesium
3803magnetic
3804magnetism
3805magnetize
3806magnifier
3807magnify
3808magnitude
3809magnolia
3810mahogany
3811maimed
3812majestic
3813majesty
3814majorette
3815majority
3816makeover
3817maker
3818makeshift
3819making
3820malformed
3821malt
3822mama
3823mammal
3824mammary
3825mammogram
3826manager
3827managing
3828manatee
3829mandarin
3830mandate
3831mandatory
3832mandolin
3833manger
3834mangle
3835mango
3836mangy
3837manhandle
3838manhole
3839manhood
3840manhunt
3841manicotti
3842manicure
3843manifesto
3844manila
3845mankind
3846manlike
3847manliness
3848manly
3849manmade
3850manned
3851mannish
3852manor
3853manpower
3854mantis
3855mantra
3856manual
3857many
3858map
3859marathon
3860marauding
3861marbled
3862marbles
3863marbling
3864march
3865mardi
3866margarine
3867margarita
3868margin
3869marigold
3870marina
3871marine
3872marital
3873maritime
3874marlin
3875marmalade
3876maroon
3877married
3878marrow
3879marry
3880marshland
3881marshy
3882marsupial
3883marvelous
3884marxism
3885mascot
3886masculine
3887mashed
3888mashing
3889massager
3890masses
3891massive
3892mastiff
3893matador
3894matchbook
3895matchbox
3896matcher
3897matching
3898matchless
3899material
3900maternal
3901maternity
3902math
3903mating
3904matriarch
3905matrimony
3906matrix
3907matron
3908matted
3909matter
3910maturely
3911maturing
3912maturity
3913mauve
3914maverick
3915maximize
3916maximum
3917maybe
3918mayday
3919mayflower
3920moaner
3921moaning
3922mobile
3923mobility
3924mobilize
3925mobster
3926mocha
3927mocker
3928mockup
3929modified
3930modify
3931modular
3932modulator
3933module
3934moisten
3935moistness
3936moisture
3937molar
3938molasses
3939mold
3940molecular
3941molecule
3942molehill
3943mollusk
3944mom
3945monastery
3946monday
3947monetary
3948monetize
3949moneybags
3950moneyless
3951moneywise
3952mongoose
3953mongrel
3954monitor
3955monkhood
3956monogamy
3957monogram
3958monologue
3959monopoly
3960monorail
3961monotone
3962monotype
3963monoxide
3964monsieur
3965monsoon
3966monstrous
3967monthly
3968monument
3969moocher
3970moodiness
3971moody
3972mooing
3973moonbeam
3974mooned
3975moonlight
3976moonlike
3977moonlit
3978moonrise
3979moonscape
3980moonshine
3981moonstone
3982moonwalk
3983mop
3984morale
3985morality
3986morally
3987morbidity
3988morbidly
3989morphine
3990morphing
3991morse
3992mortality
3993mortally
3994mortician
3995mortified
3996mortify
3997mortuary
3998mosaic
3999mossy
4000most
4001mothball
4002mothproof
4003motion
4004motivate
4005motivator
4006motive
4007motocross
4008motor
4009motto
4010mountable
4011mountain
4012mounted
4013mounting
4014mourner
4015mournful
4016mouse
4017mousiness
4018moustache
4019mousy
4020mouth
4021movable
4022move
4023movie
4024moving
4025mower
4026mowing
4027much
4028muck
4029mud
4030mug
4031mulberry
4032mulch
4033mule
4034mulled
4035mullets
4036multiple
4037multiply
4038multitask
4039multitude
4040mumble
4041mumbling
4042mumbo
4043mummified
4044mummify
4045mummy
4046mumps
4047munchkin
4048mundane
4049municipal
4050muppet
4051mural
4052murkiness
4053murky
4054murmuring
4055muscular
4056museum
4057mushily
4058mushiness
4059mushroom
4060mushy
4061music
4062musket
4063muskiness
4064musky
4065mustang
4066mustard
4067muster
4068mustiness
4069musty
4070mutable
4071mutate
4072mutation
4073mute
4074mutilated
4075mutilator
4076mutiny
4077mutt
4078mutual
4079muzzle
4080myself
4081myspace
4082mystified
4083mystify
4084myth
4085nacho
4086nag
4087nail
4088name
4089naming
4090nanny
4091nanometer
4092nape
4093napkin
4094napped
4095napping
4096nappy
4097narrow
4098nastily
4099nastiness
4100national
4101native
4102nativity
4103natural
4104nature
4105naturist
4106nautical
4107navigate
4108navigator
4109navy
4110nearby
4111nearest
4112nearly
4113nearness
4114neatly
4115neatness
4116nebula
4117nebulizer
4118nectar
4119negate
4120negation
4121negative
4122neglector
4123negligee
4124negligent
4125negotiate
4126nemeses
4127nemesis
4128neon
4129nephew
4130nerd
4131nervous
4132nervy
4133nest
4134net
4135neurology
4136neuron
4137neurosis
4138neurotic
4139neuter
4140neutron
4141never
4142next
4143nibble
4144nickname
4145nicotine
4146niece
4147nifty
4148nimble
4149nimbly
4150nineteen
4151ninetieth
4152ninja
4153nintendo
4154ninth
4155nuclear
4156nuclei
4157nucleus
4158nugget
4159nullify
4160number
4161numbing
4162numbly
4163numbness
4164numeral
4165numerate
4166numerator
4167numeric
4168numerous
4169nuptials
4170nursery
4171nursing
4172nurture
4173nutcase
4174nutlike
4175nutmeg
4176nutrient
4177nutshell
4178nuttiness
4179nutty
4180nuzzle
4181nylon
4182oaf
4183oak
4184oasis
4185oat
4186obedience
4187obedient
4188obituary
4189object
4190obligate
4191obliged
4192oblivion
4193oblivious
4194oblong
4195obnoxious
4196oboe
4197obscure
4198obscurity
4199observant
4200observer
4201observing
4202obsessed
4203obsession
4204obsessive
4205obsolete
4206obstacle
4207obstinate
4208obstruct
4209obtain
4210obtrusive
4211obtuse
4212obvious
4213occultist
4214occupancy
4215occupant
4216occupier
4217occupy
4218ocean
4219ocelot
4220octagon
4221octane
4222october
4223octopus
4224ogle
4225oil
4226oink
4227ointment
4228okay
4229old
4230olive
4231olympics
4232omega
4233omen
4234ominous
4235omission
4236omit
4237omnivore
4238onboard
4239oncoming
4240ongoing
4241onion
4242online
4243onlooker
4244only
4245onscreen
4246onset
4247onshore
4248onslaught
4249onstage
4250onto
4251onward
4252onyx
4253oops
4254ooze
4255oozy
4256opacity
4257opal
4258open
4259operable
4260operate
4261operating
4262operation
4263operative
4264operator
4265opium
4266opossum
4267opponent
4268oppose
4269opposing
4270opposite
4271oppressed
4272oppressor
4273opt
4274opulently
4275osmosis
4276other
4277otter
4278ouch
4279ought
4280ounce
4281outage
4282outback
4283outbid
4284outboard
4285outbound
4286outbreak
4287outburst
4288outcast
4289outclass
4290outcome
4291outdated
4292outdoors
4293outer
4294outfield
4295outfit
4296outflank
4297outgoing
4298outgrow
4299outhouse
4300outing
4301outlast
4302outlet
4303outline
4304outlook
4305outlying
4306outmatch
4307outmost
4308outnumber
4309outplayed
4310outpost
4311outpour
4312output
4313outrage
4314outrank
4315outreach
4316outright
4317outscore
4318outsell
4319outshine
4320outshoot
4321outsider
4322outskirts
4323outsmart
4324outsource
4325outspoken
4326outtakes
4327outthink
4328outward
4329outweigh
4330outwit
4331oval
4332ovary
4333oven
4334overact
4335overall
4336overarch
4337overbid
4338overbill
4339overbite
4340overblown
4341overboard
4342overbook
4343overbuilt
4344overcast
4345overcoat
4346overcome
4347overcook
4348overcrowd
4349overdraft
4350overdrawn
4351overdress
4352overdrive
4353overdue
4354overeager
4355overeater
4356overexert
4357overfed
4358overfeed
4359overfill
4360overflow
4361overfull
4362overgrown
4363overhand
4364overhang
4365overhaul
4366overhead
4367overhear
4368overheat
4369overhung
4370overjoyed
4371overkill
4372overlabor
4373overlaid
4374overlap
4375overlay
4376overload
4377overlook
4378overlord
4379overlying
4380overnight
4381overpass
4382overpay
4383overplant
4384overplay
4385overpower
4386overprice
4387overrate
4388overreach
4389overreact
4390override
4391overripe
4392overrule
4393overrun
4394overshoot
4395overshot
4396oversight
4397oversized
4398oversleep
4399oversold
4400overspend
4401overstate
4402overstay
4403overstep
4404overstock
4405overstuff
4406oversweet
4407overtake
4408overthrow
4409overtime
4410overtly
4411overtone
4412overture
4413overturn
4414overuse
4415overvalue
4416overview
4417overwrite
4418owl
4419oxford
4420oxidant
4421oxidation
4422oxidize
4423oxidizing
4424oxygen
4425oxymoron
4426oyster
4427ozone
4428paced
4429pacemaker
4430pacific
4431pacifier
4432pacifism
4433pacifist
4434pacify
4435padded
4436padding
4437paddle
4438paddling
4439padlock
4440pagan
4441pager
4442paging
4443pajamas
4444palace
4445palatable
4446palm
4447palpable
4448palpitate
4449paltry
4450pampered
4451pamperer
4452pampers
4453pamphlet
4454panama
4455pancake
4456pancreas
4457panda
4458pandemic
4459pang
4460panhandle
4461panic
4462panning
4463panorama
4464panoramic
4465panther
4466pantomime
4467pantry
4468pants
4469pantyhose
4470paparazzi
4471papaya
4472paper
4473paprika
4474papyrus
4475parabola
4476parachute
4477parade
4478paradox
4479paragraph
4480parakeet
4481paralegal
4482paralyses
4483paralysis
4484paralyze
4485paramedic
4486parameter
4487paramount
4488parasail
4489parasite
4490parasitic
4491parcel
4492parched
4493parchment
4494pardon
4495parish
4496parka
4497parking
4498parkway
4499parlor
4500parmesan
4501parole
4502parrot
4503parsley
4504parsnip
4505partake
4506parted
4507parting
4508partition
4509partly
4510partner
4511partridge
4512party
4513passable
4514passably
4515passage
4516passcode
4517passenger
4518passerby
4519passing
4520passion
4521passive
4522passivism
4523passover
4524passport
4525password
4526pasta
4527pasted
4528pastel
4529pastime
4530pastor
4531pastrami
4532pasture
4533pasty
4534patchwork
4535patchy
4536paternal
4537paternity
4538path
4539patience
4540patient
4541patio
4542patriarch
4543patriot
4544patrol
4545patronage
4546patronize
4547pauper
4548pavement
4549paver
4550pavestone
4551pavilion
4552paving
4553pawing
4554payable
4555payback
4556paycheck
4557payday
4558payee
4559payer
4560paying
4561payment
4562payphone
4563payroll
4564pebble
4565pebbly
4566pecan
4567pectin
4568peculiar
4569peddling
4570pediatric
4571pedicure
4572pedigree
4573pedometer
4574pegboard
4575pelican
4576pellet
4577pelt
4578pelvis
4579penalize
4580penalty
4581pencil
4582pendant
4583pending
4584penholder
4585penknife
4586pennant
4587penniless
4588penny
4589penpal
4590pension
4591pentagon
4592pentagram
4593pep
4594perceive
4595percent
4596perch
4597percolate
4598perennial
4599perfected
4600perfectly
4601perfume
4602periscope
4603perish
4604perjurer
4605perjury
4606perkiness
4607perky
4608perm
4609peroxide
4610perpetual
4611perplexed
4612persecute
4613persevere
4614persuaded
4615persuader
4616pesky
4617peso
4618pessimism
4619pessimist
4620pester
4621pesticide
4622petal
4623petite
4624petition
4625petri
4626petroleum
4627petted
4628petticoat
4629pettiness
4630petty
4631petunia
4632phantom
4633phobia
4634phoenix
4635phonebook
4636phoney
4637phonics
4638phoniness
4639phony
4640phosphate
4641photo
4642phrase
4643phrasing
4644placard
4645placate
4646placidly
4647plank
4648planner
4649plant
4650plasma
4651plaster
4652plastic
4653plated
4654platform
4655plating
4656platinum
4657platonic
4658platter
4659platypus
4660plausible
4661plausibly
4662playable
4663playback
4664player
4665playful
4666playgroup
4667playhouse
4668playing
4669playlist
4670playmaker
4671playmate
4672playoff
4673playpen
4674playroom
4675playset
4676plaything
4677playtime
4678plaza
4679pleading
4680pleat
4681pledge
4682plentiful
4683plenty
4684plethora
4685plexiglas
4686pliable
4687plod
4688plop
4689plot
4690plow
4691ploy
4692pluck
4693plug
4694plunder
4695plunging
4696plural
4697plus
4698plutonium
4699plywood
4700poach
4701pod
4702poem
4703poet
4704pogo
4705pointed
4706pointer
4707pointing
4708pointless
4709pointy
4710poise
4711poison
4712poker
4713poking
4714polar
4715police
4716policy
4717polio
4718polish
4719politely
4720polka
4721polo
4722polyester
4723polygon
4724polygraph
4725polymer
4726poncho
4727pond
4728pony
4729popcorn
4730pope
4731poplar
4732popper
4733poppy
4734popsicle
4735populace
4736popular
4737populate
4738porcupine
4739pork
4740porous
4741porridge
4742portable
4743portal
4744portfolio
4745porthole
4746portion
4747portly
4748portside
4749poser
4750posh
4751posing
4752possible
4753possibly
4754possum
4755postage
4756postal
4757postbox
4758postcard
4759posted
4760poster
4761posting
4762postnasal
4763posture
4764postwar
4765pouch
4766pounce
4767pouncing
4768pound
4769pouring
4770pout
4771powdered
4772powdering
4773powdery
4774power
4775powwow
4776pox
4777praising
4778prance
4779prancing
4780pranker
4781prankish
4782prankster
4783prayer
4784praying
4785preacher
4786preaching
4787preachy
4788preamble
4789precinct
4790precise
4791precision
4792precook
4793precut
4794predator
4795predefine
4796predict
4797preface
4798prefix
4799preflight
4800preformed
4801pregame
4802pregnancy
4803pregnant
4804preheated
4805prelaunch
4806prelaw
4807prelude
4808premiere
4809premises
4810premium
4811prenatal
4812preoccupy
4813preorder
4814prepaid
4815prepay
4816preplan
4817preppy
4818preschool
4819prescribe
4820preseason
4821preset
4822preshow
4823president
4824presoak
4825press
4826presume
4827presuming
4828preteen
4829pretended
4830pretender
4831pretense
4832pretext
4833pretty
4834pretzel
4835prevail
4836prevalent
4837prevent
4838preview
4839previous
4840prewar
4841prewashed
4842prideful
4843pried
4844primal
4845primarily
4846primary
4847primate
4848primer
4849primp
4850princess
4851print
4852prior
4853prism
4854prison
4855prissy
4856pristine
4857privacy
4858private
4859privatize
4860prize
4861proactive
4862probable
4863probably
4864probation
4865probe
4866probing
4867probiotic
4868problem
4869procedure
4870process
4871proclaim
4872procreate
4873procurer
4874prodigal
4875prodigy
4876produce
4877product
4878profane
4879profanity
4880professed
4881professor
4882profile
4883profound
4884profusely
4885progeny
4886prognosis
4887program
4888progress
4889projector
4890prologue
4891prolonged
4892promenade
4893prominent
4894promoter
4895promotion
4896prompter
4897promptly
4898prone
4899prong
4900pronounce
4901pronto
4902proofing
4903proofread
4904proofs
4905propeller
4906properly
4907property
4908proponent
4909proposal
4910propose
4911props
4912prorate
4913protector
4914protegee
4915proton
4916prototype
4917protozoan
4918protract
4919protrude
4920proud
4921provable
4922proved
4923proven
4924provided
4925provider
4926providing
4927province
4928proving
4929provoke
4930provoking
4931provolone
4932prowess
4933prowler
4934prowling
4935proximity
4936proxy
4937prozac
4938prude
4939prudishly
4940prune
4941pruning
4942pry
4943psychic
4944public
4945publisher
4946pucker
4947pueblo
4948pug
4949pull
4950pulmonary
4951pulp
4952pulsate
4953pulse
4954pulverize
4955puma
4956pumice
4957pummel
4958punch
4959punctual
4960punctuate
4961punctured
4962pungent
4963punisher
4964punk
4965pupil
4966puppet
4967puppy
4968purchase
4969pureblood
4970purebred
4971purely
4972pureness
4973purgatory
4974purge
4975purging
4976purifier
4977purify
4978purist
4979puritan
4980purity
4981purple
4982purplish
4983purposely
4984purr
4985purse
4986pursuable
4987pursuant
4988pursuit
4989purveyor
4990pushcart
4991pushchair
4992pusher
4993pushiness
4994pushing
4995pushover
4996pushpin
4997pushup
4998pushy
4999putdown
5000putt
5001puzzle
5002puzzling
5003pyramid
5004pyromania
5005python
5006quack
5007quadrant
5008quail
5009quaintly
5010quake
5011quaking
5012qualified
5013qualifier
5014qualify
5015quality
5016qualm
5017quantum
5018quarrel
5019quarry
5020quartered
5021quarterly
5022quarters
5023quartet
5024quench
5025query
5026quicken
5027quickly
5028quickness
5029quicksand
5030quickstep
5031quiet
5032quill
5033quilt
5034quintet
5035quintuple
5036quirk
5037quit
5038quiver
5039quizzical
5040quotable
5041quotation
5042quote
5043rabid
5044race
5045racing
5046racism
5047rack
5048racoon
5049radar
5050radial
5051radiance
5052radiantly
5053radiated
5054radiation
5055radiator
5056radio
5057radish
5058raffle
5059raft
5060rage
5061ragged
5062raging
5063ragweed
5064raider
5065railcar
5066railing
5067railroad
5068railway
5069raisin
5070rake
5071raking
5072rally
5073ramble
5074rambling
5075ramp
5076ramrod
5077ranch
5078rancidity
5079random
5080ranged
5081ranger
5082ranging
5083ranked
5084ranking
5085ransack
5086ranting
5087rants
5088rare
5089rarity
5090rascal
5091rash
5092rasping
5093ravage
5094raven
5095ravine
5096raving
5097ravioli
5098ravishing
5099reabsorb
5100reach
5101reacquire
5102reaction
5103reactive
5104reactor
5105reaffirm
5106ream
5107reanalyze
5108reappear
5109reapply
5110reappoint
5111reapprove
5112rearrange
5113rearview
5114reason
5115reassign
5116reassure
5117reattach
5118reawake
5119rebalance
5120rebate
5121rebel
5122rebirth
5123reboot
5124reborn
5125rebound
5126rebuff
5127rebuild
5128rebuilt
5129reburial
5130rebuttal
5131recall
5132recant
5133recapture
5134recast
5135recede
5136recent
5137recess
5138recharger
5139recipient
5140recital
5141recite
5142reckless
5143reclaim
5144recliner
5145reclining
5146recluse
5147reclusive
5148recognize
5149recoil
5150recollect
5151recolor
5152reconcile
5153reconfirm
5154reconvene
5155recopy
5156record
5157recount
5158recoup
5159recovery
5160recreate
5161rectal
5162rectangle
5163rectified
5164rectify
5165recycled
5166recycler
5167recycling
5168reemerge
5169reenact
5170reenter
5171reentry
5172reexamine
5173referable
5174referee
5175reference
5176refill
5177refinance
5178refined
5179refinery
5180refining
5181refinish
5182reflected
5183reflector
5184reflex
5185reflux
5186refocus
5187refold
5188reforest
5189reformat
5190reformed
5191reformer
5192reformist
5193refract
5194refrain
5195refreeze
5196refresh
5197refried
5198refueling
5199refund
5200refurbish
5201refurnish
5202refusal
5203refuse
5204refusing
5205refutable
5206refute
5207regain
5208regalia
5209regally
5210reggae
5211regime
5212region
5213register
5214registrar
5215registry
5216regress
5217regretful
5218regroup
5219regular
5220regulate
5221regulator
5222rehab
5223reheat
5224rehire
5225rehydrate
5226reimburse
5227reissue
5228reiterate
5229rejoice
5230rejoicing
5231rejoin
5232rekindle
5233relapse
5234relapsing
5235relatable
5236related
5237relation
5238relative
5239relax
5240relay
5241relearn
5242release
5243relenting
5244reliable
5245reliably
5246reliance
5247reliant
5248relic
5249relieve
5250relieving
5251relight
5252relish
5253relive
5254reload
5255relocate
5256relock
5257reluctant
5258rely
5259remake
5260remark
5261remarry
5262rematch
5263remedial
5264remedy
5265remember
5266reminder
5267remindful
5268remission
5269remix
5270remnant
5271remodeler
5272remold
5273remorse
5274remote
5275removable
5276removal
5277removed
5278remover
5279removing
5280rename
5281renderer
5282rendering
5283rendition
5284renegade
5285renewable
5286renewably
5287renewal
5288renewed
5289renounce
5290renovate
5291renovator
5292rentable
5293rental
5294rented
5295renter
5296reoccupy
5297reoccur
5298reopen
5299reorder
5300repackage
5301repacking
5302repaint
5303repair
5304repave
5305repaying
5306repayment
5307repeal
5308repeated
5309repeater
5310repent
5311rephrase
5312replace
5313replay
5314replica
5315reply
5316reporter
5317repose
5318repossess
5319repost
5320repressed
5321reprimand
5322reprint
5323reprise
5324reproach
5325reprocess
5326reproduce
5327reprogram
5328reps
5329reptile
5330reptilian
5331repugnant
5332repulsion
5333repulsive
5334repurpose
5335reputable
5336reputably
5337request
5338require
5339requisite
5340reroute
5341rerun
5342resale
5343resample
5344rescuer
5345reseal
5346research
5347reselect
5348reseller
5349resemble
5350resend
5351resent
5352reset
5353reshape
5354reshoot
5355reshuffle
5356residence
5357residency
5358resident
5359residual
5360residue
5361resigned
5362resilient
5363resistant
5364resisting
5365resize
5366resolute
5367resolved
5368resonant
5369resonate
5370resort
5371resource
5372respect
5373resubmit
5374result
5375resume
5376resupply
5377resurface
5378resurrect
5379retail
5380retainer
5381retaining
5382retake
5383retaliate
5384retention
5385rethink
5386retinal
5387retired
5388retiree
5389retiring
5390retold
5391retool
5392retorted
5393retouch
5394retrace
5395retract
5396retrain
5397retread
5398retreat
5399retrial
5400retrieval
5401retriever
5402retry
5403return
5404retying
5405retype
5406reunion
5407reunite
5408reusable
5409reuse
5410reveal
5411reveler
5412revenge
5413revenue
5414reverb
5415revered
5416reverence
5417reverend
5418reversal
5419reverse
5420reversing
5421reversion
5422revert
5423revisable
5424revise
5425revision
5426revisit
5427revivable
5428revival
5429reviver
5430reviving
5431revocable
5432revoke
5433revolt
5434revolver
5435revolving
5436reward
5437rewash
5438rewind
5439rewire
5440reword
5441rework
5442rewrap
5443rewrite
5444rhyme
5445ribbon
5446ribcage
5447rice
5448riches
5449richly
5450richness
5451rickety
5452ricotta
5453riddance
5454ridden
5455ride
5456riding
5457rifling
5458rift
5459rigging
5460rigid
5461rigor
5462rimless
5463rimmed
5464rind
5465rink
5466rinse
5467rinsing
5468riot
5469ripcord
5470ripeness
5471ripening
5472ripping
5473ripple
5474rippling
5475riptide
5476rise
5477rising
5478risk
5479risotto
5480ritalin
5481ritzy
5482rival
5483riverbank
5484riverbed
5485riverboat
5486riverside
5487riveter
5488riveting
5489roamer
5490roaming
5491roast
5492robbing
5493robe
5494robin
5495robotics
5496robust
5497rockband
5498rocker
5499rocket
5500rockfish
5501rockiness
5502rocking
5503rocklike
5504rockslide
5505rockstar
5506rocky
5507rogue
5508roman
5509romp
5510rope
5511roping
5512roster
5513rosy
5514rotten
5515rotting
5516rotunda
5517roulette
5518rounding
5519roundish
5520roundness
5521roundup
5522roundworm
5523routine
5524routing
5525rover
5526roving
5527royal
5528rubbed
5529rubber
5530rubbing
5531rubble
5532rubdown
5533ruby
5534ruckus
5535rudder
5536rug
5537ruined
5538rule
5539rumble
5540rumbling
5541rummage
5542rumor
5543runaround
5544rundown
5545runner
5546running
5547runny
5548runt
5549runway
5550rupture
5551rural
5552ruse
5553rush
5554rust
5555rut
5556sabbath
5557sabotage
5558sacrament
5559sacred
5560sacrifice
5561sadden
5562saddlebag
5563saddled
5564saddling
5565sadly
5566sadness
5567safari
5568safeguard
5569safehouse
5570safely
5571safeness
5572saffron
5573saga
5574sage
5575sagging
5576saggy
5577said
5578saint
5579sake
5580salad
5581salami
5582salaried
5583salary
5584saline
5585salon
5586saloon
5587salsa
5588salt
5589salutary
5590salute
5591salvage
5592salvaging
5593salvation
5594same
5595sample
5596sampling
5597sanction
5598sanctity
5599sanctuary
5600sandal
5601sandbag
5602sandbank
5603sandbar
5604sandblast
5605sandbox
5606sanded
5607sandfish
5608sanding
5609sandlot
5610sandpaper
5611sandpit
5612sandstone
5613sandstorm
5614sandworm
5615sandy
5616sanitary
5617sanitizer
5618sank
5619santa
5620sapling
5621sappiness
5622sappy
5623sarcasm
5624sarcastic
5625sardine
5626sash
5627sasquatch
5628sassy
5629satchel
5630satiable
5631satin
5632satirical
5633satisfied
5634satisfy
5635saturate
5636saturday
5637sauciness
5638saucy
5639sauna
5640savage
5641savanna
5642saved
5643savings
5644savior
5645savor
5646saxophone
5647say
5648scabbed
5649scabby
5650scalded
5651scalding
5652scale
5653scaling
5654scallion
5655scallop
5656scalping
5657scam
5658scandal
5659scanner
5660scanning
5661scant
5662scapegoat
5663scarce
5664scarcity
5665scarecrow
5666scared
5667scarf
5668scarily
5669scariness
5670scarring
5671scary
5672scavenger
5673scenic
5674schedule
5675schematic
5676scheme
5677scheming
5678schilling
5679schnapps
5680scholar
5681science
5682scientist
5683scion
5684scoff
5685scolding
5686scone
5687scoop
5688scooter
5689scope
5690scorch
5691scorebook
5692scorecard
5693scored
5694scoreless
5695scorer
5696scoring
5697scorn
5698scorpion
5699scotch
5700scoundrel
5701scoured
5702scouring
5703scouting
5704scouts
5705scowling
5706scrabble
5707scraggly
5708scrambled
5709scrambler
5710scrap
5711scratch
5712scrawny
5713screen
5714scribble
5715scribe
5716scribing
5717scrimmage
5718script
5719scroll
5720scrooge
5721scrounger
5722scrubbed
5723scrubber
5724scruffy
5725scrunch
5726scrutiny
5727scuba
5728scuff
5729sculptor
5730sculpture
5731scurvy
5732scuttle
5733secluded
5734secluding
5735seclusion
5736second
5737secrecy
5738secret
5739sectional
5740sector
5741secular
5742securely
5743security
5744sedan
5745sedate
5746sedation
5747sedative
5748sediment
5749seduce
5750seducing
5751segment
5752seismic
5753seizing
5754seldom
5755selected
5756selection
5757selective
5758selector
5759self
5760seltzer
5761semantic
5762semester
5763semicolon
5764semifinal
5765seminar
5766semisoft
5767semisweet
5768senate
5769senator
5770send
5771senior
5772senorita
5773sensation
5774sensitive
5775sensitize
5776sensually
5777sensuous
5778sepia
5779september
5780septic
5781septum
5782sequel
5783sequence
5784sequester
5785series
5786sermon
5787serotonin
5788serpent
5789serrated
5790serve
5791service
5792serving
5793sesame
5794sessions
5795setback
5796setting
5797settle
5798settling
5799setup
5800sevenfold
5801seventeen
5802seventh
5803seventy
5804severity
5805shabby
5806shack
5807shaded
5808shadily
5809shadiness
5810shading
5811shadow
5812shady
5813shaft
5814shakable
5815shakily
5816shakiness
5817shaking
5818shaky
5819shale
5820shallot
5821shallow
5822shame
5823shampoo
5824shamrock
5825shank
5826shanty
5827shape
5828shaping
5829share
5830sharpener
5831sharper
5832sharpie
5833sharply
5834sharpness
5835shawl
5836sheath
5837shed
5838sheep
5839sheet
5840shelf
5841shell
5842shelter
5843shelve
5844shelving
5845sherry
5846shield
5847shifter
5848shifting
5849shiftless
5850shifty
5851shimmer
5852shimmy
5853shindig
5854shine
5855shingle
5856shininess
5857shining
5858shiny
5859ship
5860shirt
5861shivering
5862shock
5863shone
5864shoplift
5865shopper
5866shopping
5867shoptalk
5868shore
5869shortage
5870shortcake
5871shortcut
5872shorten
5873shorter
5874shorthand
5875shortlist
5876shortly
5877shortness
5878shorts
5879shortwave
5880shorty
5881shout
5882shove
5883showbiz
5884showcase
5885showdown
5886shower
5887showgirl
5888showing
5889showman
5890shown
5891showoff
5892showpiece
5893showplace
5894showroom
5895showy
5896shrank
5897shrapnel
5898shredder
5899shredding
5900shrewdly
5901shriek
5902shrill
5903shrimp
5904shrine
5905shrink
5906shrivel
5907shrouded
5908shrubbery
5909shrubs
5910shrug
5911shrunk
5912shucking
5913shudder
5914shuffle
5915shuffling
5916shun
5917shush
5918shut
5919shy
5920siamese
5921siberian
5922sibling
5923siding
5924sierra
5925siesta
5926sift
5927sighing
5928silenced
5929silencer
5930silent
5931silica
5932silicon
5933silk
5934silliness
5935silly
5936silo
5937silt
5938silver
5939similarly
5940simile
5941simmering
5942simple
5943simplify
5944simply
5945sincere
5946sincerity
5947singer
5948singing
5949single
5950singular
5951sinister
5952sinless
5953sinner
5954sinuous
5955sip
5956siren
5957sister
5958sitcom
5959sitter
5960sitting
5961situated
5962situation
5963sixfold
5964sixteen
5965sixth
5966sixties
5967sixtieth
5968sixtyfold
5969sizable
5970sizably
5971size
5972sizing
5973sizzle
5974sizzling
5975skater
5976skating
5977skedaddle
5978skeletal
5979skeleton
5980skeptic
5981sketch
5982skewed
5983skewer
5984skid
5985skied
5986skier
5987skies
5988skiing
5989skilled
5990skillet
5991skillful
5992skimmed
5993skimmer
5994skimming
5995skimpily
5996skincare
5997skinhead
5998skinless
5999skinning
6000skinny
6001skintight
6002skipper
6003skipping
6004skirmish
6005skirt
6006skittle
6007skydiver
6008skylight
6009skyline
6010skype
6011skyrocket
6012skyward
6013slab
6014slacked
6015slacker
6016slacking
6017slackness
6018slacks
6019slain
6020slam
6021slander
6022slang
6023slapping
6024slapstick
6025slashed
6026slashing
6027slate
6028slather
6029slaw
6030sled
6031sleek
6032sleep
6033sleet
6034sleeve
6035slept
6036sliceable
6037sliced
6038slicer
6039slicing
6040slick
6041slider
6042slideshow
6043sliding
6044slighted
6045slighting
6046slightly
6047slimness
6048slimy
6049slinging
6050slingshot
6051slinky
6052slip
6053slit
6054sliver
6055slobbery
6056slogan
6057sloped
6058sloping
6059sloppily
6060sloppy
6061slot
6062slouching
6063slouchy
6064sludge
6065slug
6066slum
6067slurp
6068slush
6069sly
6070small
6071smartly
6072smartness
6073smasher
6074smashing
6075smashup
6076smell
6077smelting
6078smile
6079smilingly
6080smirk
6081smite
6082smith
6083smitten
6084smock
6085smog
6086smoked
6087smokeless
6088smokiness
6089smoking
6090smoky
6091smolder
6092smooth
6093smother
6094smudge
6095smudgy
6096smuggler
6097smuggling
6098smugly
6099smugness
6100snack
6101snagged
6102snaking
6103snap
6104snare
6105snarl
6106snazzy
6107sneak
6108sneer
6109sneeze
6110sneezing
6111snide
6112sniff
6113snippet
6114snipping
6115snitch
6116snooper
6117snooze
6118snore
6119snoring
6120snorkel
6121snort
6122snout
6123snowbird
6124snowboard
6125snowbound
6126snowcap
6127snowdrift
6128snowdrop
6129snowfall
6130snowfield
6131snowflake
6132snowiness
6133snowless
6134snowman
6135snowplow
6136snowshoe
6137snowstorm
6138snowsuit
6139snowy
6140snub
6141snuff
6142snuggle
6143snugly
6144snugness
6145speak
6146spearfish
6147spearhead
6148spearman
6149spearmint
6150species
6151specimen
6152specked
6153speckled
6154specks
6155spectacle
6156spectator
6157spectrum
6158speculate
6159speech
6160speed
6161spellbind
6162speller
6163spelling
6164spendable
6165spender
6166spending
6167spent
6168spew
6169sphere
6170spherical
6171sphinx
6172spider
6173spied
6174spiffy
6175spill
6176spilt
6177spinach
6178spinal
6179spindle
6180spinner
6181spinning
6182spinout
6183spinster
6184spiny
6185spiral
6186spirited
6187spiritism
6188spirits
6189spiritual
6190splashed
6191splashing
6192splashy
6193splatter
6194spleen
6195splendid
6196splendor
6197splice
6198splicing
6199splinter
6200splotchy
6201splurge
6202spoilage
6203spoiled
6204spoiler
6205spoiling
6206spoils
6207spoken
6208spokesman
6209sponge
6210spongy
6211sponsor
6212spoof
6213spookily
6214spooky
6215spool
6216spoon
6217spore
6218sporting
6219sports
6220sporty
6221spotless
6222spotlight
6223spotted
6224spotter
6225spotting
6226spotty
6227spousal
6228spouse
6229spout
6230sprain
6231sprang
6232sprawl
6233spray
6234spree
6235sprig
6236spring
6237sprinkled
6238sprinkler
6239sprint
6240sprite
6241sprout
6242spruce
6243sprung
6244spry
6245spud
6246spur
6247sputter
6248spyglass
6249squabble
6250squad
6251squall
6252squander
6253squash
6254squatted
6255squatter
6256squatting
6257squeak
6258squealer
6259squealing
6260squeamish
6261squeegee
6262squeeze
6263squeezing
6264squid
6265squiggle
6266squiggly
6267squint
6268squire
6269squirt
6270squishier
6271squishy
6272stability
6273stabilize
6274stable
6275stack
6276stadium
6277staff
6278stage
6279staging
6280stagnant
6281stagnate
6282stainable
6283stained
6284staining
6285stainless
6286stalemate
6287staleness
6288stalling
6289stallion
6290stamina
6291stammer
6292stamp
6293stand
6294stank
6295staple
6296stapling
6297starboard
6298starch
6299stardom
6300stardust
6301starfish
6302stargazer
6303staring
6304stark
6305starless
6306starlet
6307starlight
6308starlit
6309starring
6310starry
6311starship
6312starter
6313starting
6314startle
6315startling
6316startup
6317starved
6318starving
6319stash
6320state
6321static
6322statistic
6323statue
6324stature
6325status
6326statute
6327statutory
6328staunch
6329stays
6330steadfast
6331steadier
6332steadily
6333steadying
6334steam
6335steed
6336steep
6337steerable
6338steering
6339steersman
6340stegosaur
6341stellar
6342stem
6343stench
6344stencil
6345step
6346stereo
6347sterile
6348sterility
6349sterilize
6350sterling
6351sternness
6352sternum
6353stew
6354stick
6355stiffen
6356stiffly
6357stiffness
6358stifle
6359stifling
6360stillness
6361stilt
6362stimulant
6363stimulate
6364stimuli
6365stimulus
6366stinger
6367stingily
6368stinging
6369stingray
6370stingy
6371stinking
6372stinky
6373stipend
6374stipulate
6375stir
6376stitch
6377stock
6378stoic
6379stoke
6380stole
6381stomp
6382stonewall
6383stoneware
6384stonework
6385stoning
6386stony
6387stood
6388stooge
6389stool
6390stoop
6391stoplight
6392stoppable
6393stoppage
6394stopped
6395stopper
6396stopping
6397stopwatch
6398storable
6399storage
6400storeroom
6401storewide
6402storm
6403stout
6404stove
6405stowaway
6406stowing
6407straddle
6408straggler
6409strained
6410strainer
6411straining
6412strangely
6413stranger
6414strangle
6415strategic
6416strategy
6417stratus
6418straw
6419stray
6420streak
6421stream
6422street
6423strength
6424strenuous
6425strep
6426stress
6427stretch
6428strewn
6429stricken
6430strict
6431stride
6432strife
6433strike
6434striking
6435strive
6436striving
6437strobe
6438strode
6439stroller
6440strongbox
6441strongly
6442strongman
6443struck
6444structure
6445strudel
6446struggle
6447strum
6448strung
6449strut
6450stubbed
6451stubble
6452stubbly
6453stubborn
6454stucco
6455stuck
6456student
6457studied
6458studio
6459study
6460stuffed
6461stuffing
6462stuffy
6463stumble
6464stumbling
6465stump
6466stung
6467stunned
6468stunner
6469stunning
6470stunt
6471stupor
6472sturdily
6473sturdy
6474styling
6475stylishly
6476stylist
6477stylized
6478stylus
6479suave
6480subarctic
6481subatomic
6482subdivide
6483subdued
6484subduing
6485subfloor
6486subgroup
6487subheader
6488subject
6489sublease
6490sublet
6491sublevel
6492sublime
6493submarine
6494submerge
6495submersed
6496submitter
6497subpanel
6498subpar
6499subplot
6500subprime
6501subscribe
6502subscript
6503subsector
6504subside
6505subsiding
6506subsidize
6507subsidy
6508subsoil
6509subsonic
6510substance
6511subsystem
6512subtext
6513subtitle
6514subtly
6515subtotal
6516subtract
6517subtype
6518suburb
6519subway
6520subwoofer
6521subzero
6522succulent
6523such
6524suction
6525sudden
6526sudoku
6527suds
6528sufferer
6529suffering
6530suffice
6531suffix
6532suffocate
6533suffrage
6534sugar
6535suggest
6536suing
6537suitable
6538suitably
6539suitcase
6540suitor
6541sulfate
6542sulfide
6543sulfite
6544sulfur
6545sulk
6546sullen
6547sulphate
6548sulphuric
6549sultry
6550superbowl
6551superglue
6552superhero
6553superior
6554superjet
6555superman
6556supermom
6557supernova
6558supervise
6559supper
6560supplier
6561supply
6562support
6563supremacy
6564supreme
6565surcharge
6566surely
6567sureness
6568surface
6569surfacing
6570surfboard
6571surfer
6572surgery
6573surgical
6574surging
6575surname
6576surpass
6577surplus
6578surprise
6579surreal
6580surrender
6581surrogate
6582surround
6583survey
6584survival
6585survive
6586surviving
6587survivor
6588sushi
6589suspect
6590suspend
6591suspense
6592sustained
6593sustainer
6594swab
6595swaddling
6596swagger
6597swampland
6598swan
6599swapping
6600swarm
6601sway
6602swear
6603sweat
6604sweep
6605swell
6606swept
6607swerve
6608swifter
6609swiftly
6610swiftness
6611swimmable
6612swimmer
6613swimming
6614swimsuit
6615swimwear
6616swinger
6617swinging
6618swipe
6619swirl
6620switch
6621swivel
6622swizzle
6623swooned
6624swoop
6625swoosh
6626swore
6627sworn
6628swung
6629sycamore
6630sympathy
6631symphonic
6632symphony
6633symptom
6634synapse
6635syndrome
6636synergy
6637synopses
6638synopsis
6639synthesis
6640synthetic
6641syrup
6642system
6643t-shirt
6644tabasco
6645tabby
6646tableful
6647tables
6648tablet
6649tableware
6650tabloid
6651tackiness
6652tacking
6653tackle
6654tackling
6655tacky
6656taco
6657tactful
6658tactical
6659tactics
6660tactile
6661tactless
6662tadpole
6663taekwondo
6664tag
6665tainted
6666take
6667taking
6668talcum
6669talisman
6670tall
6671talon
6672tamale
6673tameness
6674tamer
6675tamper
6676tank
6677tanned
6678tannery
6679tanning
6680tantrum
6681tapeless
6682tapered
6683tapering
6684tapestry
6685tapioca
6686tapping
6687taps
6688tarantula
6689target
6690tarmac
6691tarnish
6692tarot
6693tartar
6694tartly
6695tartness
6696task
6697tassel
6698taste
6699tastiness
6700tasting
6701tasty
6702tattered
6703tattle
6704tattling
6705tattoo
6706taunt
6707tavern
6708thank
6709that
6710thaw
6711theater
6712theatrics
6713thee
6714theft
6715theme
6716theology
6717theorize
6718thermal
6719thermos
6720thesaurus
6721these
6722thesis
6723thespian
6724thicken
6725thicket
6726thickness
6727thieving
6728thievish
6729thigh
6730thimble
6731thing
6732think
6733thinly
6734thinner
6735thinness
6736thinning
6737thirstily
6738thirsting
6739thirsty
6740thirteen
6741thirty
6742thong
6743thorn
6744those
6745thousand
6746thrash
6747thread
6748threaten
6749threefold
6750thrift
6751thrill
6752thrive
6753thriving
6754throat
6755throbbing
6756throng
6757throttle
6758throwaway
6759throwback
6760thrower
6761throwing
6762thud
6763thumb
6764thumping
6765thursday
6766thus
6767thwarting
6768thyself
6769tiara
6770tibia
6771tidal
6772tidbit
6773tidiness
6774tidings
6775tidy
6776tiger
6777tighten
6778tightly
6779tightness
6780tightrope
6781tightwad
6782tigress
6783tile
6784tiling
6785till
6786tilt
6787timid
6788timing
6789timothy
6790tinderbox
6791tinfoil
6792tingle
6793tingling
6794tingly
6795tinker
6796tinkling
6797tinsel
6798tinsmith
6799tint
6800tinwork
6801tiny
6802tipoff
6803tipped
6804tipper
6805tipping
6806tiptoeing
6807tiptop
6808tiring
6809tissue
6810trace
6811tracing
6812track
6813traction
6814tractor
6815trade
6816trading
6817tradition
6818traffic
6819tragedy
6820trailing
6821trailside
6822train
6823traitor
6824trance
6825tranquil
6826transfer
6827transform
6828translate
6829transpire
6830transport
6831transpose
6832trapdoor
6833trapeze
6834trapezoid
6835trapped
6836trapper
6837trapping
6838traps
6839trash
6840travel
6841traverse
6842travesty
6843tray
6844treachery
6845treading
6846treadmill
6847treason
6848treat
6849treble
6850tree
6851trekker
6852tremble
6853trembling
6854tremor
6855trench
6856trend
6857trespass
6858triage
6859trial
6860triangle
6861tribesman
6862tribunal
6863tribune
6864tributary
6865tribute
6866triceps
6867trickery
6868trickily
6869tricking
6870trickle
6871trickster
6872tricky
6873tricolor
6874tricycle
6875trident
6876tried
6877trifle
6878trifocals
6879trillion
6880trilogy
6881trimester
6882trimmer
6883trimming
6884trimness
6885trinity
6886trio
6887tripod
6888tripping
6889triumph
6890trivial
6891trodden
6892trolling
6893trombone
6894trophy
6895tropical
6896tropics
6897trouble
6898troubling
6899trough
6900trousers
6901trout
6902trowel
6903truce
6904truck
6905truffle
6906trump
6907trunks
6908trustable
6909trustee
6910trustful
6911trusting
6912trustless
6913truth
6914try
6915tubby
6916tubeless
6917tubular
6918tucking
6919tuesday
6920tug
6921tuition
6922tulip
6923tumble
6924tumbling
6925tummy
6926turban
6927turbine
6928turbofan
6929turbojet
6930turbulent
6931turf
6932turkey
6933turmoil
6934turret
6935turtle
6936tusk
6937tutor
6938tutu
6939tux
6940tweak
6941tweed
6942tweet
6943tweezers
6944twelve
6945twentieth
6946twenty
6947twerp
6948twice
6949twiddle
6950twiddling
6951twig
6952twilight
6953twine
6954twins
6955twirl
6956twistable
6957twisted
6958twister
6959twisting
6960twisty
6961twitch
6962twitter
6963tycoon
6964tying
6965tyke
6966udder
6967ultimate
6968ultimatum
6969ultra
6970umbilical
6971umbrella
6972umpire
6973unabashed
6974unable
6975unadorned
6976unadvised
6977unafraid
6978unaired
6979unaligned
6980unaltered
6981unarmored
6982unashamed
6983unaudited
6984unawake
6985unaware
6986unbaked
6987unbalance
6988unbeaten
6989unbend
6990unbent
6991unbiased
6992unbitten
6993unblended
6994unblessed
6995unblock
6996unbolted
6997unbounded
6998unboxed
6999unbraided
7000unbridle
7001unbroken
7002unbuckled
7003unbundle
7004unburned
7005unbutton
7006uncanny
7007uncapped
7008uncaring
7009uncertain
7010unchain
7011unchanged
7012uncharted
7013uncheck
7014uncivil
7015unclad
7016unclaimed
7017unclamped
7018unclasp
7019uncle
7020unclip
7021uncloak
7022unclog
7023unclothed
7024uncoated
7025uncoiled
7026uncolored
7027uncombed
7028uncommon
7029uncooked
7030uncork
7031uncorrupt
7032uncounted
7033uncouple
7034uncouth
7035uncover
7036uncross
7037uncrown
7038uncrushed
7039uncured
7040uncurious
7041uncurled
7042uncut
7043undamaged
7044undated
7045undaunted
7046undead
7047undecided
7048undefined
7049underage
7050underarm
7051undercoat
7052undercook
7053undercut
7054underdog
7055underdone
7056underfed
7057underfeed
7058underfoot
7059undergo
7060undergrad
7061underhand
7062underline
7063underling
7064undermine
7065undermost
7066underpaid
7067underpass
7068underpay
7069underrate
7070undertake
7071undertone
7072undertook
7073undertow
7074underuse
7075underwear
7076underwent
7077underwire
7078undesired
7079undiluted
7080undivided
7081undocked
7082undoing
7083undone
7084undrafted
7085undress
7086undrilled
7087undusted
7088undying
7089unearned
7090unearth
7091unease
7092uneasily
7093uneasy
7094uneatable
7095uneaten
7096unedited
7097unelected
7098unending
7099unengaged
7100unenvied
7101unequal
7102unethical
7103uneven
7104unexpired
7105unexposed
7106unfailing
7107unfair
7108unfasten
7109unfazed
7110unfeeling
7111unfiled
7112unfilled
7113unfitted
7114unfitting
7115unfixable
7116unfixed
7117unflawed
7118unfocused
7119unfold
7120unfounded
7121unframed
7122unfreeze
7123unfrosted
7124unfrozen
7125unfunded
7126unglazed
7127ungloved
7128unglue
7129ungodly
7130ungraded
7131ungreased
7132unguarded
7133unguided
7134unhappily
7135unhappy
7136unharmed
7137unhealthy
7138unheard
7139unhearing
7140unheated
7141unhelpful
7142unhidden
7143unhinge
7144unhitched
7145unholy
7146unhook
7147unicorn
7148unicycle
7149unified
7150unifier
7151uniformed
7152uniformly
7153unify
7154unimpeded
7155uninjured
7156uninstall
7157uninsured
7158uninvited
7159union
7160uniquely
7161unisexual
7162unison
7163unissued
7164unit
7165universal
7166universe
7167unjustly
7168unkempt
7169unkind
7170unknotted
7171unknowing
7172unknown
7173unlaced
7174unlatch
7175unlawful
7176unleaded
7177unlearned
7178unleash
7179unless
7180unleveled
7181unlighted
7182unlikable
7183unlimited
7184unlined
7185unlinked
7186unlisted
7187unlit
7188unlivable
7189unloaded
7190unloader
7191unlocked
7192unlocking
7193unlovable
7194unloved
7195unlovely
7196unloving
7197unluckily
7198unlucky
7199unmade
7200unmanaged
7201unmanned
7202unmapped
7203unmarked
7204unmasked
7205unmasking
7206unmatched
7207unmindful
7208unmixable
7209unmixed
7210unmolded
7211unmoral
7212unmovable
7213unmoved
7214unmoving
7215unnamable
7216unnamed
7217unnatural
7218unneeded
7219unnerve
7220unnerving
7221unnoticed
7222unopened
7223unopposed
7224unpack
7225unpadded
7226unpaid
7227unpainted
7228unpaired
7229unpaved
7230unpeeled
7231unpicked
7232unpiloted
7233unpinned
7234unplanned
7235unplanted
7236unpleased
7237unpledged
7238unplowed
7239unplug
7240unpopular
7241unproven
7242unquote
7243unranked
7244unrated
7245unraveled
7246unreached
7247unread
7248unreal
7249unreeling
7250unrefined
7251unrelated
7252unrented
7253unrest
7254unretired
7255unrevised
7256unrigged
7257unripe
7258unrivaled
7259unroasted
7260unrobed
7261unroll
7262unruffled
7263unruly
7264unrushed
7265unsaddle
7266unsafe
7267unsaid
7268unsalted
7269unsaved
7270unsavory
7271unscathed
7272unscented
7273unscrew
7274unsealed
7275unseated
7276unsecured
7277unseeing
7278unseemly
7279unseen
7280unselect
7281unselfish
7282unsent
7283unsettled
7284unshackle
7285unshaken
7286unshaved
7287unshaven
7288unsheathe
7289unshipped
7290unsightly
7291unsigned
7292unskilled
7293unsliced
7294unsmooth
7295unsnap
7296unsocial
7297unsoiled
7298unsold
7299unsolved
7300unsorted
7301unspoiled
7302unspoken
7303unstable
7304unstaffed
7305unstamped
7306unsteady
7307unsterile
7308unstirred
7309unstitch
7310unstopped
7311unstuck
7312unstuffed
7313unstylish
7314unsubtle
7315unsubtly
7316unsuited
7317unsure
7318unsworn
7319untagged
7320untainted
7321untaken
7322untamed
7323untangled
7324untapped
7325untaxed
7326unthawed
7327unthread
7328untidy
7329untie
7330until
7331untimed
7332untimely
7333untitled
7334untoasted
7335untold
7336untouched
7337untracked
7338untrained
7339untreated
7340untried
7341untrimmed
7342untrue
7343untruth
7344unturned
7345untwist
7346untying
7347unusable
7348unused
7349unusual
7350unvalued
7351unvaried
7352unvarying
7353unveiled
7354unveiling
7355unvented
7356unviable
7357unvisited
7358unvocal
7359unwanted
7360unwarlike
7361unwary
7362unwashed
7363unwatched
7364unweave
7365unwed
7366unwelcome
7367unwell
7368unwieldy
7369unwilling
7370unwind
7371unwired
7372unwitting
7373unwomanly
7374unworldly
7375unworn
7376unworried
7377unworthy
7378unwound
7379unwoven
7380unwrapped
7381unwritten
7382unzip
7383upbeat
7384upchuck
7385upcoming
7386upcountry
7387update
7388upfront
7389upgrade
7390upheaval
7391upheld
7392uphill
7393uphold
7394uplifted
7395uplifting
7396upload
7397upon
7398upper
7399upright
7400uprising
7401upriver
7402uproar
7403uproot
7404upscale
7405upside
7406upstage
7407upstairs
7408upstart
7409upstate
7410upstream
7411upstroke
7412upswing
7413uptake
7414uptight
7415uptown
7416upturned
7417upward
7418upwind
7419uranium
7420urban
7421urchin
7422urethane
7423urgency
7424urgent
7425urging
7426urologist
7427urology
7428usable
7429usage
7430useable
7431used
7432uselessly
7433user
7434usher
7435usual
7436utensil
7437utility
7438utilize
7439utmost
7440utopia
7441utter
7442vacancy
7443vacant
7444vacate
7445vacation
7446vagabond
7447vagrancy
7448vagrantly
7449vaguely
7450vagueness
7451valiant
7452valid
7453valium
7454valley
7455valuables
7456value
7457vanilla
7458vanish
7459vanity
7460vanquish
7461vantage
7462vaporizer
7463variable
7464variably
7465varied
7466variety
7467various
7468varmint
7469varnish
7470varsity
7471varying
7472vascular
7473vaseline
7474vastly
7475vastness
7476veal
7477vegan
7478veggie
7479vehicular
7480velcro
7481velocity
7482velvet
7483vendetta
7484vending
7485vendor
7486veneering
7487vengeful
7488venomous
7489ventricle
7490venture
7491venue
7492venus
7493verbalize
7494verbally
7495verbose
7496verdict
7497verify
7498verse
7499version
7500versus
7501vertebrae
7502vertical
7503vertigo
7504very
7505vessel
7506vest
7507veteran
7508veto
7509vexingly
7510viability
7511viable
7512vibes
7513vice
7514vicinity
7515victory
7516video
7517viewable
7518viewer
7519viewing
7520viewless
7521viewpoint
7522vigorous
7523village
7524villain
7525vindicate
7526vineyard
7527vintage
7528violate
7529violation
7530violator
7531violet
7532violin
7533viper
7534viral
7535virtual
7536virtuous
7537virus
7538visa
7539viscosity
7540viscous
7541viselike
7542visible
7543visibly
7544vision
7545visiting
7546visitor
7547visor
7548vista
7549vitality
7550vitalize
7551vitally
7552vitamins
7553vivacious
7554vividly
7555vividness
7556vixen
7557vocalist
7558vocalize
7559vocally
7560vocation
7561voice
7562voicing
7563void
7564volatile
7565volley
7566voltage
7567volumes
7568voter
7569voting
7570voucher
7571vowed
7572vowel
7573voyage
7574wackiness
7575wad
7576wafer
7577waffle
7578waged
7579wager
7580wages
7581waggle
7582wagon
7583wake
7584waking
7585walk
7586walmart
7587walnut
7588walrus
7589waltz
7590wand
7591wannabe
7592wanted
7593wanting
7594wasabi
7595washable
7596washbasin
7597washboard
7598washbowl
7599washcloth
7600washday
7601washed
7602washer
7603washhouse
7604washing
7605washout
7606washroom
7607washstand
7608washtub
7609wasp
7610wasting
7611watch
7612water
7613waviness
7614waving
7615wavy
7616whacking
7617whacky
7618wham
7619wharf
7620wheat
7621whenever
7622whiff
7623whimsical
7624whinny
7625whiny
7626whisking
7627whoever
7628whole
7629whomever
7630whoopee
7631whooping
7632whoops
7633why
7634wick
7635widely
7636widen
7637widget
7638widow
7639width
7640wieldable
7641wielder
7642wife
7643wifi
7644wikipedia
7645wildcard
7646wildcat
7647wilder
7648wildfire
7649wildfowl
7650wildland
7651wildlife
7652wildly
7653wildness
7654willed
7655willfully
7656willing
7657willow
7658willpower
7659wilt
7660wimp
7661wince
7662wincing
7663wind
7664wing
7665winking
7666winner
7667winnings
7668winter
7669wipe
7670wired
7671wireless
7672wiring
7673wiry
7674wisdom
7675wise
7676wish
7677wisplike
7678wispy
7679wistful
7680wizard
7681wobble
7682wobbling
7683wobbly
7684wok
7685wolf
7686wolverine
7687womanhood
7688womankind
7689womanless
7690womanlike
7691womanly
7692womb
7693woof
7694wooing
7695wool
7696woozy
7697word
7698work
7699worried
7700worrier
7701worrisome
7702worry
7703worsening
7704worshiper
7705worst
7706wound
7707woven
7708wow
7709wrangle
7710wrath
7711wreath
7712wreckage
7713wrecker
7714wrecking
7715wrench
7716wriggle
7717wriggly
7718wrinkle
7719wrinkly
7720wrist
7721writing
7722written
7723wrongdoer
7724wronged
7725wrongful
7726wrongly
7727wrongness
7728wrought
7729xbox
7730xerox
7731yahoo
7732yam
7733yanking
7734yapping
7735yard
7736yarn
7737yeah
7738yearbook
7739yearling
7740yearly
7741yearning
7742yeast
7743yelling
7744yelp
7745yen
7746yesterday
7747yiddish
7748yield
7749yin
7750yippee
7751yo-yo
7752yodel
7753yoga
7754yogurt
7755yonder
7756yoyo
7757yummy
7758zap
7759zealous
7760zebra
7761zen
7762zeppelin
7763zero
7764zestfully
7765zesty
7766zigzagged
7767zipfile
7768zipping
7769zippy
7770zips
7771zit
7772zodiac
7773zombie
7774zone
7775zoning
7776zookeeper
7777zoologist
7778zoology
7779zoom
7780"""
7781
7782public enum Wordlist {
7783 public static let words: [String] = effLongWordlist.split(separator: "\n").map(String.init)
7784}
Sources/keycask/main.swift added +3
@@ -0,0 +1,3 @@
1import KeycaskCore
2
3print("keycask")
Tests/KeycaskCLITests/CLI.swift added +32
@@ -0,0 +1,32 @@
1import Foundation
2import Testing
3
4enum Binary {
5 static let url: URL = {
6 #if os(macOS)
7 if let index = CommandLine.arguments.firstIndex(of: "--test-bundle-path"),
8 CommandLine.arguments.count > index + 1
9 {
10 return URL(fileURLWithPath: CommandLine.arguments[index + 1])
11 .deletingLastPathComponent() // MacOS
12 .deletingLastPathComponent() // Contents
13 .deletingLastPathComponent() // *.xctest
14 .deletingLastPathComponent() // Products/Debug
15 .appendingPathComponent("keycask")
16 }
17 for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
18 return bundle.bundleURL.deletingLastPathComponent().appendingPathComponent(
19 "keycask")
20 }
21 fatalError("test bundle not found")
22 #elseif os(Windows)
23 return Bundle.main.bundleURL.appendingPathComponent("keycask.exe")
24 #else
25 return Bundle.main.bundleURL.appendingPathComponent("keycask")
26 #endif
27 }()
28}
29
30@Test func binaryIsBuilt() {
31 #expect(FileManager.default.isExecutableFile(atPath: Binary.url.path))
32}
Tests/KeycaskCoreTests/EntryIDTests.swift added +55
@@ -0,0 +1,55 @@
1import Foundation
2import Testing
3
4@testable import KeycaskCore
5
6@Suite struct EntryIDTests {
7 @Test func randomIDsHaveLengthEightFromTheAlphabet() {
8 let allowed = Set(EntryID.alphabet)
9 for _ in 0..<200 {
10 let id = EntryID.random()
11 #expect(id.rawValue.count == 8)
12 #expect(id.rawValue.allSatisfy { allowed.contains($0) })
13 }
14 }
15
16 @Test func alphabetExcludesAmbiguousCharacters() {
17 let alphabet = Set(EntryID.alphabet)
18 #expect(alphabet.count == 32)
19 for bad in ["l", "o", "0", "1"] {
20 #expect(!alphabet.contains(Character(bad)))
21 }
22 }
23
24 @Test func parsingValidatesLengthAndAlphabet() {
25 #expect(EntryID("abcd2345") != nil)
26 #expect(EntryID("abcd234") == nil)
27 #expect(EntryID("abcd23456") == nil)
28 #expect(EntryID("abcd234l") == nil)
29 #expect(EntryID("ABCD2345") == nil)
30 }
31
32 @Test func codableIsABareString() throws {
33 let id = EntryID("abcd2345")!
34 let data = try JSONEncoder().encode([id])
35 #expect(String(decoding: data, as: UTF8.self) == "[\"abcd2345\"]")
36 let back = try JSONDecoder().decode([EntryID].self, from: data)
37 #expect(back == [id])
38 #expect(throws: DecodingError.self) {
39 try JSONDecoder().decode([EntryID].self, from: Data("[\"bad\"]".utf8))
40 }
41 }
42
43 @Test func seededGeneratorIsDeterministic() {
44 struct Counter: RandomNumberGenerator {
45 var n: UInt64 = 0
46 mutating func next() -> UInt64 {
47 n += 1
48 return n
49 }
50 }
51 var a = Counter()
52 var b = Counter()
53 #expect(EntryID.random(using: &a) == EntryID.random(using: &b))
54 }
55}
Tests/KeycaskCoreTests/EntryTests.swift added +40
@@ -0,0 +1,40 @@
1import Foundation
2import Testing
3
4@testable import KeycaskCore
5
6@Suite struct EntryTests {
7 @Test func initNormalizesTagsAndTruncatesDates() {
8 let now = Date(timeIntervalSince1970: 1_700_000_000.75)
9 let e = Entry(
10 name: "gh", password: "p", tags: [" work", "Dev", "dev", "", "alpha"], now: now)
11 #expect(e.tags == ["alpha", "Dev", "work"])
12 #expect(e.created == Date(timeIntervalSince1970: 1_700_000_000))
13 #expect(e.updated == e.created)
14 }
15
16 @Test func normalizeSortsCaseInsensitivelyAndKeepsFirstSpelling() {
17 #expect(Entry.normalize(tags: ["b", "A", "a", "B"]) == ["A", "b"])
18 #expect(Entry.normalize(tags: []) == [])
19 }
20
21 @Test func hasTagIsCaseInsensitive() {
22 let e = Entry(name: "gh", password: "p", tags: ["Dev"])
23 #expect(e.hasTag("dev"))
24 #expect(e.hasTag("DEV"))
25 #expect(!e.hasTag("ops"))
26 }
27
28 @Test func matchesSearchesEveryTextFieldExceptPassword() {
29 let e = Entry(
30 name: "GitHub", username: "cmc", password: "hunter2", url: "https://github.com",
31 notes: "downtown office", tags: ["Dev"])
32 #expect(e.matches("github"))
33 #expect(e.matches("CMC"))
34 #expect(e.matches("github.com"))
35 #expect(e.matches("downtown"))
36 #expect(e.matches("dev"))
37 #expect(!e.matches("hunter2"))
38 #expect(!e.matches("nothing"))
39 }
40}
Tests/KeycaskCoreTests/EnvelopeTests.swift added +119
@@ -0,0 +1,119 @@
1import Crypto
2import Foundation
3import Testing
4
5@testable import KeycaskCore
6
7@Suite struct EnvelopeTests {
8 // Low iteration count keeps the suite fast. Production uses Envelope.defaultIterations.
9 let kdf = Envelope.KDFParams(
10 name: Envelope.kdfName, iterations: 1_000, salt: Data(repeating: 7, count: 16))
11
12 func hex(_ key: SymmetricKey) -> String {
13 key.withUnsafeBytes { $0.map { String(format: "%02x", $0) }.joined() }
14 }
15
16 @Test func pbkdf2MatchesPublishedVectors() throws {
17 let one = Envelope.KDFParams(name: Envelope.kdfName, iterations: 1, salt: Data("salt".utf8))
18 #expect(
19 hex(try Envelope.deriveKey(passphrase: "password", kdf: one))
20 == "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b")
21 let many = Envelope.KDFParams(
22 name: Envelope.kdfName, iterations: 4096, salt: Data("salt".utf8))
23 #expect(
24 hex(try Envelope.deriveKey(passphrase: "password", kdf: many))
25 == "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a")
26 }
27
28 @Test func sealThenOpenRoundTrips() throws {
29 let env = try Envelope.seal(Data("hello vault".utf8), passphrase: "pw", kdf: kdf)
30 #expect(env.format == 1)
31 #expect(env.kdf == kdf)
32 #expect(try env.open(passphrase: "pw") == Data("hello vault".utf8))
33 }
34
35 @Test func wrongPassphraseCannotDecrypt() throws {
36 let env = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
37 #expect(throws: KeycaskError.cannotDecrypt) { try env.open(passphrase: "PW") }
38 }
39
40 @Test func tamperedBoxCannotDecrypt() throws {
41 var env = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
42 env.box[env.box.count - 1] ^= 0x01
43 #expect(throws: KeycaskError.cannotDecrypt) { try env.open(passphrase: "pw") }
44 }
45
46 @Test func nonceIsFreshAndSaltIsKept() throws {
47 let a = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
48 let b = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
49 #expect(a.box != b.box)
50 #expect(a.kdf.salt == b.kdf.salt)
51 }
52
53 @Test func freshParamsUseDefaults() {
54 let p = Envelope.KDFParams.fresh()
55 #expect(p.name == "pbkdf2-hmac-sha256")
56 #expect(p.iterations == 600_000)
57 #expect(p.salt.count == 16)
58 #expect(p.salt != Envelope.KDFParams.fresh().salt)
59 }
60
61 @Test func encodedShapeMatchesTheSpec() throws {
62 let env = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
63 let json = try JSONSerialization.jsonObject(with: env.encoded()) as! [String: Any]
64 #expect(json["format"] as? Int == 1)
65 let k = json["kdf"] as! [String: Any]
66 #expect(k["name"] as? String == "pbkdf2-hmac-sha256")
67 #expect(k["iterations"] as? Int == 1_000)
68 #expect(Data(base64Encoded: k["salt"] as! String) == kdf.salt)
69 #expect(Data(base64Encoded: json["box"] as! String) == env.box)
70 #expect(try Envelope(parsing: env.encoded()) == env)
71 }
72
73 @Test func malformedInputsAreCorrupt() throws {
74 #expect(throws: KeycaskError.self) { try Envelope(parsing: Data("not json".utf8)) }
75 #expect(throws: KeycaskError.self) { try Envelope(parsing: Data("{\"format\":1}".utf8)) }
76
77 var wrongFormat = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
78 wrongFormat.format = 2
79 #expect(throws: KeycaskError.corrupt("unsupported format 2")) {
80 try wrongFormat.open(passphrase: "pw")
81 }
82
83 var wrongKDF = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
84 wrongKDF.kdf.name = "argon2id"
85 #expect(throws: KeycaskError.corrupt("unsupported kdf argon2id")) {
86 try wrongKDF.open(passphrase: "pw")
87 }
88
89 var shortBox = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
90 shortBox.box = Data([1, 2, 3])
91 #expect(throws: KeycaskError.corrupt("box too short")) {
92 try shortBox.open(passphrase: "pw")
93 }
94 }
95
96 @Test func outOfRangeIterationsAreCorrupt() throws {
97 var env = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
98 env.kdf.iterations = -1
99 #expect(throws: KeycaskError.corrupt("bad iteration count -1")) {
100 try env.open(passphrase: "pw")
101 }
102 env.kdf.iterations = 0
103 #expect(throws: KeycaskError.corrupt("bad iteration count 0")) {
104 try env.open(passphrase: "pw")
105 }
106 let tooMany = Envelope.KDFParams(
107 name: Envelope.kdfName, iterations: Int(UInt32.max) + 1, salt: kdf.salt)
108 #expect(throws: KeycaskError.corrupt("bad iteration count \(Int(UInt32.max) + 1)")) {
109 try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: tooMany)
110 }
111 }
112
113 @Test func passphraseIsNFCNormalized() throws {
114 let composed = "caf\u{00E9}"
115 let decomposed = "cafe\u{0301}"
116 let env = try Envelope.seal(Data("x".utf8), passphrase: composed, kdf: kdf)
117 #expect(try env.open(passphrase: decomposed) == Data("x".utf8))
118 }
119}
Tests/KeycaskCoreTests/GeneratorTests.swift added +57
@@ -0,0 +1,57 @@
1import Testing
2
3@testable import KeycaskCore
4
5@Suite struct GeneratorTests {
6 struct Counter: RandomNumberGenerator {
7 var n: UInt64 = 0
8 mutating func next() -> UInt64 {
9 n &+= 0x9E37_79B9_7F4A_7C15
10 return n
11 }
12 }
13
14 @Test func wordlistHas7776UniqueWords() {
15 #expect(Wordlist.words.count == 7776)
16 #expect(Set(Wordlist.words).count == 7776)
17 #expect(Wordlist.words.first == "abacus")
18 #expect(Wordlist.words.allSatisfy { !$0.isEmpty && !$0.contains(" ") })
19 }
20
21 @Test func passwordHasRequestedLengthFromTheAlphabet() {
22 let allowed = Set(Generator.alphabet)
23 for length in [1, 8, 24, 64] {
24 let p = Generator.password(length: length)
25 #expect(p.count == length)
26 #expect(p.allSatisfy { allowed.contains($0) })
27 }
28 #expect(Generator.password(length: 0) == "")
29 }
30
31 @Test func alphabetCoversAllClasses() {
32 let s = String(Generator.alphabet)
33 #expect(s.contains("A") && s.contains("z") && s.contains("7") && s.contains("!"))
34 #expect(Set(Generator.alphabet).count == Generator.alphabet.count)
35 }
36
37 @Test func passphraseUsesWordsFromTheList() {
38 var a = Counter()
39 var b = Counter()
40 let produced = Generator.passphrase(words: 5, using: &a)
41 let count = UInt32(Wordlist.words.count)
42 let expected = (0..<5).map { _ in Wordlist.words[Int(b.next(upperBound: count))] }
43 .joined(separator: Generator.wordSeparator)
44 #expect(produced == expected)
45 #expect(Generator.passphrase(words: 0) == "")
46 #expect(!Generator.passphrase(words: 3).isEmpty)
47 }
48
49 @Test func seededOutputIsReproducible() {
50 var a = Counter()
51 var b = Counter()
52 #expect(
53 Generator.password(length: 16, using: &a) == Generator.password(length: 16, using: &b))
54 #expect(
55 Generator.passphrase(words: 3, using: &a) == Generator.passphrase(words: 3, using: &b))
56 }
57}
Tests/KeycaskCoreTests/KeycaskErrorTests.swift added +38
@@ -0,0 +1,38 @@
1import Testing
2
3@testable import KeycaskCore
4
5@Suite struct KeycaskErrorTests {
6 @Test func exitCodesFollowTheSpec() {
7 #expect(KeycaskError.failure("x").exitCode == 1)
8 #expect(KeycaskError.io("x").exitCode == 1)
9 #expect(KeycaskError.corrupt("x").exitCode == 1)
10 #expect(KeycaskError.vaultExists("x").exitCode == 1)
11 #expect(KeycaskError.duplicateID(EntryID("abcd2345")!).exitCode == 1)
12 #expect(KeycaskError.usage("x").exitCode == 2)
13 #expect(KeycaskError.notFound("x").exitCode == 3)
14 #expect(KeycaskError.noVault("/p").exitCode == 3)
15 #expect(KeycaskError.cannotDecrypt.exitCode == 4)
16 #expect(KeycaskError.ambiguous(name: "gh", candidates: []).exitCode == 5)
17 }
18
19 @Test func messagesNameTheSubject() {
20 #expect(KeycaskError.notFound("gh").message == "gh: not found")
21 #expect(KeycaskError.noVault("/v").message == "vault /v not found (run `keycask init`)")
22 #expect(KeycaskError.vaultExists("/v").message == "vault /v already exists")
23 #expect(
24 KeycaskError.cannotDecrypt.message
25 == "cannot decrypt: wrong passphrase or damaged vault")
26 #expect(KeycaskError.corrupt("bad json").message == "vault is corrupt: bad json")
27 }
28
29 @Test func ambiguousListsCandidateIDs() {
30 let a = Entry(id: EntryID("aaaa2222")!, name: "gh", username: "one", password: "p")
31 let b = Entry(id: EntryID("bbbb3333")!, name: "gh", password: "p", url: "https://x")
32 let m = KeycaskError.ambiguous(name: "gh", candidates: [a, b]).message
33 #expect(m.hasPrefix("gh: ambiguous, use an id:\n"))
34 #expect(m.contains("aaaa2222"))
35 #expect(m.contains("bbbb3333"))
36 #expect(m.contains("https://x"))
37 }
38}
Tests/KeycaskCoreTests/VaultCodecTests.swift added +52
@@ -0,0 +1,52 @@
1import Foundation
2import Testing
3
4@testable import KeycaskCore
5
6@Suite struct VaultCodecTests {
7 @Test func roundTripsAndIsDeterministic() throws {
8 var v = Vault()
9 try v.add(
10 Entry(
11 id: EntryID("aaaa2222")!, name: "gh", username: "cmc", password: "p",
12 url: "https://github.com", notes: "n", tags: ["dev"],
13 now: Date(timeIntervalSince1970: 1_700_000_000)))
14 let a = try VaultCodec.encode(v)
15 let b = try VaultCodec.encode(v)
16 #expect(a == b)
17 #expect(try VaultCodec.decode(a) == v)
18 }
19
20 @Test func datesAreISO8601WholeSeconds() throws {
21 var v = Vault()
22 try v.add(
23 Entry(
24 id: EntryID("aaaa2222")!, name: "gh", password: "p",
25 now: Date(timeIntervalSince1970: 1_700_000_000)))
26 let text = String(decoding: try VaultCodec.encode(v), as: UTF8.self)
27 #expect(text.contains("\"created\":\"2023-11-14T22:13:20Z\""))
28 }
29
30 @Test func keysAreSorted() throws {
31 var v = Vault()
32 try v.add(Entry(id: EntryID("aaaa2222")!, name: "gh", password: "p"))
33 let text = String(decoding: try VaultCodec.encode(v), as: UTF8.self)
34 let created = text.range(of: "\"created\"")!.lowerBound
35 let id = text.range(of: "\"id\"")!.lowerBound
36 let updated = text.range(of: "\"updated\"")!.lowerBound
37 #expect(created < id && id < updated)
38 }
39
40 @Test func garbageIsCorrupt() {
41 #expect(throws: KeycaskError.self) { try VaultCodec.decode(Data("nope".utf8)) }
42 do {
43 _ = try VaultCodec.decode(Data("{\"entries\":[{\"id\":1}]}".utf8))
44 Issue.record("expected corrupt")
45 } catch let e as KeycaskError {
46 #expect(e.exitCode == 1)
47 #expect(e.message.hasPrefix("vault is corrupt:"))
48 } catch {
49 Issue.record("wrong error \(error)")
50 }
51 }
52}
Tests/KeycaskCoreTests/VaultTests.swift added +88
@@ -0,0 +1,88 @@
1import Foundation
2import Testing
3
4@testable import KeycaskCore
5
6@Suite struct VaultTests {
7 func idA() -> EntryID { EntryID("aaaa2222")! }
8 func idB() -> EntryID { EntryID("bbbb3333")! }
9
10 @Test func addRejectsDuplicateID() throws {
11 var v = Vault()
12 try v.add(Entry(id: idA(), name: "gh", password: "p"))
13 #expect(throws: KeycaskError.duplicateID(idA())) {
14 try v.add(Entry(id: idA(), name: "other", password: "p"))
15 }
16 #expect(v.entries.count == 1)
17 }
18
19 @Test func removeUnknownIsNotFound() {
20 var v = Vault()
21 #expect(throws: KeycaskError.notFound("aaaa2222")) { try v.remove(id: idA()) }
22 }
23
24 @Test func updateSetsUpdatedAndNormalizesTags() throws {
25 let t0 = Date(timeIntervalSince1970: 1_000)
26 let t1 = Date(timeIntervalSince1970: 2_000.9)
27 var v = Vault()
28 try v.add(Entry(id: idA(), name: "gh", password: "p", now: t0))
29 try v.update(id: idA(), now: t1) { e in
30 e.tags = ["z", "A", "a"]
31 e.password = "q"
32 }
33 let e = v.entry(id: idA())!
34 #expect(e.password == "q")
35 #expect(e.tags == ["A", "z"])
36 #expect(e.created == t0)
37 #expect(e.updated == Date(timeIntervalSince1970: 2_000))
38 }
39
40 @Test func resolvePrefersIDThenUniqueName() throws {
41 var v = Vault()
42 try v.add(Entry(id: idA(), name: "gh", password: "p"))
43 try v.add(Entry(id: idB(), name: "aaaa2222", password: "p"))
44 #expect(try v.resolve("aaaa2222").id == idA())
45 #expect(try v.resolve("gh").id == idA())
46 #expect(try v.resolve("bbbb3333").id == idB())
47 }
48
49 @Test func resolveReportsAmbiguousWithAllCandidates() throws {
50 var v = Vault()
51 let a = Entry(id: idA(), name: "gh", password: "p")
52 let b = Entry(id: idB(), name: "gh", password: "p")
53 try v.add(a)
54 try v.add(b)
55 #expect(throws: KeycaskError.ambiguous(name: "gh", candidates: [a, b])) {
56 try v.resolve("gh")
57 }
58 }
59
60 @Test func resolveUnknownIsNotFound() {
61 #expect(throws: KeycaskError.notFound("nope")) { try Vault().resolve("nope") }
62 }
63
64 @Test func filterAndSearch() throws {
65 var v = Vault()
66 try v.add(Entry(id: idA(), name: "GitHub", password: "p", tags: ["dev"]))
67 try v.add(Entry(id: idB(), name: "bank", password: "p", url: "https://bank.example"))
68 #expect(v.filter(tag: "DEV").map(\.id) == [idA()])
69 #expect(v.search("example").map(\.id) == [idB()])
70 #expect(v.search("zzz").isEmpty)
71 }
72
73 @Test func sortedEntriesOrderByNameThenID() throws {
74 var v = Vault()
75 try v.add(Entry(id: idB(), name: "gh", password: "p"))
76 try v.add(Entry(id: idA(), name: "gh", password: "p"))
77 try v.add(Entry(id: EntryID("cccc4444")!, name: "Alpha", password: "p"))
78 #expect(v.sortedEntries.map(\.id.rawValue) == ["cccc4444", "aaaa2222", "bbbb3333"])
79 }
80
81 @Test func addNormalizesTagsSetAfterInit() throws {
82 var e = Entry(id: idA(), name: "gh", password: "p")
83 e.tags = ["z", "A", "a", " b "]
84 var v = Vault()
85 try v.add(e)
86 #expect(v.entry(id: idA())!.tags == ["A", "b", "z"])
87 }
88}
docs/superpowers/plans/2026-09-17-core-cli.md added +3337
@@ -0,0 +1,3337 @@
1# keycask core + CLI Implementation Plan
2
3> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
5**Goal:** A Swift package with `KeycaskCore` (vault model, passphrase-encrypted envelope, generator, resolution) and a `keycask` CLI that behaves identically on macOS, Linux, and Windows, covered by in-process and black-box tests.
6
7**Architecture:** `KeycaskCore` depends on Foundation and swift-crypto only and holds every rule about entries, IDs, encryption, and lookup. The `keycask` executable wraps it with ArgumentParser commands and a small platform layer (paths, terminal, atomic write, clipboard). Tests in `KeycaskCoreTests` run in process; tests in `KeycaskCLITests` spawn the built binary and check stdout, stderr, and exit codes.
8
9**Tech Stack:** Swift 6.4, SwiftPM, Swift Testing, swift-crypto 3.15 (`Crypto`, `_CryptoExtras`), swift-argument-parser 1.8.
10
11**Spec:** `docs/superpowers/specs/2026-09-17-keycask-design.md`
12
13## Global Constraints
14
15- `// swift-tools-version:6.4`, Swift 6 language mode, `platforms: [.macOS(.v14), .iOS(.v17)]`.
16- Dependencies are exactly `apple/swift-crypto` and `apple/swift-argument-parser`. `Package.resolved` is committed.
17- `KeycaskCore` imports only `Foundation`, `Crypto`, and `_CryptoExtras`. It never imports ArgumentParser, never spawns a process, never reads the environment.
18- Envelope: `format` 1, `kdf.name` `"pbkdf2-hmac-sha256"`, 600000 iterations, 16-byte salt, ChaCha20-Poly1305 combined box, key 32 bytes, passphrase NFC-normalized UTF-8.
19- `EntryID`: 8 characters from `abcdefghijkmnpqrstuvwxyz23456789`.
20- Dates: ISO 8601 UTC, whole seconds. JSON: sorted keys.
21- Exit codes: 0 ok, 1 failure, 2 usage, 3 not found, 4 cannot decrypt, 5 ambiguous.
22- Masked secret string is exactly `********`.
23- Clipboard timeout is 45 seconds. Password default length 24. Passphrase separator `-`.
24- No code comments that mention the history of the project or how it used to work. No attribution trailers in commits.
25- Every file passes `swift format lint --strict`.
26- Work happens on a branch. Tasks 1-8 go on branch `core`, merged through one MR. Tasks 9-15 go on branch `cli`, merged through a second MR. Never commit to `main` directly.
27
28## File structure
29
30```
31Package.swift
32Package.resolved
33NOTICE
34.gitbay/ci.yml
35Sources/KeycaskCore/
36 KeycaskError.swift error enum, exit codes, messages
37 EntryID.swift 8-char random ID, Codable as a string
38 Entry.swift entry struct, tag normalization, second-truncated dates
39 Vault.swift entries, add/remove/update, resolve, filter, search
40 VaultCodec.swift deterministic JSON encode/decode of Vault
41 Envelope.swift file envelope, PBKDF2 + ChaChaPoly seal/open, parse/encode
42 Generator.swift random password and passphrase
43 Wordlist.swift EFF long list as one string literal (generated file)
44Sources/keycask/
45 main.swift parse, run, map errors to exit codes
46 Keycask.swift root command, GlobalOptions
47 Paths.swift vault path resolution
48 Terminal.swift isatty, echo-off line read, y/N confirm
49 Passphrase.swift env var or prompt
50 AtomicFile.swift temp + fsync + rename, 0600 on Unix, MoveFileExW on Windows
51 OpenVault.swift load/save/create: ties paths, passphrase, envelope, codec, atomic write
52 Output.swift text, table, JSON, masking, --field
53 Clipboard.swift tool discovery, read/write, daemon handoff
54 Commands/Init.swift
55 Commands/Add.swift
56 Commands/Show.swift
57 Commands/Ls.swift
58 Commands/Find.swift
59 Commands/Edit.swift
60 Commands/Rm.swift
61 Commands/Generate.swift
62 Commands/Clip.swift
63 Commands/ClipboardDaemon.swift
64Tests/KeycaskCoreTests/
65 EntryIDTests.swift
66 EntryTests.swift
67 VaultTests.swift
68 VaultCodecTests.swift
69 EnvelopeTests.swift
70 GeneratorTests.swift
71 KeycaskErrorTests.swift
72Tests/KeycaskCLITests/
73 CLI.swift harness: locate binary, temp vault, run with env
74 InitTests.swift
75 AddShowTests.swift
76 LsFindTests.swift
77 EditRmTests.swift
78 GenerateTests.swift
79 ClipboardTests.swift
80 PathsTests.swift
81```
82
83---
84
85### Task 1: Package scaffold and CI
86
87**Files:**
88- Create: `Package.swift`
89- Create: `Sources/KeycaskCore/KeycaskCore.swift` (temporary, deleted in Task 2)
90- Create: `Sources/keycask/main.swift` (replaced in Task 9)
91- Create: `Tests/KeycaskCoreTests/SmokeTests.swift` (deleted in Task 2)
92- Create: `.gitbay/ci.yml`
93- Create: `.swift-format`
94
95**Interfaces:**
96- Produces: the package layout every later task adds files to.
97
98- [ ] **Step 1: Create the branch**
99
100```bash
101cd /Users/cmc/git/krz/keycask && git switch -c core
102```
103
104- [ ] **Step 2: Write Package.swift**
105
106```swift
107// swift-tools-version:6.4
108import PackageDescription
109
110let package = Package(
111 name: "keycask",
112 platforms: [.macOS(.v14), .iOS(.v17)],
113 products: [
114 .library(name: "KeycaskCore", targets: ["KeycaskCore"]),
115 .executable(name: "keycask", targets: ["keycask"]),
116 ],
117 dependencies: [
118 .package(url: "https://github.com/apple/swift-crypto", from: "3.15.0"),
119 .package(url: "https://github.com/apple/swift-argument-parser", from: "1.8.0"),
120 ],
121 targets: [
122 .target(
123 name: "KeycaskCore",
124 dependencies: [
125 .product(name: "Crypto", package: "swift-crypto"),
126 .product(name: "_CryptoExtras", package: "swift-crypto"),
127 ]
128 ),
129 .executableTarget(
130 name: "keycask",
131 dependencies: [
132 "KeycaskCore",
133 .product(name: "ArgumentParser", package: "swift-argument-parser"),
134 ]
135 ),
136 .testTarget(name: "KeycaskCoreTests", dependencies: ["KeycaskCore"]),
137 .testTarget(name: "KeycaskCLITests", dependencies: ["keycask"]),
138 ]
139)
140```
141
142- [ ] **Step 3: Write placeholder sources so the package builds**
143
144`Sources/KeycaskCore/KeycaskCore.swift`:
145
146```swift
147public enum KeycaskCore {
148 public static let name = "keycask"
149}
150```
151
152`Sources/keycask/main.swift`:
153
154```swift
155import KeycaskCore
156
157print(KeycaskCore.name)
158```
159
160`Tests/KeycaskCoreTests/SmokeTests.swift`:
161
162```swift
163import Testing
164
165@testable import KeycaskCore
166
167@Test func packageBuilds() {
168 #expect(KeycaskCore.name == "keycask")
169}
170```
171
172`Tests/KeycaskCLITests/CLI.swift` (a real file, extended in Task 9; this version only locates the binary):
173
174```swift
175import Foundation
176import Testing
177
178enum Binary {
179 static let url: URL = {
180 #if os(macOS)
181 for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
182 return bundle.bundleURL.deletingLastPathComponent().appendingPathComponent("keycask")
183 }
184 fatalError("test bundle not found")
185 #elseif os(Windows)
186 return Bundle.main.bundleURL.appendingPathComponent("keycask.exe")
187 #else
188 return Bundle.main.bundleURL.appendingPathComponent("keycask")
189 #endif
190 }()
191}
192
193@Test func binaryIsBuilt() {
194 #expect(FileManager.default.isExecutableFile(atPath: Binary.url.path))
195}
196```
197
198- [ ] **Step 4: Write .swift-format**
199
200```json
201{
202 "version": 1,
203 "indentation": { "spaces": 4 },
204 "lineLength": 100,
205 "maximumBlankLines": 1,
206 "respectsExistingLineBreaks": true,
207 "rules": {
208 "AlwaysUseLowerCamelCase": true,
209 "NeverForceUnwrap": false,
210 "NeverUseImplicitlyUnwrappedOptionals": true
211 }
212}
213```
214
215- [ ] **Step 5: Build and test**
216
217Run: `swift build && swift test`
218Expected: `Build complete`, two tests pass. `Package.resolved` now exists.
219
220- [ ] **Step 6: Lint**
221
222Run: `swift format lint --strict --recursive Sources Tests Package.swift`
223Expected: no output. If it reports findings, run `swift format --in-place --recursive Sources Tests Package.swift` and re-lint.
224
225- [ ] **Step 7: Write .gitbay/ci.yml**
226
227```yaml
228# Each step runs in its own `sh -c`; exports do not survive between steps.
229# swiftly installs into $HOME, which persists across builds.
230jobs:
231 build:
232 steps:
233 - |
234 set -eu
235 command -v curl >/dev/null || { echo "runner is missing: curl"; exit 1; }
236 if ! command -v "$HOME/.local/bin/swiftly" >/dev/null 2>&1; then
237 curl -fsSL "https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz" | tar -xz -C /tmp
238 /tmp/swiftly init --assume-yes --skip-install --quiet-shell-followup
239 fi
240 . "$HOME/.local/share/swiftly/env.sh"
241 swiftly install --use 6.4
242 swift format lint --strict --recursive Sources Tests Package.swift
243 swift build
244 test:
245 steps:
246 - |
247 set -eu
248 . "$HOME/.local/share/swiftly/env.sh"
249 swiftly install --use 6.4
250 swift test
251 paths-ignore:
252 - docs/**
253```
254
255- [ ] **Step 8: Commit**
256
257```bash
258git add Package.swift Package.resolved .swift-format .gitbay/ci.yml Sources Tests
259git commit -m "Add package scaffold and CI"
260```
261
262---
263
264### Task 2: KeycaskError
265
266**Files:**
267- Create: `Sources/KeycaskCore/KeycaskError.swift`
268- Create: `Tests/KeycaskCoreTests/KeycaskErrorTests.swift`
269- Delete: `Sources/KeycaskCore/KeycaskCore.swift`, `Tests/KeycaskCoreTests/SmokeTests.swift`
270
271**Interfaces:**
272- Produces: `public enum KeycaskError: Error, Equatable, Sendable` with cases `notFound(String)`, `ambiguous(name: String, candidates: [Entry])`, `cannotDecrypt`, `corrupt(String)`, `vaultExists(String)`, `noVault(String)`, `duplicateID(EntryID)`, `io(String)`, `usage(String)`, `failure(String)`; properties `exitCode: Int32`, `message: String`.
273- Note: `Entry` and `EntryID` do not exist yet. Write this task with `ambiguous(name: String, candidates: [String])` and `duplicateID(String)` and change them to the real types in Tasks 3 and 4.
274
275- [ ] **Step 1: Delete placeholders**
276
277```bash
278git rm -q Sources/KeycaskCore/KeycaskCore.swift Tests/KeycaskCoreTests/SmokeTests.swift
279```
280
281- [ ] **Step 2: Write the failing test**
282
283`Tests/KeycaskCoreTests/KeycaskErrorTests.swift`:
284
285```swift
286import Testing
287
288@testable import KeycaskCore
289
290@Suite struct KeycaskErrorTests {
291 @Test func exitCodesFollowTheSpec() {
292 #expect(KeycaskError.failure("x").exitCode == 1)
293 #expect(KeycaskError.io("x").exitCode == 1)
294 #expect(KeycaskError.corrupt("x").exitCode == 1)
295 #expect(KeycaskError.vaultExists("x").exitCode == 1)
296 #expect(KeycaskError.duplicateID("abcd2345").exitCode == 1)
297 #expect(KeycaskError.usage("x").exitCode == 2)
298 #expect(KeycaskError.notFound("x").exitCode == 3)
299 #expect(KeycaskError.noVault("/p").exitCode == 3)
300 #expect(KeycaskError.cannotDecrypt.exitCode == 4)
301 #expect(KeycaskError.ambiguous(name: "gh", candidates: []).exitCode == 5)
302 }
303
304 @Test func messagesNameTheSubject() {
305 #expect(KeycaskError.notFound("gh").message == "gh: not found")
306 #expect(KeycaskError.noVault("/v").message == "vault /v not found (run `keycask init`)")
307 #expect(KeycaskError.vaultExists("/v").message == "vault /v already exists")
308 #expect(KeycaskError.cannotDecrypt.message == "cannot decrypt: wrong passphrase or damaged vault")
309 #expect(KeycaskError.corrupt("bad json").message == "vault is corrupt: bad json")
310 }
311}
312```
313
314- [ ] **Step 3: Run test to verify it fails**
315
316Run: `swift test --filter KeycaskErrorTests`
317Expected: compile error, `KeycaskError` not found.
318
319- [ ] **Step 4: Write the implementation**
320
321`Sources/KeycaskCore/KeycaskError.swift`:
322
323```swift
324public enum KeycaskError: Error, Equatable, Sendable {
325 case notFound(String)
326 case ambiguous(name: String, candidates: [String])
327 case cannotDecrypt
328 case corrupt(String)
329 case vaultExists(String)
330 case noVault(String)
331 case duplicateID(String)
332 case io(String)
333 case usage(String)
334 case failure(String)
335
336 public var exitCode: Int32 {
337 switch self {
338 case .failure, .io, .corrupt, .vaultExists, .duplicateID: 1
339 case .usage: 2
340 case .notFound, .noVault: 3
341 case .cannotDecrypt: 4
342 case .ambiguous: 5
343 }
344 }
345
346 public var message: String {
347 switch self {
348 case .notFound(let what): "\(what): not found"
349 case .ambiguous(let name, let candidates):
350 (["\(name): ambiguous, use an id:"] + candidates).joined(separator: "\n")
351 case .cannotDecrypt: "cannot decrypt: wrong passphrase or damaged vault"
352 case .corrupt(let why): "vault is corrupt: \(why)"
353 case .vaultExists(let path): "vault \(path) already exists"
354 case .noVault(let path): "vault \(path) not found (run `keycask init`)"
355 case .duplicateID(let id): "duplicate id \(id)"
356 case .io(let why): why
357 case .usage(let why): why
358 case .failure(let why): why
359 }
360 }
361}
362```
363
364- [ ] **Step 5: Run tests**
365
366Run: `swift test --filter KeycaskErrorTests`
367Expected: 2 tests pass.
368
369- [ ] **Step 6: Commit**
370
371```bash
372git add -A Sources/KeycaskCore Tests/KeycaskCoreTests
373git commit -m "Add KeycaskError with exit codes"
374```
375
376---
377
378### Task 3: EntryID
379
380**Files:**
381- Create: `Sources/KeycaskCore/EntryID.swift`
382- Create: `Tests/KeycaskCoreTests/EntryIDTests.swift`
383- Modify: `Sources/KeycaskCore/KeycaskError.swift` (`duplicateID(EntryID)`)
384- Modify: `Tests/KeycaskCoreTests/KeycaskErrorTests.swift`
385
386**Interfaces:**
387- Produces: `public struct EntryID: Hashable, Sendable, Codable, CustomStringConvertible` with `static let alphabet: [Character]`, `static let length = 8`, `let rawValue: String`, `init?(_ raw: String)`, `static func random() -> EntryID`, `static func random(using: inout some RandomNumberGenerator) -> EntryID`. Codable as a bare JSON string.
388
389- [ ] **Step 1: Write the failing test**
390
391`Tests/KeycaskCoreTests/EntryIDTests.swift`:
392
393```swift
394import Foundation
395import Testing
396
397@testable import KeycaskCore
398
399@Suite struct EntryIDTests {
400 @Test func randomIDsHaveLengthEightFromTheAlphabet() {
401 let allowed = Set(EntryID.alphabet)
402 for _ in 0..<200 {
403 let id = EntryID.random()
404 #expect(id.rawValue.count == 8)
405 #expect(id.rawValue.allSatisfy { allowed.contains($0) })
406 }
407 }
408
409 @Test func alphabetExcludesAmbiguousCharacters() {
410 let alphabet = Set(EntryID.alphabet)
411 #expect(alphabet.count == 32)
412 for bad in ["l", "o", "0", "1"] {
413 #expect(!alphabet.contains(Character(bad)))
414 }
415 }
416
417 @Test func parsingValidatesLengthAndAlphabet() {
418 #expect(EntryID("abcd2345") != nil)
419 #expect(EntryID("abcd234") == nil)
420 #expect(EntryID("abcd23456") == nil)
421 #expect(EntryID("abcd234l") == nil)
422 #expect(EntryID("ABCD2345") == nil)
423 }
424
425 @Test func codableIsABareString() throws {
426 let id = EntryID("abcd2345")!
427 let data = try JSONEncoder().encode([id])
428 #expect(String(decoding: data, as: UTF8.self) == "[\"abcd2345\"]")
429 let back = try JSONDecoder().decode([EntryID].self, from: data)
430 #expect(back == [id])
431 #expect(throws: DecodingError.self) {
432 try JSONDecoder().decode([EntryID].self, from: Data("[\"bad\"]".utf8))
433 }
434 }
435
436 @Test func seededGeneratorIsDeterministic() {
437 struct Counter: RandomNumberGenerator {
438 var n: UInt64 = 0
439 mutating func next() -> UInt64 {
440 n += 1
441 return n
442 }
443 }
444 var a = Counter()
445 var b = Counter()
446 #expect(EntryID.random(using: &a) == EntryID.random(using: &b))
447 }
448}
449```
450
451- [ ] **Step 2: Run test to verify it fails**
452
453Run: `swift test --filter EntryIDTests`
454Expected: compile error, `EntryID` not found.
455
456- [ ] **Step 3: Write the implementation**
457
458`Sources/KeycaskCore/EntryID.swift`:
459
460```swift
461public struct EntryID: Hashable, Sendable, CustomStringConvertible {
462 public static let alphabet: [Character] = Array("abcdefghijkmnpqrstuvwxyz23456789")
463 public static let length = 8
464
465 public let rawValue: String
466
467 public init?(_ raw: String) {
468 guard raw.count == Self.length else { return nil }
469 let allowed = Set(Self.alphabet)
470 guard raw.allSatisfy({ allowed.contains($0) }) else { return nil }
471 rawValue = raw
472 }
473
474 public static func random() -> EntryID {
475 var rng = SystemRandomNumberGenerator()
476 return random(using: &rng)
477 }
478
479 public static func random(using rng: inout some RandomNumberGenerator) -> EntryID {
480 var chars: [Character] = []
481 chars.reserveCapacity(length)
482 for _ in 0..<length {
483 chars.append(alphabet[Int(rng.next(upperBound: UInt32(alphabet.count)))])
484 }
485 return EntryID(String(chars))!
486 }
487
488 public var description: String { rawValue }
489}
490
491extension EntryID: Codable {
492 public init(from decoder: any Decoder) throws {
493 let raw = try decoder.singleValueContainer().decode(String.self)
494 guard let id = EntryID(raw) else {
495 throw DecodingError.dataCorrupted(
496 .init(codingPath: decoder.codingPath, debugDescription: "invalid entry id \(raw)"))
497 }
498 self = id
499 }
500
501 public func encode(to encoder: any Encoder) throws {
502 var container = encoder.singleValueContainer()
503 try container.encode(rawValue)
504 }
505}
506```
507
508- [ ] **Step 4: Switch `duplicateID` to the real type**
509
510In `KeycaskError.swift` change `case duplicateID(String)` to `case duplicateID(EntryID)` and the message to `"duplicate id \(id.rawValue)"`. In `KeycaskErrorTests.swift` change `.duplicateID("abcd2345")` to `.duplicateID(EntryID("abcd2345")!)`.
511
512- [ ] **Step 5: Run tests**
513
514Run: `swift test --filter 'EntryIDTests|KeycaskErrorTests'`
515Expected: 7 tests pass.
516
517- [ ] **Step 6: Commit**
518
519```bash
520git add Sources/KeycaskCore Tests/KeycaskCoreTests
521git commit -m "Add EntryID"
522```
523
524---
525
526### Task 4: Entry
527
528**Files:**
529- Create: `Sources/KeycaskCore/Entry.swift`
530- Create: `Tests/KeycaskCoreTests/EntryTests.swift`
531- Modify: `Sources/KeycaskCore/KeycaskError.swift` (`ambiguous(name:candidates: [Entry])`)
532- Modify: `Tests/KeycaskCoreTests/KeycaskErrorTests.swift`
533
534**Interfaces:**
535- Consumes: `EntryID`.
536- Produces:
537
538```swift
539public struct Entry: Codable, Equatable, Sendable {
540 public let id: EntryID
541 public var name: String
542 public var username: String?
543 public var password: String
544 public var url: String?
545 public var notes: String?
546 public var tags: [String]
547 public let created: Date
548 public var updated: Date
549
550 public init(id: EntryID = .random(), name: String, username: String? = nil,
551 password: String, url: String? = nil, notes: String? = nil,
552 tags: [String] = [], now: Date = .now)
553 public static func normalize(tags: [String]) -> [String]
554 public static func truncateToSeconds(_ date: Date) -> Date
555 public func hasTag(_ tag: String) -> Bool
556 public func matches(_ query: String) -> Bool
557}
558```
559
560- [ ] **Step 1: Write the failing test**
561
562`Tests/KeycaskCoreTests/EntryTests.swift`:
563
564```swift
565import Foundation
566import Testing
567
568@testable import KeycaskCore
569
570@Suite struct EntryTests {
571 @Test func initNormalizesTagsAndTruncatesDates() {
572 let now = Date(timeIntervalSince1970: 1_700_000_000.75)
573 let e = Entry(name: "gh", password: "p", tags: [" work", "Dev", "dev", "", "alpha"], now: now)
574 #expect(e.tags == ["alpha", "Dev", "work"])
575 #expect(e.created == Date(timeIntervalSince1970: 1_700_000_000))
576 #expect(e.updated == e.created)
577 }
578
579 @Test func normalizeSortsCaseInsensitivelyAndKeepsFirstSpelling() {
580 #expect(Entry.normalize(tags: ["b", "A", "a", "B"]) == ["A", "b"])
581 #expect(Entry.normalize(tags: []) == [])
582 }
583
584 @Test func hasTagIsCaseInsensitive() {
585 let e = Entry(name: "gh", password: "p", tags: ["Dev"])
586 #expect(e.hasTag("dev"))
587 #expect(e.hasTag("DEV"))
588 #expect(!e.hasTag("ops"))
589 }
590
591 @Test func matchesSearchesEveryTextFieldExceptPassword() {
592 let e = Entry(
593 name: "GitHub", username: "cmc", password: "hunter2", url: "https://github.com",
594 notes: "downtown office", tags: ["Dev"])
595 #expect(e.matches("github"))
596 #expect(e.matches("CMC"))
597 #expect(e.matches("github.com"))
598 #expect(e.matches("downtown"))
599 #expect(e.matches("dev"))
600 #expect(!e.matches("hunter2"))
601 #expect(!e.matches("nothing"))
602 }
603}
604```
605
606- [ ] **Step 2: Run test to verify it fails**
607
608Run: `swift test --filter EntryTests`
609Expected: compile error, `Entry` not found.
610
611- [ ] **Step 3: Write the implementation**
612
613`Sources/KeycaskCore/Entry.swift`:
614
615```swift
616import Foundation
617
618public struct Entry: Codable, Equatable, Sendable {
619 public let id: EntryID
620 public var name: String
621 public var username: String?
622 public var password: String
623 public var url: String?
624 public var notes: String?
625 public var tags: [String]
626 public let created: Date
627 public var updated: Date
628
629 public init(
630 id: EntryID = .random(),
631 name: String,
632 username: String? = nil,
633 password: String,
634 url: String? = nil,
635 notes: String? = nil,
636 tags: [String] = [],
637 now: Date = .now
638 ) {
639 self.id = id
640 self.name = name
641 self.username = username
642 self.password = password
643 self.url = url
644 self.notes = notes
645 self.tags = Self.normalize(tags: tags)
646 let stamp = Self.truncateToSeconds(now)
647 created = stamp
648 updated = stamp
649 }
650
651 public static func normalize(tags: [String]) -> [String] {
652 var seen: Set<String> = []
653 var out: [String] = []
654 for raw in tags {
655 let tag = raw.trimmingCharacters(in: .whitespaces)
656 guard !tag.isEmpty, seen.insert(tag.lowercased()).inserted else { continue }
657 out.append(tag)
658 }
659 return out.sorted { a, b in
660 let (la, lb) = (a.lowercased(), b.lowercased())
661 return la == lb ? a < b : la < lb
662 }
663 }
664
665 public static func truncateToSeconds(_ date: Date) -> Date {
666 Date(timeIntervalSince1970: date.timeIntervalSince1970.rounded(.down))
667 }
668
669 public func hasTag(_ tag: String) -> Bool {
670 let needle = tag.lowercased()
671 return tags.contains { $0.lowercased() == needle }
672 }
673
674 public func matches(_ query: String) -> Bool {
675 let needle = query.lowercased()
676 guard !needle.isEmpty else { return false }
677 let haystacks = [name, username ?? "", url ?? "", notes ?? ""] + tags
678 return haystacks.contains { $0.lowercased().contains(needle) }
679 }
680}
681```
682
683- [ ] **Step 4: Switch `ambiguous` to carry entries**
684
685In `KeycaskError.swift` change the case to `case ambiguous(name: String, candidates: [Entry])` and the message body to:
686
687```swift
688case .ambiguous(let name, let candidates):
689 (["\(name): ambiguous, use an id:"]
690 + candidates.map { " \($0.id.rawValue) \($0.username ?? "") \($0.url ?? "")" })
691 .joined(separator: "\n")
692```
693
694`KeycaskErrorTests.swift` already passes `candidates: []`, which now infers `[Entry]`. Add one test there:
695
696```swift
697@Test func ambiguousListsCandidateIDs() {
698 let a = Entry(id: EntryID("aaaa2222")!, name: "gh", username: "one", password: "p")
699 let b = Entry(id: EntryID("bbbb3333")!, name: "gh", password: "p", url: "https://x")
700 let m = KeycaskError.ambiguous(name: "gh", candidates: [a, b]).message
701 #expect(m.hasPrefix("gh: ambiguous, use an id:\n"))
702 #expect(m.contains("aaaa2222"))
703 #expect(m.contains("bbbb3333"))
704 #expect(m.contains("https://x"))
705}
706```
707
708- [ ] **Step 5: Run tests**
709
710Run: `swift test --filter 'EntryTests|KeycaskErrorTests'`
711Expected: all pass.
712
713- [ ] **Step 6: Commit**
714
715```bash
716git add Sources/KeycaskCore Tests/KeycaskCoreTests
717git commit -m "Add Entry with tag normalization and search"
718```
719
720---
721
722### Task 5: Vault and VaultCodec
723
724**Files:**
725- Create: `Sources/KeycaskCore/Vault.swift`
726- Create: `Sources/KeycaskCore/VaultCodec.swift`
727- Create: `Tests/KeycaskCoreTests/VaultTests.swift`
728- Create: `Tests/KeycaskCoreTests/VaultCodecTests.swift`
729
730**Interfaces:**
731- Consumes: `Entry`, `EntryID`, `KeycaskError`.
732- Produces:
733
734```swift
735public struct Vault: Codable, Equatable, Sendable {
736 public var entries: [Entry]
737 public init(entries: [Entry] = [])
738 public func entry(id: EntryID) -> Entry?
739 public mutating func add(_ entry: Entry) throws // duplicateID
740 public mutating func remove(id: EntryID) throws // notFound(id)
741 public mutating func update(id: EntryID, now: Date = .now,
742 _ change: (inout Entry) -> Void) throws // notFound(id); normalizes tags, sets updated
743 public func resolve(_ ref: String) throws -> Entry // id, unique name, ambiguous, notFound
744 public func filter(tag: String) -> [Entry]
745 public func search(_ query: String) -> [Entry]
746 public var sortedEntries: [Entry] // by name (case-insensitive), then id
747}
748
749public enum VaultCodec {
750 public static func encode(_ vault: Vault) throws -> Data // sortedKeys, iso8601; io on failure
751 public static func decode(_ data: Data) throws -> Vault // corrupt on failure
752 public static func makeEncoder() -> JSONEncoder // shared settings, also used by CLI output
753}
754```
755
756- [ ] **Step 1: Write the failing tests**
757
758`Tests/KeycaskCoreTests/VaultTests.swift`:
759
760```swift
761import Foundation
762import Testing
763
764@testable import KeycaskCore
765
766@Suite struct VaultTests {
767 func idA() -> EntryID { EntryID("aaaa2222")! }
768 func idB() -> EntryID { EntryID("bbbb3333")! }
769
770 @Test func addRejectsDuplicateID() throws {
771 var v = Vault()
772 try v.add(Entry(id: idA(), name: "gh", password: "p"))
773 #expect(throws: KeycaskError.duplicateID(idA())) {
774 try v.add(Entry(id: idA(), name: "other", password: "p"))
775 }
776 #expect(v.entries.count == 1)
777 }
778
779 @Test func removeUnknownIsNotFound() {
780 var v = Vault()
781 #expect(throws: KeycaskError.notFound("aaaa2222")) { try v.remove(id: idA()) }
782 }
783
784 @Test func updateSetsUpdatedAndNormalizesTags() throws {
785 let t0 = Date(timeIntervalSince1970: 1_000)
786 let t1 = Date(timeIntervalSince1970: 2_000.9)
787 var v = Vault()
788 try v.add(Entry(id: idA(), name: "gh", password: "p", now: t0))
789 try v.update(id: idA(), now: t1) { e in
790 e.tags = ["z", "A", "a"]
791 e.password = "q"
792 }
793 let e = v.entry(id: idA())!
794 #expect(e.password == "q")
795 #expect(e.tags == ["A", "z"])
796 #expect(e.created == t0)
797 #expect(e.updated == Date(timeIntervalSince1970: 2_000))
798 }
799
800 @Test func resolvePrefersIDThenUniqueName() throws {
801 var v = Vault()
802 try v.add(Entry(id: idA(), name: "gh", password: "p"))
803 try v.add(Entry(id: idB(), name: "aaaa2222", password: "p"))
804 #expect(try v.resolve("aaaa2222").id == idA())
805 #expect(try v.resolve("gh").id == idA())
806 #expect(try v.resolve("bbbb3333").id == idB())
807 }
808
809 @Test func resolveReportsAmbiguousWithAllCandidates() throws {
810 var v = Vault()
811 let a = Entry(id: idA(), name: "gh", password: "p")
812 let b = Entry(id: idB(), name: "gh", password: "p")
813 try v.add(a)
814 try v.add(b)
815 #expect(throws: KeycaskError.ambiguous(name: "gh", candidates: [a, b])) {
816 try v.resolve("gh")
817 }
818 }
819
820 @Test func resolveUnknownIsNotFound() {
821 #expect(throws: KeycaskError.notFound("nope")) { try Vault().resolve("nope") }
822 }
823
824 @Test func filterAndSearch() throws {
825 var v = Vault()
826 try v.add(Entry(id: idA(), name: "GitHub", password: "p", tags: ["dev"]))
827 try v.add(Entry(id: idB(), name: "bank", password: "p", url: "https://bank.example"))
828 #expect(v.filter(tag: "DEV").map(\.id) == [idA()])
829 #expect(v.search("example").map(\.id) == [idB()])
830 #expect(v.search("zzz").isEmpty)
831 }
832
833 @Test func sortedEntriesOrderByNameThenID() throws {
834 var v = Vault()
835 try v.add(Entry(id: idB(), name: "gh", password: "p"))
836 try v.add(Entry(id: idA(), name: "gh", password: "p"))
837 try v.add(Entry(id: EntryID("cccc4444")!, name: "Alpha", password: "p"))
838 #expect(v.sortedEntries.map(\.id.rawValue) == ["cccc4444", "aaaa2222", "bbbb3333"])
839 }
840}
841```
842
843`Tests/KeycaskCoreTests/VaultCodecTests.swift`:
844
845```swift
846import Foundation
847import Testing
848
849@testable import KeycaskCore
850
851@Suite struct VaultCodecTests {
852 @Test func roundTripsAndIsDeterministic() throws {
853 var v = Vault()
854 try v.add(
855 Entry(
856 id: EntryID("aaaa2222")!, name: "gh", username: "cmc", password: "p",
857 url: "https://github.com", notes: "n", tags: ["dev"],
858 now: Date(timeIntervalSince1970: 1_700_000_000)))
859 let a = try VaultCodec.encode(v)
860 let b = try VaultCodec.encode(v)
861 #expect(a == b)
862 #expect(try VaultCodec.decode(a) == v)
863 }
864
865 @Test func datesAreISO8601WholeSeconds() throws {
866 var v = Vault()
867 try v.add(
868 Entry(id: EntryID("aaaa2222")!, name: "gh", password: "p",
869 now: Date(timeIntervalSince1970: 1_700_000_000)))
870 let text = String(decoding: try VaultCodec.encode(v), as: UTF8.self)
871 #expect(text.contains("\"created\":\"2023-11-14T22:13:20Z\""))
872 }
873
874 @Test func keysAreSorted() throws {
875 var v = Vault()
876 try v.add(Entry(id: EntryID("aaaa2222")!, name: "gh", password: "p"))
877 let text = String(decoding: try VaultCodec.encode(v), as: UTF8.self)
878 let created = text.range(of: "\"created\"")!.lowerBound
879 let id = text.range(of: "\"id\"")!.lowerBound
880 let updated = text.range(of: "\"updated\"")!.lowerBound
881 #expect(created < id && id < updated)
882 }
883
884 @Test func garbageIsCorrupt() {
885 #expect(throws: KeycaskError.self) { try VaultCodec.decode(Data("nope".utf8)) }
886 do {
887 _ = try VaultCodec.decode(Data("{\"entries\":[{\"id\":1}]}".utf8))
888 Issue.record("expected corrupt")
889 } catch let e as KeycaskError {
890 #expect(e.exitCode == 1)
891 #expect(e.message.hasPrefix("vault is corrupt:"))
892 } catch {
893 Issue.record("wrong error \(error)")
894 }
895 }
896}
897```
898
899- [ ] **Step 2: Run tests to verify they fail**
900
901Run: `swift test --filter 'VaultTests|VaultCodecTests'`
902Expected: compile error, `Vault` not found.
903
904- [ ] **Step 3: Write Vault.swift**
905
906```swift
907import Foundation
908
909public struct Vault: Codable, Equatable, Sendable {
910 public var entries: [Entry]
911
912 public init(entries: [Entry] = []) {
913 self.entries = entries
914 }
915
916 public func entry(id: EntryID) -> Entry? {
917 entries.first { $0.id == id }
918 }
919
920 public mutating func add(_ entry: Entry) throws {
921 guard self.entry(id: entry.id) == nil else { throw KeycaskError.duplicateID(entry.id) }
922 entries.append(entry)
923 }
924
925 public mutating func remove(id: EntryID) throws {
926 guard let index = entries.firstIndex(where: { $0.id == id }) else {
927 throw KeycaskError.notFound(id.rawValue)
928 }
929 entries.remove(at: index)
930 }
931
932 public mutating func update(
933 id: EntryID, now: Date = .now, _ change: (inout Entry) -> Void
934 ) throws {
935 guard let index = entries.firstIndex(where: { $0.id == id }) else {
936 throw KeycaskError.notFound(id.rawValue)
937 }
938 change(&entries[index])
939 entries[index].tags = Entry.normalize(tags: entries[index].tags)
940 entries[index].updated = Entry.truncateToSeconds(now)
941 }
942
943 public func resolve(_ ref: String) throws -> Entry {
944 if let id = EntryID(ref), let hit = entry(id: id) {
945 return hit
946 }
947 let byName = entries.filter { $0.name == ref }
948 switch byName.count {
949 case 0: throw KeycaskError.notFound(ref)
950 case 1: return byName[0]
951 default: throw KeycaskError.ambiguous(name: ref, candidates: byName)
952 }
953 }
954
955 public func filter(tag: String) -> [Entry] {
956 sortedEntries.filter { $0.hasTag(tag) }
957 }
958
959 public func search(_ query: String) -> [Entry] {
960 sortedEntries.filter { $0.matches(query) }
961 }
962
963 public var sortedEntries: [Entry] {
964 entries.sorted { a, b in
965 let (la, lb) = (a.name.lowercased(), b.name.lowercased())
966 return la == lb ? a.id.rawValue < b.id.rawValue : la < lb
967 }
968 }
969}
970```
971
972- [ ] **Step 4: Write VaultCodec.swift**
973
974```swift
975import Foundation
976
977public enum VaultCodec {
978 public static func makeEncoder() -> JSONEncoder {
979 let encoder = JSONEncoder()
980 encoder.outputFormatting = [.sortedKeys, .withoutEscapingSlashes]
981 encoder.dateEncodingStrategy = .iso8601
982 return encoder
983 }
984
985 public static func makeDecoder() -> JSONDecoder {
986 let decoder = JSONDecoder()
987 decoder.dateDecodingStrategy = .iso8601
988 return decoder
989 }
990
991 public static func encode(_ vault: Vault) throws -> Data {
992 do {
993 return try makeEncoder().encode(vault)
994 } catch {
995 throw KeycaskError.io("encode vault: \(error)")
996 }
997 }
998
999 public static func decode(_ data: Data) throws -> Vault {
1000 do {
1001 return try makeDecoder().decode(Vault.self, from: data)
1002 } catch {
1003 throw KeycaskError.corrupt("\(error)")
1004 }
1005 }
1006}
1007```
1008
1009- [ ] **Step 5: Run tests**
1010
1011Run: `swift test --filter 'VaultTests|VaultCodecTests'`
1012Expected: 12 tests pass.
1013
1014- [ ] **Step 6: Commit**
1015
1016```bash
1017git add Sources/KeycaskCore Tests/KeycaskCoreTests
1018git commit -m "Add Vault operations and deterministic JSON codec"
1019```
1020
1021---
1022
1023### Task 6: Generator and word list
1024
1025**Files:**
1026- Create: `Sources/KeycaskCore/Wordlist.swift` (generated)
1027- Create: `Sources/KeycaskCore/Generator.swift`
1028- Create: `NOTICE`
1029- Create: `Tests/KeycaskCoreTests/GeneratorTests.swift`
1030
1031**Interfaces:**
1032- Produces:
1033
1034```swift
1035public enum Wordlist { public static let words: [String] } // 7776 entries
1036public enum Generator {
1037 public static let alphabet: [Character] // A-Z a-z 0-9 and !@#$%^&*()-_=+[]{};:,.<>?
1038 public static let defaultLength = 24
1039 public static let wordSeparator = "-"
1040 public static func password(length: Int) -> String
1041 public static func password(length: Int, using: inout some RandomNumberGenerator) -> String
1042 public static func passphrase(words: Int) -> String
1043 public static func passphrase(words: Int, using: inout some RandomNumberGenerator) -> String
1044}
1045```
1046
1047- [ ] **Step 1: Generate Wordlist.swift from the EFF list**
1048
1049```bash
1050cd /Users/cmc/git/krz/keycask
1051curl -fsSL https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt -o /tmp/eff.txt
1052test "$(wc -l < /tmp/eff.txt)" -eq 7776
1053{
1054 printf '// EFF long word list, https://www.eff.org/dice. See NOTICE.\n'
1055 printf 'let effLongWordlist = """\n'
1056 cut -f2 /tmp/eff.txt
1057 printf '"""\n\npublic enum Wordlist {\n'
1058 printf ' public static let words: [String] = effLongWordlist.split(separator: "\\n").map(String.init)\n'
1059 printf '}\n'
1060} > Sources/KeycaskCore/Wordlist.swift
1061rm /tmp/eff.txt
1062```
1063
1064- [ ] **Step 2: Write NOTICE**
1065
1066```
1067The word list in Sources/KeycaskCore/Wordlist.swift is the EFF Long
1068Wordlist by the Electronic Frontier Foundation, licensed under the
1069Creative Commons Attribution 3.0 United States License.
1070https://www.eff.org/dice
1071https://creativecommons.org/licenses/by/3.0/us/
1072```
1073
1074- [ ] **Step 3: Write the failing test**
1075
1076`Tests/KeycaskCoreTests/GeneratorTests.swift`:
1077
1078```swift
1079import Testing
1080
1081@testable import KeycaskCore
1082
1083@Suite struct GeneratorTests {
1084 struct Counter: RandomNumberGenerator {
1085 var n: UInt64 = 0
1086 mutating func next() -> UInt64 {
1087 n &+= 0x9E37_79B9_7F4A_7C15
1088 return n
1089 }
1090 }
1091
1092 @Test func wordlistHas7776UniqueWords() {
1093 #expect(Wordlist.words.count == 7776)
1094 #expect(Set(Wordlist.words).count == 7776)
1095 #expect(Wordlist.words.first == "abacus")
1096 #expect(Wordlist.words.allSatisfy { !$0.isEmpty && !$0.contains(" ") })
1097 }
1098
1099 @Test func passwordHasRequestedLengthFromTheAlphabet() {
1100 let allowed = Set(Generator.alphabet)
1101 for length in [1, 8, 24, 64] {
1102 let p = Generator.password(length: length)
1103 #expect(p.count == length)
1104 #expect(p.allSatisfy { allowed.contains($0) })
1105 }
1106 #expect(Generator.password(length: 0) == "")
1107 }
1108
1109 @Test func alphabetCoversAllClasses() {
1110 let s = String(Generator.alphabet)
1111 #expect(s.contains("A") && s.contains("z") && s.contains("7") && s.contains("!"))
1112 #expect(Set(Generator.alphabet).count == Generator.alphabet.count)
1113 }
1114
1115 @Test func passphraseUsesWordsFromTheList() {
1116 let words = Set(Wordlist.words)
1117 let p = Generator.passphrase(words: 5)
1118 let parts = p.split(separator: "-").map(String.init)
1119 #expect(parts.count == 5)
1120 #expect(parts.allSatisfy { words.contains($0) })
1121 #expect(Generator.passphrase(words: 0) == "")
1122 }
1123
1124 @Test func seededOutputIsReproducible() {
1125 var a = Counter()
1126 var b = Counter()
1127 #expect(Generator.password(length: 16, using: &a) == Generator.password(length: 16, using: &b))
1128 #expect(Generator.passphrase(words: 3, using: &a) == Generator.passphrase(words: 3, using: &b))
1129 }
1130}
1131```
1132
1133- [ ] **Step 4: Run test to verify it fails**
1134
1135Run: `swift test --filter GeneratorTests`
1136Expected: compile error, `Generator` not found.
1137
1138- [ ] **Step 5: Write Generator.swift**
1139
1140```swift
1141public enum Generator {
1142 public static let alphabet: [Character] = Array(
1143 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{};:,.<>?"
1144 )
1145 public static let defaultLength = 24
1146 public static let wordSeparator = "-"
1147
1148 public static func password(length: Int) -> String {
1149 var rng = SystemRandomNumberGenerator()
1150 return password(length: length, using: &rng)
1151 }
1152
1153 public static func password(length: Int, using rng: inout some RandomNumberGenerator) -> String {
1154 var chars: [Character] = []
1155 chars.reserveCapacity(max(length, 0))
1156 for _ in 0..<max(length, 0) {
1157 chars.append(alphabet[Int(rng.next(upperBound: UInt32(alphabet.count)))])
1158 }
1159 return String(chars)
1160 }
1161
1162 public static func passphrase(words: Int) -> String {
1163 var rng = SystemRandomNumberGenerator()
1164 return passphrase(words: words, using: &rng)
1165 }
1166
1167 public static func passphrase(words: Int, using rng: inout some RandomNumberGenerator) -> String {
1168 let list = Wordlist.words
1169 var picked: [String] = []
1170 for _ in 0..<max(words, 0) {
1171 picked.append(list[Int(rng.next(upperBound: UInt32(list.count)))])
1172 }
1173 return picked.joined(separator: wordSeparator)
1174 }
1175}
1176```
1177
1178- [ ] **Step 6: Run tests and lint**
1179
1180Run: `swift test --filter GeneratorTests && swift format lint --strict --recursive Sources Tests`
1181Expected: 5 tests pass. If the linter complains about the long string literal in `Wordlist.swift`, add `"// swift-format-ignore-file"` as its first line.
1182
1183- [ ] **Step 7: Commit**
1184
1185```bash
1186git add NOTICE Sources/KeycaskCore Tests/KeycaskCoreTests
1187git commit -m "Add password and passphrase generator with EFF word list"
1188```
1189
1190---
1191
1192### Task 7: Envelope
1193
1194**Files:**
1195- Create: `Sources/KeycaskCore/Envelope.swift`
1196- Create: `Tests/KeycaskCoreTests/EnvelopeTests.swift`
1197
1198**Interfaces:**
1199- Consumes: `KeycaskError`.
1200- Produces:
1201
1202```swift
1203public struct Envelope: Codable, Equatable, Sendable {
1204 public struct KDFParams: Codable, Equatable, Sendable {
1205 public var name: String
1206 public var iterations: Int
1207 public var salt: Data
1208 public init(name: String, iterations: Int, salt: Data)
1209 public static func fresh(iterations: Int = Envelope.defaultIterations) -> KDFParams
1210 }
1211 public static let currentFormat = 1
1212 public static let defaultIterations = 600_000
1213 public static let kdfName = "pbkdf2-hmac-sha256"
1214 public static let saltLength = 16
1215 public var format: Int
1216 public var kdf: KDFParams
1217 public var box: Data
1218
1219 public static func seal(_ plaintext: Data, passphrase: String, kdf: KDFParams) throws -> Envelope
1220 public func open(passphrase: String) throws -> Data // cannotDecrypt / corrupt
1221 public init(parsing data: Data) throws // corrupt
1222 public func encoded() throws -> Data
1223 static func deriveKey(passphrase: String, kdf: KDFParams) throws -> SymmetricKey
1224}
1225```
1226
1227- [ ] **Step 1: Write the failing test**
1228
1229`Tests/KeycaskCoreTests/EnvelopeTests.swift`:
1230
1231```swift
1232import Crypto
1233import Foundation
1234import Testing
1235
1236@testable import KeycaskCore
1237
1238@Suite struct EnvelopeTests {
1239 // Low iteration count keeps the suite fast. Production uses Envelope.defaultIterations.
1240 let kdf = Envelope.KDFParams(
1241 name: Envelope.kdfName, iterations: 1_000, salt: Data(repeating: 7, count: 16))
1242
1243 func hex(_ key: SymmetricKey) -> String {
1244 key.withUnsafeBytes { $0.map { String(format: "%02x", $0) }.joined() }
1245 }
1246
1247 @Test func pbkdf2MatchesPublishedVectors() throws {
1248 let one = Envelope.KDFParams(name: Envelope.kdfName, iterations: 1, salt: Data("salt".utf8))
1249 #expect(
1250 hex(try Envelope.deriveKey(passphrase: "password", kdf: one))
1251 == "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b")
1252 let many = Envelope.KDFParams(name: Envelope.kdfName, iterations: 4096, salt: Data("salt".utf8))
1253 #expect(
1254 hex(try Envelope.deriveKey(passphrase: "password", kdf: many))
1255 == "c5e478d59288c841aa530db6845c4c8d962893a001ce4e11a4963873aa98134a")
1256 }
1257
1258 @Test func sealThenOpenRoundTrips() throws {
1259 let env = try Envelope.seal(Data("hello vault".utf8), passphrase: "pw", kdf: kdf)
1260 #expect(env.format == 1)
1261 #expect(env.kdf == kdf)
1262 #expect(try env.open(passphrase: "pw") == Data("hello vault".utf8))
1263 }
1264
1265 @Test func wrongPassphraseCannotDecrypt() throws {
1266 let env = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
1267 #expect(throws: KeycaskError.cannotDecrypt) { try env.open(passphrase: "PW") }
1268 }
1269
1270 @Test func tamperedBoxCannotDecrypt() throws {
1271 var env = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
1272 env.box[env.box.count - 1] ^= 0x01
1273 #expect(throws: KeycaskError.cannotDecrypt) { try env.open(passphrase: "pw") }
1274 }
1275
1276 @Test func nonceIsFreshAndSaltIsKept() throws {
1277 let a = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
1278 let b = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
1279 #expect(a.box != b.box)
1280 #expect(a.kdf.salt == b.kdf.salt)
1281 }
1282
1283 @Test func freshParamsUseDefaults() {
1284 let p = Envelope.KDFParams.fresh()
1285 #expect(p.name == "pbkdf2-hmac-sha256")
1286 #expect(p.iterations == 600_000)
1287 #expect(p.salt.count == 16)
1288 #expect(p.salt != Envelope.KDFParams.fresh().salt)
1289 }
1290
1291 @Test func encodedShapeMatchesTheSpec() throws {
1292 let env = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
1293 let json = try JSONSerialization.jsonObject(with: env.encoded()) as! [String: Any]
1294 #expect(json["format"] as? Int == 1)
1295 let k = json["kdf"] as! [String: Any]
1296 #expect(k["name"] as? String == "pbkdf2-hmac-sha256")
1297 #expect(k["iterations"] as? Int == 1_000)
1298 #expect(Data(base64Encoded: k["salt"] as! String) == kdf.salt)
1299 #expect(Data(base64Encoded: json["box"] as! String) == env.box)
1300 #expect(try Envelope(parsing: env.encoded()) == env)
1301 }
1302
1303 @Test func malformedInputsAreCorrupt() throws {
1304 #expect(throws: KeycaskError.self) { try Envelope(parsing: Data("not json".utf8)) }
1305 #expect(throws: KeycaskError.self) { try Envelope(parsing: Data("{\"format\":1}".utf8)) }
1306
1307 var wrongFormat = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
1308 wrongFormat.format = 2
1309 #expect(throws: KeycaskError.corrupt("unsupported format 2")) {
1310 try wrongFormat.open(passphrase: "pw")
1311 }
1312
1313 var wrongKDF = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
1314 wrongKDF.kdf.name = "argon2id"
1315 #expect(throws: KeycaskError.corrupt("unsupported kdf argon2id")) {
1316 try wrongKDF.open(passphrase: "pw")
1317 }
1318
1319 var shortBox = try Envelope.seal(Data("x".utf8), passphrase: "pw", kdf: kdf)
1320 shortBox.box = Data([1, 2, 3])
1321 #expect(throws: KeycaskError.corrupt("box too short")) { try shortBox.open(passphrase: "pw") }
1322 }
1323
1324 @Test func passphraseIsNFCNormalized() throws {
1325 let composed = "caf\u{00E9}"
1326 let decomposed = "cafe\u{0301}"
1327 let env = try Envelope.seal(Data("x".utf8), passphrase: composed, kdf: kdf)
1328 #expect(try env.open(passphrase: decomposed) == Data("x".utf8))
1329 }
1330}
1331```
1332
1333- [ ] **Step 2: Run test to verify it fails**
1334
1335Run: `swift test --filter EnvelopeTests`
1336Expected: compile error, `Envelope` not found.
1337
1338- [ ] **Step 3: Write Envelope.swift**
1339
1340```swift
1341import Crypto
1342import Foundation
1343import _CryptoExtras
1344
1345public struct Envelope: Codable, Equatable, Sendable {
1346 public struct KDFParams: Codable, Equatable, Sendable {
1347 public var name: String
1348 public var iterations: Int
1349 public var salt: Data
1350
1351 public init(name: String, iterations: Int, salt: Data) {
1352 self.name = name
1353 self.iterations = iterations
1354 self.salt = salt
1355 }
1356
1357 public static func fresh(iterations: Int = Envelope.defaultIterations) -> KDFParams {
1358 var rng = SystemRandomNumberGenerator()
1359 let salt = Data((0..<Envelope.saltLength).map { _ in UInt8.random(in: .min ... .max, using: &rng) })
1360 return KDFParams(name: Envelope.kdfName, iterations: iterations, salt: salt)
1361 }
1362 }
1363
1364 public static let currentFormat = 1
1365 public static let defaultIterations = 600_000
1366 public static let kdfName = "pbkdf2-hmac-sha256"
1367 public static let saltLength = 16
1368 static let keyLength = 32
1369 static let minimumBoxLength = 12 + 16
1370
1371 public var format: Int
1372 public var kdf: KDFParams
1373 public var box: Data
1374
1375 public static func seal(_ plaintext: Data, passphrase: String, kdf: KDFParams) throws -> Envelope {
1376 let key = try deriveKey(passphrase: passphrase, kdf: kdf)
1377 do {
1378 let sealed = try ChaChaPoly.seal(plaintext, using: key)
1379 return Envelope(format: currentFormat, kdf: kdf, box: sealed.combined)
1380 } catch {
1381 throw KeycaskError.failure("encrypt: \(error)")
1382 }
1383 }
1384
1385 public func open(passphrase: String) throws -> Data {
1386 guard format == Self.currentFormat else {
1387 throw KeycaskError.corrupt("unsupported format \(format)")
1388 }
1389 guard kdf.name == Self.kdfName else {
1390 throw KeycaskError.corrupt("unsupported kdf \(kdf.name)")
1391 }
1392 guard box.count >= Self.minimumBoxLength else {
1393 throw KeycaskError.corrupt("box too short")
1394 }
1395 let key = try Self.deriveKey(passphrase: passphrase, kdf: kdf)
1396 let sealed: ChaChaPoly.SealedBox
1397 do {
1398 sealed = try ChaChaPoly.SealedBox(combined: box)
1399 } catch {
1400 throw KeycaskError.corrupt("box is malformed")
1401 }
1402 do {
1403 return try ChaChaPoly.open(sealed, using: key)
1404 } catch {
1405 throw KeycaskError.cannotDecrypt
1406 }
1407 }
1408
1409 public init(parsing data: Data) throws {
1410 do {
1411 self = try JSONDecoder().decode(Envelope.self, from: data)
1412 } catch {
1413 throw KeycaskError.corrupt("not a keycask vault: \(error)")
1414 }
1415 }
1416
1417 public func encoded() throws -> Data {
1418 let encoder = JSONEncoder()
1419 encoder.outputFormatting = [.sortedKeys, .prettyPrinted]
1420 do {
1421 return try encoder.encode(self)
1422 } catch {
1423 throw KeycaskError.io("encode envelope: \(error)")
1424 }
1425 }
1426
1427 init(format: Int, kdf: KDFParams, box: Data) {
1428 self.format = format
1429 self.kdf = kdf
1430 self.box = box
1431 }
1432
1433 static func deriveKey(passphrase: String, kdf: KDFParams) throws -> SymmetricKey {
1434 let normalized = Array(passphrase.precomposedStringWithCanonicalMapping.utf8)
1435 do {
1436 return try KDF.Insecure.PBKDF2.deriveKey(
1437 from: normalized, salt: kdf.salt, using: .sha256,
1438 outputByteCount: keyLength, unsafeUncheckedRounds: kdf.iterations)
1439 } catch {
1440 throw KeycaskError.failure("derive key: \(error)")
1441 }
1442 }
1443}
1444```
1445
1446`unsafeUncheckedRounds` is used because the checked overload rejects fewer than 210000 rounds, and the vault decides the count. `KDFParams.fresh()` always produces 600000.
1447
1448- [ ] **Step 4: Run tests**
1449
1450Run: `swift test --filter EnvelopeTests`
1451Expected: 9 tests pass.
1452
1453- [ ] **Step 5: Commit**
1454
1455```bash
1456git add Sources/KeycaskCore Tests/KeycaskCoreTests
1457git commit -m "Add passphrase-encrypted vault envelope"
1458```
1459
1460---
1461
1462### Task 8: Core merge request
1463
1464**Files:** none new.
1465
1466- [ ] **Step 1: Full suite and lint**
1467
1468Run: `swift test && swift format lint --strict --recursive Sources Tests Package.swift`
1469Expected: all pass, no lint output.
1470
1471- [ ] **Step 2: Push and open the MR**
1472
1473```bash
1474git push -u origin core
1475gitbay mr create --source core --target main --title "Core library: model, envelope, generator" --file - <<'EOF'
1476KeycaskCore: Entry, EntryID, Vault, VaultCodec, Envelope, Generator, Wordlist, KeycaskError.
1477Package scaffold and gitbay CI.
1478EOF
1479```
1480
1481- [ ] **Step 3: Wait for CI, merge, clean up**
1482
1483Run `gitbay build list --json` until the build for `core` is green. Then:
1484
1485```bash
1486gitbay mr merge <n> --strategy squash
1487git switch main && git pull && git branch -D core && git push origin --delete core
1488```
1489
1490If the CI job fails on the swiftly install lines, read `gitbay build log <n>`, fix `.gitbay/ci.yml` on the branch, push, and re-check. Do not merge red.
1491
1492---
1493
1494### Task 9: CLI skeleton, paths, passphrase, atomic write, `init`
1495
1496**Files:**
1497- Create: `Sources/keycask/main.swift` (replace)
1498- Create: `Sources/keycask/Keycask.swift`
1499- Create: `Sources/keycask/Paths.swift`
1500- Create: `Sources/keycask/Terminal.swift`
1501- Create: `Sources/keycask/Passphrase.swift`
1502- Create: `Sources/keycask/AtomicFile.swift`
1503- Create: `Sources/keycask/OpenVault.swift`
1504- Create: `Sources/keycask/Commands/Init.swift`
1505- Modify: `Tests/KeycaskCLITests/CLI.swift`
1506- Create: `Tests/KeycaskCLITests/InitTests.swift`
1507- Create: `Tests/KeycaskCLITests/PathsTests.swift`
1508
1509**Interfaces:**
1510- Consumes: `Vault`, `VaultCodec`, `Envelope`, `KeycaskError`.
1511- Produces:
1512
1513```swift
1514struct GlobalOptions: ParsableArguments { var vault: String? }
1515enum Paths { static func vaultURL(override: String?, environment: [String: String]) -> URL }
1516enum Terminal {
1517 static var stdinIsTTY: Bool
1518 static func readSecretLine(prompt: String) throws -> String
1519 static func readLine(prompt: String) -> String?
1520 static func confirm(_ question: String) -> Bool
1521}
1522enum Passphrase {
1523 static let variable = "KEYCASK_PASSPHRASE"
1524 static func obtain(confirm: Bool, environment: [String: String]) throws -> String
1525}
1526enum AtomicFile { static func write(_ data: Data, to url: URL) throws }
1527struct OpenVault {
1528 var vault: Vault
1529 let kdf: Envelope.KDFParams
1530 let url: URL
1531 let passphrase: String
1532 static func load(_ options: GlobalOptions) throws -> OpenVault
1533 static func create(_ options: GlobalOptions) throws -> URL
1534 func save() throws
1535}
1536struct CLI { // test harness
1537 struct Result { let status: Int32; let stdout: String; let stderr: String }
1538 let dir: URL; let vault: URL; static let passphrase = "correct horse battery"
1539 init() throws
1540 func run(_ args: [String], stdin: String? = nil, passphrase: String? = CLI.passphrase,
1541 extraEnvironment: [String: String] = [:]) throws -> Result
1542}
1543```
1544
1545- [ ] **Step 1: Create the branch**
1546
1547```bash
1548git switch -c cli
1549```
1550
1551- [ ] **Step 2: Extend the test harness**
1552
1553Replace `Tests/KeycaskCLITests/CLI.swift`:
1554
1555```swift
1556import Foundation
1557import Testing
1558
1559enum Binary {
1560 static let url: URL = {
1561 #if os(macOS)
1562 for bundle in Bundle.allBundles where bundle.bundlePath.hasSuffix(".xctest") {
1563 return bundle.bundleURL.deletingLastPathComponent().appendingPathComponent("keycask")
1564 }
1565 fatalError("test bundle not found")
1566 #elseif os(Windows)
1567 return Bundle.main.bundleURL.appendingPathComponent("keycask.exe")
1568 #else
1569 return Bundle.main.bundleURL.appendingPathComponent("keycask")
1570 #endif
1571 }()
1572}
1573
1574struct CLI {
1575 struct Result {
1576 let status: Int32
1577 let stdout: String
1578 let stderr: String
1579 var lines: [String] { stdout.split(separator: "\n").map(String.init) }
1580 }
1581
1582 static let passphrase = "correct horse battery"
1583
1584 let dir: URL
1585 let vault: URL
1586
1587 init() throws {
1588 dir = FileManager.default.temporaryDirectory
1589 .appendingPathComponent("keycask-tests-\(UUID().uuidString)")
1590 try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
1591 vault = dir.appendingPathComponent("vault.kc")
1592 }
1593
1594 @discardableResult
1595 func run(
1596 _ args: [String],
1597 stdin: String? = nil,
1598 passphrase: String? = CLI.passphrase,
1599 extraEnvironment: [String: String] = [:]
1600 ) throws -> Result {
1601 let process = Process()
1602 process.executableURL = Binary.url
1603 process.arguments = args
1604 var env = ProcessInfo.processInfo.environment
1605 env["KEYCASK_VAULT"] = vault.path
1606 env.removeValue(forKey: "KEYCASK_PASSPHRASE")
1607 if let passphrase { env["KEYCASK_PASSPHRASE"] = passphrase }
1608 for (k, v) in extraEnvironment { env[k] = v }
1609 process.environment = env
1610
1611 let out = Pipe()
1612 let err = Pipe()
1613 let input = Pipe()
1614 process.standardOutput = out
1615 process.standardError = err
1616 process.standardInput = input
1617 try process.run()
1618 if let stdin {
1619 input.fileHandleForWriting.write(Data(stdin.utf8))
1620 }
1621 try input.fileHandleForWriting.close()
1622 let outData = out.fileHandleForReading.readDataToEndOfFile()
1623 let errData = err.fileHandleForReading.readDataToEndOfFile()
1624 process.waitUntilExit()
1625 return Result(
1626 status: process.terminationStatus,
1627 stdout: String(decoding: outData, as: UTF8.self),
1628 stderr: String(decoding: errData, as: UTF8.self))
1629 }
1630
1631 /// Runs `init` and returns the harness, for tests that need a vault.
1632 static func initialized() throws -> CLI {
1633 let cli = try CLI()
1634 let r = try cli.run(["init"])
1635 precondition(r.status == 0, "init failed: \(r.stderr)")
1636 return cli
1637 }
1638}
1639```
1640
1641Remove the `binaryIsBuilt` test from this file; the harness replaces it.
1642
1643- [ ] **Step 3: Write the failing tests**
1644
1645`Tests/KeycaskCLITests/InitTests.swift`:
1646
1647```swift
1648import Foundation
1649import Testing
1650
1651@Suite struct InitTests {
1652 @Test func initCreatesVaultAndPrintsPath() throws {
1653 let cli = try CLI()
1654 let r = try cli.run(["init"])
1655 #expect(r.status == 0)
1656 #expect(r.stdout.contains(cli.vault.path))
1657 #expect(FileManager.default.fileExists(atPath: cli.vault.path))
1658 let text = try String(contentsOf: cli.vault, encoding: .utf8)
1659 #expect(text.contains("\"format\" : 1"))
1660 #expect(text.contains("pbkdf2-hmac-sha256"))
1661 #expect(!text.contains("entries"))
1662 }
1663
1664 @Test func initRefusesExistingVault() throws {
1665 let cli = try CLI.initialized()
1666 let r = try cli.run(["init"])
1667 #expect(r.status == 1)
1668 #expect(r.stderr.contains("already exists"))
1669 }
1670
1671 @Test func initWithoutPassphraseOrTTYIsUsageError() throws {
1672 let cli = try CLI()
1673 let r = try cli.run(["init"], passphrase: nil)
1674 #expect(r.status == 2)
1675 #expect(r.stderr.contains("KEYCASK_PASSPHRASE"))
1676 }
1677
1678 @Test func emptyPassphraseIsRejected() throws {
1679 let cli = try CLI()
1680 let r = try cli.run(["init"], passphrase: "")
1681 #expect(r.status == 1)
1682 #expect(r.stderr.contains("empty"))
1683 }
1684
1685 @Test func vaultFlagBeatsEnvironment() throws {
1686 let cli = try CLI()
1687 let other = cli.dir.appendingPathComponent("elsewhere.kc")
1688 let r = try cli.run(["--vault", other.path, "init"])
1689 #expect(r.status == 0)
1690 #expect(FileManager.default.fileExists(atPath: other.path))
1691 #expect(!FileManager.default.fileExists(atPath: cli.vault.path))
1692 }
1693
1694 @Test func unknownSubcommandIsUsageError() throws {
1695 let cli = try CLI()
1696 let r = try cli.run(["frobnicate"])
1697 #expect(r.status == 2)
1698 #expect(r.stderr.contains("Usage"))
1699 }
1700
1701 @Test func helpExitsZero() throws {
1702 let cli = try CLI()
1703 let r = try cli.run(["--help"])
1704 #expect(r.status == 0)
1705 #expect(r.stdout.contains("init"))
1706 }
1707
1708 #if !os(Windows)
1709 @Test func vaultIsPrivateOnUnix() throws {
1710 let cli = try CLI.initialized()
1711 let attrs = try FileManager.default.attributesOfItem(atPath: cli.vault.path)
1712 let mode = (attrs[.posixPermissions] as! NSNumber).intValue & 0o777
1713 #expect(mode == 0o600)
1714 }
1715 #endif
1716
1717 @Test func noTempFileLeftBehind() throws {
1718 let cli = try CLI.initialized()
1719 let names = try FileManager.default.contentsOfDirectory(atPath: cli.dir.path)
1720 #expect(names == ["vault.kc"])
1721 }
1722}
1723```
1724
1725`Tests/KeycaskCLITests/PathsTests.swift` tests `Paths` in process. It needs `@testable import keycask`, which works because the test target depends on the executable target:
1726
1727```swift
1728import Foundation
1729import Testing
1730
1731@testable import keycask
1732
1733@Suite struct PathsTests {
1734 @Test func overrideWinsOverEverything() {
1735 let url = Paths.vaultURL(
1736 override: "/x/v.kc", environment: ["KEYCASK_VAULT": "/y", "HOME": "/h"])
1737 #expect(url.path == "/x/v.kc")
1738 }
1739
1740 @Test func environmentVariableWinsOverDefaults() {
1741 let url = Paths.vaultURL(override: nil, environment: ["KEYCASK_VAULT": "/y/v.kc", "HOME": "/h"])
1742 #expect(url.path == "/y/v.kc")
1743 }
1744
1745 #if os(Windows)
1746 @Test func windowsUsesLocalAppData() {
1747 let url = Paths.vaultURL(override: nil, environment: ["LOCALAPPDATA": "C:\\Users\\u\\AppData\\Local"])
1748 #expect(url.path.hasSuffix("keycask/vault.kc") || url.path.hasSuffix("keycask\\vault.kc"))
1749 }
1750 #else
1751 @Test func xdgDataHomeIsUsedWhenSet() {
1752 let url = Paths.vaultURL(override: nil, environment: ["XDG_DATA_HOME": "/d", "HOME": "/h"])
1753 #expect(url.path == "/d/keycask/vault.kc")
1754 }
1755
1756 @Test func homeFallback() {
1757 let url = Paths.vaultURL(override: nil, environment: ["HOME": "/h"])
1758 #expect(url.path == "/h/.local/share/keycask/vault.kc")
1759 }
1760 #endif
1761}
1762```
1763
1764- [ ] **Step 4: Run tests to verify they fail**
1765
1766Run: `swift test --filter 'InitTests|PathsTests'`
1767Expected: compile error, `Paths` not found.
1768
1769- [ ] **Step 5: Write Paths.swift**
1770
1771```swift
1772import Foundation
1773
1774enum Paths {
1775 static let variable = "KEYCASK_VAULT"
1776
1777 static func vaultURL(
1778 override: String?, environment: [String: String] = ProcessInfo.processInfo.environment
1779 ) -> URL {
1780 if let override { return URL(fileURLWithPath: override) }
1781 if let env = environment[variable], !env.isEmpty { return URL(fileURLWithPath: env) }
1782 return defaultDirectory(environment: environment)
1783 .appendingPathComponent("keycask").appendingPathComponent("vault.kc")
1784 }
1785
1786 private static func defaultDirectory(environment: [String: String]) -> URL {
1787 #if os(Windows)
1788 let base = environment["LOCALAPPDATA"] ?? environment["USERPROFILE"] ?? "."
1789 return URL(fileURLWithPath: base)
1790 #else
1791 if let xdg = environment["XDG_DATA_HOME"], !xdg.isEmpty {
1792 return URL(fileURLWithPath: xdg)
1793 }
1794 let home = environment["HOME"] ?? "."
1795 return URL(fileURLWithPath: home).appendingPathComponent(".local/share")
1796 #endif
1797 }
1798}
1799```
1800
1801- [ ] **Step 6: Write Terminal.swift**
1802
1803```swift
1804import Foundation
1805import KeycaskCore
1806
1807#if canImport(Darwin)
1808 import Darwin
1809#elseif canImport(Glibc)
1810 import Glibc
1811#elseif canImport(Musl)
1812 import Musl
1813#elseif os(Windows)
1814 import CRT
1815 import WinSDK
1816#endif
1817
1818enum Terminal {
1819 static var stdinIsTTY: Bool {
1820 #if os(Windows)
1821 return _isatty(_fileno(stdin)) != 0
1822 #else
1823 return isatty(STDIN_FILENO) != 0
1824 #endif
1825 }
1826
1827 static func write(_ text: String) {
1828 FileHandle.standardError.write(Data(text.utf8))
1829 }
1830
1831 static func readLine(prompt: String) -> String? {
1832 write(prompt)
1833 return Swift.readLine(strippingNewline: true)
1834 }
1835
1836 static func confirm(_ question: String) -> Bool {
1837 guard let answer = readLine(prompt: question + " [y/N] ") else { return false }
1838 return answer.lowercased().hasPrefix("y")
1839 }
1840
1841 static func readSecretLine(prompt: String) throws -> String {
1842 write(prompt)
1843 defer { write("\n") }
1844 return try withEchoDisabled { Swift.readLine(strippingNewline: true) ?? "" }
1845 }
1846
1847 #if os(Windows)
1848 private static func withEchoDisabled<T>(_ body: () throws -> T) throws -> T {
1849 let handle = GetStdHandle(DWORD(bitPattern: -10))
1850 var mode: DWORD = 0
1851 guard GetConsoleMode(handle, &mode).boolValue else {
1852 throw KeycaskError.io("GetConsoleMode failed")
1853 }
1854 SetConsoleMode(handle, mode & ~DWORD(ENABLE_ECHO_INPUT))
1855 defer { SetConsoleMode(handle, mode) }
1856 return try body()
1857 }
1858 #else
1859 private static func withEchoDisabled<T>(_ body: () throws -> T) throws -> T {
1860 var original = termios()
1861 guard tcgetattr(STDIN_FILENO, &original) == 0 else {
1862 throw KeycaskError.io("tcgetattr failed")
1863 }
1864 var quiet = original
1865 quiet.c_lflag &= ~tcflag_t(ECHO)
1866 tcsetattr(STDIN_FILENO, TCSANOW, &quiet)
1867 defer { tcsetattr(STDIN_FILENO, TCSANOW, &original) }
1868 return try body()
1869 }
1870 #endif
1871}
1872```
1873
1874- [ ] **Step 7: Write Passphrase.swift**
1875
1876```swift
1877import Foundation
1878import KeycaskCore
1879
1880enum Passphrase {
1881 static let variable = "KEYCASK_PASSPHRASE"
1882
1883 static func obtain(
1884 confirm: Bool, environment: [String: String] = ProcessInfo.processInfo.environment
1885 ) throws -> String {
1886 if let fromEnv = environment[variable] {
1887 return try validated(fromEnv)
1888 }
1889 guard Terminal.stdinIsTTY else {
1890 throw KeycaskError.usage("no passphrase: set \(variable) or run on a terminal")
1891 }
1892 let first = try Terminal.readSecretLine(prompt: "Passphrase: ")
1893 if confirm {
1894 let second = try Terminal.readSecretLine(prompt: "Confirm passphrase: ")
1895 guard first == second else { throw KeycaskError.failure("passphrases do not match") }
1896 }
1897 return try validated(first)
1898 }
1899
1900 private static func validated(_ passphrase: String) throws -> String {
1901 guard !passphrase.isEmpty else { throw KeycaskError.failure("passphrase is empty") }
1902 return passphrase
1903 }
1904}
1905```
1906
1907- [ ] **Step 8: Write AtomicFile.swift**
1908
1909```swift
1910import Foundation
1911import KeycaskCore
1912
1913#if canImport(Darwin)
1914 import Darwin
1915#elseif canImport(Glibc)
1916 import Glibc
1917#elseif canImport(Musl)
1918 import Musl
1919#elseif os(Windows)
1920 import WinSDK
1921#endif
1922
1923enum AtomicFile {
1924 static func write(_ data: Data, to url: URL) throws {
1925 let directory = url.deletingLastPathComponent()
1926 let temp = url.appendingPathExtension("tmp")
1927 do {
1928 try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
1929 try writePrivate(data, to: temp)
1930 try replace(url, with: temp)
1931 } catch let error as KeycaskError {
1932 try? FileManager.default.removeItem(at: temp)
1933 throw error
1934 } catch {
1935 try? FileManager.default.removeItem(at: temp)
1936 throw KeycaskError.io("write \(url.path): \(error)")
1937 }
1938 }
1939
1940 #if os(Windows)
1941 private static func writePrivate(_ data: Data, to url: URL) throws {
1942 try data.write(to: url)
1943 let handle = try FileHandle(forWritingTo: url)
1944 try handle.synchronize()
1945 try handle.close()
1946 }
1947
1948 private static func replace(_ target: URL, with temp: URL) throws {
1949 let ok = temp.path.withCString(encodedAs: UTF16.self) { src in
1950 target.path.withCString(encodedAs: UTF16.self) { dst in
1951 MoveFileExW(src, dst, DWORD(MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH))
1952 }
1953 }
1954 guard ok.boolValue else { throw KeycaskError.io("rename \(temp.path): error \(GetLastError())") }
1955 }
1956 #else
1957 private static func writePrivate(_ data: Data, to url: URL) throws {
1958 let fd = open(url.path, O_WRONLY | O_CREAT | O_TRUNC, 0o600)
1959 guard fd >= 0 else {
1960 throw KeycaskError.io("open \(url.path): \(String(cString: strerror(errno)))")
1961 }
1962 let handle = FileHandle(fileDescriptor: fd, closeOnDealloc: true)
1963 try handle.write(contentsOf: data)
1964 try handle.synchronize()
1965 try handle.close()
1966 }
1967
1968 private static func replace(_ target: URL, with temp: URL) throws {
1969 guard rename(temp.path, target.path) == 0 else {
1970 throw KeycaskError.io("rename \(temp.path): \(String(cString: strerror(errno)))")
1971 }
1972 }
1973 #endif
1974}
1975```
1976
1977- [ ] **Step 9: Write OpenVault.swift**
1978
1979```swift
1980import Foundation
1981import KeycaskCore
1982
1983struct OpenVault {
1984 var vault: Vault
1985 let kdf: Envelope.KDFParams
1986 let url: URL
1987 let passphrase: String
1988
1989 static func load(_ options: GlobalOptions) throws -> OpenVault {
1990 let url = Paths.vaultURL(override: options.vault)
1991 let data: Data
1992 do {
1993 data = try Data(contentsOf: url)
1994 } catch let error as CocoaError where error.code == .fileReadNoSuchFile {
1995 throw KeycaskError.noVault(url.path)
1996 } catch {
1997 if !FileManager.default.fileExists(atPath: url.path) {
1998 throw KeycaskError.noVault(url.path)
1999 }
2000 throw KeycaskError.io("read \(url.path): \(error)")
2001 }
2002 let envelope = try Envelope(parsing: data)
2003 let passphrase = try Passphrase.obtain(confirm: false)
2004 let plaintext = try envelope.open(passphrase: passphrase)
2005 let vault = try VaultCodec.decode(plaintext)
2006 return OpenVault(vault: vault, kdf: envelope.kdf, url: url, passphrase: passphrase)
2007 }
2008
2009 static func create(_ options: GlobalOptions) throws -> URL {
2010 let url = Paths.vaultURL(override: options.vault)
2011 guard !FileManager.default.fileExists(atPath: url.path) else {
2012 throw KeycaskError.vaultExists(url.path)
2013 }
2014 let passphrase = try Passphrase.obtain(confirm: true)
2015 let fresh = OpenVault(vault: Vault(), kdf: .fresh(), url: url, passphrase: passphrase)
2016 try fresh.save()
2017 return url
2018 }
2019
2020 func save() throws {
2021 let plaintext = try VaultCodec.encode(vault)
2022 let envelope = try Envelope.seal(plaintext, passphrase: passphrase, kdf: kdf)
2023 try AtomicFile.write(try envelope.encoded(), to: url)
2024 }
2025}
2026```
2027
2028- [ ] **Step 10: Write Keycask.swift and Commands/Init.swift**
2029
2030`Sources/keycask/Keycask.swift`:
2031
2032```swift
2033import ArgumentParser
2034
2035struct GlobalOptions: ParsableArguments {
2036 @Option(name: .long, help: "Path to the vault file.")
2037 var vault: String?
2038}
2039
2040struct Keycask: ParsableCommand {
2041 static let configuration = CommandConfiguration(
2042 commandName: "keycask",
2043 abstract: "Command-line password manager. One passphrase-encrypted vault file.",
2044 subcommands: [Init.self]
2045 )
2046}
2047```
2048
2049`Sources/keycask/Commands/Init.swift`:
2050
2051```swift
2052import ArgumentParser
2053
2054struct Init: ParsableCommand {
2055 static let configuration = CommandConfiguration(abstract: "Create an empty vault.")
2056
2057 @OptionGroup var global: GlobalOptions
2058
2059 func run() throws {
2060 let url = try OpenVault.create(global)
2061 print("created \(url.path)")
2062 }
2063}
2064```
2065
2066- [ ] **Step 11: Write main.swift**
2067
2068```swift
2069import ArgumentParser
2070import Foundation
2071import KeycaskCore
2072
2073func fail(_ text: String, code: Int32) -> Never {
2074 FileHandle.standardError.write(Data((text + "\n").utf8))
2075 exit(code)
2076}
2077
2078do {
2079 var command = try Keycask.parseAsRoot()
2080 try command.run()
2081} catch let error as KeycaskError {
2082 fail(error.message, code: error.exitCode)
2083} catch {
2084 let text = Keycask.fullMessage(for: error)
2085 if Keycask.exitCode(for: error).isSuccess {
2086 print(text)
2087 exit(0)
2088 }
2089 fail(text, code: 2)
2090}
2091```
2092
2093- [ ] **Step 12: Run tests**
2094
2095Run: `swift test --filter 'InitTests|PathsTests'`
2096Expected: all pass. The `init` tests take about half a second each because of 600000 PBKDF2 rounds; that is expected.
2097
2098- [ ] **Step 13: Lint and commit**
2099
2100```bash
2101swift format lint --strict --recursive Sources Tests
2102git add Sources/keycask Tests/KeycaskCLITests
2103git commit -m "Add CLI skeleton with init, paths, passphrase, atomic write"
2104```
2105
2106---
2107
2108### Task 10: `add`, `show`, and output formatting
2109
2110**Files:**
2111- Create: `Sources/keycask/Output.swift`
2112- Create: `Sources/keycask/Commands/Add.swift`
2113- Create: `Sources/keycask/Commands/Show.swift`
2114- Modify: `Sources/keycask/Keycask.swift` (register subcommands)
2115- Create: `Tests/KeycaskCLITests/AddShowTests.swift`
2116
2117**Interfaces:**
2118- Consumes: `OpenVault`, `Generator`, `Entry`, `Terminal`, `VaultCodec.makeEncoder()`.
2119- Produces:
2120
2121```swift
2122enum Output {
2123 static let mask = "********"
2124 static func masked(_ entry: Entry, reveal: Bool) -> Entry
2125 static func text(_ entry: Entry, reveal: Bool) -> String // "field: value" lines
2126 static func table(_ entries: [Entry]) -> String // id name username url
2127 static func json(_ entries: [Entry], reveal: Bool) throws -> String
2128 static func json(_ entry: Entry, reveal: Bool) throws -> String
2129 static func field(_ entry: Entry, named: String) throws -> String // usage error for unknown field
2130}
2131enum PasswordInput {
2132 static func read(prompt: String) throws -> String // TTY: hidden prompt; else first line of stdin
2133}
2134struct PasswordOptions: ParsableArguments { var generate: Bool; var length: Int?; var words: Int?; func validate(); func newPassword() throws -> String? }
2135```
2136
2137Non-TTY password entry: when stdin is not a terminal, `add` and `edit --password` read the password as the first line of stdin. Add this sentence to the spec's CLI section in this task.
2138
2139- [ ] **Step 1: Write the failing tests**
2140
2141`Tests/KeycaskCLITests/AddShowTests.swift`:
2142
2143```swift
2144import Foundation
2145import Testing
2146
2147@Suite struct AddShowTests {
2148 @Test func addReadsPasswordFromStdinAndPrintsID() throws {
2149 let cli = try CLI.initialized()
2150 let r = try cli.run(["add", "github", "-u", "cmc", "--url", "https://github.com", "--tag", "dev"],
2151 stdin: "hunter2\n")
2152 #expect(r.status == 0)
2153 let id = r.stdout.trimmingCharacters(in: .whitespacesAndNewlines)
2154 #expect(id.count == 8)
2155
2156 let shown = try cli.run(["show", id])
2157 #expect(shown.status == 0)
2158 #expect(shown.stdout.contains("name: github"))
2159 #expect(shown.stdout.contains("username: cmc"))
2160 #expect(shown.stdout.contains("password: ********"))
2161 #expect(shown.stdout.contains("tags: dev"))
2162 #expect(!shown.stdout.contains("hunter2"))
2163 }
2164
2165 @Test func showByNameRevealAndField() throws {
2166 let cli = try CLI.initialized()
2167 try cli.run(["add", "github"], stdin: "hunter2\n")
2168 let revealed = try cli.run(["show", "github", "--reveal"])
2169 #expect(revealed.stdout.contains("password: hunter2"))
2170 let field = try cli.run(["show", "github", "--field", "password"])
2171 #expect(field.stdout == "hunter2\n")
2172 let missing = try cli.run(["show", "github", "--field", "url"])
2173 #expect(missing.status == 0)
2174 #expect(missing.stdout == "\n")
2175 let unknown = try cli.run(["show", "github", "--field", "nope"])
2176 #expect(unknown.status == 2)
2177 }
2178
2179 @Test func jsonMasksUnlessReveal() throws {
2180 let cli = try CLI.initialized()
2181 try cli.run(["add", "github", "-u", "cmc"], stdin: "hunter2\n")
2182 let masked = try cli.run(["show", "github", "--json"])
2183 let obj = try JSONSerialization.jsonObject(with: Data(masked.stdout.utf8)) as! [String: Any]
2184 #expect(obj["name"] as? String == "github")
2185 #expect(obj["username"] as? String == "cmc")
2186 #expect(obj["password"] as? String == "********")
2187 #expect((obj["id"] as? String)?.count == 8)
2188 #expect((obj["created"] as? String)?.hasSuffix("Z") == true)
2189 let revealed = try cli.run(["show", "github", "--json", "--reveal"])
2190 let obj2 = try JSONSerialization.jsonObject(with: Data(revealed.stdout.utf8)) as! [String: Any]
2191 #expect(obj2["password"] as? String == "hunter2")
2192 }
2193
2194 @Test func addGenerateAndWords() throws {
2195 let cli = try CLI.initialized()
2196 try cli.run(["add", "a", "--generate"])
2197 try cli.run(["add", "b", "--generate", "--length", "40"])
2198 try cli.run(["add", "c", "--words", "4"])
2199 #expect(try cli.run(["show", "a", "--field", "password"]).stdout.count == 25)
2200 #expect(try cli.run(["show", "b", "--field", "password"]).stdout.count == 41)
2201 let words = try cli.run(["show", "c", "--field", "password"]).stdout
2202 .trimmingCharacters(in: .newlines).split(separator: "-")
2203 #expect(words.count == 4)
2204 }
2205
2206 @Test func generateAndWordsTogetherIsUsageError() throws {
2207 let cli = try CLI.initialized()
2208 let r = try cli.run(["add", "a", "--generate", "--words", "3"])
2209 #expect(r.status == 2)
2210 }
2211
2212 @Test func duplicateNamesAreAllowedAndAmbiguousOnShow() throws {
2213 let cli = try CLI.initialized()
2214 let a = try cli.run(["add", "gh", "-u", "one", "--generate"]).stdout.trimmingCharacters(in: .newlines)
2215 let b = try cli.run(["add", "gh", "-u", "two", "--generate"]).stdout.trimmingCharacters(in: .newlines)
2216 let r = try cli.run(["show", "gh"])
2217 #expect(r.status == 5)
2218 #expect(r.stderr.contains(a) && r.stderr.contains(b))
2219 #expect(try cli.run(["show", a]).stdout.contains("username: one"))
2220 }
2221
2222 @Test func missingEntryIsNotFound() throws {
2223 let cli = try CLI.initialized()
2224 let r = try cli.run(["show", "nope"])
2225 #expect(r.status == 3)
2226 #expect(r.stderr == "nope: not found\n")
2227 }
2228
2229 @Test func wrongPassphraseCannotDecrypt() throws {
2230 let cli = try CLI.initialized()
2231 let r = try cli.run(["show", "x"], passphrase: "wrong")
2232 #expect(r.status == 4)
2233 #expect(r.stderr.contains("cannot decrypt"))
2234 }
2235
2236 @Test func missingVaultIsNotFound() throws {
2237 let cli = try CLI()
2238 let r = try cli.run(["show", "x"])
2239 #expect(r.status == 3)
2240 #expect(r.stderr.contains("keycask init"))
2241 }
2242
2243 @Test func corruptVaultIsFailure() throws {
2244 let cli = try CLI.initialized()
2245 try Data("{}".utf8).write(to: cli.vault)
2246 let r = try cli.run(["show", "x"])
2247 #expect(r.status == 1)
2248 #expect(r.stderr.hasPrefix("vault is corrupt"))
2249 }
2250}
2251```
2252
2253- [ ] **Step 2: Run tests to verify they fail**
2254
2255Run: `swift test --filter AddShowTests`
2256Expected: failures, `add` is an unknown subcommand (exit 2).
2257
2258- [ ] **Step 3: Write Output.swift**
2259
2260```swift
2261import Foundation
2262import KeycaskCore
2263
2264enum Output {
2265 static let mask = "********"
2266
2267 static func masked(_ entry: Entry, reveal: Bool) -> Entry {
2268 guard !reveal else { return entry }
2269 var copy = entry
2270 copy.password = mask
2271 return copy
2272 }
2273
2274 static func text(_ entry: Entry, reveal: Bool) -> String {
2275 let e = masked(entry, reveal: reveal)
2276 var lines = ["id: \(e.id.rawValue)", "name: \(e.name)"]
2277 if let u = e.username { lines.append("username: \(u)") }
2278 lines.append("password: \(e.password)")
2279 if let u = e.url { lines.append("url: \(u)") }
2280 if !e.tags.isEmpty { lines.append("tags: \(e.tags.joined(separator: ", "))") }
2281 if let n = e.notes { lines.append("notes: \(n)") }
2282 lines.append("created: \(iso(e.created))")
2283 lines.append("updated: \(iso(e.updated))")
2284 return lines.joined(separator: "\n") + "\n"
2285 }
2286
2287 static func table(_ entries: [Entry]) -> String {
2288 guard !entries.isEmpty else { return "" }
2289 let rows = entries.map { [$0.id.rawValue, $0.name, $0.username ?? "", $0.url ?? ""] }
2290 let widths = (0..<3).map { col in rows.map { $0[col].count }.max() ?? 0 }
2291 return rows.map { row in
2292 let padded = (0..<3).map { row[$0].padding(toLength: widths[$0], withPad: " ", startingAt: 0) }
2293 return (padded + [row[3]]).joined(separator: " ")
2294 .trimmingCharacters(in: .whitespaces)
2295 }.joined(separator: "\n") + "\n"
2296 }
2297
2298 static func json(_ entries: [Entry], reveal: Bool) throws -> String {
2299 try encode(entries.map { masked($0, reveal: reveal) })
2300 }
2301
2302 static func json(_ entry: Entry, reveal: Bool) throws -> String {
2303 try encode(masked(entry, reveal: reveal))
2304 }
2305
2306 static func field(_ entry: Entry, named name: String) throws -> String {
2307 switch name {
2308 case "id": entry.id.rawValue
2309 case "name": entry.name
2310 case "username": entry.username ?? ""
2311 case "password": entry.password
2312 case "url": entry.url ?? ""
2313 case "notes": entry.notes ?? ""
2314 case "tags": entry.tags.joined(separator: ",")
2315 case "created": iso(entry.created)
2316 case "updated": iso(entry.updated)
2317 default: throw KeycaskError.usage("unknown field \(name)")
2318 }
2319 }
2320
2321 private static func encode(_ value: some Encodable) throws -> String {
2322 let encoder = VaultCodec.makeEncoder()
2323 encoder.outputFormatting.insert(.prettyPrinted)
2324 do {
2325 return String(decoding: try encoder.encode(value), as: UTF8.self) + "\n"
2326 } catch {
2327 throw KeycaskError.io("encode json: \(error)")
2328 }
2329 }
2330
2331 private static func iso(_ date: Date) -> String {
2332 date.formatted(.iso8601)
2333 }
2334}
2335```
2336
2337- [ ] **Step 4: Write password input and the shared password options**
2338
2339Add to `Sources/keycask/Commands/Add.swift`:
2340
2341```swift
2342import ArgumentParser
2343import Foundation
2344import KeycaskCore
2345
2346enum PasswordInput {
2347 static func read(prompt: String) throws -> String {
2348 if Terminal.stdinIsTTY {
2349 return try Terminal.readSecretLine(prompt: prompt)
2350 }
2351 guard let line = Swift.readLine(strippingNewline: true) else {
2352 throw KeycaskError.usage("no password: pass one on stdin or run on a terminal")
2353 }
2354 return line
2355 }
2356}
2357
2358struct PasswordOptions: ParsableArguments {
2359 @Flag(name: .long, help: "Generate a random password.")
2360 var generate = false
2361
2362 @Option(name: .long, help: "Length of the generated password (default 24).")
2363 var length: Int?
2364
2365 @Option(name: .long, help: "Generate a passphrase of this many words instead.")
2366 var words: Int?
2367
2368 mutating func validate() throws {
2369 if generate, words != nil {
2370 throw ValidationError("--generate and --words are mutually exclusive")
2371 }
2372 if let length, length < 1 { throw ValidationError("--length must be at least 1") }
2373 if let words, words < 1 { throw ValidationError("--words must be at least 1") }
2374 if length != nil, !generate, words == nil {
2375 throw ValidationError("--length requires --generate")
2376 }
2377 }
2378
2379 /// nil means the caller must prompt.
2380 func newPassword() -> String? {
2381 if let words { return Generator.passphrase(words: words) }
2382 if generate { return Generator.password(length: length ?? Generator.defaultLength) }
2383 return nil
2384 }
2385}
2386
2387struct Add: ParsableCommand {
2388 static let configuration = CommandConfiguration(abstract: "Add an entry.")
2389
2390 @OptionGroup var global: GlobalOptions
2391 @Argument(help: "Entry name. Names may repeat; the printed id is unique.") var name: String
2392 @Option(name: [.short, .customLong("username")], help: "Username.") var username: String?
2393 @Option(name: .long, help: "URL.") var url: String?
2394 @Option(name: .long, help: "Notes.") var notes: String?
2395 @Option(name: .long, help: "Tag. Repeatable.") var tag: [String] = []
2396 @OptionGroup var password: PasswordOptions
2397
2398 func run() throws {
2399 var open = try OpenVault.load(global)
2400 let secret = try password.newPassword() ?? PasswordInput.read(prompt: "Password: ")
2401 var entry = Entry(name: name, username: username, password: secret, url: url,
2402 notes: notes, tags: tag)
2403 while open.vault.entry(id: entry.id) != nil {
2404 entry = Entry(name: name, username: username, password: secret, url: url,
2405 notes: notes, tags: tag)
2406 }
2407 try open.vault.add(entry)
2408 try open.save()
2409 print(entry.id.rawValue)
2410 }
2411}
2412```
2413
2414- [ ] **Step 5: Write Commands/Show.swift**
2415
2416```swift
2417import ArgumentParser
2418import KeycaskCore
2419
2420struct Show: ParsableCommand {
2421 static let configuration = CommandConfiguration(abstract: "Show an entry.")
2422
2423 @OptionGroup var global: GlobalOptions
2424 @Argument(help: "Entry id or name.") var ref: String
2425 @Flag(name: .long, help: "Show the password.") var reveal = false
2426 @Option(name: .long, help: "Print one field, unmasked.") var field: String?
2427 @Flag(name: .long, help: "JSON output.") var json = false
2428
2429 func run() throws {
2430 let open = try OpenVault.load(global)
2431 let entry = try open.vault.resolve(ref)
2432 if let field {
2433 print(try Output.field(entry, named: field))
2434 } else if json {
2435 print(try Output.json(entry, reveal: reveal), terminator: "")
2436 } else {
2437 print(Output.text(entry, reveal: reveal), terminator: "")
2438 }
2439 }
2440}
2441```
2442
2443- [ ] **Step 6: Register the subcommands**
2444
2445In `Keycask.swift`: `subcommands: [Init.self, Add.self, Show.self]`.
2446
2447- [ ] **Step 7: Amend the spec**
2448
2449In `docs/superpowers/specs/2026-09-17-keycask-design.md`, after the sentence beginning "Passphrase input:", add a paragraph:
2450
2451```
2452Password input for `add` and `edit --password`: on a terminal, a hidden
2453prompt. Without a terminal, the first line of stdin. Neither available is
2454exit 2.
2455```
2456
2457- [ ] **Step 8: Run tests**
2458
2459Run: `swift test --filter AddShowTests`
2460Expected: 10 tests pass.
2461
2462- [ ] **Step 9: Lint and commit**
2463
2464```bash
2465swift format lint --strict --recursive Sources Tests
2466git add Sources/keycask Tests/KeycaskCLITests docs
2467git commit -m "Add add and show commands with masked output"
2468```
2469
2470---
2471
2472### Task 11: `ls` and `find`
2473
2474**Files:**
2475- Create: `Sources/keycask/Commands/Ls.swift`
2476- Create: `Sources/keycask/Commands/Find.swift`
2477- Modify: `Sources/keycask/Keycask.swift`
2478- Create: `Tests/KeycaskCLITests/LsFindTests.swift`
2479
2480**Interfaces:**
2481- Consumes: `OpenVault`, `Output.table`, `Output.json(_:[Entry])`, `Vault.filter(tag:)`, `Vault.search`, `Vault.sortedEntries`.
2482
2483- [ ] **Step 1: Write the failing tests**
2484
2485```swift
2486import Foundation
2487import Testing
2488
2489@Suite struct LsFindTests {
2490 func seeded() throws -> CLI {
2491 let cli = try CLI.initialized()
2492 try cli.run(["add", "github", "-u", "cmc", "--url", "https://github.com", "--tag", "Dev", "--generate"])
2493 try cli.run(["add", "bank", "--url", "https://bank.example", "--notes", "downtown branch", "--generate"])
2494 try cli.run(["add", "Alpha", "--tag", "dev", "--generate"])
2495 return cli
2496 }
2497
2498 @Test func lsSortsByNameAndShowsColumns() throws {
2499 let cli = try seeded()
2500 let r = try cli.run(["ls"])
2501 #expect(r.status == 0)
2502 let names = r.lines.map { String($0.split(separator: " ", omittingEmptySubsequences: true)[1]) }
2503 #expect(names == ["Alpha", "bank", "github"])
2504 #expect(r.stdout.contains("cmc"))
2505 #expect(r.stdout.contains("https://github.com"))
2506 }
2507
2508 @Test func lsTagFilterIsCaseInsensitive() throws {
2509 let cli = try seeded()
2510 let r = try cli.run(["ls", "--tag", "DEV"])
2511 #expect(r.lines.count == 2)
2512 #expect(!r.stdout.contains("bank"))
2513 }
2514
2515 @Test func lsJsonIsAnArrayWithMaskedPasswords() throws {
2516 let cli = try seeded()
2517 let r = try cli.run(["ls", "--json"])
2518 let arr = try JSONSerialization.jsonObject(with: Data(r.stdout.utf8)) as! [[String: Any]]
2519 #expect(arr.count == 3)
2520 #expect(arr.allSatisfy { $0["password"] as? String == "********" })
2521 }
2522
2523 @Test func emptyVaultListsNothing() throws {
2524 let cli = try CLI.initialized()
2525 let r = try cli.run(["ls"])
2526 #expect(r.status == 0)
2527 #expect(r.stdout == "")
2528 let j = try cli.run(["ls", "--json"])
2529 #expect(j.stdout.trimmingCharacters(in: .whitespacesAndNewlines) == "[]")
2530 }
2531
2532 @Test func findMatchesNotesURLTagsCaseInsensitively() throws {
2533 let cli = try seeded()
2534 #expect(try cli.run(["find", "DOWNTOWN"]).lines.count == 1)
2535 #expect(try cli.run(["find", "github.com"]).lines.count == 1)
2536 #expect(try cli.run(["find", "dev"]).lines.count == 2)
2537 let none = try cli.run(["find", "zzz"])
2538 #expect(none.status == 0)
2539 #expect(none.stdout == "")
2540 }
2541
2542 @Test func findJson() throws {
2543 let cli = try seeded()
2544 let r = try cli.run(["find", "bank", "--json"])
2545 let arr = try JSONSerialization.jsonObject(with: Data(r.stdout.utf8)) as! [[String: Any]]
2546 #expect(arr.count == 1)
2547 #expect(arr[0]["name"] as? String == "bank")
2548 }
2549}
2550```
2551
2552- [ ] **Step 2: Run tests to verify they fail**
2553
2554Run: `swift test --filter LsFindTests`
2555Expected: failures, unknown subcommand.
2556
2557- [ ] **Step 3: Write Ls.swift and Find.swift**
2558
2559`Commands/Ls.swift`:
2560
2561```swift
2562import ArgumentParser
2563import KeycaskCore
2564
2565struct Ls: ParsableCommand {
2566 static let configuration = CommandConfiguration(abstract: "List entries.")
2567
2568 @OptionGroup var global: GlobalOptions
2569 @Option(name: .long, help: "Only entries with this tag.") var tag: String?
2570 @Flag(name: .long, help: "JSON output.") var json = false
2571
2572 func run() throws {
2573 let open = try OpenVault.load(global)
2574 let entries = tag.map { open.vault.filter(tag: $0) } ?? open.vault.sortedEntries
2575 if json {
2576 print(try Output.json(entries, reveal: false), terminator: "")
2577 } else {
2578 print(Output.table(entries), terminator: "")
2579 }
2580 }
2581}
2582```
2583
2584`Commands/Find.swift`:
2585
2586```swift
2587import ArgumentParser
2588import KeycaskCore
2589
2590struct Find: ParsableCommand {
2591 static let configuration = CommandConfiguration(abstract: "Search entries.")
2592
2593 @OptionGroup var global: GlobalOptions
2594 @Argument(help: "Case-insensitive substring.") var query: String
2595 @Flag(name: .long, help: "JSON output.") var json = false
2596
2597 func run() throws {
2598 let open = try OpenVault.load(global)
2599 let entries = open.vault.search(query)
2600 if json {
2601 print(try Output.json(entries, reveal: false), terminator: "")
2602 } else {
2603 print(Output.table(entries), terminator: "")
2604 }
2605 }
2606}
2607```
2608
2609Register both: `subcommands: [Init.self, Add.self, Show.self, Ls.self, Find.self]`.
2610
2611- [ ] **Step 4: Run tests**
2612
2613Run: `swift test --filter LsFindTests`
2614Expected: 6 tests pass.
2615
2616- [ ] **Step 5: Lint and commit**
2617
2618```bash
2619swift format lint --strict --recursive Sources Tests
2620git add Sources/keycask Tests/KeycaskCLITests
2621git commit -m "Add ls and find commands"
2622```
2623
2624---
2625
2626### Task 12: `edit` and `rm`
2627
2628**Files:**
2629- Create: `Sources/keycask/Commands/Edit.swift`
2630- Create: `Sources/keycask/Commands/Rm.swift`
2631- Modify: `Sources/keycask/Keycask.swift`
2632- Create: `Tests/KeycaskCLITests/EditRmTests.swift`
2633
2634**Interfaces:**
2635- Consumes: `OpenVault`, `Vault.update`, `Vault.remove`, `PasswordOptions`, `PasswordInput`, `Terminal.confirm`, `Terminal.stdinIsTTY`.
2636
2637- [ ] **Step 1: Write the failing tests**
2638
2639```swift
2640import Foundation
2641import Testing
2642
2643@Suite struct EditRmTests {
2644 @Test func editChangesFieldsAndBumpsUpdated() throws {
2645 let cli = try CLI.initialized()
2646 try cli.run(["add", "gh", "--tag", "a", "--generate"])
2647 let before = try JSONSerialization.jsonObject(
2648 with: Data(try cli.run(["show", "gh", "--json"]).stdout.utf8)) as! [String: Any]
2649 let r = try cli.run([
2650 "edit", "gh", "--name", "github", "-u", "cmc", "--url", "https://x", "--notes", "n",
2651 "--tag", "b", "--untag", "a",
2652 ])
2653 #expect(r.status == 0)
2654 let after = try JSONSerialization.jsonObject(
2655 with: Data(try cli.run(["show", "github", "--json"]).stdout.utf8)) as! [String: Any]
2656 #expect(after["name"] as? String == "github")
2657 #expect(after["username"] as? String == "cmc")
2658 #expect(after["url"] as? String == "https://x")
2659 #expect(after["notes"] as? String == "n")
2660 #expect(after["tags"] as? [String] == ["b"])
2661 #expect(after["created"] as? String == before["created"] as? String)
2662 #expect(after["id"] as? String == before["id"] as? String)
2663 }
2664
2665 @Test func editPasswordFromStdinAndGenerate() throws {
2666 let cli = try CLI.initialized()
2667 try cli.run(["add", "gh", "--generate"])
2668 try cli.run(["edit", "gh", "--password"], stdin: "newpass\n")
2669 #expect(try cli.run(["show", "gh", "--field", "password"]).stdout == "newpass\n")
2670 try cli.run(["edit", "gh", "--generate", "--length", "30"])
2671 #expect(try cli.run(["show", "gh", "--field", "password"]).stdout.count == 31)
2672 }
2673
2674 @Test func editWithNoChangesIsUsageError() throws {
2675 let cli = try CLI.initialized()
2676 try cli.run(["add", "gh", "--generate"])
2677 let r = try cli.run(["edit", "gh"])
2678 #expect(r.status == 2)
2679 }
2680
2681 @Test func editUnknownIsNotFound() throws {
2682 let cli = try CLI.initialized()
2683 #expect(try cli.run(["edit", "nope", "--url", "x"]).status == 3)
2684 }
2685
2686 @Test func rmWithYesRemoves() throws {
2687 let cli = try CLI.initialized()
2688 let id = try cli.run(["add", "gh", "--generate"]).stdout.trimmingCharacters(in: .newlines)
2689 let r = try cli.run(["rm", id, "--yes"])
2690 #expect(r.status == 0)
2691 #expect(try cli.run(["show", id]).status == 3)
2692 #expect(try cli.run(["ls"]).stdout == "")
2693 }
2694
2695 @Test func rmWithoutYesAndWithoutTTYIsUsageError() throws {
2696 let cli = try CLI.initialized()
2697 try cli.run(["add", "gh", "--generate"])
2698 let r = try cli.run(["rm", "gh"], stdin: "y\n")
2699 #expect(r.status == 2)
2700 #expect(r.stderr.contains("--yes"))
2701 #expect(try cli.run(["ls"]).lines.count == 1)
2702 }
2703
2704 @Test func rmAmbiguousNameLists() throws {
2705 let cli = try CLI.initialized()
2706 try cli.run(["add", "gh", "--generate"])
2707 try cli.run(["add", "gh", "--generate"])
2708 let r = try cli.run(["rm", "gh", "--yes"])
2709 #expect(r.status == 5)
2710 #expect(try cli.run(["ls"]).lines.count == 2)
2711 }
2712}
2713```
2714
2715- [ ] **Step 2: Run tests to verify they fail**
2716
2717Run: `swift test --filter EditRmTests`
2718Expected: failures, unknown subcommand.
2719
2720- [ ] **Step 3: Write Edit.swift**
2721
2722```swift
2723import ArgumentParser
2724import KeycaskCore
2725
2726struct Edit: ParsableCommand {
2727 static let configuration = CommandConfiguration(abstract: "Change an entry.")
2728
2729 @OptionGroup var global: GlobalOptions
2730 @Argument(help: "Entry id or name.") var ref: String
2731 @Option(name: .long, help: "New name.") var name: String?
2732 @Option(name: [.short, .customLong("username")], help: "New username.") var username: String?
2733 @Option(name: .long, help: "New URL.") var url: String?
2734 @Option(name: .long, help: "New notes.") var notes: String?
2735 @Option(name: .long, help: "Add a tag. Repeatable.") var tag: [String] = []
2736 @Option(name: .long, help: "Remove a tag. Repeatable.") var untag: [String] = []
2737 @Flag(name: .long, help: "Prompt for a new password.") var password = false
2738 @OptionGroup var generated: PasswordOptions
2739
2740 mutating func validate() throws {
2741 let changes = [name, username, url, notes].contains { $0 != nil }
2742 || !tag.isEmpty || !untag.isEmpty || password || generated.generate || generated.words != nil
2743 guard changes else { throw ValidationError("nothing to change") }
2744 if password, generated.newPassword() != nil {
2745 throw ValidationError("--password cannot be combined with --generate or --words")
2746 }
2747 }
2748
2749 func run() throws {
2750 var open = try OpenVault.load(global)
2751 let target = try open.vault.resolve(ref)
2752 let newSecret: String? =
2753 password ? try PasswordInput.read(prompt: "New password: ") : generated.newPassword()
2754 try open.vault.update(id: target.id) { e in
2755 if let name { e.name = name }
2756 if let username { e.username = username }
2757 if let url { e.url = url }
2758 if let notes { e.notes = notes }
2759 if let newSecret { e.password = newSecret }
2760 let drop = Set(untag.map { $0.lowercased() })
2761 e.tags = e.tags.filter { !drop.contains($0.lowercased()) } + tag
2762 }
2763 try open.save()
2764 }
2765}
2766```
2767
2768- [ ] **Step 4: Write Rm.swift**
2769
2770```swift
2771import ArgumentParser
2772import KeycaskCore
2773
2774struct Rm: ParsableCommand {
2775 static let configuration = CommandConfiguration(abstract: "Remove an entry.")
2776
2777 @OptionGroup var global: GlobalOptions
2778 @Argument(help: "Entry id or name.") var ref: String
2779 @Flag(name: .long, help: "Do not ask for confirmation.") var yes = false
2780
2781 func run() throws {
2782 var open = try OpenVault.load(global)
2783 let target = try open.vault.resolve(ref)
2784 if !yes {
2785 guard Terminal.stdinIsTTY else {
2786 throw KeycaskError.usage("refusing to remove without --yes when not on a terminal")
2787 }
2788 guard Terminal.confirm("remove \(target.name) (\(target.id.rawValue))?") else {
2789 throw KeycaskError.failure("aborted")
2790 }
2791 }
2792 try open.vault.remove(id: target.id)
2793 try open.save()
2794 }
2795}
2796```
2797
2798Register: `subcommands: [Init.self, Add.self, Show.self, Ls.self, Find.self, Edit.self, Rm.self]`.
2799
2800- [ ] **Step 5: Run tests**
2801
2802Run: `swift test --filter EditRmTests`
2803Expected: 7 tests pass.
2804
2805- [ ] **Step 6: Lint and commit**
2806
2807```bash
2808swift format lint --strict --recursive Sources Tests
2809git add Sources/keycask Tests/KeycaskCLITests
2810git commit -m "Add edit and rm commands"
2811```
2812
2813---
2814
2815### Task 13: `generate`
2816
2817**Files:**
2818- Create: `Sources/keycask/Commands/Generate.swift`
2819- Modify: `Sources/keycask/Keycask.swift`
2820- Create: `Tests/KeycaskCLITests/GenerateTests.swift`
2821
2822**Interfaces:**
2823- Consumes: `Generator`. `--copy` calls `Clipboard.copyWithTimeout`, which does not exist until Task 14; in this task `--copy` is declared but `run()` throws `KeycaskError.failure("clipboard not available")` when it is set. Task 14 replaces that line.
2824
2825- [ ] **Step 1: Write the failing tests**
2826
2827```swift
2828import Foundation
2829import Testing
2830
2831@Suite struct GenerateTests {
2832 @Test func defaultIs24Characters() throws {
2833 let cli = try CLI()
2834 let r = try cli.run(["generate"], passphrase: nil)
2835 #expect(r.status == 0)
2836 #expect(r.stdout.count == 25)
2837 }
2838
2839 @Test func lengthAndWords() throws {
2840 let cli = try CLI()
2841 #expect(try cli.run(["generate", "--length", "12"], passphrase: nil).stdout.count == 13)
2842 let w = try cli.run(["generate", "--words", "6"], passphrase: nil).stdout
2843 .trimmingCharacters(in: .newlines).split(separator: "-")
2844 #expect(w.count == 6)
2845 }
2846
2847 @Test func doesNotNeedAVault() throws {
2848 let cli = try CLI()
2849 #expect(!FileManager.default.fileExists(atPath: cli.vault.path))
2850 #expect(try cli.run(["generate"], passphrase: nil).status == 0)
2851 }
2852
2853 @Test func lengthAndWordsTogetherIsUsageError() throws {
2854 let cli = try CLI()
2855 #expect(try cli.run(["generate", "--length", "3", "--words", "3"], passphrase: nil).status == 2)
2856 }
2857}
2858```
2859
2860- [ ] **Step 2: Run tests to verify they fail**
2861
2862Run: `swift test --filter GenerateTests`
2863Expected: failures, unknown subcommand.
2864
2865- [ ] **Step 3: Write Generate.swift**
2866
2867```swift
2868import ArgumentParser
2869import KeycaskCore
2870
2871struct Generate: ParsableCommand {
2872 static let configuration = CommandConfiguration(abstract: "Generate a password.")
2873
2874 @Option(name: .long, help: "Password length (default 24).") var length: Int?
2875 @Option(name: .long, help: "Passphrase of this many words instead.") var words: Int?
2876 @Flag(name: .long, help: "Copy to the clipboard instead of printing.") var copy = false
2877
2878 mutating func validate() throws {
2879 if length != nil, words != nil {
2880 throw ValidationError("--length and --words are mutually exclusive")
2881 }
2882 if let length, length < 1 { throw ValidationError("--length must be at least 1") }
2883 if let words, words < 1 { throw ValidationError("--words must be at least 1") }
2884 }
2885
2886 func run() throws {
2887 let secret =
2888 words.map { Generator.passphrase(words: $0) }
2889 ?? Generator.password(length: length ?? Generator.defaultLength)
2890 if copy {
2891 throw KeycaskError.failure("clipboard not available")
2892 }
2893 print(secret)
2894 }
2895}
2896```
2897
2898Register: add `Generate.self` to the subcommand list.
2899
2900- [ ] **Step 4: Run tests**
2901
2902Run: `swift test --filter GenerateTests`
2903Expected: 4 tests pass.
2904
2905- [ ] **Step 5: Lint and commit**
2906
2907```bash
2908swift format lint --strict --recursive Sources Tests
2909git add Sources/keycask Tests/KeycaskCLITests
2910git commit -m "Add generate command"
2911```
2912
2913---
2914
2915### Task 14: Clipboard, `clip`, daemon, `generate --copy`
2916
2917**Files:**
2918- Create: `Sources/keycask/Clipboard.swift`
2919- Create: `Sources/keycask/Commands/Clip.swift`
2920- Create: `Sources/keycask/Commands/ClipboardDaemon.swift`
2921- Modify: `Sources/keycask/Commands/Generate.swift`
2922- Modify: `Sources/keycask/Keycask.swift`
2923- Create: `Tests/KeycaskCLITests/ClipboardTests.swift`
2924
2925**Interfaces:**
2926- Produces:
2927
2928```swift
2929enum Clipboard {
2930 struct Handoff: Codable, Equatable { var secret: String; var previous: String }
2931 struct Tool { let copy: [String]; let paste: [String] }
2932 static let timeoutSeconds = 45
2933 static func shouldRestore(secret: String, current: String?) -> Bool
2934 static func findTool(path: String, fileManager: FileManager = .default) -> Tool?
2935 static func read() throws -> String
2936 static func write(_ text: String) throws
2937 static func copyWithTimeout(_ secret: String, seconds: Int = timeoutSeconds) throws
2938 static func runDaemon(seconds: Int) throws
2939}
2940```
2941
2942The tests never touch the real clipboard. They cover the restore decision, tool discovery against a fake PATH, and the daemon's handoff parsing.
2943
2944- [ ] **Step 1: Write the failing tests**
2945
2946`Tests/KeycaskCLITests/ClipboardTests.swift`:
2947
2948```swift
2949import Foundation
2950import Testing
2951
2952@testable import keycask
2953
2954@Suite struct ClipboardTests {
2955 @Test func restoresOnlyWhenClipboardStillHoldsTheSecret() {
2956 #expect(Clipboard.shouldRestore(secret: "s", current: "s"))
2957 #expect(!Clipboard.shouldRestore(secret: "s", current: "user pasted"))
2958 #expect(!Clipboard.shouldRestore(secret: "s", current: nil))
2959 }
2960
2961 @Test func handoffRoundTrips() throws {
2962 let h = Clipboard.Handoff(secret: "s3cret", previous: "old")
2963 let data = try JSONEncoder().encode(h)
2964 #expect(try JSONDecoder().decode(Clipboard.Handoff.self, from: data) == h)
2965 }
2966
2967 @Test func findToolScansPathInOrder() throws {
2968 let dir = FileManager.default.temporaryDirectory
2969 .appendingPathComponent("keycask-clip-\(UUID().uuidString)")
2970 try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
2971 #expect(Clipboard.findTool(path: dir.path) == nil)
2972
2973 #if os(macOS)
2974 let names = ["pbcopy", "pbpaste"]
2975 #elseif os(Windows)
2976 let names = ["clip.exe", "powershell.exe"]
2977 #else
2978 let names = ["xclip"]
2979 #endif
2980 for n in names {
2981 let f = dir.appendingPathComponent(n)
2982 try Data("#!/bin/sh\n".utf8).write(to: f)
2983 try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: f.path)
2984 }
2985 let tool = Clipboard.findTool(path: dir.path)
2986 #expect(tool != nil)
2987 #expect(tool?.copy.first?.hasPrefix(dir.path) == true)
2988 }
2989
2990 @Test func daemonWithoutHandoffFails() throws {
2991 let cli = try CLI()
2992 let r = try cli.run(["clipboard-daemon", "1"], stdin: "not json", passphrase: nil)
2993 #expect(r.status == 1)
2994 }
2995
2996 @Test func daemonIsHiddenFromHelp() throws {
2997 let cli = try CLI()
2998 let r = try cli.run(["--help"], passphrase: nil)
2999 #expect(!r.stdout.contains("clipboard-daemon"))
3000 #expect(r.stdout.contains("clip"))
3001 }
3002
3003 @Test func clipOfMissingEntryIsNotFoundBeforeTouchingClipboard() throws {
3004 let cli = try CLI.initialized()
3005 let r = try cli.run(["clip", "nope"])
3006 #expect(r.status == 3)
3007 }
3008}
3009```
3010
3011- [ ] **Step 2: Run tests to verify they fail**
3012
3013Run: `swift test --filter ClipboardTests`
3014Expected: compile error, `Clipboard` not found.
3015
3016- [ ] **Step 3: Write Clipboard.swift**
3017
3018```swift
3019import Foundation
3020import KeycaskCore
3021
3022enum Clipboard {
3023 struct Handoff: Codable, Equatable {
3024 var secret: String
3025 var previous: String
3026 }
3027
3028 struct Tool: Equatable {
3029 let copy: [String]
3030 let paste: [String]
3031 }
3032
3033 static let timeoutSeconds = 45
3034
3035 static func shouldRestore(secret: String, current: String?) -> Bool {
3036 current == secret
3037 }
3038
3039 static func findTool(
3040 path: String = ProcessInfo.processInfo.environment["PATH"] ?? "",
3041 fileManager: FileManager = .default
3042 ) -> Tool? {
3043 #if os(Windows)
3044 let separator: Character = ";"
3045 let candidates: [(copy: [String], paste: [String])] = [
3046 (["clip.exe"], ["powershell.exe", "-NoProfile", "-Command", "Get-Clipboard -Raw"])
3047 ]
3048 #elseif os(macOS)
3049 let separator: Character = ":"
3050 let candidates: [(copy: [String], paste: [String])] = [(["pbcopy"], ["pbpaste"])]
3051 #else
3052 let separator: Character = ":"
3053 let candidates: [(copy: [String], paste: [String])] = [
3054 (["wl-copy"], ["wl-paste", "--no-newline"]),
3055 (["xclip", "-selection", "clipboard"], ["xclip", "-selection", "clipboard", "-o"]),
3056 ]
3057 #endif
3058 let dirs = path.split(separator: separator).map(String.init)
3059 func locate(_ name: String) -> String? {
3060 for dir in dirs {
3061 let full = URL(fileURLWithPath: dir).appendingPathComponent(name).path
3062 if fileManager.isExecutableFile(atPath: full) { return full }
3063 }
3064 return nil
3065 }
3066 for candidate in candidates {
3067 guard let copy = locate(candidate.copy[0]), let paste = locate(candidate.paste[0]) else {
3068 continue
3069 }
3070 return Tool(
3071 copy: [copy] + candidate.copy.dropFirst(),
3072 paste: [paste] + candidate.paste.dropFirst())
3073 }
3074 return nil
3075 }
3076
3077 static func read() throws -> String {
3078 let tool = try requireTool()
3079 let (status, output) = try runTool(tool.paste, input: nil)
3080 guard status == 0 else { return "" }
3081 return output
3082 }
3083
3084 static func write(_ text: String) throws {
3085 let tool = try requireTool()
3086 let (status, _) = try runTool(tool.copy, input: text)
3087 guard status == 0 else { throw KeycaskError.failure("clipboard tool failed") }
3088 }
3089
3090 static func copyWithTimeout(_ secret: String, seconds: Int = timeoutSeconds) throws {
3091 _ = try requireTool()
3092 let handoff = Handoff(secret: secret, previous: try read())
3093 let process = Process()
3094 process.executableURL = Bundle.main.executableURL
3095 process.arguments = ["clipboard-daemon", String(seconds)]
3096 process.standardOutput = FileHandle.nullDevice
3097 process.standardError = FileHandle.nullDevice
3098 let input = Pipe()
3099 process.standardInput = input
3100 do {
3101 try process.run()
3102 input.fileHandleForWriting.write(try JSONEncoder().encode(handoff))
3103 try input.fileHandleForWriting.close()
3104 } catch {
3105 throw KeycaskError.failure("start clipboard daemon: \(error)")
3106 }
3107 }
3108
3109 static func runDaemon(seconds: Int) throws {
3110 let data = FileHandle.standardInput.readDataToEndOfFile()
3111 let handoff: Handoff
3112 do {
3113 handoff = try JSONDecoder().decode(Handoff.self, from: data)
3114 } catch {
3115 throw KeycaskError.failure("bad handoff")
3116 }
3117 try write(handoff.secret)
3118 Thread.sleep(forTimeInterval: TimeInterval(seconds))
3119 let current = try? read()
3120 guard shouldRestore(secret: handoff.secret, current: current) else { return }
3121 try write(handoff.previous)
3122 }
3123
3124 private static func requireTool() throws -> Tool {
3125 guard let tool = findTool() else {
3126 #if os(Windows)
3127 let hint = "clip.exe and powershell.exe"
3128 #elseif os(macOS)
3129 let hint = "pbcopy and pbpaste"
3130 #else
3131 let hint = "wl-clipboard or xclip"
3132 #endif
3133 throw KeycaskError.failure("no clipboard tool found: install \(hint)")
3134 }
3135 return tool
3136 }
3137
3138 private static func runTool(_ argv: [String], input: String?) throws -> (Int32, String) {
3139 let process = Process()
3140 process.executableURL = URL(fileURLWithPath: argv[0])
3141 process.arguments = Array(argv.dropFirst())
3142 let out = Pipe()
3143 process.standardOutput = out
3144 process.standardError = FileHandle.nullDevice
3145 let inPipe = Pipe()
3146 process.standardInput = inPipe
3147 do {
3148 try process.run()
3149 } catch {
3150 throw KeycaskError.failure("run \(argv[0]): \(error)")
3151 }
3152 if let input { inPipe.fileHandleForWriting.write(Data(input.utf8)) }
3153 try? inPipe.fileHandleForWriting.close()
3154 let data = out.fileHandleForReading.readDataToEndOfFile()
3155 process.waitUntilExit()
3156 return (process.terminationStatus, String(decoding: data, as: UTF8.self))
3157 }
3158}
3159```
3160
3161- [ ] **Step 4: Write Clip.swift and ClipboardDaemon.swift**
3162
3163`Commands/Clip.swift`:
3164
3165```swift
3166import ArgumentParser
3167import KeycaskCore
3168
3169struct Clip: ParsableCommand {
3170 static let configuration = CommandConfiguration(
3171 abstract: "Copy a field to the clipboard. Clears after \(Clipboard.timeoutSeconds) seconds.")
3172
3173 @OptionGroup var global: GlobalOptions
3174 @Argument(help: "Entry id or name.") var ref: String
3175 @Option(name: .long, help: "Field to copy (default password).") var field = "password"
3176
3177 func run() throws {
3178 let open = try OpenVault.load(global)
3179 let entry = try open.vault.resolve(ref)
3180 let value = try Output.field(entry, named: field)
3181 try Clipboard.copyWithTimeout(value)
3182 print("copied \(field) of \(entry.name); clears in \(Clipboard.timeoutSeconds)s")
3183 }
3184}
3185```
3186
3187`Commands/ClipboardDaemon.swift`:
3188
3189```swift
3190import ArgumentParser
3191
3192struct ClipboardDaemon: ParsableCommand {
3193 static let configuration = CommandConfiguration(
3194 commandName: "clipboard-daemon", shouldDisplay: false)
3195
3196 @Argument var seconds: Int
3197
3198 func run() throws {
3199 try Clipboard.runDaemon(seconds: seconds)
3200 }
3201}
3202```
3203
3204In `Generate.swift` replace the `throw KeycaskError.failure("clipboard not available")` line with:
3205
3206```swift
3207try Clipboard.copyWithTimeout(secret)
3208print("copied; clears in \(Clipboard.timeoutSeconds)s")
3209return
3210```
3211
3212Register: `subcommands: [Init.self, Add.self, Show.self, Ls.self, Find.self, Edit.self, Rm.self, Generate.self, Clip.self, ClipboardDaemon.self]`.
3213
3214- [ ] **Step 5: Run tests**
3215
3216Run: `swift test --filter ClipboardTests`
3217Expected: 6 tests pass.
3218
3219- [ ] **Step 6: Manual check on macOS**
3220
3221```bash
3222swift build && KEYCASK_VAULT=/tmp/kc-manual.kc KEYCASK_PASSPHRASE=pw .build/debug/keycask init
3223KEYCASK_VAULT=/tmp/kc-manual.kc KEYCASK_PASSPHRASE=pw .build/debug/keycask add t --generate
3224KEYCASK_VAULT=/tmp/kc-manual.kc KEYCASK_PASSPHRASE=pw .build/debug/keycask clip t && pbpaste | wc -c
3225sleep 46 && pbpaste | wc -c
3226rm /tmp/kc-manual.kc
3227```
3228
3229Expected: first `wc -c` prints 24, second prints the length of whatever was on the clipboard before (0 if it was empty). Record the actual output in the commit message body if it differs.
3230
3231- [ ] **Step 7: Lint and commit**
3232
3233```bash
3234swift format lint --strict --recursive Sources Tests
3235git add Sources/keycask Tests/KeycaskCLITests
3236git commit -m "Add clipboard support: clip, generate --copy, timed clear"
3237```
3238
3239---
3240
3241### Task 15: README, full verification, CLI merge request
3242
3243**Files:**
3244- Modify: `README.md`
3245
3246- [ ] **Step 1: Write the README**
3247
3248````markdown
3249# keycask
3250
3251Command-line password manager. One passphrase-encrypted vault file.
3252Swift, runs on macOS, Linux, and Windows.
3253
3254## install
3255
3256```sh
3257swift build -c release
3258cp .build/release/keycask ~/.local/bin/
3259```
3260
3261## use
3262
3263```sh
3264keycask init
3265keycask add github -u cmc --url https://github.com --tag dev --generate
3266keycask add mail --words 6
3267keycask add bank # prompts for the password
3268keycask show github # password masked
3269keycask show github --reveal
3270keycask show github --field password # raw value, for scripts
3271keycask clip github # clipboard, clears after 45s
3272keycask ls --tag dev
3273keycask find example
3274keycask edit github --tag work --untag dev
3275keycask rm github --yes
3276keycask generate --words 5 --copy
3277```
3278
3279Every read command takes `--json`. Passwords are masked unless `--reveal`.
3280
3281Names are labels and may repeat. Every command that takes a name also
3282takes the entry's 8-character id, which `ls` and `add` print. An
3283ambiguous name lists the candidates.
3284
3285## files
3286
3287| what | default | override |
3288|---|---|---|
3289| vault | `~/.local/share/keycask/vault.kc` (`%LOCALAPPDATA%\keycask\vault.kc` on Windows) | `KEYCASK_VAULT`, `--vault` |
3290| passphrase | prompted | `KEYCASK_PASSPHRASE` |
3291
3292The vault is a JSON envelope: PBKDF2-HMAC-SHA256 (600000 rounds) over
3293the passphrase, ChaCha20-Poly1305 over the entries. Writes are atomic.
3294
3295## exit codes
3296
32970 ok, 1 failure, 2 usage, 3 not found, 4 cannot decrypt, 5 ambiguous name.
3298
3299## develop
3300
3301```sh
3302swift build
3303swift test
3304swift format lint --strict --recursive Sources Tests
3305```
3306
3307Design: `docs/superpowers/specs/2026-09-17-keycask-design.md`.
3308````
3309
3310- [ ] **Step 2: Full suite, lint, release build**
3311
3312Run: `swift test && swift format lint --strict --recursive Sources Tests Package.swift && swift build -c release`
3313Expected: all tests pass, no lint output, release binary at `.build/release/keycask`.
3314
3315- [ ] **Step 3: Commit, push, open MR**
3316
3317```bash
3318git add README.md
3319git commit -m "Write README for the CLI"
3320git push -u origin cli
3321gitbay mr create --source cli --target main --title "CLI: init, add, show, ls, find, edit, rm, generate, clip" --file - <<'EOF'
3322ArgumentParser commands over KeycaskCore. Paths, hidden passphrase prompt,
3323atomic writes, shell-out clipboard with timed clear. Black-box CLI tests
3324cover every command and exit code.
3325EOF
3326```
3327
3328- [ ] **Step 4: Wait for CI, merge, clean up**
3329
3330Run `gitbay build list --json` until green. Then:
3331
3332```bash
3333gitbay mr merge <n> --strategy squash
3334git switch main && git pull && git branch -D cli && git push origin --delete cli
3335```
3336
3337Do not merge red. If Linux CI fails on something platform-specific (a `Glibc` import, `posixPermissions`), fix it on the branch and push again.