├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── build-release.yml │ ├── create-release.yml │ └── test-build.yml ├── .gitignore ├── Inter Font License.txt ├── LICENSE ├── README.md ├── Taskfile.yml ├── assets ├── icons.go ├── password.png └── passworddark.png ├── backend └── app.go ├── build ├── Info.dev.plist ├── Info.plist ├── Taskfile.common.yml ├── Taskfile.darwin.yml ├── Taskfile.linux.yml ├── Taskfile.windows.yml ├── appicon.png ├── appimage │ └── build.sh ├── config.yml ├── icon.ico ├── icons.icns ├── info.json ├── nfpm │ ├── nfpm.yaml │ └── scripts │ │ ├── postinstall.sh │ │ ├── postremove.sh │ │ ├── preinstall.sh │ │ └── preremove.sh ├── nsis │ ├── MicrosoftEdgeWebview2Setup.exe │ ├── project.nsi │ └── wails_tools.nsh └── wails.exe.manifest ├── constants ├── CommmonVariables.go ├── FIlePathConstants.go ├── FilePathConstant_darwin.go ├── FilePathConstant_linux.go └── FilePathConstant_windows.go ├── frontend ├── README.md ├── bindings │ ├── clave │ │ ├── backend │ │ │ ├── app.js │ │ │ ├── index.js │ │ │ └── models.js │ │ └── objects │ │ │ ├── index.js │ │ │ └── models.js │ └── github.com │ │ └── wailsapp │ │ └── wails │ │ └── v3 │ │ └── pkg │ │ └── application │ │ ├── index.js │ │ └── models.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── src │ ├── app.css │ ├── app.d.ts │ ├── app.html │ ├── lib │ │ ├── components │ │ │ ├── about │ │ │ │ └── About.svelte │ │ │ ├── auth │ │ │ │ └── PinSetup.svelte │ │ │ ├── intro │ │ │ │ ├── Footer.svelte │ │ │ │ ├── Intro.svelte │ │ │ │ └── LoadingSpinner.svelte │ │ │ └── totp │ │ │ │ └── TotpList.svelte │ │ ├── index.ts │ │ ├── stores │ │ │ └── onboarding.ts │ │ ├── types │ │ │ ├── index.ts │ │ │ └── totp.ts │ │ └── utils │ │ │ ├── encoding.ts │ │ │ └── totp.ts │ └── routes │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ └── +page.svelte ├── static │ ├── Inter-Medium.ttf │ ├── favicon.png │ └── style.css ├── svelte.config.js ├── tailwind.config.js ├── tsconfig.json └── vite.config.ts ├── go.mod ├── go.sum ├── localstorage ├── PersistentStorage.go └── StorageHelpers.go ├── main.go ├── objects ├── TotpSecretObject.go ├── auth.go └── totp.go ├── package.json └── services ├── auth └── auth.go ├── totp └── totp.go └── window └── manager.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ansxuman -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Clave 2 | 3 | Thank you for considering contributing to Clave! We welcome contributions from the community. 4 | 5 | ## How to Contribute 6 | 7 | 1. Fork the repository. 8 | 2. Create a new branch (`git checkout -b feature/YourFeature`). 9 | 3. Make your changes. 10 | 4. Commit your changes (`git commit -m 'Add some feature'`). 11 | 5. Push to the branch (`git push origin feature/YourFeature`). 12 | 6. Open a pull request. 13 | 14 | ## Code of Conduct 15 | 16 | Please note that this project is released with a [Contributor Code of Conduct](https://www.contributor-covenant.org/version/2/0/code_of_conduct/). By participating in this project you agree to abide by its terms. 17 | 18 | ## Reporting Bugs 19 | 20 | Please use the [bug report template](./ISSUE_TEMPLATE/bug_report.md) to report any bugs you find. 21 | 22 | ## Requesting Features 23 | 24 | Please use the [feature request template](./ISSUE_TEMPLATE/feature_request.md) to suggest new features. 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | buy_me_a_coffee: ansxuman 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Report a bug or issue you've found in Clave 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | --- 8 | 9 | **Describe the Bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 1. Go to '...' 15 | 2. Click on '...' 16 | 3. Do '...' 17 | 4. See error 18 | 19 | **Expected Behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Environment:** 26 | - OS: [e.g. macOS, Windows, Linux] 27 | - OS Version: [e.g. 12.1] 28 | - Clave Version: [e.g. 1.0.0] 29 | 30 | **Additional Context** 31 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🤔 Questions & Support 4 | url: https://github.com/ansxuman/clave/discussions 5 | about: Please use GitHub Discussions for questions, support, and general discussion 6 | 7 | - name: 🐛 Bug Report 8 | url: https://github.com/ansxuman/clave/issues/new?assignees=&labels=bug&template=bug_report.md&title=%5BBUG%5D 9 | about: Report a bug or issue you've found in Clave 10 | 11 | - name: 💡 Feature Request 12 | url: https://github.com/ansxuman/clave/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=%5BFEATURE%5D 13 | about: Suggest new features or improvements for Clave -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: Suggest an idea or enhancement for Clave 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | --- 8 | 9 | **Problem Description** 10 | A clear and concise description of the problem or limitation you're facing. Ex. I'm always frustrated when [...] 11 | 12 | **Proposed Solution** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Alternative Solutions** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional Context** 19 | Add any other context, mockups, or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the changes and the related issue. Please also include relevant motivation and context. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | 16 | ## How Has This Been Tested? 17 | 18 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. 19 | 20 | - [ ] Test A 21 | - [ ] Test B 22 | 23 | ## Checklist: 24 | 25 | - [ ] My code follows the style guidelines of this project 26 | - [ ] I have performed a self-review of my code 27 | - [ ] I have commented my code, particularly in hard-to-understand areas 28 | - [ ] I have made corresponding changes to the documentation 29 | - [ ] My changes generate no new warnings 30 | - [ ] I have added tests that prove my fix is effective or that my feature works 31 | - [ ] New and existing unit tests pass locally with my changes 32 | - [ ] Any dependent changes have been merged and published in downstream modules -------------------------------------------------------------------------------- /.github/workflows/build-release.yml: -------------------------------------------------------------------------------- 1 | name: Build Release 2 | 3 | permissions: 4 | contents: write 5 | actions: write 6 | 7 | on: 8 | release: 9 | types: [created] 10 | workflow_dispatch: 11 | inputs: 12 | release_url: 13 | description: 'Release upload URL' 14 | required: true 15 | version: 16 | description: 'Version tag' 17 | required: true 18 | 19 | jobs: 20 | build-macos: 21 | name: Build macOS 22 | runs-on: macos-latest 23 | environment: Prod 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | ref: ${{ github.event.release.tag_name || inputs.version }} 28 | fetch-depth: 0 29 | 30 | - name: Set up Node.js 31 | uses: actions/setup-node@v4 32 | with: 33 | node-version: 'lts/*' 34 | 35 | - name: Set up Go 36 | uses: actions/setup-go@v4 37 | with: 38 | go-version: '1.23' 39 | cache: true 40 | 41 | - name: Install Task 42 | uses: arduino/setup-task@v2 43 | with: 44 | version: 3.x 45 | repo-token: ${{ secrets.GITHUB_TOKEN }} 46 | 47 | - name: Setup Wails Dependencies 48 | run: | 49 | mkdir -p ../github 50 | cd ../github 51 | git clone https://github.com/ansxuman/wails.git 52 | cd wails 53 | git checkout start_on_login 54 | cd v3/cmd/wails3 55 | go install 56 | cd ../../../.. 57 | 58 | - name: Update go.mod 59 | run: | 60 | sed -i '' 's|=> ../wails/v3|=> ../github/wails/v3|g' go.mod 61 | 62 | - name: Import Code-Signing Certificates 63 | uses: Apple-Actions/import-codesign-certs@v3 64 | with: 65 | p12-file-base64: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12_BASE64 }} 66 | p12-password: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} 67 | 68 | - name: Build and Sign Release Binaries 69 | run: | 70 | mkdir -p darwinBinaries 71 | wails3 doctor 72 | 73 | # Update build assets 74 | wails3 task common:update:build-assets 75 | 76 | # Build the app package 77 | task darwin:package:universal 78 | 79 | # Store signing identity and version in variables 80 | SIGN_IDENTITY="${{ secrets.APPLE_SIGNING_IDENTITY }}" 81 | VERSION="${{ github.event.release.tag_name || inputs.version }}" 82 | 83 | echo "Using version: $VERSION" 84 | 85 | # Sign the binary 86 | codesign --deep --force --verbose --options=runtime --sign "$SIGN_IDENTITY" bin/Clave.app/Contents/MacOS/Clave 87 | 88 | # Sign the app bundle 89 | codesign --deep --force --verbose --options=runtime --sign "$SIGN_IDENTITY" bin/Clave.app 90 | 91 | # Create DMG 92 | npm install --global create-dmg 93 | cd bin 94 | create-dmg Clave.app --dmg-title "Clave-${VERSION}" 95 | 96 | # Process and sign DMG 97 | # Move any .dmg file found to darwinBinaries with the correct name 98 | for dmg in *.dmg; do 99 | mv "$dmg" "../darwinBinaries/Clave-${VERSION}-universal.dmg" 100 | break 101 | done 102 | 103 | echo "universalDMGPath=darwinBinaries/Clave-${VERSION}-universal.dmg" >> $GITHUB_ENV 104 | 105 | cd .. 106 | ls -alh darwinBinaries 107 | 108 | - name: Notarize Release Binaries 109 | run: | 110 | xcrun notarytool submit ${{ env.universalDMGPath }} --apple-id ${{ secrets.MACOS_SIGNING_GON_USERNAME }} --team-id ${{secrets.APPLE_DEVELOPER_TEAM_ID}} --password ${{ secrets.MACOS_SIGNING_GON_APPLICATION_PASSWORD }} --verbose --wait 111 | xcrun stapler staple ${{ env.universalDMGPath }} 112 | 113 | - name: Upload Release Asset 114 | uses: actions/upload-release-asset@v1 115 | env: 116 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 117 | with: 118 | upload_url: ${{ github.event_name == 'workflow_dispatch' && inputs.release_url || github.event.release.upload_url }} 119 | asset_path: ${{ env.universalDMGPath }} 120 | asset_name: Clave-${{ github.event.release.tag_name || inputs.version }}-universal.dmg 121 | asset_content_type: application/x-apple-diskimage 122 | 123 | build-windows: 124 | name: Build Windows 125 | runs-on: windows-latest 126 | steps: 127 | - uses: actions/checkout@v4 128 | with: 129 | ref: ${{ github.event.release.tag_name || inputs.version }} 130 | 131 | - name: Set up Node.js 132 | uses: actions/setup-node@v4 133 | with: 134 | node-version: 'lts/*' 135 | 136 | - name: Set up Go 137 | uses: actions/setup-go@v4 138 | with: 139 | go-version: '1.23' 140 | cache: true 141 | 142 | - name: Install Task 143 | uses: arduino/setup-task@v2 144 | with: 145 | version: 3.x 146 | repo-token: ${{ secrets.GITHUB_TOKEN }} 147 | 148 | - name: Setup Wails Dependencies 149 | shell: cmd 150 | run: | 151 | mkdir ..\github 152 | cd ..\github 153 | git clone https://github.com/ansxuman/wails.git 154 | cd wails 155 | git checkout start_on_login 156 | cd v3\cmd\wails3 157 | go install 158 | cd ..\..\..\.. 159 | 160 | - name: Update go.mod 161 | shell: pwsh 162 | run: | 163 | (Get-Content go.mod) -replace 'replace.*=> ../wails/v3', 'replace github.com/wailsapp/wails/v3 => ../github/wails/v3' | Set-Content go.mod 164 | 165 | - name: Build Release Binaries 166 | shell: pwsh 167 | run: | 168 | mkdir windowsBinaries 169 | wails3 doctor 170 | 171 | # Update build assets 172 | wails3 task common:update:build-assets 173 | 174 | # Build the app package 175 | task package 176 | 177 | $VERSION="${{ github.event.release.tag_name || inputs.version }}" 178 | Move-Item -Path "bin\Clave-amd64-installer.exe" -Destination "windowsBinaries\Clave-Setup-${VERSION}-x64.exe" 179 | echo "WINDOWS_INSTALLER_PATH=windowsBinaries/Clave-Setup-${VERSION}-x64.exe" >> $env:GITHUB_ENV 180 | 181 | - name: Upload Release Asset 182 | uses: actions/upload-release-asset@v1 183 | env: 184 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 185 | with: 186 | upload_url: ${{ github.event_name == 'workflow_dispatch' && inputs.release_url || github.event.release.upload_url }} 187 | asset_path: ${{ env.WINDOWS_INSTALLER_PATH }} 188 | asset_name: Clave-Setup-${{ github.event.release.tag_name || inputs.version }}-x64.exe 189 | asset_content_type: application/vnd.microsoft.portable-executable 190 | 191 | build-linux: 192 | name: Build Linux 193 | runs-on: ubuntu-latest 194 | steps: 195 | - uses: actions/checkout@v4 196 | with: 197 | ref: ${{ github.event.release.tag_name || inputs.version }} 198 | 199 | - name: Set up Node.js 200 | uses: actions/setup-node@v4 201 | with: 202 | node-version: 'lts/*' 203 | 204 | - name: Set up Go 205 | uses: actions/setup-go@v4 206 | with: 207 | go-version: '1.23' 208 | cache: true 209 | 210 | - name: Install Task 211 | uses: arduino/setup-task@v2 212 | with: 213 | version: 3.x 214 | repo-token: ${{ secrets.GITHUB_TOKEN }} 215 | 216 | - name: Install System Dependencies 217 | run: | 218 | sudo apt-get update 219 | sudo apt-get install -y libwebkit2gtk-4.1-dev gcc libgtk-3-dev pkg-config 220 | 221 | - name: Setup Wails Dependencies 222 | run: | 223 | mkdir -p ../github 224 | cd ../github 225 | git clone https://github.com/ansxuman/wails.git 226 | cd wails 227 | git checkout start_on_login 228 | cd v3/cmd/wails3 229 | go install 230 | cd ../../../.. 231 | 232 | - name: Update go.mod 233 | run: | 234 | sed -i 's|=> ../wails/v3|=> ../github/wails/v3|g' go.mod 235 | 236 | - name: Build Release Binaries 237 | run: | 238 | mkdir -p linuxAppImage 239 | wails3 doctor 240 | 241 | # Update build assets 242 | wails3 task common:update:build-assets 243 | 244 | # Build the app package 245 | task package 246 | 247 | VERSION="${{ github.event.release.tag_name || inputs.version }}" 248 | mv bin/clave-x86_64.AppImage "linuxAppImage/clave_${VERSION}_amd64.AppImage" 249 | echo "LINUX_APP_PATH=linuxAppImage/clave_${VERSION}_amd64.AppImage" >> $GITHUB_ENV 250 | 251 | ls -alh linuxAppImage 252 | 253 | - name: Build deb using dpkg 254 | run: | 255 | VERSION="${{ github.event.release.tag_name || inputs.version }}" 256 | CLEAN_VERSION="${VERSION#v}" 257 | mkdir linuxDeb 258 | mkdir build_deb 259 | cd build_deb 260 | mkdir -p clave_${VERSION}_amd64/{DEBIAN,opt,usr,var} 261 | cat > clave_${VERSION}_amd64/DEBIAN/control <<- EOF 262 | Package: clave 263 | Version: $CLEAN_VERSION 264 | Section: net 265 | Priority: optional 266 | Architecture: amd64 267 | Maintainer: Clave 268 | Description: A lightweight cross-platform desktop authenticator app. 269 | Depends: libwebkit2gtk-4.1-0, libwebkit2gtk-4.1-dev 270 | EOF 271 | 272 | mkdir -p clave_${VERSION}_amd64/usr/{bin,share} 273 | cp ../bin/Clave clave_${VERSION}_amd64/usr/bin/clave 274 | chmod +x clave_${VERSION}_amd64/usr/bin/clave 275 | mkdir clave_${VERSION}_amd64/usr/share/{applications,icons} 276 | cat > clave_${VERSION}_amd64/usr/share/applications/clave.desktop <<- EOF 277 | [Desktop Entry] 278 | Type=Application 279 | Name=Clave 280 | Exec=/usr/bin/clave 281 | Icon=/usr/share/icons/appicon.png 282 | Categories=Utility; 283 | Terminal=false 284 | EOF 285 | cp ../build/appicon.png clave_${VERSION}_amd64/usr/share/icons 286 | dpkg-deb --build clave_${VERSION}_amd64 287 | ls -alh 288 | mv *.deb ../linuxDeb/clave_${VERSION}_amd64.deb 289 | cd .. 290 | echo "LINUX_DEB_PATH=linuxDeb/clave_${VERSION}_amd64.deb" >> $GITHUB_ENV 291 | ls -alh linuxDeb/ 292 | 293 | 294 | - name: Upload AppImage to Release 295 | uses: actions/upload-release-asset@v1 296 | env: 297 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 298 | with: 299 | upload_url: ${{ github.event_name == 'workflow_dispatch' && inputs.release_url || github.event.release.upload_url }} 300 | asset_path: ${{ env.LINUX_APP_PATH }} 301 | asset_name: clave_${{ github.event.release.tag_name || inputs.version }}_amd64.AppImage 302 | asset_content_type: application/x-executable 303 | 304 | - name: Upload deb to Release 305 | uses: actions/upload-release-asset@v1 306 | env: 307 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 308 | with: 309 | upload_url: ${{ github.event_name == 'workflow_dispatch' && inputs.release_url || github.event.release.upload_url }} 310 | asset_path: ${{ env.LINUX_DEB_PATH }} 311 | asset_name: clave_${{ github.event.release.tag_name || inputs.version }}_amd64.deb 312 | asset_content_type: application/x-debian-package 313 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | permissions: 4 | contents: write 5 | pull-requests: read 6 | issues: read 7 | repository-projects: read 8 | actions: write 9 | 10 | on: 11 | workflow_dispatch: 12 | inputs: 13 | version: 14 | description: 'Version for the release (e.g., 1.1.0)' 15 | required: true 16 | 17 | jobs: 18 | create-release: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v4 27 | with: 28 | go-version: '1.23' 29 | cache: true 30 | 31 | - name: Set up Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: 'lts/*' 35 | 36 | - name: Install Task 37 | uses: arduino/setup-task@v2 38 | with: 39 | version: 3.x 40 | repo-token: ${{ secrets.GITHUB_TOKEN }} 41 | 42 | - name: Get Merged PR Details 43 | id: changelog 44 | run: | 45 | # Get changes since last release 46 | LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") 47 | if [ -z "$LAST_TAG" ]; then 48 | START_DATE="2024-01-01" 49 | else 50 | START_DATE=$(git log -1 --format=%ai $LAST_TAG) 51 | fi 52 | 53 | # Get PRs merged since last release using GitHub CLI 54 | CHANGES=$(gh api graphql -f query=' 55 | query($repo:String!, $owner:String!) { 56 | repository(owner: $owner, name: $repo) { 57 | pullRequests(first: 100, states: MERGED, orderBy: {field: UPDATED_AT, direction: DESC}) { 58 | nodes { 59 | title 60 | number 61 | author { 62 | login 63 | } 64 | mergedAt 65 | } 66 | } 67 | } 68 | } 69 | ' -F owner="${{ github.repository_owner }}" -F repo="${{ github.event.repository.name }}" | \ 70 | jq -r --arg date "$START_DATE" '.data.repository.pullRequests.nodes[] | select(.mergedAt > $date) | "* " + .title + " (#" + (.number|tostring) + ") by @" + .author.login') 71 | 72 | echo "changes<> $GITHUB_ENV 73 | echo "$CHANGES" >> $GITHUB_ENV 74 | echo "EOF" >> $GITHUB_ENV 75 | 76 | # Get contributors 77 | CONTRIBUTORS=$(gh api graphql -f query=' 78 | query($repo:String!, $owner:String!) { 79 | repository(owner: $owner, name: $repo) { 80 | pullRequests(first: 100, states: MERGED, orderBy: {field: UPDATED_AT, direction: DESC}) { 81 | nodes { 82 | author { 83 | login 84 | } 85 | mergedAt 86 | } 87 | } 88 | } 89 | } 90 | ' -F owner="${{ github.repository_owner }}" -F repo="${{ github.event.repository.name }}" | \ 91 | jq -r --arg date "$START_DATE" '.data.repository.pullRequests.nodes[] | select(.mergedAt > $date) | .author.login' | sort -u) 92 | 93 | echo "new_contributors<> $GITHUB_ENV 94 | echo "$CONTRIBUTORS" | while read -r user; do 95 | echo "* [@$user](https://github.com/$user)" 96 | done 97 | echo "EOF" >> $GITHUB_ENV 98 | env: 99 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 100 | 101 | - name: Install System Dependencies 102 | run: | 103 | sudo apt-get update 104 | sudo apt-get install -y libwebkit2gtk-4.1-dev gcc libgtk-3-dev pkg-config 105 | 106 | - name: Setup Wails Dependencies 107 | run: | 108 | mkdir -p ../github 109 | cd ../github 110 | git clone https://github.com/ansxuman/wails.git 111 | cd wails 112 | git checkout start_on_login 113 | cd v3/cmd/wails3 114 | go install 115 | cd ../../../.. 116 | 117 | - name: Update go.mod 118 | run: | 119 | sed -i 's|=> ../wails/v3|=> ../github/wails/v3|g' go.mod 120 | 121 | - name: Update Version in Config 122 | run: | 123 | # Configure git 124 | git config --local user.email "action@github.com" 125 | git config --local user.name "GitHub Action" 126 | 127 | # Strip the 'v' prefix if it exists 128 | VERSION="${{ github.event.inputs.version }}" 129 | CLEAN_VERSION="${VERSION#v}" 130 | 131 | # Update AppVersion in CommmonVariables.go 132 | sed -i "s/const AppVersion = .*/const AppVersion = \"$CLEAN_VERSION\"/" constants/CommmonVariables.go 133 | 134 | # Update version in config.yml (specifically in the info section) 135 | sed -i "/^info:/,/^[^ ]/ s/ version: .*/ version: \"$CLEAN_VERSION\"/" build/config.yml 136 | 137 | # Update build assets 138 | wails3 task common:update:build-assets 139 | 140 | # Commit changes excluding go.mod and go.sum 141 | git add . 142 | git reset go.mod go.sum 143 | git commit -m "chore: update version to $CLEAN_VERSION" 144 | git push origin main 145 | 146 | - name: Create and Push Tag 147 | run: | 148 | git tag -a "v${{ github.event.inputs.version }}" -m "Release v${{ github.event.inputs.version }}" 149 | git push origin "v${{ github.event.inputs.version }}" 150 | 151 | - name: Create Release 152 | id: create-release 153 | uses: actions/create-release@v1 154 | with: 155 | tag_name: "v${{ github.event.inputs.version }}" 156 | release_name: "Clave v${{ github.event.inputs.version }}" 157 | body: | 158 | ## 🚀 What's Changed 159 | ${{ env.changes}} 160 | 161 | ## 👥 New Contributors 162 | ${{ env.new_contributors}} 163 | 164 | draft: false 165 | env: 166 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 167 | 168 | - name: Trigger Build Workflow 169 | uses: actions/github-script@v7 170 | with: 171 | github-token: ${{ secrets.GITHUB_TOKEN }} 172 | script: | 173 | await github.rest.actions.createWorkflowDispatch({ 174 | owner: context.repo.owner, 175 | repo: context.repo.repo, 176 | workflow_id: 'build-release.yml', 177 | ref: 'main', 178 | inputs: { 179 | release_url: '${{ steps.create-release.outputs.upload_url }}', 180 | version: 'v${{ github.event.inputs.version }}' 181 | } 182 | }) -------------------------------------------------------------------------------- /.github/workflows/test-build.yml: -------------------------------------------------------------------------------- 1 | name: Test Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | test-build: 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: [macos-latest, windows-latest, ubuntu-latest] 13 | runs-on: ${{ matrix.os }} 14 | name: Test Build (${{ matrix.os }}) 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Set up Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 'lts/*' 22 | 23 | - name: Set up Go 24 | uses: actions/setup-go@v4 25 | with: 26 | go-version: '1.23' 27 | cache: true 28 | 29 | - name: Install Task 30 | uses: arduino/setup-task@v2 31 | with: 32 | version: 3.x 33 | repo-token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Install Linux Dependencies 36 | if: runner.os == 'Linux' 37 | run: | 38 | sudo apt-get update 39 | sudo apt-get install -y libgtk-3-dev webkit2gtk-4.1-dev 40 | 41 | - name: Setup Wails Dependencies 42 | shell: bash 43 | run: | 44 | mkdir -p ../github 45 | cd ../github 46 | git clone https://github.com/ansxuman/wails.git 47 | cd wails 48 | git checkout start_on_login 49 | cd v3/cmd/wails3 50 | go install 51 | cd ../../../.. 52 | 53 | - name: Update go.mod on darwin 54 | if: runner.os == 'macOS' 55 | run: | 56 | sed -i '' 's|=> ../wails/v3|=> ../github/wails/v3|g' go.mod 57 | 58 | - name: Update go.mod on linux 59 | if: runner.os == 'Linux' 60 | run: | 61 | sed -i 's|=> ../wails/v3|=> ../github/wails/v3|g' go.mod 62 | 63 | - name: Update go.mod (Windows) 64 | if: runner.os == 'Windows' 65 | shell: pwsh 66 | run: | 67 | (Get-Content go.mod) -replace 'replace.*=> ../wails/v3', 'replace github.com/wailsapp/wails/v3 => ../github/wails/v3' | Set-Content go.mod 68 | 69 | - name: Build Project 70 | run: wails3 build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Wails 2 | frontend/dist 3 | .task 4 | bin 5 | 6 | # Svelte 7 | frontend/node_modules/ 8 | frontend/public/build/ 9 | frontend/.svelte-kit/ 10 | frontend/dist/ 11 | 12 | 13 | # TailwindCSS 14 | frontend/public/tailwind.css 15 | 16 | # Logs 17 | logs 18 | *.log 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | 23 | # Runtime data 24 | pids 25 | *.pid 26 | *.seed 27 | *.pid.lock 28 | 29 | # Dependency directories 30 | node_modules/ 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional eslint cache 36 | .eslintcache 37 | 38 | # Optional REPL history 39 | .node_repl_history 40 | 41 | # Output of 'npm pack' 42 | *.tgz 43 | 44 | # dotenv environment variable files 45 | .env 46 | .env.local 47 | .env.development.local 48 | .env.test.local 49 | .env.production.local 50 | 51 | # Mac files 52 | .DS_Store 53 | 54 | # Windows files 55 | Thumbs.db 56 | 57 | # Editor directories and files 58 | .idea 59 | .vscode 60 | *.suo 61 | *.ntvs* 62 | *.njsproj 63 | *.sln 64 | *.sw? 65 | 66 | -------------------------------------------------------------------------------- /Inter Font License.txt: -------------------------------------------------------------------------------- 1 | Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Anshuman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Clave Logo 3 |

Clave

4 |

A SIMPLE, SECURE, and FREE lightweight cross-platform desktop authenticator app packed with powerful features.

5 | 6 | [![License](https://img.shields.io/github/license/ansxuman/clave)](https://github.com/ansxuman/clave/blob/main/LICENSE) 7 | [![GitHub stars](https://img.shields.io/github/stars/ansxuman/clave)](https://github.com/ansxuman/clave/stargazers) 8 | [![GitHub issues](https://img.shields.io/github/issues/ansxuman/clave)](https://github.com/ansxuman/clave/issues) 9 | [![GitHub release](https://img.shields.io/github/v/release/ansxuman/clave)](https://github.com/ansxuman/clave/releases) 10 | [![Build Release](https://github.com/ansxuman/clave/actions/workflows/build-release.yml/badge.svg)](https://github.com/ansxuman/clave/actions/workflows/build-release.yml) 11 | [![Notarized by Apple](https://img.shields.io/badge/Release_Notarized_by_Apple-000000?style=flat-square&logo=apple&logoColor=white)](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution) 12 | 13 | Clave - Simple, secure authentication | Product Hunt 14 | 15 | ![Clave](https://github.com/user-attachments/assets/ac80de84-77a3-48af-ab15-e91afb8a7664) 16 | 17 | 18 |
19 | 20 | ## **Features** 21 | 22 | - **Easy Profile Addition**: 23 | Add accounts via manual entry or QR code image import. 24 | 25 | - **Multi-Factor Authentication (MFA)**: 26 | Protects access to your TOTP codes with an additional layer of security using a **PIN** or **Touch ID** (macOS only). 27 | 28 | - **User-Friendly Interface**: 29 | Designed with simplicity in mind, ensuring an intuitive and hassle-free user experience. 30 | 31 | - **System Tray Integration**: 32 | Quickly access your authenticator from the system tray for seamless usability. 33 | 34 | - **Cross-Platform**: 35 | Available for **macOS**, **Windows**, and **Linux** . 36 | 37 | - **Import & Export**: 38 | Easily backup and restore your profiles. 39 | 40 | ## Development 41 | 42 | ### Prerequisites 43 | - **NPM** 44 | - **Go** 45 | - **Task** 46 | 47 | ### Install Wails 48 | 49 | 1. Clone the Wails repository: 50 | ```bash 51 | git clone https://github.com/ansxuman/wails.git 52 | ``` 53 | 2. Navigate to the Wails directory: 54 | ```bash 55 | cd wails 56 | ``` 57 | 3. Check out the `start_on_login` branch: 58 | ```bash 59 | git checkout start_on_login 60 | ``` 61 | 4. Go to the wails3 directory: 62 | ```bash 63 | cd v3/cmd/wails3 64 | ``` 65 | 5. Install Wails: 66 | ```bash 67 | go install 68 | ``` 69 | 70 | ### Set Up Clave 71 | 72 | 1. Clone the Clave repository: 73 | ```bash 74 | https://github.com/ansxuman/clave.git 75 | ``` 76 | 2. Navigate to the Clave directory: 77 | ```bash 78 | cd clave 79 | ``` 80 | 3. Update the `go.mod` file in the Clave project to replace the Wails path with the local path of your cloned Wails repository. 81 | 4. Run the App in Dev Mode 82 | ```bash 83 | task dev 84 | ``` 85 | 86 | ## Contributing 87 | 88 | Contributions are what make the open-source community an incredible space for learning, inspiration, and creativity. Any contribution you make is deeply appreciated.Please see our [contributing guidelines](./.github/CONTRIBUTING.md) for more information. 89 | 90 | ## **Donations** 91 | 92 | If you find Clave helpful, consider supporting its development and future updates. Every contribution helps! 93 | 94 | 95 | Buy Me A Coffee 96 | 97 | 98 | --- 99 | 100 | ## **License** 101 | 102 | This project is open-source and available under the [MIT License](LICENSE). 103 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | includes: 4 | common: ./build/Taskfile.common.yml 5 | windows: ./build/Taskfile.windows.yml 6 | darwin: ./build/Taskfile.darwin.yml 7 | linux: ./build/Taskfile.linux.yml 8 | 9 | vars: 10 | APP_NAME: "Clave" 11 | BIN_DIR: "bin" 12 | VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' 13 | 14 | tasks: 15 | build: 16 | summary: Builds the application 17 | cmds: 18 | - task: "{{OS}}:build" 19 | 20 | package: 21 | summary: Packages a production build of the application 22 | cmds: 23 | - task: "{{OS}}:package" 24 | 25 | run: 26 | summary: Runs the application 27 | cmds: 28 | - task: "{{OS}}:run" 29 | 30 | dev: 31 | summary: Runs the application in development mode 32 | cmds: 33 | - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}} 34 | 35 | darwin:build:universal: 36 | summary: Builds darwin universal binary (arm64 + amd64) 37 | cmds: 38 | - task: darwin:build 39 | vars: 40 | ARCH: amd64 41 | - mv {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}-amd64 42 | - task: darwin:build 43 | vars: 44 | ARCH: arm64 45 | - mv {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}-arm64 46 | - lipo -create -output {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}-amd64 {{.BIN_DIR}}/{{.APP_NAME}}-arm64 47 | - rm {{.BIN_DIR}}/{{.APP_NAME}}-amd64 {{.BIN_DIR}}/{{.APP_NAME}}-arm64 48 | 49 | darwin:package:universal: 50 | summary: Packages darwin universal binary (arm64 + amd64) 51 | deps: 52 | - darwin:build:universal 53 | cmds: 54 | - task: darwin:create:app:bundle 55 | -------------------------------------------------------------------------------- /assets/icons.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import ( 4 | _ "embed" 5 | ) 6 | 7 | //go:embed password.png 8 | var iconData []byte 9 | 10 | //go:embed passworddark.png 11 | var darkIconData []byte 12 | 13 | // Icon for the application 14 | var Icon = iconData 15 | var DarkIcon = darkIconData 16 | -------------------------------------------------------------------------------- /assets/password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansxuman/clave/3b853209d1428042f9df7d8eaac74672f3941092/assets/password.png -------------------------------------------------------------------------------- /assets/passworddark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansxuman/clave/3b853209d1428042f9df7d8eaac74672f3941092/assets/passworddark.png -------------------------------------------------------------------------------- /backend/app.go: -------------------------------------------------------------------------------- 1 | package backend 2 | 3 | import ( 4 | "clave/constants" 5 | "clave/localstorage" 6 | "clave/objects" 7 | "clave/services/auth" 8 | "clave/services/totp" 9 | "clave/services/window" 10 | "fmt" 11 | "runtime" 12 | 13 | cmap "github.com/orcaman/concurrent-map/v2" 14 | "github.com/wailsapp/wails/v3/pkg/application" 15 | ) 16 | 17 | type App struct { 18 | cancel cmap.ConcurrentMap[string, func()] 19 | authService *auth.Service 20 | totpService *totp.Service 21 | winManager *window.Manager 22 | isVerified bool 23 | isMacOS bool 24 | firstMount bool 25 | } 26 | 27 | func NewApp() *App { 28 | storage := localstorage.GetPersistentStorage() 29 | authService := auth.NewService(storage) 30 | 31 | app := &App{ 32 | cancel: cmap.New[func()](), 33 | authService: authService, 34 | isVerified: false, 35 | isMacOS: runtime.GOOS == "darwin", 36 | firstMount: true, 37 | } 38 | 39 | app.winManager = window.NewManager(app) 40 | app.totpService = totp.NewService(storage, app.winManager) 41 | 42 | return app 43 | } 44 | 45 | func (a *App) HasPin() bool { 46 | return a.authService.HasPin() 47 | } 48 | 49 | func (a *App) IsVerified() bool { 50 | return a.isVerified 51 | } 52 | 53 | func (a *App) SetVerified(state bool) { 54 | a.isVerified = state 55 | } 56 | 57 | func (a *App) Initialize() objects.InitResult { 58 | if !a.HasPin() { 59 | return objects.InitResult{NeedsOnboarding: true} 60 | } 61 | return objects.InitResult{NeedsVerification: false} 62 | } 63 | 64 | func (a *App) SetWindow(window *application.WebviewWindow) { 65 | a.winManager.SetWindow(window) 66 | if a.totpService != nil { 67 | a.totpService.SetWindow(window) 68 | } 69 | } 70 | func (a *App) SetupPin(pin string) error { 71 | return a.authService.SetupPin(pin) 72 | } 73 | 74 | func (a *App) VerifyPin(pin string) (bool, error) { 75 | isValid, err := a.authService.VerifyPin(pin) 76 | if isValid { 77 | a.SetVerified(true) 78 | } 79 | return isValid, err 80 | } 81 | 82 | func (a *App) IsMacOS() bool { 83 | return a.isMacOS 84 | } 85 | 86 | func (a *App) VerifyTouchID() bool { 87 | return a.winManager.HandleTouchID() 88 | } 89 | 90 | func (a *App) GetAppVersion() string { 91 | return constants.AppVersion 92 | } 93 | 94 | func (a *App) IsFirstMount() bool { 95 | if a.firstMount { 96 | a.firstMount = false 97 | return true 98 | } 99 | return false 100 | } 101 | 102 | func (a *App) OpenQR() error { 103 | return a.totpService.OpenQR() 104 | } 105 | 106 | func (a *App) SendTOTPData() { 107 | if a.totpService == nil { 108 | return 109 | } 110 | a.totpService.SendTOTPData() 111 | } 112 | 113 | func (a *App) RemoveTotpProfile(profileId string) error { 114 | if a.totpService == nil { 115 | return nil 116 | } 117 | return a.totpService.RemoveTotpProfile(profileId) 118 | } 119 | 120 | func (a *App) AddManualProfile(issuer string, secret string) error { 121 | if a.totpService == nil { 122 | return fmt.Errorf("TOTP service not initialized") 123 | } 124 | return a.totpService.AddManualProfile(issuer, secret) 125 | } 126 | 127 | func (a *App) BackupProfiles() error { 128 | if a.totpService == nil { 129 | return fmt.Errorf("TOTP service not initialized") 130 | } 131 | return a.totpService.BackupProfiles() 132 | } 133 | 134 | func (a *App) RestoreProfiles() error { 135 | if a.totpService == nil { 136 | return fmt.Errorf("TOTP service not initialized") 137 | } 138 | return a.totpService.RestoreProfiles() 139 | } 140 | -------------------------------------------------------------------------------- /build/Info.dev.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundlePackageType 5 | APPL 6 | CFBundleName 7 | Clave 8 | CFBundleExecutable 9 | Clave 10 | CFBundleIdentifier 11 | com.ansxuman.clave 12 | CFBundleVersion 13 | 1.0.0 14 | CFBundleGetInfoString 15 | Secure Authentication at Your Fingertips 16 | CFBundleShortVersionString 17 | 1.0.0 18 | CFBundleIconFile 19 | icons 20 | LSMinimumSystemVersion 21 | 10.13.0 22 | NSHighResolutionCapable 23 | true 24 | NSHumanReadableCopyright 25 | © 2024 Clave. Licensed under MIT License. 26 | NSAppTransportSecurity 27 | 28 | NSAllowsLocalNetworking 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /build/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundlePackageType 5 | APPL 6 | CFBundleName 7 | Clave 8 | CFBundleExecutable 9 | Clave 10 | CFBundleIdentifier 11 | com.ansxuman.clave 12 | CFBundleVersion 13 | 1.0.0 14 | CFBundleGetInfoString 15 | Secure Authentication at Your Fingertips 16 | CFBundleShortVersionString 17 | 1.0.0 18 | CFBundleIconFile 19 | icons 20 | LSMinimumSystemVersion 21 | 10.13.0 22 | NSHighResolutionCapable 23 | true 24 | NSHumanReadableCopyright 25 | © 2024 Clave. Licensed under MIT License. 26 | 27 | -------------------------------------------------------------------------------- /build/Taskfile.common.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | tasks: 4 | go:mod:tidy: 5 | summary: Runs `go mod tidy` 6 | internal: true 7 | generates: 8 | - go.sum 9 | sources: 10 | - go.mod 11 | cmds: 12 | - go mod tidy 13 | 14 | install:frontend:deps: 15 | summary: Install frontend dependencies 16 | dir: frontend 17 | sources: 18 | - package.json 19 | - package-lock.json 20 | generates: 21 | - node_modules/* 22 | preconditions: 23 | - sh: npm version 24 | msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/" 25 | cmds: 26 | - npm install 27 | 28 | build:frontend: 29 | summary: Build the frontend project 30 | dir: frontend 31 | sources: 32 | - "**/*" 33 | generates: 34 | - dist/* 35 | deps: 36 | - task: install:frontend:deps 37 | - task: generate:bindings 38 | cmds: 39 | - npm run build -q 40 | 41 | generate:bindings: 42 | summary: Generates bindings for the frontend 43 | sources: 44 | - "**/*.go" 45 | - go.mod 46 | - go.sum 47 | generates: 48 | - "frontend/bindings/**/*" 49 | cmds: 50 | - wails3 generate bindings -f '{{.BUILD_FLAGS}}'{{if .UseTypescript}} -ts{{end}} 51 | 52 | generate:icons: 53 | summary: Generates Windows `.ico` and Mac `.icns` files from an image 54 | dir: build 55 | sources: 56 | - "appicon.png" 57 | generates: 58 | - "icons.icns" 59 | - "icons.ico" 60 | cmds: 61 | - wails3 generate icons -input appicon.png 62 | 63 | dev:frontend: 64 | summary: Runs the frontend in development mode 65 | dir: frontend 66 | deps: 67 | - task: install:frontend:deps 68 | cmds: 69 | - npm run dev -- --port {{.VITE_PORT}} --strictPort 70 | 71 | update:build-assets: 72 | summary: Updates the build assets 73 | dir: build 74 | cmds: 75 | - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . -------------------------------------------------------------------------------- /build/Taskfile.darwin.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | includes: 4 | common: Taskfile.common.yml 5 | 6 | tasks: 7 | build: 8 | summary: Creates a production build of the application 9 | deps: 10 | - task: common:go:mod:tidy 11 | - task: common:build:frontend 12 | - task: common:generate:icons 13 | cmds: 14 | - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}} 15 | vars: 16 | BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}' 17 | env: 18 | GOOS: darwin 19 | CGO_ENABLED: 1 20 | GOARCH: '{{.ARCH | default ARCH}}' 21 | CGO_CFLAGS: "-mmacosx-version-min=10.15" 22 | CGO_LDFLAGS: "-mmacosx-version-min=10.15" 23 | MACOSX_DEPLOYMENT_TARGET: "10.15" 24 | PRODUCTION: '{{.PRODUCTION | default "false"}}' 25 | 26 | package: 27 | summary: Packages a production build of the application into a `.app` bundle 28 | deps: 29 | - task: build 30 | vars: 31 | PRODUCTION: "true" 32 | cmds: 33 | - task: create:app:bundle 34 | 35 | create:app:bundle: 36 | summary: Creates an `.app` bundle 37 | cmds: 38 | - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources} 39 | - cp build/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources 40 | - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS 41 | - cp build/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents 42 | 43 | run: 44 | cmds: 45 | - '{{.BIN_DIR}}/{{.APP_NAME}}' 46 | -------------------------------------------------------------------------------- /build/Taskfile.linux.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | includes: 4 | common: Taskfile.common.yml 5 | 6 | tasks: 7 | build: 8 | summary: Builds the application for Linux 9 | deps: 10 | - task: common:go:mod:tidy 11 | - task: common:build:frontend 12 | - task: common:generate:icons 13 | cmds: 14 | - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}} 15 | vars: 16 | BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s"{{else}}-gcflags=all="-l"{{end}}' 17 | env: 18 | GOOS: linux 19 | CGO_ENABLED: 1 20 | GOARCH: '{{.ARCH | default ARCH}}' 21 | PRODUCTION: '{{.PRODUCTION | default "false"}}' 22 | 23 | package: 24 | summary: Packages a production build of the application for Linux 25 | deps: 26 | - task: build 27 | vars: 28 | PRODUCTION: "true" 29 | cmds: 30 | - task: create:appimage 31 | 32 | create:appimage: 33 | summary: Creates an AppImage 34 | dir: build/appimage 35 | deps: 36 | - task: build 37 | vars: 38 | PRODUCTION: "true" 39 | - task: generate:dotdesktop 40 | cmds: 41 | - cp {{.APP_BINARY}} {{.APP_NAME}} 42 | - cp ../appicon.png appicon.png 43 | - wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/appimage 44 | vars: 45 | APP_NAME: '{{.APP_NAME}}' 46 | APP_BINARY: '../../bin/{{.APP_NAME}}' 47 | ICON: '../appicon.png' 48 | DESKTOP_FILE: '{{.APP_NAME}}.desktop' 49 | OUTPUT_DIR: '../../bin' 50 | 51 | generate:dotdesktop: 52 | summary: Generates a `.desktop` file 53 | dir: build 54 | cmds: 55 | - mkdir -p {{.ROOT_DIR}}/build/appimage 56 | - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}" 57 | vars: 58 | APP_NAME: '{{.APP_NAME}}' 59 | EXEC: '{{.APP_NAME}}' 60 | ICON: 'appicon' 61 | CATEGORIES: 'Development;' 62 | OUTPUTFILE: '{{.ROOT_DIR}}/build/appimage/{{.APP_NAME}}.desktop' 63 | 64 | run: 65 | cmds: 66 | - '{{.BIN_DIR}}/{{.APP_NAME}}' 67 | -------------------------------------------------------------------------------- /build/Taskfile.windows.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | includes: 4 | common: Taskfile.common.yml 5 | 6 | tasks: 7 | build: 8 | summary: Builds the application for Windows 9 | deps: 10 | - task: common:go:mod:tidy 11 | - task: common:build:frontend 12 | - task: common:generate:icons 13 | - task: generate:syso 14 | cmds: 15 | - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe 16 | vars: 17 | BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -ldflags="-w -s -H windowsgui"{{else}}-gcflags=all="-l"{{end}}' 18 | env: 19 | GOOS: windows 20 | CGO_ENABLED: 0 21 | GOARCH: '{{.ARCH | default ARCH}}' 22 | PRODUCTION: '{{.PRODUCTION | default "false"}}' 23 | 24 | package: 25 | summary: Packages a production build of the application into a `.exe` bundle 26 | cmds: 27 | - task: create:nsis:installer 28 | 29 | generate:syso: 30 | summary: Generates Windows `.syso` file 31 | dir: build 32 | cmds: 33 | - wails3 generate syso -arch {{.ARCH}} -icon icon.ico -manifest wails.exe.manifest -info info.json -out ../wails.syso 34 | vars: 35 | ARCH: '{{.ARCH | default ARCH}}' 36 | 37 | create:nsis:installer: 38 | summary: Creates an NSIS installer 39 | dir: build/nsis 40 | deps: 41 | - task: build 42 | vars: 43 | PRODUCTION: "true" 44 | cmds: 45 | - makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi 46 | vars: 47 | ARCH: '{{.ARCH | default ARCH}}' 48 | ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' 49 | 50 | run: 51 | cmds: 52 | - '{{.BIN_DIR}}\\{{.APP_NAME}}.exe' -------------------------------------------------------------------------------- /build/appicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansxuman/clave/3b853209d1428042f9df7d8eaac74672f3941092/build/appicon.png -------------------------------------------------------------------------------- /build/appimage/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Copyright (c) 2018-Present Lea Anthony 3 | # SPDX-License-Identifier: MIT 4 | 5 | # Fail script on any error 6 | set -euxo pipefail 7 | 8 | # Define variables 9 | APP_DIR="${APP_NAME}.AppDir" 10 | 11 | # Create AppDir structure 12 | mkdir -p "${APP_DIR}/usr/bin" 13 | cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/" 14 | cp "${ICON_PATH}" "${APP_DIR}/" 15 | cp "${DESKTOP_FILE}" "${APP_DIR}/" 16 | 17 | # Download linuxdeploy and make it executable 18 | wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage 19 | chmod +x linuxdeploy-x86_64.AppImage 20 | 21 | # Run linuxdeploy to bundle the application 22 | ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage 23 | 24 | # Rename the generated AppImage 25 | mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage" 26 | 27 | -------------------------------------------------------------------------------- /build/config.yml: -------------------------------------------------------------------------------- 1 | # This file contains the configuration for this project. 2 | # When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. 3 | # Note that this will overwrite any changes you have made to the assets. 4 | version: '3' 5 | 6 | # This information is used to generate the build assets. 7 | info: 8 | companyName: "Clave" # The name of the company 9 | productName: "Clave" # The name of the application 10 | productIdentifier: "com.ansxuman.clave" # The unique product identifier 11 | description: "Secure Authentication at Your Fingertips" # The application description 12 | copyright: "© 2024 Clave. Licensed under MIT License." # Copyright text 13 | comments: "Secure Authentication at Your Fingertips" # Comments 14 | version: "1.0.0" 15 | 16 | # Dev mode configuration 17 | dev_mode: 18 | root_path: . 19 | log_level: warn 20 | debounce: 1000 21 | ignore: 22 | dir: 23 | - .git 24 | - node_modules 25 | - frontend 26 | - bin 27 | file: 28 | - .DS_Store 29 | - .gitignore 30 | - .gitkeep 31 | watched_extension: 32 | - "*.go" 33 | git_ignore: true 34 | executes: 35 | - cmd: wails3 task common:install:frontend:deps 36 | type: once 37 | - cmd: wails3 task common:dev:frontend 38 | type: background 39 | - cmd: go mod tidy 40 | type: blocking 41 | - cmd: wails3 task build 42 | type: blocking 43 | - cmd: wails3 task run 44 | type: primary 45 | 46 | # File Associations 47 | # More information at: https://v3alpha.wails.io/noit/done/yet 48 | fileAssociations: 49 | # - ext: wails 50 | # name: Wails 51 | # description: Wails Application File 52 | # iconName: wailsFileIcon 53 | # role: Editor 54 | # - ext: jpg 55 | # name: JPEG 56 | # description: Image File 57 | # iconName: jpegFileIcon 58 | # role: Editor 59 | 60 | # Other data 61 | other: 62 | - name: My Other Data -------------------------------------------------------------------------------- /build/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansxuman/clave/3b853209d1428042f9df7d8eaac74672f3941092/build/icon.ico -------------------------------------------------------------------------------- /build/icons.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansxuman/clave/3b853209d1428042f9df7d8eaac74672f3941092/build/icons.icns -------------------------------------------------------------------------------- /build/info.json: -------------------------------------------------------------------------------- 1 | { 2 | "fixed": { 3 | "file_version": "1.0.0" 4 | }, 5 | "info": { 6 | "0000": { 7 | "ProductVersion": "1.0.0", 8 | "CompanyName": "Clave", 9 | "FileDescription": "Secure Authentication at Your Fingertips", 10 | "LegalCopyright": "© 2024 Clave. Licensed under MIT License.", 11 | "ProductName": "Clave", 12 | "Comments": "Secure Authentication at Your Fingertips" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /build/nfpm/nfpm.yaml: -------------------------------------------------------------------------------- 1 | # Feel free to remove those if you don't want/need to use them. 2 | # Make sure to check the documentation at https://nfpm.goreleaser.com 3 | # 4 | # The lines below are called `modelines`. See `:help modeline` 5 | 6 | name: "Clave" 7 | arch: ${GOARCH} 8 | platform: "linux" 9 | version: "1.0.0" 10 | section: "default" 11 | priority: "extra" 12 | maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> 13 | description: "Secure Authentication at Your Fingertips" 14 | vendor: "Clave" 15 | homepage: "https://wails.io" 16 | license: "MIT" 17 | release: "1" 18 | 19 | contents: 20 | - src: "./bin/Clave" 21 | dst: "/usr/local/bin/Clave" 22 | - src: "./build/appicon.png" 23 | dst: "/usr/share/icons/hicolor/128x128/apps/Clave.png" 24 | - src: "./build/Clave.desktop" 25 | dst: "/usr/share/applications/Clave.desktop" 26 | 27 | depends: 28 | - gtk3 29 | - libwebkit2gtk 30 | 31 | # replaces: 32 | # - foobar 33 | # provides: 34 | # - bar 35 | # depends: 36 | # - gtk3 37 | # - libwebkit2gtk 38 | # recommends: 39 | # - whatever 40 | # suggests: 41 | # - something-else 42 | # conflicts: 43 | # - not-foo 44 | # - not-bar 45 | # changelog: "changelog.yaml" 46 | # scripts: 47 | # preinstall: ./build/nfpm/scripts/preinstall.sh 48 | # postinstall: ./build/nfpm/scripts/postinstall.sh 49 | # preremove: ./build/nfpm/scripts/preremove.sh 50 | # postremove: ./build/nfpm/scripts/postremove.sh 51 | -------------------------------------------------------------------------------- /build/nfpm/scripts/postinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | -------------------------------------------------------------------------------- /build/nfpm/scripts/postremove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | -------------------------------------------------------------------------------- /build/nfpm/scripts/preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | -------------------------------------------------------------------------------- /build/nfpm/scripts/preremove.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | -------------------------------------------------------------------------------- /build/nsis/MicrosoftEdgeWebview2Setup.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansxuman/clave/3b853209d1428042f9df7d8eaac74672f3941092/build/nsis/MicrosoftEdgeWebview2Setup.exe -------------------------------------------------------------------------------- /build/nsis/project.nsi: -------------------------------------------------------------------------------- 1 | Unicode true 2 | 3 | #### 4 | ## Please note: Template replacements don't work in this file. They are provided with default defines like 5 | ## mentioned underneath. 6 | ## If the keyword is not defined, "wails_tools.nsh" will populate them. 7 | ## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually 8 | ## from outside of Wails for debugging and development of the installer. 9 | ## 10 | ## For development first make a wails nsis build to populate the "wails_tools.nsh": 11 | ## > wails build --target windows/amd64 --nsis 12 | ## Then you can call makensis on this file with specifying the path to your binary: 13 | ## For a AMD64 only installer: 14 | ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe 15 | ## For a ARM64 only installer: 16 | ## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe 17 | ## For a installer with both architectures: 18 | ## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe 19 | #### 20 | ## The following information is taken from the wails_tools.nsh file, but they can be overwritten here. 21 | #### 22 | ## !define INFO_PROJECTNAME "my-project" # Default "Clave" 23 | ## !define INFO_COMPANYNAME "My Company" # Default "My Company" 24 | ## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product" 25 | ## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" 26 | ## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© now, My Company" 27 | ### 28 | ## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" 29 | ## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" 30 | #### 31 | ## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html 32 | #### 33 | ## Include the wails tools 34 | #### 35 | !include "wails_tools.nsh" 36 | 37 | # The version information for this two must consist of 4 parts 38 | VIProductVersion "${INFO_PRODUCTVERSION}.0" 39 | VIFileVersion "${INFO_PRODUCTVERSION}.0" 40 | 41 | VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" 42 | VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" 43 | VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" 44 | VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" 45 | VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" 46 | VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" 47 | 48 | # Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware 49 | ManifestDPIAware true 50 | 51 | !include "MUI.nsh" 52 | 53 | !define MUI_ICON "..\icon.ico" 54 | !define MUI_UNICON "..\icon.ico" 55 | # !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 56 | !define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps 57 | !define MUI_ABORTWARNING # This will warn the user if they exit from the installer. 58 | 59 | !insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. 60 | # !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer 61 | !insertmacro MUI_PAGE_DIRECTORY # In which folder install page. 62 | !insertmacro MUI_PAGE_INSTFILES # Installing page. 63 | !insertmacro MUI_PAGE_FINISH # Finished installation page. 64 | 65 | !insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page 66 | 67 | !insertmacro MUI_LANGUAGE "English" # Set the Language of the installer 68 | 69 | ## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 70 | #!uninstfinalize 'signtool --file "%1"' 71 | #!finalize 'signtool --file "%1"' 72 | 73 | Name "${INFO_PRODUCTNAME}" 74 | OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. 75 | InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). 76 | ShowInstDetails show # This will always show the installation details. 77 | 78 | Function .onInit 79 | !insertmacro wails.checkArchitecture 80 | FunctionEnd 81 | 82 | Section 83 | !insertmacro wails.setShellContext 84 | 85 | !insertmacro wails.webview2runtime 86 | 87 | SetOutPath $INSTDIR 88 | 89 | !insertmacro wails.files 90 | 91 | CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" 92 | CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" 93 | 94 | !insertmacro wails.associateFiles 95 | 96 | !insertmacro wails.writeUninstaller 97 | SectionEnd 98 | 99 | Section "uninstall" 100 | !insertmacro wails.setShellContext 101 | 102 | RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath 103 | 104 | RMDir /r $INSTDIR 105 | 106 | Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" 107 | Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" 108 | 109 | !insertmacro wails.unassociateFiles 110 | 111 | !insertmacro wails.deleteUninstaller 112 | SectionEnd 113 | -------------------------------------------------------------------------------- /build/nsis/wails_tools.nsh: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT - Generated automatically by `wails build` 2 | 3 | !include "x64.nsh" 4 | !include "WinVer.nsh" 5 | !include "FileFunc.nsh" 6 | 7 | !ifndef INFO_PROJECTNAME 8 | !define INFO_PROJECTNAME "Clave" 9 | !endif 10 | !ifndef INFO_COMPANYNAME 11 | !define INFO_COMPANYNAME "Clave" 12 | !endif 13 | !ifndef INFO_PRODUCTNAME 14 | !define INFO_PRODUCTNAME "Clave" 15 | !endif 16 | !ifndef INFO_PRODUCTVERSION 17 | !define INFO_PRODUCTVERSION "1.0.0" 18 | !endif 19 | !ifndef INFO_COPYRIGHT 20 | !define INFO_COPYRIGHT "© 2024 Clave. Licensed under MIT License." 21 | !endif 22 | !ifndef PRODUCT_EXECUTABLE 23 | !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" 24 | !endif 25 | !ifndef UNINST_KEY_NAME 26 | !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" 27 | !endif 28 | !define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" 29 | 30 | !ifndef REQUEST_EXECUTION_LEVEL 31 | !define REQUEST_EXECUTION_LEVEL "admin" 32 | !endif 33 | 34 | RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" 35 | 36 | !ifdef ARG_WAILS_AMD64_BINARY 37 | !define SUPPORTS_AMD64 38 | !endif 39 | 40 | !ifdef ARG_WAILS_ARM64_BINARY 41 | !define SUPPORTS_ARM64 42 | !endif 43 | 44 | !ifdef SUPPORTS_AMD64 45 | !ifdef SUPPORTS_ARM64 46 | !define ARCH "amd64_arm64" 47 | !else 48 | !define ARCH "amd64" 49 | !endif 50 | !else 51 | !ifdef SUPPORTS_ARM64 52 | !define ARCH "arm64" 53 | !else 54 | !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" 55 | !endif 56 | !endif 57 | 58 | !macro wails.checkArchitecture 59 | !ifndef WAILS_WIN10_REQUIRED 60 | !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." 61 | !endif 62 | 63 | !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED 64 | !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" 65 | !endif 66 | 67 | ${If} ${AtLeastWin10} 68 | !ifdef SUPPORTS_AMD64 69 | ${if} ${IsNativeAMD64} 70 | Goto ok 71 | ${EndIf} 72 | !endif 73 | 74 | !ifdef SUPPORTS_ARM64 75 | ${if} ${IsNativeARM64} 76 | Goto ok 77 | ${EndIf} 78 | !endif 79 | 80 | IfSilent silentArch notSilentArch 81 | silentArch: 82 | SetErrorLevel 65 83 | Abort 84 | notSilentArch: 85 | MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" 86 | Quit 87 | ${else} 88 | IfSilent silentWin notSilentWin 89 | silentWin: 90 | SetErrorLevel 64 91 | Abort 92 | notSilentWin: 93 | MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" 94 | Quit 95 | ${EndIf} 96 | 97 | ok: 98 | !macroend 99 | 100 | !macro wails.files 101 | !ifdef SUPPORTS_AMD64 102 | ${if} ${IsNativeAMD64} 103 | File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" 104 | ${EndIf} 105 | !endif 106 | 107 | !ifdef SUPPORTS_ARM64 108 | ${if} ${IsNativeARM64} 109 | File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" 110 | ${EndIf} 111 | !endif 112 | !macroend 113 | 114 | !macro wails.writeUninstaller 115 | WriteUninstaller "$INSTDIR\uninstall.exe" 116 | 117 | SetRegView 64 118 | WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" 119 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" 120 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" 121 | WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" 122 | WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" 123 | WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" 124 | 125 | ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 126 | IntFmt $0 "0x%08X" $0 127 | WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" 128 | !macroend 129 | 130 | !macro wails.deleteUninstaller 131 | Delete "$INSTDIR\uninstall.exe" 132 | 133 | SetRegView 64 134 | DeleteRegKey HKLM "${UNINST_KEY}" 135 | !macroend 136 | 137 | !macro wails.setShellContext 138 | ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" 139 | SetShellVarContext all 140 | ${else} 141 | SetShellVarContext current 142 | ${EndIf} 143 | !macroend 144 | 145 | # Install webview2 by launching the bootstrapper 146 | # See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment 147 | !macro wails.webview2runtime 148 | !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT 149 | !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" 150 | !endif 151 | 152 | SetRegView 64 153 | # If the admin key exists and is not empty then webview2 is already installed 154 | ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" 155 | ${If} $0 != "" 156 | Goto ok 157 | ${EndIf} 158 | 159 | ${If} ${REQUEST_EXECUTION_LEVEL} == "user" 160 | # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed 161 | ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" 162 | ${If} $0 != "" 163 | Goto ok 164 | ${EndIf} 165 | ${EndIf} 166 | 167 | SetDetailsPrint both 168 | DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" 169 | SetDetailsPrint listonly 170 | 171 | InitPluginsDir 172 | CreateDirectory "$pluginsdir\webview2bootstrapper" 173 | SetOutPath "$pluginsdir\webview2bootstrapper" 174 | File "MicrosoftEdgeWebview2Setup.exe" 175 | ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' 176 | 177 | SetDetailsPrint both 178 | ok: 179 | !macroend 180 | 181 | # Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b 182 | !macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND 183 | ; Backup the previously associated file class 184 | ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" 185 | WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" 186 | 187 | WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" 188 | 189 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` 190 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` 191 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" 192 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` 193 | WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` 194 | !macroend 195 | 196 | !macro APP_UNASSOCIATE EXT FILECLASS 197 | ; Backup the previously associated file class 198 | ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` 199 | WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" 200 | 201 | DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` 202 | !macroend 203 | 204 | !macro wails.associateFiles 205 | ; Create file associations 206 | 207 | !macroend 208 | 209 | !macro wails.unassociateFiles 210 | ; Delete app associations 211 | 212 | !macroend -------------------------------------------------------------------------------- /build/wails.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | true/pm 12 | permonitorv2,permonitor 13 | 14 | 15 | -------------------------------------------------------------------------------- /constants/CommmonVariables.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | var AgentName = "Clave" 4 | var Description = "Secure Authentication at Your Fingertips" 5 | var Company = "© 2024 Clave. Licensed under MIT License." 6 | var MaxWidth = 350 7 | var MaxHeight = 500 8 | var Stage = "dev" 9 | 10 | const CommonDatabasePassword string = "s#6Y5$wdqwd*TVC3!tu@_hHk#5" 11 | const ApplicationName = "Clave" 12 | const AppVersion = "1.0.0" 13 | -------------------------------------------------------------------------------- /constants/FIlePathConstants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import ( 4 | "path/filepath" 5 | ) 6 | 7 | const AppFolder string = "Clave" 8 | 9 | var AppFolderPATH string = filepath.Join(ProgramData, AppFolder) 10 | 11 | var DatabaseLocation string = filepath.Join(AppFolderPATH, "Databases") 12 | 13 | var SecureVaultDB string = filepath.Join(DatabaseLocation, "secure_vault.db") 14 | -------------------------------------------------------------------------------- /constants/FilePathConstant_darwin.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | var homeDir, _ = os.UserHomeDir() 9 | var ProgramData = filepath.Join(homeDir, "Library", "Application Support") 10 | -------------------------------------------------------------------------------- /constants/FilePathConstant_linux.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | ) 7 | 8 | var homeDir, _ = os.UserHomeDir() 9 | var ProgramData = filepath.Join(homeDir, ".local", "share") 10 | -------------------------------------------------------------------------------- /constants/FilePathConstant_windows.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | var ProgramData = os.Getenv("LOCALAPPDATA") 8 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # create-svelte 2 | 3 | Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npm create svelte@latest 12 | 13 | # create a new project in my-app 14 | npm create svelte@latest my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /frontend/bindings/clave/backend/app.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore: Unused imports 7 | import {Call as $Call, Create as $Create} from "@wailsio/runtime"; 8 | 9 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 10 | // @ts-ignore: Unused imports 11 | import * as objects$0 from "../objects/models.js"; 12 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 13 | // @ts-ignore: Unused imports 14 | import * as application$0 from "../../github.com/wailsapp/wails/v3/pkg/application/models.js"; 15 | 16 | /** 17 | * @param {string} issuer 18 | * @param {string} secret 19 | * @returns {Promise & { cancel(): void }} 20 | */ 21 | export function AddManualProfile(issuer, secret) { 22 | let $resultPromise = /** @type {any} */($Call.ByID(2179556590, issuer, secret)); 23 | return $resultPromise; 24 | } 25 | 26 | /** 27 | * @returns {Promise & { cancel(): void }} 28 | */ 29 | export function BackupProfiles() { 30 | let $resultPromise = /** @type {any} */($Call.ByID(2929148704)); 31 | return $resultPromise; 32 | } 33 | 34 | /** 35 | * @returns {Promise & { cancel(): void }} 36 | */ 37 | export function GetAppVersion() { 38 | let $resultPromise = /** @type {any} */($Call.ByID(1288659937)); 39 | return $resultPromise; 40 | } 41 | 42 | /** 43 | * @returns {Promise & { cancel(): void }} 44 | */ 45 | export function HasPin() { 46 | let $resultPromise = /** @type {any} */($Call.ByID(2064516343)); 47 | return $resultPromise; 48 | } 49 | 50 | /** 51 | * @returns {Promise & { cancel(): void }} 52 | */ 53 | export function Initialize() { 54 | let $resultPromise = /** @type {any} */($Call.ByID(1738565190)); 55 | let $typingPromise = /** @type {any} */($resultPromise.then(($result) => { 56 | return $$createType0($result); 57 | })); 58 | $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); 59 | return $typingPromise; 60 | } 61 | 62 | /** 63 | * @returns {Promise & { cancel(): void }} 64 | */ 65 | export function IsFirstMount() { 66 | let $resultPromise = /** @type {any} */($Call.ByID(3776458967)); 67 | return $resultPromise; 68 | } 69 | 70 | /** 71 | * @returns {Promise & { cancel(): void }} 72 | */ 73 | export function IsMacOS() { 74 | let $resultPromise = /** @type {any} */($Call.ByID(2874596637)); 75 | return $resultPromise; 76 | } 77 | 78 | /** 79 | * @returns {Promise & { cancel(): void }} 80 | */ 81 | export function IsVerified() { 82 | let $resultPromise = /** @type {any} */($Call.ByID(2657582620)); 83 | return $resultPromise; 84 | } 85 | 86 | /** 87 | * @returns {Promise & { cancel(): void }} 88 | */ 89 | export function OpenQR() { 90 | let $resultPromise = /** @type {any} */($Call.ByID(1582336875)); 91 | return $resultPromise; 92 | } 93 | 94 | /** 95 | * @param {string} profileId 96 | * @returns {Promise & { cancel(): void }} 97 | */ 98 | export function RemoveTotpProfile(profileId) { 99 | let $resultPromise = /** @type {any} */($Call.ByID(4287256182, profileId)); 100 | return $resultPromise; 101 | } 102 | 103 | /** 104 | * @returns {Promise & { cancel(): void }} 105 | */ 106 | export function RestoreProfiles() { 107 | let $resultPromise = /** @type {any} */($Call.ByID(1862854628)); 108 | return $resultPromise; 109 | } 110 | 111 | /** 112 | * @returns {Promise & { cancel(): void }} 113 | */ 114 | export function SendTOTPData() { 115 | let $resultPromise = /** @type {any} */($Call.ByID(3266318903)); 116 | return $resultPromise; 117 | } 118 | 119 | /** 120 | * @param {boolean} state 121 | * @returns {Promise & { cancel(): void }} 122 | */ 123 | export function SetVerified(state) { 124 | let $resultPromise = /** @type {any} */($Call.ByID(734155620, state)); 125 | return $resultPromise; 126 | } 127 | 128 | /** 129 | * @param {application$0.WebviewWindow | null} window 130 | * @returns {Promise & { cancel(): void }} 131 | */ 132 | export function SetWindow(window) { 133 | let $resultPromise = /** @type {any} */($Call.ByID(64129708, window)); 134 | return $resultPromise; 135 | } 136 | 137 | /** 138 | * @param {string} pin 139 | * @returns {Promise & { cancel(): void }} 140 | */ 141 | export function SetupPin(pin) { 142 | let $resultPromise = /** @type {any} */($Call.ByID(2893959452, pin)); 143 | return $resultPromise; 144 | } 145 | 146 | /** 147 | * @param {string} pin 148 | * @returns {Promise & { cancel(): void }} 149 | */ 150 | export function VerifyPin(pin) { 151 | let $resultPromise = /** @type {any} */($Call.ByID(90512156, pin)); 152 | return $resultPromise; 153 | } 154 | 155 | /** 156 | * @returns {Promise & { cancel(): void }} 157 | */ 158 | export function VerifyTouchID() { 159 | let $resultPromise = /** @type {any} */($Call.ByID(2099673405)); 160 | return $resultPromise; 161 | } 162 | 163 | // Private type creation functions 164 | const $$createType0 = objects$0.InitResult.createFrom; 165 | -------------------------------------------------------------------------------- /frontend/bindings/clave/backend/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | import * as App from "./app.js"; 6 | export { 7 | App 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/bindings/clave/backend/models.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore: Unused imports 7 | import {Create as $Create} from "@wailsio/runtime"; 8 | 9 | export class InitResult { 10 | /** 11 | * Creates a new InitResult instance. 12 | * @param {Partial} [$$source = {}] - The source object to create the InitResult. 13 | */ 14 | constructor($$source = {}) { 15 | if (!("needsOnboarding" in $$source)) { 16 | /** 17 | * @member 18 | * @type {boolean} 19 | */ 20 | this["needsOnboarding"] = false; 21 | } 22 | if (!("needsVerification" in $$source)) { 23 | /** 24 | * @member 25 | * @type {boolean} 26 | */ 27 | this["needsVerification"] = false; 28 | } 29 | 30 | Object.assign(this, $$source); 31 | } 32 | 33 | /** 34 | * Creates a new InitResult instance from a string or object. 35 | * @param {any} [$$source = {}] 36 | * @returns {InitResult} 37 | */ 38 | static createFrom($$source = {}) { 39 | let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; 40 | return new InitResult(/** @type {Partial} */($$parsedSource)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/bindings/clave/objects/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | export * from "./models.js"; 6 | -------------------------------------------------------------------------------- /frontend/bindings/clave/objects/models.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore: Unused imports 7 | import {Create as $Create} from "@wailsio/runtime"; 8 | 9 | export class InitResult { 10 | /** 11 | * Creates a new InitResult instance. 12 | * @param {Partial} [$$source = {}] - The source object to create the InitResult. 13 | */ 14 | constructor($$source = {}) { 15 | if (!("needsOnboarding" in $$source)) { 16 | /** 17 | * @member 18 | * @type {boolean} 19 | */ 20 | this["needsOnboarding"] = false; 21 | } 22 | if (!("needsVerification" in $$source)) { 23 | /** 24 | * @member 25 | * @type {boolean} 26 | */ 27 | this["needsVerification"] = false; 28 | } 29 | 30 | Object.assign(this, $$source); 31 | } 32 | 33 | /** 34 | * Creates a new InitResult instance from a string or object. 35 | * @param {any} [$$source = {}] 36 | * @returns {InitResult} 37 | */ 38 | static createFrom($$source = {}) { 39 | let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; 40 | return new InitResult(/** @type {Partial} */($$parsedSource)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /frontend/bindings/github.com/wailsapp/wails/v3/pkg/application/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | export * from "./models.js"; 6 | -------------------------------------------------------------------------------- /frontend/bindings/github.com/wailsapp/wails/v3/pkg/application/models.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL 3 | // This file is automatically generated. DO NOT EDIT 4 | 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore: Unused imports 7 | import {Create as $Create} from "@wailsio/runtime"; 8 | 9 | export class WebviewWindow { 10 | /** 11 | * Creates a new WebviewWindow instance. 12 | * @param {Partial} [$$source = {}] - The source object to create the WebviewWindow. 13 | */ 14 | constructor($$source = {}) { 15 | 16 | Object.assign(this, $$source); 17 | } 18 | 19 | /** 20 | * Creates a new WebviewWindow instance from a string or object. 21 | * @param {any} [$$source = {}] 22 | * @returns {WebviewWindow} 23 | */ 24 | static createFrom($$source = {}) { 25 | let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; 26 | return new WebviewWindow(/** @type {Partial} */($$parsedSource)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" 11 | }, 12 | "devDependencies": { 13 | "@sveltejs/kit": "^2.0.0", 14 | "@sveltejs/vite-plugin-svelte": "^3.0.0", 15 | "@types/node": "^22.9.0", 16 | "autoprefixer": "^10.4.20", 17 | "postcss": "^8.4.48", 18 | "svelte": "^4.2.7", 19 | "svelte-check": "^4.0.0", 20 | "tailwindcss": "^3.4.14", 21 | "typescript": "^5.0.0", 22 | "vite": "^5.0.3" 23 | }, 24 | "type": "module", 25 | "dependencies": { 26 | "@sveltejs/adapter-static": "^3.0.5", 27 | "@wailsio/runtime": "^3.0.0-alpha.28", 28 | "jssha": "^3.3.1", 29 | "lucide-svelte": "^0.460.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /frontend/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /frontend/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %sveltekit.head% 7 | 8 | 9 |
%sveltekit.body%
10 | 11 | 12 | -------------------------------------------------------------------------------- /frontend/src/lib/components/about/About.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 |
29 | 35 |

About

36 |
37 | 38 |
39 |
40 |
41 |

42 | 43 | Clave 44 | 45 |

46 | {#if version} 47 |

Version {version}

48 | {/if} 49 |
50 | 51 |
52 |

Tech Stack

53 |
54 | {#each ['Go', 'Wails v3', 'SvelteKit', 'TypeScript'] as tech} 55 |
56 | {tech} 57 |
58 | {/each} 59 |
60 |
61 | 62 |
63 |

Links

64 |
65 | 74 | 75 | 84 | 85 | 94 |
95 |
96 | 97 |
98 |

Support

99 | 106 |
107 |
108 |
109 | 110 |
111 |
112 | Built with 113 | 114 | openURL('https://wails.io')}>in Wails v3 115 |
116 |
117 |
118 | 119 | -------------------------------------------------------------------------------- /frontend/src/lib/components/auth/PinSetup.svelte: -------------------------------------------------------------------------------- 1 | 184 | 185 |
190 |
191 |

194 | {mode === "verify" 195 | ? "Welcome Back" 196 | : step === 1 197 | ? "Create PIN" 198 | : "Confirm PIN"} 199 |

200 |

201 | {#if mode === "verify"} 202 | Enter your PIN {#if isMacOS}or use TouchID{/if} to continue 203 | {:else} 204 | {step === 1 205 | ? "Choose a secure 6-digit PIN" 206 | : "Enter the same PIN again"} 207 | {/if} 208 |

209 |
210 | 211 |
215 |
216 | {#each Array(6) as _, i} 217 |
228 | {#if (mode === 'verify' || step === 1 ? pin.length > i : confirmPin.length > i)} 229 | 230 | {/if} 231 |
232 | {/each} 233 |
234 |
235 | 236 | 249 | 250 | {#if error} 251 |
255 | 256 | 257 | 258 | {error} 259 |
260 | {/if} 261 | 262 | {#if mode === "verify" && isMacOS} 263 | 282 | {/if} 283 | 284 |

285 | {mode === "verify" ? "" : "Set your PIN to secure your account"} 286 |

287 |
288 | 289 | 300 | -------------------------------------------------------------------------------- /frontend/src/lib/components/intro/Footer.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |

Version v{version}

10 |
11 | -------------------------------------------------------------------------------- /frontend/src/lib/components/intro/Intro.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 |

Welcome to Clave!

10 | {#if currentStep === 1} 11 |

Clave offers three easy ways to add your profile:

12 | {:else if currentStep === 2} 13 |

Choose the method that works best for you:

14 | {:else} 15 |

Ready to secure your accounts?

16 | {/if} 17 | 18 |
19 | {#if currentStep === 1} 20 |

1. Manual Entry

21 |

Enter your secret key and issuer details manually.

22 | {:else if currentStep === 2} 23 |

2. Import QR Code

24 |

Import a QR code image from your device to set up your profile.

25 | {:else} 26 |

3. Drag & Drop QR (Coming Soon)

27 |

Simply drag and drop a QR code image to add your profile.

28 | {/if} 29 |
30 | 31 |
32 | 38 | 44 |
45 |
-------------------------------------------------------------------------------- /frontend/src/lib/components/intro/LoadingSpinner.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |
8 |
9 |
10 |
11 |

Initializing Clave...

12 |
-------------------------------------------------------------------------------- /frontend/src/lib/components/totp/TotpList.svelte: -------------------------------------------------------------------------------- 1 | 282 | 283 | {#if currentView === 'main'} 284 |
285 |
286 |
287 |
288 |

289 | 290 | Clave 291 | 292 |

293 |
294 | 295 |
296 | 305 | 306 | {#if showMenu} 307 |
311 | {#each menuItems as item} 312 | 321 | {/each} 322 | {#if hasUpdate} 323 | 329 | {/if} 330 |
331 | {/if} 332 |
333 |
334 |
335 | 336 |
337 | {#if isLoading} 338 |
339 |
340 |

Loading your profiles...

341 |
342 | {:else if !profiles || profiles.length === 0} 343 |
344 |
345 |

346 | No Profiles Yet 347 |

348 |

Click the + button below to add your first profile

349 |
350 |
351 | {:else} 352 |
353 | {#each profiles as profile (profile.id)} 354 |
355 |
356 |
357 |
{profile.issuer}
358 |
359 | {profile.otp || "Loading..."} 360 |
361 |
362 |
363 |
364 | {profile.countdown}s 365 |
366 | 372 | 378 |
379 |
380 |
381 | {/each} 382 |
383 | {/if} 384 |
385 | 386 |
387 |
388 | 394 | 395 | {#if showAddMenu} 396 |
400 | {#each addOptions as option} 401 | 411 | {/each} 412 |
413 | {/if} 414 |
415 |
416 | 417 | {#if toastMessage} 418 |
422 | {toastMessage} 423 |
424 | {/if} 425 | 426 | {#if showManualEntryModal} 427 |
showManualEntryModal = false} 431 | > 432 |
436 |

Add Manual Entry

437 | 438 |
439 |
440 | 441 | 447 |
448 | 449 |
450 | 451 | 457 |
458 |
459 | 460 |
461 | 467 | 473 |
474 |
475 |
476 | {/if} 477 | 478 | {#if showConfirmModal} 479 |
showConfirmModal = false} 483 | > 484 |
488 |

Remove Profile

489 |

490 | Are you sure you want to remove this profile? 491 |

492 | 493 |
494 | 503 | 509 |
510 |
511 |
512 | {/if} 513 |
514 | {:else if currentView === 'about'} 515 | 516 | {/if} 517 | 518 | 519 | -------------------------------------------------------------------------------- /frontend/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /frontend/src/lib/stores/onboarding.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store'; 2 | 3 | interface OnboardingState { 4 | isLoading: boolean; 5 | showIntro: boolean; 6 | currentStep: number; 7 | introComplete: boolean; 8 | } 9 | 10 | const createOnboardingStore = () => { 11 | const initialState: OnboardingState = { 12 | isLoading: false, 13 | showIntro: false, 14 | currentStep: 1, 15 | introComplete: false 16 | }; 17 | 18 | const { subscribe, set, update } = writable(initialState); 19 | 20 | return { 21 | subscribe, 22 | startIntro: () => update(state => ({ ...state, showIntro: true, currentStep: 1 })), 23 | nextStep: () => update(state => { 24 | if (state.currentStep < 3) { 25 | return { ...state, currentStep: state.currentStep + 1 }; 26 | } 27 | return { ...state, introComplete: true, showIntro: false }; 28 | }), 29 | prevStep: () => update(state => { 30 | if (state.currentStep > 1) { 31 | return { ...state, currentStep: state.currentStep - 1 }; 32 | } 33 | return { ...state, showIntro: false }; 34 | }), 35 | setLoading: (loading: boolean) => update(state => ({ ...state, isLoading: loading })) 36 | }; 37 | }; 38 | 39 | export const onboardingStore = createOnboardingStore(); -------------------------------------------------------------------------------- /frontend/src/lib/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface VersionProps { 2 | version: string; 3 | } 4 | 5 | export interface IntroProps { 6 | currentStep: number; 7 | nextStep: () => void; 8 | prevStep: () => void; 9 | } -------------------------------------------------------------------------------- /frontend/src/lib/types/totp.ts: -------------------------------------------------------------------------------- 1 | export interface TOTPProfile { 2 | id: string; 3 | issuer: string; 4 | secret: string; 5 | otp: string | null; 6 | countdown: number; 7 | error?: string; 8 | } 9 | 10 | export interface MenuOption { 11 | label: string; 12 | icon?: any; 13 | action: () => void; 14 | } 15 | 16 | export interface TOTPEvent { 17 | data: TOTPProfile[] | [TOTPProfile[]] | null; 18 | } -------------------------------------------------------------------------------- /frontend/src/lib/utils/encoding.ts: -------------------------------------------------------------------------------- 1 | export function base32tohex(base32: string): string { 2 | const base32chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; 3 | let bits = ''; 4 | let hex = ''; 5 | 6 | base32 = base32.replace(/=+$/, ''); 7 | 8 | for (let i = 0; i < base32.length; i++) { 9 | const val = base32chars.indexOf(base32.charAt(i).toUpperCase()); 10 | if (val === -1) throw new Error('Invalid base32 character in key'); 11 | bits += leftpad(val.toString(2), 5, '0'); 12 | } 13 | 14 | for (let i = 0; i + 8 <= bits.length; i += 8) { 15 | const chunk = bits.substr(i, 8); 16 | hex = hex + leftpad(parseInt(chunk, 2).toString(16), 2, '0'); 17 | } 18 | return hex; 19 | } 20 | 21 | export function dec2hex(s: number): string { 22 | return (s < 15.5 ? '0' : '') + Math.round(s).toString(16); 23 | } 24 | 25 | export function hex2dec(s: string): number { 26 | return parseInt(s, 16); 27 | } 28 | 29 | export function leftpad(str: string, len: number, pad: string): string { 30 | if (len + 1 >= str.length) { 31 | str = Array(len + 1 - str.length).join(pad) + str; 32 | } 33 | return str; 34 | } -------------------------------------------------------------------------------- /frontend/src/lib/utils/totp.ts: -------------------------------------------------------------------------------- 1 | import jsSHA from "jssha"; 2 | import { base32tohex, dec2hex, hex2dec, leftpad } from './encoding' 3 | 4 | interface TOTPResult { 5 | otp: string; 6 | countdown: number; 7 | } 8 | 9 | export function generateTOTP(secret: string): TOTPResult { 10 | const epoch = Math.round(new Date().getTime() / 1000.0); 11 | const key = base32tohex(secret); 12 | const time = leftpad(dec2hex(Math.floor(epoch / 30)), 16, "0"); 13 | 14 | const shaObj = new jsSHA("SHA-1", "HEX"); 15 | shaObj.setHMACKey(key, "HEX"); 16 | shaObj.update(time); 17 | 18 | const hmac = shaObj.getHMAC("HEX"); 19 | const offset = hex2dec(hmac.substring(hmac.length - 1)); 20 | let otp = (hex2dec(hmac.substr(offset * 2, 8)) & hex2dec("7fffffff")) + ""; 21 | otp = otp.substr(otp.length - 6, 6); 22 | 23 | return { 24 | otp, 25 | countdown: 30 - (epoch % 30) 26 | }; 27 | } -------------------------------------------------------------------------------- /frontend/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 | 7 |
-------------------------------------------------------------------------------- /frontend/src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | export const prerender = true; 2 | export const ssr = false; 3 | -------------------------------------------------------------------------------- /frontend/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 100 | 101 | {#if isInitializing} 102 |
103 | 104 |
105 | {:else if setupComplete} 106 |
107 | 108 |
109 | {:else if needsVerification} 110 |
111 |
112 | 116 |
117 |
118 |
119 | {:else} 120 |
121 |
122 |

123 | 124 | {title} 125 | 126 |

127 |

Effortless Security, One Tap Away

128 |
129 | 130 |
131 | {#if showPinSetup} 132 | 136 | {:else if showIntro} 137 | { 140 | if (currentStep === 3) { 141 | handleOnboardingComplete(); 142 | } else { 143 | onboardingStore.nextStep(); 144 | } 145 | }} 146 | prevStep={onboardingStore.prevStep} 147 | /> 148 | {:else} 149 | 156 | {/if} 157 |
158 | 159 |
160 |
161 | {/if} 162 | 163 | -------------------------------------------------------------------------------- /frontend/static/Inter-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansxuman/clave/3b853209d1428042f9df7d8eaac74672f3941092/frontend/static/Inter-Medium.ttf -------------------------------------------------------------------------------- /frontend/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ansxuman/clave/3b853209d1428042f9df7d8eaac74672f3941092/frontend/static/favicon.png -------------------------------------------------------------------------------- /frontend/static/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", 3 | "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 4 | sans-serif; 5 | font-size: 16px; 6 | line-height: 24px; 7 | font-weight: 400; 8 | color-scheme: light dark; 9 | color: rgba(255, 255, 255, 0.87); 10 | background-color: rgba(27, 38, 54, 1); 11 | font-synthesis: none; 12 | text-rendering: optimizeLegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-text-size-adjust: 100%; 16 | } 17 | 18 | * { 19 | user-select: none; 20 | -webkit-user-select: none; 21 | -moz-user-select: none; 22 | -ms-user-select: none; 23 | } 24 | 25 | @font-face { 26 | font-family: "Inter"; 27 | font-style: normal; 28 | font-weight: 400; 29 | src: local(""), 30 | url("./Inter-Medium.ttf") format("truetype"); 31 | } 32 | 33 | h3 { 34 | font-size: 3em; 35 | line-height: 1.1; 36 | } 37 | 38 | a { 39 | font-weight: 500; 40 | color: #646cff; 41 | text-decoration: inherit; 42 | } 43 | 44 | a:hover { 45 | color: #535bf2; 46 | } 47 | 48 | button { 49 | width: 60px; 50 | height: 30px; 51 | line-height: 30px; 52 | border-radius: 3px; 53 | border: none; 54 | margin: 0 0 0 20px; 55 | padding: 0 8px; 56 | cursor: pointer; 57 | } 58 | 59 | .result { 60 | height: 20px; 61 | line-height: 20px; 62 | } 63 | 64 | body { 65 | margin: 0; 66 | display: flex; 67 | place-items: center; 68 | place-content: center; 69 | min-width: 320px; 70 | min-height: 100vh; 71 | } 72 | 73 | .container { 74 | display: flex; 75 | flex-direction: column; 76 | align-items: center; 77 | justify-content: center; 78 | } 79 | 80 | h1 { 81 | font-size: 3.2em; 82 | line-height: 1.1; 83 | } 84 | 85 | #app { 86 | max-width: 1280px; 87 | margin: 0 auto; 88 | padding: 2rem; 89 | text-align: center; 90 | } 91 | 92 | .logo { 93 | height: 6em; 94 | padding: 1.5em; 95 | will-change: filter; 96 | } 97 | 98 | .logo:hover { 99 | filter: drop-shadow(0 0 2em #e80000aa); 100 | } 101 | 102 | .logo.vanilla:hover { 103 | filter: drop-shadow(0 0 2em #f7df1eaa); 104 | } 105 | 106 | .result { 107 | height: 20px; 108 | line-height: 20px; 109 | margin: 1.5rem auto; 110 | text-align: center; 111 | } 112 | 113 | .footer { 114 | margin-top: 1rem; 115 | align-content: center; 116 | text-align: center; 117 | color: rgba(255, 255, 255, 0.67); 118 | } 119 | 120 | @media (prefers-color-scheme: light) { 121 | :root { 122 | color: #213547; 123 | background-color: #ffffff; 124 | } 125 | 126 | a:hover { 127 | color: #747bff; 128 | } 129 | 130 | button { 131 | background-color: #f9f9f9; 132 | } 133 | } 134 | 135 | 136 | .input-box .btn:hover { 137 | background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); 138 | color: #333333; 139 | } 140 | 141 | .input-box .input { 142 | border: none; 143 | border-radius: 3px; 144 | outline: none; 145 | height: 30px; 146 | line-height: 30px; 147 | padding: 0 10px; 148 | color: black; 149 | background-color: rgba(240, 240, 240, 1); 150 | -webkit-font-smoothing: antialiased; 151 | } 152 | 153 | .input-box .input:hover { 154 | border: none; 155 | background-color: rgba(255, 255, 255, 1); 156 | } 157 | 158 | .input-box .input:focus { 159 | border: none; 160 | background-color: rgba(255, 255, 255, 1); 161 | } -------------------------------------------------------------------------------- /frontend/svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-static'; 2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | preprocess: vitePreprocess(), 7 | kit: { 8 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 9 | // If your environment is not supported, or you settled on a specific environment, switch out the adapter. 10 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 11 | adapter: adapter({ 12 | pages: 'dist', 13 | assets: 'dist', 14 | fallback: undefined, 15 | precompress: false, 16 | strict: true 17 | }) 18 | } 19 | }; 20 | 21 | export default config; 22 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ['./src/**/*.{html,js,svelte,ts}'], 4 | theme: { 5 | extend: {} 6 | }, 7 | plugins: [] 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "allowUnusedLabels": true, 13 | "noUnusedLocals": false, 14 | "moduleResolution": "bundler" 15 | } 16 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 17 | // except $lib which is handled by https://kit.svelte.dev/docs/configuration#files 18 | // 19 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 20 | // from the referenced tsconfig.json - TypeScript does not merge them in 21 | } 22 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig, searchForWorkspaceRoot } from 'vite'; 3 | import path from 'path'; 4 | 5 | export default defineConfig({ 6 | server: { 7 | fs: { 8 | allow: [ 9 | searchForWorkspaceRoot(process.cwd()), 10 | './bindings/*' 11 | ] 12 | } 13 | }, 14 | resolve: { 15 | alias: { 16 | '@clave/backend': path.resolve('./bindings/clave/backend/app'), 17 | } 18 | }, 19 | plugins: [sveltekit()] 20 | }); -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module clave 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/ansxuman/go-touchid v0.0.0-20241021115423-60941306d4c3 7 | github.com/dgraph-io/badger/v4 v4.4.0 8 | github.com/orcaman/concurrent-map/v2 v2.0.1 9 | github.com/wailsapp/wails/v3 v3.0.0-alpha.7 10 | golang.org/x/crypto v0.28.0 11 | ) 12 | 13 | require ( 14 | dario.cat/mergo v1.0.1 // indirect 15 | github.com/Microsoft/go-winio v0.6.1 // indirect 16 | github.com/ProtonMail/go-crypto v1.0.0 // indirect 17 | github.com/adrg/xdg v0.5.0 // indirect 18 | github.com/bep/debounce v1.2.1 // indirect 19 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 20 | github.com/cloudflare/circl v1.3.8 // indirect 21 | github.com/cyphar/filepath-securejoin v0.2.5 // indirect 22 | github.com/dgraph-io/ristretto/v2 v2.0.0 // indirect 23 | github.com/dustin/go-humanize v1.0.1 // indirect 24 | github.com/ebitengine/purego v0.4.0-alpha.4 // indirect 25 | github.com/emirpasic/gods v1.18.1 // indirect 26 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 27 | github.com/go-git/go-billy/v5 v5.6.0 // indirect 28 | github.com/go-git/go-git/v5 v5.12.0 // indirect 29 | github.com/go-ole/go-ole v1.2.6 // indirect 30 | github.com/godbus/dbus/v5 v5.1.0 // indirect 31 | github.com/gogo/protobuf v1.3.2 // indirect 32 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 33 | github.com/golang/protobuf v1.5.4 // indirect 34 | github.com/google/flatbuffers v24.3.25+incompatible // indirect 35 | github.com/google/uuid v1.4.0 36 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 37 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect 38 | github.com/kevinburke/ssh_config v1.2.0 // indirect 39 | github.com/klauspost/compress v1.17.11 // indirect 40 | github.com/leaanthony/go-ansi-parser v1.6.1 // indirect 41 | github.com/leaanthony/u v1.1.0 // indirect 42 | github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea 43 | github.com/lmittmann/tint v1.0.4 // indirect 44 | github.com/mattn/go-colorable v0.1.13 // indirect 45 | github.com/mattn/go-isatty v0.0.20 // indirect 46 | github.com/pjbgf/sha1cd v0.3.0 // indirect 47 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect 48 | github.com/pkg/errors v0.9.1 // indirect 49 | github.com/rivo/uniseg v0.4.7 // indirect 50 | github.com/samber/lo v1.38.1 // indirect 51 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 52 | github.com/skeema/knownhosts v1.2.2 // indirect 53 | github.com/wailsapp/go-webview2 v1.0.18-0.20241130004144-dd8667af33c1 // indirect 54 | github.com/wailsapp/mimetype v1.4.1 // indirect 55 | github.com/xanzy/ssh-agent v0.3.3 // indirect 56 | go.opencensus.io v0.24.0 // indirect 57 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect 58 | golang.org/x/mod v0.19.0 // indirect 59 | golang.org/x/net v0.30.0 // indirect 60 | golang.org/x/sync v0.8.0 // indirect 61 | golang.org/x/sys v0.27.0 // indirect 62 | golang.org/x/tools v0.23.0 // indirect 63 | google.golang.org/protobuf v1.33.0 // indirect 64 | gopkg.in/warnings.v0 v0.1.2 // indirect 65 | ) 66 | 67 | replace github.com/wailsapp/wails/v3 => ../wails/v3 68 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 3 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 6 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 7 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 8 | github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= 9 | github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 10 | github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY= 11 | github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4= 12 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 13 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 14 | github.com/ansxuman/go-touchid v0.0.0-20241021115423-60941306d4c3 h1:cgxcPY4gHIzqoTPzZWo2cdj0wIdUeQDlgxzLWpcWJZE= 15 | github.com/ansxuman/go-touchid v0.0.0-20241021115423-60941306d4c3/go.mod h1:SZgGQD5WyV7ZMh6FMUmfozePvKhK3uxoHTnlo7lzM/E= 16 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 17 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 18 | github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= 19 | github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= 20 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 21 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 22 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 23 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 24 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 25 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 26 | github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= 27 | github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= 28 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 29 | github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= 30 | github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 31 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 32 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 33 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 34 | github.com/dgraph-io/badger/v4 v4.4.0 h1:rA48XiDynZLyMdlaJl67p9+lqfqwxlgKtCpYLAio7Zk= 35 | github.com/dgraph-io/badger/v4 v4.4.0/go.mod h1:sONMmPPfbnj9FPwS/etCqky/ULth6CQJuAZSuWCmixE= 36 | github.com/dgraph-io/ristretto/v2 v2.0.0 h1:l0yiSOtlJvc0otkqyMaDNysg8E9/F/TYZwMbxscNOAQ= 37 | github.com/dgraph-io/ristretto/v2 v2.0.0/go.mod h1:FVFokF2dRqXyPyeMnK1YDy8Fc6aTe0IKgbcd03CYeEk= 38 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= 39 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 40 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 41 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 42 | github.com/ebitengine/purego v0.4.0-alpha.4 h1:Y7yIV06Yo5M2BAdD7EVPhfp6LZ0tEcQo5770OhYUVes= 43 | github.com/ebitengine/purego v0.4.0-alpha.4/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= 44 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= 45 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 46 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 47 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 48 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 49 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 50 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 51 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 52 | github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= 53 | github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= 54 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 55 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 56 | github.com/go-git/go-billy/v5 v5.6.0 h1:w2hPNtoehvJIxR00Vb4xX94qHQi/ApZfX+nBE2Cjio8= 57 | github.com/go-git/go-billy/v5 v5.6.0/go.mod h1:sFDq7xD3fn3E0GOwUSZqHo9lrkmx8xJhA0ZrfvjBRGM= 58 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 59 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 60 | github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= 61 | github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= 62 | github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= 63 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 64 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 65 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 66 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 67 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 68 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 69 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 70 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 71 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 72 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 73 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 74 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 75 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 76 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 77 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 78 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 79 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 80 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 81 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 82 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 83 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 84 | github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= 85 | github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= 86 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 87 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 88 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 89 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 90 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 91 | github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 92 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 93 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 94 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 95 | github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= 96 | github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 97 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 98 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 99 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck= 100 | github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= 101 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 102 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 103 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 104 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 105 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 106 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 107 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 108 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 109 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 110 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 111 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 112 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 113 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 114 | github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A= 115 | github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU= 116 | github.com/leaanthony/u v1.1.0 h1:2n0d2BwPVXSUq5yhe8lJPHdxevE2qK5G99PMStMZMaI= 117 | github.com/leaanthony/u v1.1.0/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI= 118 | github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea h1:uyJ13zfy6l79CM3HnVhDalIyZ4RJAyVfDrbnfFeJoC4= 119 | github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea/go.mod h1:w4pGU9PkiX2hAWyF0yuHEHmYTQFAd6WHzp6+IY7JVjE= 120 | github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= 121 | github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= 122 | github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= 123 | github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= 124 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 125 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 126 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 127 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 128 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 129 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 130 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 131 | github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= 132 | github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= 133 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 134 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 135 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= 136 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 137 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 138 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 139 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 140 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 141 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 142 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 143 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 144 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 145 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 146 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 147 | github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= 148 | github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= 149 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 150 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 151 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 152 | github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= 153 | github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= 154 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 155 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 156 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 157 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 158 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 159 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 160 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 161 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 162 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 163 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 164 | github.com/wailsapp/go-webview2 v1.0.18-0.20241130004144-dd8667af33c1 h1:El7t3J32//oQIsdvXAmbgeNKqgmKfKzU2SVp50yn0UM= 165 | github.com/wailsapp/go-webview2 v1.0.18-0.20241130004144-dd8667af33c1/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= 166 | github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= 167 | github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= 168 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 169 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 170 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 171 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 172 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 173 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 174 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 175 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 176 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 177 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 178 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 179 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 180 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 181 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 182 | golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= 183 | golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= 184 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 185 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 186 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 187 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 188 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 189 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 190 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 191 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 192 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 193 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 194 | golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= 195 | golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 196 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 197 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 198 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 199 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 200 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 201 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 202 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 203 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 204 | golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 205 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 206 | golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 207 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 208 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 209 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 210 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 211 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 212 | golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= 213 | golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= 214 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 215 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 216 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 217 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 218 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 219 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 220 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 221 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 222 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 223 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 224 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 225 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 226 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 227 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 228 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 229 | golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 230 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 231 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 232 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 233 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 235 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 236 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 237 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 238 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 239 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 240 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 241 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 242 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 243 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 244 | golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= 245 | golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 246 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 247 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 248 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 249 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 250 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 251 | golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= 252 | golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= 253 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 254 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 255 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 256 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 257 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 258 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 259 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 260 | golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= 261 | golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 262 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 263 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 264 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 265 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 266 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 267 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 268 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 269 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 270 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 271 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 272 | golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= 273 | golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= 274 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 275 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 276 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 277 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 278 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 279 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 280 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 281 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 282 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 283 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 284 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 285 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 286 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 287 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 288 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 289 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 290 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 291 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 292 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 293 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 294 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 295 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 296 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 297 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 298 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 299 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 300 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 301 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 302 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 303 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 304 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 305 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 306 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 307 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 308 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 309 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 310 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 311 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 312 | -------------------------------------------------------------------------------- /localstorage/PersistentStorage.go: -------------------------------------------------------------------------------- 1 | package localstorage 2 | 3 | import ( 4 | "bytes" 5 | "clave/constants" 6 | "crypto/aes" 7 | "crypto/cipher" 8 | "crypto/rand" 9 | "crypto/sha256" 10 | "encoding/gob" 11 | "errors" 12 | "io" 13 | "log" 14 | "os" 15 | "path/filepath" 16 | "sync" 17 | "time" 18 | 19 | "github.com/dgraph-io/badger/v4" 20 | "github.com/dgraph-io/badger/v4/options" 21 | ) 22 | 23 | var ( 24 | ErrNotFound = errors.New("storage: key not found") 25 | ErrBadValue = errors.New("storage: bad value") 26 | 27 | storageSync sync.Once 28 | instance *PersistentStore 29 | ) 30 | 31 | type PersistentStore struct { 32 | db *badger.DB 33 | } 34 | 35 | func GetPersistentStorage() *PersistentStore { 36 | storageSync.Do(func() { 37 | 38 | dbPath := constants.SecureVaultDB 39 | if err := os.MkdirAll(filepath.Dir(dbPath), 0755); err != nil { 40 | log.Printf("[Storage] Failed to create database directory: %v", err) 41 | panic(err) 42 | } 43 | key := sha256.Sum256([]byte(constants.CommonDatabasePassword)) 44 | opts := badger.DefaultOptions(dbPath). 45 | WithLogger(nil). 46 | WithCompression(options.ZSTD). 47 | WithEncryptionKey(key[:]). 48 | WithIndexCacheSize(100 << 20) 49 | 50 | db, err := badger.Open(opts) 51 | if err != nil { 52 | log.Printf("[Storage] Failed to open DB: %v", err) 53 | panic(err) 54 | } 55 | 56 | instance = &PersistentStore{db: db} 57 | go instance.runGC() 58 | }) 59 | 60 | if instance == nil { 61 | panic("Failed to initialize storage") 62 | } 63 | 64 | return instance 65 | } 66 | 67 | func (ps *PersistentStore) SetValue(key string, value interface{}) error { 68 | if value == nil { 69 | return ErrBadValue 70 | } 71 | 72 | var buf bytes.Buffer 73 | if err := gob.NewEncoder(&buf).Encode(value); err != nil { 74 | return err 75 | } 76 | 77 | encrypted, err := ps.encrypt(buf.Bytes()) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | return ps.db.Update(func(txn *badger.Txn) error { 83 | return txn.Set([]byte(key), encrypted) 84 | }) 85 | } 86 | 87 | func (ps *PersistentStore) Get(key string, value interface{}) error { 88 | if value == nil { 89 | return ErrBadValue 90 | } 91 | 92 | var data []byte 93 | err := ps.db.View(func(txn *badger.Txn) error { 94 | item, err := txn.Get([]byte(key)) 95 | if err == badger.ErrKeyNotFound { 96 | return ErrNotFound 97 | } 98 | if err != nil { 99 | return err 100 | } 101 | 102 | return item.Value(func(val []byte) error { 103 | data = append([]byte{}, val...) 104 | return nil 105 | }) 106 | }) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | decrypted, err := ps.decrypt(data) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | return gob.NewDecoder(bytes.NewReader(decrypted)).Decode(value) 117 | } 118 | 119 | func (ps *PersistentStore) DeleteKey(key string) error { 120 | return ps.db.Update(func(txn *badger.Txn) error { 121 | return txn.Delete([]byte(key)) 122 | }) 123 | } 124 | 125 | func (ps *PersistentStore) Close() error { 126 | if ps.db != nil { 127 | return ps.db.Close() 128 | } 129 | return nil 130 | } 131 | 132 | func (ps *PersistentStore) encrypt(data []byte) ([]byte, error) { 133 | key := sha256.Sum256([]byte(constants.CommonDatabasePassword)) 134 | 135 | c, err := aes.NewCipher(key[:]) 136 | if err != nil { 137 | return nil, err 138 | } 139 | 140 | gcm, err := cipher.NewGCM(c) 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | nonce := make([]byte, gcm.NonceSize()) 146 | if _, err := io.ReadFull(rand.Reader, nonce); err != nil { 147 | return nil, err 148 | } 149 | 150 | return gcm.Seal(nonce, nonce, data, nil), nil 151 | } 152 | 153 | func (ps *PersistentStore) decrypt(data []byte) ([]byte, error) { 154 | key := sha256.Sum256([]byte(constants.CommonDatabasePassword)) 155 | 156 | c, err := aes.NewCipher(key[:]) 157 | if err != nil { 158 | return nil, err 159 | } 160 | 161 | gcm, err := cipher.NewGCM(c) 162 | if err != nil { 163 | return nil, err 164 | } 165 | 166 | nonceSize := gcm.NonceSize() 167 | if len(data) < nonceSize { 168 | return nil, errors.New("malformed data") 169 | } 170 | 171 | nonce, ciphertext := data[:nonceSize], data[nonceSize:] 172 | return gcm.Open(nil, nonce, ciphertext, nil) 173 | } 174 | 175 | func (ps *PersistentStore) IsHealthy() bool { 176 | if ps.db == nil { 177 | return false 178 | } 179 | 180 | err := ps.db.View(func(txn *badger.Txn) error { 181 | _, err := txn.Get([]byte("health_check")) 182 | if err == badger.ErrKeyNotFound { 183 | return nil 184 | } 185 | return err 186 | }) 187 | 188 | return err == nil 189 | } 190 | 191 | func (ps *PersistentStore) runGC() { 192 | ticker := time.NewTicker(10 * time.Minute) 193 | defer ticker.Stop() 194 | 195 | for range ticker.C { 196 | func() { 197 | defer func() { 198 | if r := recover(); r != nil { 199 | log.Printf("[Storage] GC panic recovered: %v", r) 200 | } 201 | }() 202 | 203 | err := ps.db.RunValueLogGC(0.5) 204 | if err != nil && err != badger.ErrNoRewrite { 205 | log.Printf("[Storage] GC error: %v", err) 206 | } 207 | }() 208 | } 209 | } 210 | 211 | func (ps *PersistentStore) HasKey(key string) bool { 212 | err := ps.db.View(func(txn *badger.Txn) error { 213 | _, err := txn.Get([]byte(key)) 214 | return err 215 | }) 216 | return err == nil 217 | } 218 | 219 | func (ps *PersistentStore) Encrypt(data []byte) ([]byte, error) { 220 | return ps.encrypt(data) 221 | } 222 | 223 | func (ps *PersistentStore) Decrypt(data []byte) ([]byte, error) { 224 | return ps.decrypt(data) 225 | } 226 | -------------------------------------------------------------------------------- /localstorage/StorageHelpers.go: -------------------------------------------------------------------------------- 1 | package localstorage 2 | 3 | import ( 4 | "clave/objects" 5 | "errors" 6 | "log" 7 | "strings" 8 | ) 9 | 10 | const ListOfSecrets = "ListOfSecrets" 11 | 12 | func (kvs *PersistentStore) AddTotpSecretObject(issuer, secret string) error { 13 | object := objects.CreateNewTotpSecretObject(issuer, secret) 14 | 15 | listOfSecrets := []objects.TotpSecretObject{} 16 | 17 | err := kvs.Get(ListOfSecrets, &listOfSecrets) 18 | 19 | if err != nil { 20 | 21 | if errors.Is(err, ErrNotFound) { 22 | listOfSecrets = []objects.TotpSecretObject{} 23 | } else { 24 | log.Println("[AddTotpSecretObject][Error] Error getting list of secrets ", err) 25 | return err 26 | } 27 | 28 | } 29 | 30 | listOfSecrets = append(listOfSecrets, object) 31 | err = kvs.SetValue(ListOfSecrets, listOfSecrets) 32 | 33 | if err != nil { 34 | log.Println("Error saving list of secrets ", err) 35 | return err 36 | } 37 | 38 | return nil 39 | 40 | } 41 | func (kvs *PersistentStore) GetListOfTotpSecretObjects() objects.TotpSecretObjectList { 42 | 43 | listOfSecrets := []objects.TotpSecretObject{} 44 | 45 | err := kvs.Get(ListOfSecrets, &listOfSecrets) 46 | 47 | if err != nil { 48 | if errors.Is(err, ErrNotFound) { 49 | return listOfSecrets 50 | } else { 51 | log.Println("Error getting list of secrets ", err) 52 | return listOfSecrets 53 | } 54 | } 55 | 56 | return listOfSecrets 57 | 58 | } 59 | 60 | func (kvs *PersistentStore) DeleteTotpSecretObject(secretId string) error { 61 | 62 | listOfSecrets := []objects.TotpSecretObject{} 63 | 64 | err := kvs.Get(ListOfSecrets, &listOfSecrets) 65 | 66 | if err != nil { 67 | if errors.Is(err, ErrNotFound) { 68 | return nil 69 | } else { 70 | log.Println("Error getting list of secrets ", err) 71 | return err 72 | } 73 | } 74 | 75 | for i, _obj := range listOfSecrets { 76 | if strings.EqualFold(_obj.Id, secretId) { 77 | listOfSecrets = append(listOfSecrets[:i], listOfSecrets[i+1:]...) 78 | break 79 | } 80 | } 81 | 82 | err = kvs.SetValue(ListOfSecrets, listOfSecrets) 83 | 84 | if err != nil { 85 | log.Println("Error saving list of secrets ", err) 86 | return err 87 | } 88 | 89 | return nil 90 | 91 | } 92 | 93 | func (kvs *PersistentStore) CheckIfIssuerOrSecretExists(issuer, secret string) (bool, objects.TotpSecretObject) { 94 | 95 | listOfSecrets := []objects.TotpSecretObject{} 96 | 97 | err := kvs.Get(ListOfSecrets, &listOfSecrets) 98 | 99 | if err != nil { 100 | if errors.Is(err, ErrNotFound) { 101 | return false, objects.TotpSecretObject{} 102 | } else { 103 | log.Println("Error getting list of secrets ", err) 104 | return false, objects.TotpSecretObject{} 105 | } 106 | } 107 | 108 | for _, _obj := range listOfSecrets { 109 | if strings.EqualFold(_obj.Issuer, issuer) || strings.EqualFold(_obj.Secret, secret) { 110 | log.Println("Issuer or secret already exists") 111 | return true, _obj 112 | } 113 | } 114 | 115 | return false, objects.TotpSecretObject{} 116 | } 117 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | a "clave/assets" 5 | "clave/backend" 6 | "clave/constants" 7 | "embed" 8 | _ "embed" 9 | "log" 10 | "runtime" 11 | 12 | "github.com/wailsapp/wails/v3/pkg/application" 13 | "github.com/wailsapp/wails/v3/pkg/events" 14 | ) 15 | 16 | //go:embed all:frontend/dist 17 | var assets embed.FS 18 | 19 | func main() { 20 | backendApp := backend.NewApp() 21 | app := application.New(application.Options{ 22 | Name: constants.ApplicationName, 23 | Description: constants.Description, 24 | Services: []application.Service{ 25 | application.NewService(backendApp, application.ServiceOptions{}), 26 | }, 27 | OnShutdown: func() { 28 | log.Println("Shutting down...") 29 | }, 30 | ShouldQuit: func() bool { 31 | return true 32 | }, 33 | Assets: application.AssetOptions{ 34 | Handler: application.AssetFileServerFS(assets), 35 | }, 36 | StartAtLogin: true, 37 | Mac: application.MacOptions{ 38 | ActivationPolicy: application.ActivationPolicyAccessory, 39 | }, 40 | }) 41 | 42 | systemTray := app.NewSystemTray() 43 | 44 | window := app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ 45 | Width: constants.MaxWidth, 46 | Height: constants.MaxHeight, 47 | Title: constants.ApplicationName, 48 | Frameless: true, 49 | // Always on top is disabled 50 | // because its crashing the 51 | // application 52 | // AlwaysOnTop: true, 53 | Hidden: true, 54 | DisableResize: true, 55 | DevToolsEnabled: false, 56 | Windows: application.WindowsWindow{ 57 | HiddenOnTaskbar: true, 58 | }, 59 | Mac: application.MacWindow{ 60 | InvisibleTitleBarHeight: 50, 61 | Backdrop: application.MacBackdropTranslucent, 62 | TitleBar: application.MacTitleBarHiddenInset, 63 | Appearance: application.NSAppearanceNameAccessibilityHighContrastVibrantLight, 64 | }, 65 | }) 66 | 67 | app.ShowAboutDialog() 68 | 69 | backendApp.SetWindow(window) 70 | 71 | app.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) { 72 | log.Println("Application started, initializing security...") 73 | }) 74 | 75 | app.Hide() 76 | 77 | systemTray.SetTemplateIcon(a.Icon) 78 | if runtime.GOOS != "windows" { 79 | systemTray.SetDarkModeIcon(a.Icon) 80 | systemTray.SetIcon(a.DarkIcon) 81 | } else { 82 | systemTray.SetIcon(a.Icon) 83 | systemTray.SetDarkModeIcon(a.DarkIcon) 84 | } 85 | 86 | myMenu := app.NewMenu() 87 | myMenu.Add("Clave").SetEnabled(false) 88 | 89 | myMenu.Add("Go Back to App").OnClick(func(ctx *application.Context) { 90 | window.Show() 91 | }) 92 | 93 | myMenu.AddSeparator() 94 | myMenu.Add("Quit Clave").OnClick(func(ctx *application.Context) { 95 | q := application.QuestionDialog(). 96 | SetTitle("Quit Clave"). 97 | SetMessage("Are you sure you want to quit?") 98 | 99 | q.AddButton("Yes").OnClick(func() { 100 | app.Quit() 101 | }) 102 | 103 | q.AddButton("No").SetAsDefault().OnClick(func() { 104 | 105 | }) 106 | 107 | q.Show() 108 | }) 109 | 110 | systemTray.SetMenu(myMenu) 111 | 112 | systemTray.AttachWindow(window).WindowOffset(5) 113 | 114 | err := app.Run() 115 | if err != nil { 116 | log.Fatal(err) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /objects/TotpSecretObject.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type TotpSecretObject struct { 10 | Id string `json:"id"` 11 | Timestamp int64 `json:"timestamp"` 12 | Issuer string `json:"issuer"` 13 | Secret string `json:"secret"` 14 | LastCopiedAt int64 `json:"lastCopiedAt"` 15 | } 16 | 17 | func CreateNewTotpSecretObject(issuer, secret string) TotpSecretObject { 18 | return TotpSecretObject{ 19 | Id: uuid.NewString(), 20 | Timestamp: time.Now().UnixMilli(), 21 | Issuer: issuer, 22 | Secret: secret, 23 | } 24 | } 25 | 26 | type TotpSecretObjectList []TotpSecretObject 27 | 28 | func (list TotpSecretObjectList) Len() int { 29 | return len(list) 30 | } 31 | 32 | func (list TotpSecretObjectList) Less(i, j int) bool { 33 | return list[i].Timestamp < list[j].Timestamp 34 | } 35 | 36 | func (list TotpSecretObjectList) Swap(i, j int) { 37 | list[i], list[j] = list[j], list[i] 38 | } 39 | -------------------------------------------------------------------------------- /objects/auth.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | type InitResult struct { 4 | NeedsOnboarding bool `json:"needsOnboarding"` 5 | NeedsVerification bool `json:"needsVerification"` 6 | } 7 | -------------------------------------------------------------------------------- /objects/totp.go: -------------------------------------------------------------------------------- 1 | package objects 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type TOTPProfile struct { 8 | ID string `json:"id"` 9 | Issuer string `json:"issuer"` 10 | Secret string `json:"secret"` 11 | Period int `json:"period"` 12 | Digits int `json:"digits"` 13 | CreatedAt time.Time `json:"createdAt"` 14 | } 15 | 16 | type TOTPCode struct { 17 | Code string `json:"code"` 18 | ExpiresIn int `json:"expiresIn"` 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "lucide-svelte": "^0.456.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /services/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/subtle" 6 | "encoding/base64" 7 | "errors" 8 | 9 | "golang.org/x/crypto/argon2" 10 | ) 11 | 12 | const ( 13 | PIN_KEY = "user_pin" 14 | SALT_KEY = "pin_salt" 15 | ) 16 | 17 | type Service struct { 18 | storage Storage 19 | } 20 | 21 | type Storage interface { 22 | IsHealthy() bool 23 | HasKey(key string) bool 24 | SetValue(key string, value interface{}) error 25 | Get(key string, value interface{}) error 26 | } 27 | 28 | func NewService(storage Storage) *Service { 29 | return &Service{storage: storage} 30 | } 31 | 32 | func (s *Service) HasPin() bool { 33 | return s.storage.HasKey(PIN_KEY) && s.storage.HasKey(SALT_KEY) 34 | } 35 | 36 | func (s *Service) SetupPin(pin string) error { 37 | if len(pin) < 6 { 38 | return errors.New("PIN must be at least 6 characters") 39 | } 40 | if s.HasPin() { 41 | return errors.New("PIN is already set") 42 | } 43 | 44 | salt := make([]byte, 16) 45 | if _, err := rand.Read(salt); err != nil { 46 | return err 47 | } 48 | 49 | hash := argon2.IDKey([]byte(pin), salt, 1, 64*1024, 4, 32) 50 | 51 | if err := s.storage.SetValue(SALT_KEY, base64.StdEncoding.EncodeToString(salt)); err != nil { 52 | return err 53 | } 54 | 55 | return s.storage.SetValue(PIN_KEY, base64.StdEncoding.EncodeToString(hash)) 56 | } 57 | 58 | func (s *Service) VerifyPin(pin string) (bool, error) { 59 | if !s.storage.IsHealthy() { 60 | return false, errors.New("storage is not healthy") 61 | } 62 | 63 | var storedHashStr string 64 | var saltStr string 65 | 66 | if err := s.storage.Get(PIN_KEY, &storedHashStr); err != nil { 67 | return false, err 68 | } 69 | 70 | if err := s.storage.Get(SALT_KEY, &saltStr); err != nil { 71 | return false, err 72 | } 73 | 74 | storedHash, err := base64.StdEncoding.DecodeString(storedHashStr) 75 | if err != nil { 76 | return false, err 77 | } 78 | 79 | salt, err := base64.StdEncoding.DecodeString(saltStr) 80 | if err != nil { 81 | return false, err 82 | } 83 | 84 | hash := argon2.IDKey([]byte(pin), salt, 1, 64*1024, 4, 32) 85 | return subtle.ConstantTimeCompare(hash, storedHash) == 1, nil 86 | } 87 | -------------------------------------------------------------------------------- /services/totp/totp.go: -------------------------------------------------------------------------------- 1 | package totp 2 | 3 | import ( 4 | "bytes" 5 | "clave/localstorage" 6 | "clave/objects" 7 | "encoding/json" 8 | "fmt" 9 | "image" 10 | _ "image/jpeg" 11 | _ "image/png" 12 | "log" 13 | "net/url" 14 | "os" 15 | "regexp" 16 | "runtime" 17 | "strings" 18 | "time" 19 | 20 | "github.com/liyue201/goqr" 21 | "github.com/wailsapp/wails/v3/pkg/application" 22 | ) 23 | 24 | type Service struct { 25 | storage Storage 26 | window *application.WebviewWindow 27 | winManager WindowManager 28 | } 29 | 30 | type Storage interface { 31 | CheckIfIssuerOrSecretExists(issuer, secret string) (bool, objects.TotpSecretObject) 32 | AddTotpSecretObject(issuer, secret string) error 33 | GetListOfTotpSecretObjects() objects.TotpSecretObjectList 34 | DeleteTotpSecretObject(profileId string) error 35 | SetValue(key string, value interface{}) error 36 | Get(key string, value interface{}) error 37 | DeleteKey(key string) error 38 | } 39 | 40 | type WindowManager interface { 41 | StartProfileAddition() 42 | EndProfileAddition() 43 | } 44 | 45 | func NewService(storage Storage, winManager WindowManager) *Service { 46 | return &Service{ 47 | storage: storage, 48 | winManager: winManager, 49 | } 50 | } 51 | 52 | func (s *Service) SetWindow(window *application.WebviewWindow) { 53 | if window != nil { 54 | s.window = window 55 | } 56 | } 57 | func (s *Service) OpenQR() error { 58 | s.winManager.StartProfileAddition() 59 | 60 | dialog := application.OpenFileDialog(). 61 | SetTitle("Select QR Code Image") 62 | 63 | if runtime.GOOS != "linux" { 64 | dialog = dialog.AddFilter("Image Files", "*.png;*.jpg;*.jpeg") 65 | } 66 | 67 | result, err := dialog.PromptForSingleSelection() 68 | if err != nil { 69 | s.winManager.EndProfileAddition() 70 | log.Printf("[TOTP] Failed to open QR image: %v", err) 71 | s.window.Show() 72 | s.window.EmitEvent(EventQRScanError, []string{"Unable to open image selector"}) 73 | return err 74 | } 75 | 76 | if result != "" { 77 | err = s.scanQRCode(result) 78 | s.winManager.EndProfileAddition() 79 | return err 80 | } 81 | 82 | s.winManager.EndProfileAddition() 83 | return nil 84 | } 85 | 86 | func (s *Service) scanQRCode(path string) error { 87 | imgdata, err := os.ReadFile(path) 88 | if err != nil { 89 | log.Printf("[TOTP] Failed to read image: %v", err) 90 | s.window.Show() 91 | s.window.EmitEvent(EventQRScanError, []string{"Unable to read the selected image"}) 92 | return err 93 | } 94 | 95 | img, _, err := image.Decode(bytes.NewReader(imgdata)) 96 | if err != nil { 97 | log.Printf("[TOTP] Failed to decode image: %v", err) 98 | s.window.Show() 99 | s.window.EmitEvent(EventQRScanError, []string{"The selected file is not a valid image"}) 100 | return err 101 | } 102 | 103 | qrCodes, err := goqr.Recognize(img) 104 | if err != nil { 105 | log.Printf("[TOTP] Failed to recognize QR: %v", err) 106 | s.window.Show() 107 | s.window.EmitEvent(EventQRScanError, []string{"No QR code found in the image"}) 108 | return err 109 | } 110 | 111 | if len(qrCodes) == 0 { 112 | s.window.Show() 113 | s.window.EmitEvent(EventQRScanError, []string{"No valid QR code found in the image"}) 114 | return fmt.Errorf("no QR codes found") 115 | } 116 | 117 | for _, qrCode := range qrCodes { 118 | if err := s.configureQRProfile(qrCode.Payload); err != nil { 119 | return err 120 | } 121 | } 122 | return nil 123 | } 124 | 125 | const ( 126 | EventTOTPData = "totpData" 127 | EventFailedToAddProfile = "failedToAddProfile" 128 | EventRefreshProfiles = "refreshProfiles" 129 | EventDuplicateProfile = "duplicateProfile" 130 | EventBackupError = "backupError" 131 | EventBackupSuccess = "backupSuccess" 132 | EventRestoreError = "restoreError" 133 | EventRestoreSuccess = "restoreSuccess" 134 | EventQRScanError = "qrScanError" 135 | ) 136 | 137 | func (s *Service) configureQRProfile(qrData []uint8) error { 138 | parsedURL, err := url.Parse(string(qrData)) 139 | if err != nil { 140 | log.Printf("[TOTP] Failed to parse QR data: %v", err) 141 | s.window.Show() 142 | s.window.EmitEvent(EventFailedToAddProfile, "Invalid QR code format") 143 | return err 144 | } 145 | 146 | if parsedURL.Scheme != "otpauth" || parsedURL.Host != "totp" { 147 | s.window.Show() 148 | s.window.EmitEvent(EventFailedToAddProfile, "Invalid QR code") 149 | return fmt.Errorf("invalid URI format") 150 | } 151 | 152 | secret := parsedURL.Query().Get("secret") 153 | if secret == "" { 154 | s.window.Show() 155 | s.window.EmitEvent(EventFailedToAddProfile, "Missing secret parameter") 156 | return fmt.Errorf("missing secret") 157 | } 158 | base32Regex := regexp.MustCompile(`^[A-Z2-7]+=*$`) 159 | if !base32Regex.MatchString(secret) { 160 | s.window.Show() 161 | s.window.EmitEvent(EventFailedToAddProfile, "Invalid secret format") 162 | return fmt.Errorf("invalid secret format") 163 | } 164 | 165 | path := strings.TrimPrefix(parsedURL.Path, "/") 166 | issuer := strings.TrimSuffix(path, "/") 167 | if issuer == "" { 168 | s.window.Show() 169 | s.window.EmitEvent(EventFailedToAddProfile, "Missing issuer") 170 | return fmt.Errorf("missing issuer") 171 | } 172 | 173 | exists, profile := s.storage.CheckIfIssuerOrSecretExists(issuer, secret) 174 | if exists { 175 | s.window.Show() 176 | log.Printf("[TOTP] Profile already exists for issuer: %s", issuer) 177 | s.window.EmitEvent(EventDuplicateProfile, fmt.Sprintf("Profile '%s' already exists", profile.Issuer)) 178 | return nil 179 | } 180 | 181 | if err := s.storage.AddTotpSecretObject(issuer, secret); err != nil { 182 | log.Printf("[TOTP] Failed to save profile: %v", err) 183 | s.window.Show() 184 | s.window.EmitEvent(EventFailedToAddProfile, "Failed to save profile") 185 | s.winManager.EndProfileAddition() 186 | return err 187 | } 188 | s.window.Show() 189 | s.window.EmitEvent(EventRefreshProfiles, nil) 190 | s.winManager.EndProfileAddition() 191 | return nil 192 | } 193 | 194 | func (s *Service) SendTOTPData() { 195 | if s.window == nil { 196 | log.Printf("[TOTP] Window not initialized") 197 | return 198 | } 199 | 200 | listOfSecrets := s.storage.GetListOfTotpSecretObjects() 201 | if len(listOfSecrets) == 0 { 202 | log.Printf("[TOTP] No profiles found") 203 | s.window.EmitEvent(EventTOTPData, []objects.TotpSecretObject{}) 204 | return 205 | } 206 | 207 | log.Printf("[TOTP] Sending %d profiles", len(listOfSecrets)) 208 | s.window.EmitEvent(EventTOTPData, listOfSecrets) 209 | } 210 | 211 | func (s *Service) RemoveTotpProfile(profileId string) error { 212 | return s.storage.DeleteTotpSecretObject(profileId) 213 | } 214 | 215 | func (s *Service) AddManualProfile(issuer string, secret string) error { 216 | base32Regex := regexp.MustCompile(`^[A-Z2-7]+=*$`) 217 | if !base32Regex.MatchString(secret) { 218 | s.window.EmitEvent(EventFailedToAddProfile, "Invalid secret format") 219 | return fmt.Errorf("invalid secret format") 220 | } 221 | 222 | exists, profile := s.storage.CheckIfIssuerOrSecretExists(issuer, secret) 223 | if exists { 224 | log.Printf("[TOTP] Profile already exists for issuer: %s", issuer) 225 | s.window.EmitEvent(EventDuplicateProfile, fmt.Sprintf("Profile '%s' already exists", profile.Issuer)) 226 | return nil 227 | } 228 | 229 | if err := s.storage.AddTotpSecretObject(issuer, secret); err != nil { 230 | log.Printf("[TOTP] Failed to save profile: %v", err) 231 | s.window.EmitEvent(EventFailedToAddProfile, "Failed to add profile") 232 | return err 233 | } 234 | 235 | s.window.EmitEvent("refreshTOTPProfiles", nil) 236 | return nil 237 | } 238 | 239 | func (s *Service) BackupProfiles() error { 240 | log.Printf("[TOTP] Starting backup process") 241 | s.winManager.StartProfileAddition() 242 | 243 | existingProfiles := s.storage.GetListOfTotpSecretObjects() 244 | if len(existingProfiles) == 0 { 245 | log.Printf("[TOTP] No profiles found to backup") 246 | s.window.Show() 247 | s.window.EmitEvent(EventBackupError, []string{"You don't have any profiles to backup yet"}) 248 | s.winManager.EndProfileAddition() 249 | return fmt.Errorf("no profiles to backup") 250 | } 251 | 252 | dialog := application.SaveFileDialog(). 253 | SetMessage("Save backup file"). 254 | SetButtonText("Save Backup"). 255 | AddFilter("Backup Files", "*.clave"). 256 | SetFilename(fmt.Sprintf("clave-backup-%s.clave", time.Now().Format("2006-01-02-15-04-05"))) 257 | 258 | result, err := dialog.PromptForSingleSelection() 259 | if err != nil { 260 | log.Printf("[TOTP] Backup dialog error: %v", err) 261 | s.window.Show() 262 | s.window.EmitEvent(EventBackupError, []string{"Unable to open save dialog"}) 263 | s.winManager.EndProfileAddition() 264 | return err 265 | } 266 | 267 | if result == "" { 268 | log.Printf("[TOTP] User cancelled backup") 269 | s.winManager.EndProfileAddition() 270 | return nil 271 | } 272 | 273 | backupData := struct { 274 | Profiles []objects.TotpSecretObject `json:"profiles"` 275 | Version string `json:"version"` 276 | Date string `json:"date"` 277 | }{ 278 | Profiles: existingProfiles, 279 | Version: "1.0", 280 | Date: time.Now().Format(time.RFC3339), 281 | } 282 | 283 | jsonData, err := json.MarshalIndent(backupData, "", " ") 284 | if err != nil { 285 | log.Printf("[TOTP] Failed to prepare backup data: %v", err) 286 | s.window.Show() 287 | s.window.EmitEvent(EventBackupError, []string{"Unable to prepare backup data"}) 288 | s.winManager.EndProfileAddition() 289 | return err 290 | } 291 | 292 | encrypted, err := s.storage.(*localstorage.PersistentStore).Encrypt(jsonData) 293 | if err != nil { 294 | log.Printf("[TOTP] Failed to encrypt backup: %v", err) 295 | s.window.Show() 296 | s.window.EmitEvent(EventBackupError, []string{"Unable to secure backup data"}) 297 | s.winManager.EndProfileAddition() 298 | return err 299 | } 300 | 301 | err = os.WriteFile(result, encrypted, 0644) 302 | if err != nil { 303 | log.Printf("[TOTP] Failed to write backup file: %v", err) 304 | s.window.Show() 305 | s.window.EmitEvent(EventBackupError, []string{"Unable to save backup file"}) 306 | s.winManager.EndProfileAddition() 307 | return err 308 | } 309 | 310 | s.window.Show() 311 | s.window.EmitEvent(EventBackupSuccess, []string{fmt.Sprintf("Successfully backed up %d profiles", len(existingProfiles))}) 312 | s.winManager.EndProfileAddition() 313 | return nil 314 | } 315 | 316 | func (s *Service) RestoreProfiles() error { 317 | log.Printf("[TOTP] Starting restore process") 318 | s.winManager.StartProfileAddition() 319 | 320 | dialog := application.OpenFileDialog(). 321 | SetTitle("Select Backup File") 322 | if runtime.GOOS != "linux" { 323 | dialog = dialog.AddFilter("Backup Files", "*.clave") 324 | } 325 | 326 | result, err := dialog.PromptForSingleSelection() 327 | if err != nil { 328 | log.Printf("[TOTP] Restore dialog error: %v", err) 329 | s.window.Show() 330 | s.window.EmitEvent(EventRestoreError, []string{"Unable to open file selector"}) 331 | s.winManager.EndProfileAddition() 332 | return err 333 | } 334 | 335 | if result == "" { 336 | log.Printf("[TOTP] User cancelled restore") 337 | s.winManager.EndProfileAddition() 338 | return nil 339 | } 340 | 341 | encryptedData, err := os.ReadFile(result) 342 | if err != nil { 343 | log.Printf("[TOTP] Failed to read backup file: %v", err) 344 | s.window.Show() 345 | s.window.EmitEvent(EventRestoreError, []string{"Unable to read the backup file"}) 346 | s.winManager.EndProfileAddition() 347 | return err 348 | } 349 | 350 | decrypted, err := s.storage.(*localstorage.PersistentStore).Decrypt(encryptedData) 351 | if err != nil { 352 | log.Printf("[TOTP] Failed to decrypt backup: %v", err) 353 | s.window.Show() 354 | s.window.EmitEvent(EventRestoreError, []string{"Invalid or corrupted backup file"}) 355 | s.winManager.EndProfileAddition() 356 | return err 357 | } 358 | 359 | var backupData struct { 360 | Profiles []objects.TotpSecretObject `json:"profiles"` 361 | Version string `json:"version"` 362 | Date string `json:"date"` 363 | } 364 | 365 | if err := json.Unmarshal(decrypted, &backupData); err != nil { 366 | log.Printf("[TOTP] Failed to parse backup data: %v", err) 367 | s.window.Show() 368 | s.window.EmitEvent(EventRestoreError, []string{"The backup file appears to be damaged"}) 369 | s.winManager.EndProfileAddition() 370 | return err 371 | } 372 | 373 | if len(backupData.Profiles) == 0 { 374 | log.Printf("[TOTP] Empty backup file") 375 | s.window.Show() 376 | s.window.EmitEvent(EventRestoreError, []string{"No profiles found in the backup file"}) 377 | s.winManager.EndProfileAddition() 378 | return fmt.Errorf("empty backup") 379 | } 380 | 381 | existingProfiles := s.storage.GetListOfTotpSecretObjects() 382 | existingMap := make(map[string]bool) 383 | for _, p := range existingProfiles { 384 | existingMap[p.Secret] = true 385 | } 386 | 387 | var stats struct { 388 | added int 389 | duplicate int 390 | failed int 391 | } 392 | 393 | for _, profile := range backupData.Profiles { 394 | if _, exists := existingMap[profile.Secret]; exists { 395 | log.Printf("[TOTP] Skipping duplicate profile: %s", profile.Issuer) 396 | stats.duplicate++ 397 | continue 398 | } 399 | 400 | err := s.storage.AddTotpSecretObject(profile.Issuer, profile.Secret) 401 | if err != nil { 402 | log.Printf("[TOTP] Failed to restore profile %s: %v", profile.Issuer, err) 403 | stats.failed++ 404 | } else { 405 | log.Printf("[TOTP] Added profile: %s", profile.Issuer) 406 | stats.added++ 407 | } 408 | } 409 | 410 | var statusParts []string 411 | if stats.added > 0 { 412 | statusParts = append(statusParts, fmt.Sprintf("%d new profiles added", stats.added)) 413 | } 414 | if stats.duplicate > 0 { 415 | statusParts = append(statusParts, fmt.Sprintf("%d profiles already exist", stats.duplicate)) 416 | } 417 | if stats.failed > 0 { 418 | statusParts = append(statusParts, fmt.Sprintf("%d profiles couldn't be added", stats.failed)) 419 | } 420 | 421 | statusMsg := strings.Join(statusParts, ", ") 422 | if stats.added == 0 && stats.duplicate > 0 { 423 | statusMsg = "All profiles from the backup already exist" 424 | } 425 | 426 | s.window.Show() 427 | s.window.EmitEvent(EventRestoreSuccess, []string{statusMsg}) 428 | s.SendTOTPData() 429 | s.winManager.EndProfileAddition() 430 | return nil 431 | } 432 | -------------------------------------------------------------------------------- /services/window/manager.go: -------------------------------------------------------------------------------- 1 | package window 2 | 3 | import ( 4 | "github.com/ansxuman/go-touchid" 5 | "github.com/wailsapp/wails/v3/pkg/application" 6 | "github.com/wailsapp/wails/v3/pkg/events" 7 | ) 8 | 9 | type Manager struct { 10 | window *application.WebviewWindow 11 | authService AuthService 12 | isAddingProfile bool 13 | } 14 | 15 | type AuthService interface { 16 | HasPin() bool 17 | IsVerified() bool 18 | SetVerified(bool) 19 | VerifyTouchID() bool 20 | } 21 | 22 | func NewManager(authService AuthService) *Manager { 23 | return &Manager{ 24 | authService: authService, 25 | isAddingProfile: false, 26 | } 27 | } 28 | 29 | func (m *Manager) SetWindow(window *application.WebviewWindow) { 30 | m.window = window 31 | m.setupWindowEvents() 32 | } 33 | 34 | func (m *Manager) setupWindowEvents() { 35 | focusHandler := func(e *application.WindowEvent) { 36 | if m.isAddingProfile { 37 | return 38 | } 39 | 40 | if !m.authService.HasPin() || m.authService.IsVerified() { 41 | return 42 | } 43 | 44 | m.window.EmitEvent("requirePinVerification", nil) 45 | } 46 | 47 | m.window.OnWindowEvent(events.Common.WindowFocus, focusHandler) 48 | m.window.OnWindowEvent(events.Common.WindowLostFocus, func(e *application.WindowEvent) { 49 | if !m.isAddingProfile { 50 | m.authService.SetVerified(false) 51 | } 52 | }) 53 | } 54 | 55 | func (m *Manager) HandleTouchID() bool { 56 | success, err := touchid.Auth(touchid.DeviceTypeBiometrics, "Verify Identity") 57 | if err == nil && success { 58 | m.authService.SetVerified(true) 59 | m.window.EmitEvent("verificationComplete", nil) 60 | return true 61 | } 62 | return false 63 | } 64 | 65 | func (m *Manager) StartProfileAddition() { 66 | m.isAddingProfile = true 67 | } 68 | 69 | func (m *Manager) EndProfileAddition() { 70 | m.isAddingProfile = false 71 | } 72 | --------------------------------------------------------------------------------