├── .editorconfig ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .version ├── Dockerfile ├── LICENSE ├── Makefile ├── PROTOCOL.md ├── README.md ├── browser-files ├── chromium-host.json ├── chromium-policy.json └── firefox-host.json ├── debug ├── README.md └── request-configure.hex.txt ├── errors └── errors.go ├── go.mod ├── go.sum ├── helpers └── helpers.go ├── main.go ├── openbsd ├── generic.go └── openbsd.go ├── persistentlog ├── syslog.go ├── unsupported.go └── windows.go ├── request ├── common.go ├── configure.go ├── delete.go ├── fetch.go ├── list.go ├── process.go ├── process_test.go ├── save.go └── tree.go ├── response └── response.go ├── version └── version.go └── windows-setup.wxs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.go] 12 | indent_style = tab 13 | 14 | [Makefile] 15 | indent_style = tab 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### General information 2 | 3 | 4 | 5 | - Operating system + version: 6 | - Browser + version: 7 | - Information about the host app: 8 | - How did you install it? 9 | 10 | - If installed an official release, put a version (`$ browserpass --version`): 11 | - If built from sources, put a commit id (`$ git describe --always`): 12 | - Information about the browser extension: 13 | - How did you install it? 14 | 15 | - Browserpass extension version as reported by your browser: 16 | 17 | --- 18 | 19 | If you are getting an error immediately after opening popup, have you followed the [Configure browsers](https://github.com/browserpass/browserpass-native#configure-browsers) documentation section? 20 | 21 | --- 22 | 23 | ### Exact steps to reproduce the problem 24 | 25 | 1. 26 | 27 | 2. 28 | 29 | 3. 30 | 31 | ### What should happen? 32 | 33 | ### What happened instead? 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /browserpass 2 | /browserpass-* 3 | dist 4 | vendor 5 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | 3.1.0 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:latest 2 | 3 | VOLUME /src 4 | WORKDIR /src 5 | 6 | ENTRYPOINT ["make"] 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2018-2019, Maxim Baz & Steve Gilberd 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN ?= browserpass 2 | VERSION = $(shell cat .version) 3 | 4 | PREFIX ?= /usr 5 | BIN_DIR = $(DESTDIR)$(PREFIX)/bin 6 | LIB_DIR = $(DESTDIR)$(PREFIX)/lib 7 | SHARE_DIR = $(DESTDIR)$(PREFIX)/share 8 | XDG_CONFIG_HOME ?= $(HOME)/.config 9 | 10 | BIN_PATH = $(BIN_DIR)/$(BIN) 11 | BIN_PATH_WINDOWS = C:\\\\\\\\\\\\\\\\Program Files\\\\\\\\\\\\\\\\Browserpass\\\\\\\\\\\\\\\\browserpass-windows64.exe 12 | 13 | # -buildmode=pie requires CGO on riscv64 14 | ifeq ($(shell uname -m), riscv64) 15 | export CGO_ENABLED := 1 16 | else 17 | export CGO_ENABLED := 0 18 | endif 19 | GOFLAGS := -buildmode=pie -trimpath 20 | 21 | APP_ID = com.github.browserpass.native 22 | OS = $(shell uname -s) 23 | 24 | # GNU tools 25 | SED = $(shell which gsed 2>/dev/null || which sed 2>/dev/null) 26 | INSTALL = $(shell which ginstall 2>/dev/null || which install 2>/dev/null) 27 | 28 | ####################### 29 | # For local development 30 | 31 | .PHONY: all 32 | all: browserpass test 33 | 34 | browserpass: *.go **/*.go 35 | env GOFLAGS="$(GOFLAGS)" go build -o $@ 36 | 37 | browserpass-linux64: *.go **/*.go 38 | env GOOS=linux GOARCH=amd64 go build -o $@ 39 | 40 | browserpass-arm: *.go **/*.go 41 | env GOOS=linux GOARCH=arm go build -o $@ 42 | 43 | browserpass-arm64: *.go **/*.go 44 | env GOOS=linux GOARCH=arm64 go build -o $@ 45 | 46 | browserpass-darwin64: *.go **/*.go 47 | env GOOS=darwin GOARCH=amd64 go build -o $@ 48 | 49 | browserpass-darwin-arm64: *.go **/*.go 50 | env GOOS=darwin GOARCH=arm64 go build -o $@ 51 | 52 | browserpass-openbsd64: *.go **/*.go 53 | env GOOS=openbsd GOARCH=amd64 go build -o $@ 54 | 55 | browserpass-freebsd64: *.go **/*.go 56 | env GOOS=freebsd GOARCH=amd64 go build -o $@ 57 | 58 | browserpass-dragonfly64: *.go **/*.go 59 | env GOOS=dragonfly GOARCH=amd64 go build -o $@ 60 | 61 | browserpass-windows64: *.go **/*.go 62 | env GOOS=windows GOARCH=amd64 go build -o $@.exe 63 | 64 | browserpass-windows: *.go **/*.go 65 | env GOOS=windows GOARCH=386 go build -o $@.exe 66 | 67 | .PHONY: test 68 | test: 69 | go test ./... 70 | 71 | ####################### 72 | # For official releases 73 | 74 | .PHONY: vendor 75 | vendor: 76 | go mod tidy 77 | go mod vendor 78 | 79 | .PHONY: clean 80 | clean: 81 | rm -f browserpass browserpass-* 82 | rm -rf dist 83 | rm -rf vendor 84 | 85 | .PHONY: dist 86 | dist: clean vendor browserpass-linux64 browserpass-arm browserpass-arm64 browserpass-darwin64 browserpass-darwin-arm64 browserpass-openbsd64 browserpass-freebsd64 browserpass-dragonfly64 browserpass-windows64 browserpass-windows 87 | $(eval TMP := $(shell mktemp -d)) 88 | 89 | # Full source code 90 | mkdir "$(TMP)/browserpass-native-$(VERSION)" 91 | cp -r * "$(TMP)/browserpass-native-$(VERSION)" 92 | (cd "$(TMP)" && tar -cvzf "browserpass-native-$(VERSION)-src.tar.gz" "browserpass-native-$(VERSION)") 93 | 94 | # Unix installers 95 | for os in linux64 arm arm64 darwin64 darwin-arm64 openbsd64 freebsd64 dragonfly64; do \ 96 | mkdir $(TMP)/browserpass-"$$os"-$(VERSION); \ 97 | cp -a browserpass-"$$os"* browser-files Makefile README.md LICENSE $(TMP)/browserpass-"$$os"-$(VERSION); \ 98 | (cd $(TMP) && tar -cvzf browserpass-"$$os"-$(VERSION).tar.gz browserpass-"$$os"-$(VERSION)); \ 99 | done 100 | 101 | # Windows installer 102 | mkdir $(TMP)/browserpass-windows64-$(VERSION) 103 | cp -a browserpass-windows64.exe browser-files Makefile README.md LICENSE windows-setup.wxs $(TMP)/browserpass-windows64-$(VERSION) 104 | (cd $(TMP)/browserpass-windows64-$(VERSION); \ 105 | make BIN_PATH="$(BIN_PATH_WINDOWS)" configure; \ 106 | wixl --verbose --arch x64 windows-setup.wxs --output ../browserpass-windows64-$(VERSION).msi) 107 | 108 | mkdir -p dist 109 | mv "$(TMP)/"*.tar.gz "$(TMP)/"*.msi dist 110 | git -c tar.tar.gz.command="gzip -cn" archive -o dist/browserpass-native-$(VERSION).tar.gz --format tar.gz --prefix=browserpass-native-$(VERSION)/ $(VERSION) 111 | 112 | for file in dist/*; do \ 113 | gpg --detach-sign --armor "$$file"; \ 114 | done 115 | 116 | rm -rf $(TMP) 117 | rm -f dist/browserpass-native-$(VERSION).tar.gz 118 | 119 | ####################### 120 | # For user installation 121 | 122 | .PHONY: configure 123 | configure: 124 | $(SED) -i 's|"path": ".*"|"path": "'"$(BIN_PATH)"'"|' browser-files/chromium-host.json 125 | $(SED) -i 's|"path": ".*"|"path": "'"$(BIN_PATH)"'"|' browser-files/firefox-host.json 126 | 127 | .PHONY: install 128 | install: 129 | $(INSTALL) -Dm755 -t "$(BIN_DIR)/" $(BIN) 130 | $(INSTALL) -Dm644 -t "$(LIB_DIR)/browserpass/" Makefile 131 | $(INSTALL) -Dm644 -t "$(SHARE_DIR)/licenses/browserpass/" LICENSE 132 | $(INSTALL) -Dm644 -t "$(SHARE_DIR)/doc/browserpass/" README.md 133 | 134 | $(INSTALL) -Dm644 browser-files/chromium-host.json "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" 135 | $(INSTALL) -Dm644 browser-files/chromium-policy.json "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" 136 | $(INSTALL) -Dm644 browser-files/firefox-host.json "$(LIB_DIR)/browserpass/hosts/firefox/$(APP_ID).json" 137 | 138 | # Browser-specific hosts targets 139 | 140 | .PHONY: hosts-chromium 141 | hosts-chromium: 142 | @case $(OS) in \ 143 | Linux) \ 144 | mkdir -p "/etc/chromium/native-messaging-hosts/"; \ 145 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/etc/chromium/native-messaging-hosts/$(APP_ID).json"; \ 146 | [ -e "/etc/chromium/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 147 | ;; \ 148 | Darwin) \ 149 | mkdir -p "/Library/Application Support/Chromium/NativeMessagingHosts/"; \ 150 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/Library/Application Support/Chromium/NativeMessagingHosts/$(APP_ID).json"; \ 151 | [ -e "/Library/Application Support/Chromium/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 152 | ;; \ 153 | *) \ 154 | echo "The operating system $(OS) is not supported"; \ 155 | exit 1; \ 156 | ;; \ 157 | esac 158 | 159 | .PHONY: hosts-chromium-user 160 | hosts-chromium-user: 161 | @case $(OS) in \ 162 | Linux|*BSD|DragonFly) \ 163 | mkdir -p "$(XDG_CONFIG_HOME)/chromium/NativeMessagingHosts/"; \ 164 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/chromium/NativeMessagingHosts/$(APP_ID).json"; \ 165 | [ -e "$(XDG_CONFIG_HOME)/chromium/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 166 | ;; \ 167 | Darwin) \ 168 | mkdir -p "${HOME}/Library/Application Support/Chromium/NativeMessagingHosts/"; \ 169 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Chromium/NativeMessagingHosts/$(APP_ID).json"; \ 170 | [ -e "${HOME}/Library/Application Support/Chromium/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 171 | ;; \ 172 | *) \ 173 | echo "The operating system $(OS) is not supported"; \ 174 | exit 1; \ 175 | ;; \ 176 | esac 177 | 178 | .PHONY: hosts-chrome 179 | hosts-chrome: 180 | @case $(OS) in \ 181 | Linux) \ 182 | mkdir -p "/etc/opt/chrome/native-messaging-hosts/"; \ 183 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/etc/opt/chrome/native-messaging-hosts/$(APP_ID).json"; \ 184 | [ -e "/etc/opt/chrome/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 185 | ;; \ 186 | Darwin) \ 187 | mkdir -p "/Library/Google/Chrome/NativeMessagingHosts/"; \ 188 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/Library/Google/Chrome/NativeMessagingHosts/$(APP_ID).json"; \ 189 | [ -e "/Library/Google/Chrome/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 190 | ;; \ 191 | *) \ 192 | echo "The operating system $(OS) is not supported"; \ 193 | exit 1; \ 194 | ;; \ 195 | esac 196 | 197 | .PHONY: hosts-chrome-user 198 | hosts-chrome-user: 199 | @case $(OS) in \ 200 | Linux|*BSD|DragonFly) \ 201 | mkdir -p "$(XDG_CONFIG_HOME)/google-chrome/NativeMessagingHosts/"; \ 202 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/google-chrome/NativeMessagingHosts/$(APP_ID).json"; \ 203 | [ -e "$(XDG_CONFIG_HOME)/google-chrome/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 204 | ;; \ 205 | Darwin) \ 206 | mkdir -p "${HOME}/Library/Application Support/Google/Chrome/NativeMessagingHosts/"; \ 207 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Google/Chrome/NativeMessagingHosts/$(APP_ID).json"; \ 208 | [ -e "${HOME}/Library/Application Support/Google/Chrome/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 209 | ;; \ 210 | *) \ 211 | echo "The operating system $(OS) is not supported"; \ 212 | exit 1; \ 213 | ;; \ 214 | esac 215 | 216 | .PHONY: hosts-arc 217 | hosts-arc: 218 | @case $(OS) in \ 219 | Darwin) \ 220 | mkdir -p "/Library/Application Support/Arc/User Data/NativeMessagingHosts/"; \ 221 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/Library/Application Support/Arc/User Data/NativeMessagingHosts/$(APP_ID).json"; \ 222 | [ -e "/Library/Application Support/Arc/User Data/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 223 | ;; \ 224 | *) \ 225 | echo "The operating system $(OS) is not supported"; \ 226 | exit 1; \ 227 | ;; \ 228 | esac 229 | 230 | .PHONY: hosts-arc-user 231 | hosts-arc-user: 232 | @case $(OS) in \ 233 | Darwin) \ 234 | mkdir -p "${HOME}/Library/Application Support/Arc/User Data/NativeMessagingHosts/"; \ 235 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Arc/User Data/NativeMessagingHosts/$(APP_ID).json"; \ 236 | [ -e "${HOME}/Library/Application Support/Arc/User Data/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 237 | ;; \ 238 | *) \ 239 | echo "The operating system $(OS) is not supported"; \ 240 | exit 1; \ 241 | ;; \ 242 | esac 243 | 244 | .PHONY: hosts-edge 245 | hosts-edge: 246 | @case $(OS) in \ 247 | # Linux) \ 248 | # mkdir -p "/opt/microsoft/msedge/native-messaging-hosts/"; \ 249 | # ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/opt/microsoft/msedge/native-messaging-hosts/$(APP_ID).json"; \ 250 | # [ -e "/opt/microsoft/msedge/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 251 | # ;; \ 252 | # Darwin) \ 253 | # mkdir -p "/Library/Google/Chrome/NativeMessagingHosts/"; \ 254 | # ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/Library/Google/Chrome/NativeMessagingHosts/$(APP_ID).json"; \ 255 | # [ -e "/Library/Google/Chrome/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 256 | # ;; \ 257 | *) \ 258 | echo "The operating system $(OS) is not supported"; \ 259 | exit 1; \ 260 | ;; \ 261 | esac 262 | 263 | .PHONY: hosts-edge-user 264 | hosts-edge-user: 265 | @case $(OS) in \ 266 | Linux|*BSD|DragonFly) \ 267 | mkdir -p "$(XDG_CONFIG_HOME)/microsoft-edge/NativeMessagingHosts/"; \ 268 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/microsoft-edge/NativeMessagingHosts/$(APP_ID).json"; \ 269 | [ -e "$(XDG_CONFIG_HOME)/microsoft-edge/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 270 | ;; \ 271 | # Darwin) \ 272 | # mkdir -p "${HOME}/Library/Application Support/Google/Chrome/NativeMessagingHosts/"; \ 273 | # ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Google/Chrome/NativeMessagingHosts/$(APP_ID).json"; \ 274 | # [ -e "${HOME}/Library/Application Support/Google/Chrome/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 275 | # ;; \ 276 | *) \ 277 | echo "The operating system $(OS) is not supported"; \ 278 | exit 1; \ 279 | ;; \ 280 | esac 281 | 282 | .PHONY: hosts-vivaldi 283 | hosts-vivaldi: 284 | @case $(OS) in \ 285 | Linux) \ 286 | mkdir -p "/etc/opt/vivaldi/native-messaging-hosts/"; \ 287 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/etc/opt/vivaldi/native-messaging-hosts/$(APP_ID).json"; \ 288 | [ -e "/etc/opt/vivaldi/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 289 | ;; \ 290 | Darwin) \ 291 | mkdir -p "/Library/Application Support/Vivaldi/NativeMessagingHosts/"; \ 292 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/Library/Application Support/Vivaldi/NativeMessagingHosts/$(APP_ID).json"; \ 293 | [ -e "/Library/Application Support/Vivaldi/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 294 | ;; \ 295 | *) \ 296 | echo "The operating system $(OS) is not supported"; \ 297 | exit 1; \ 298 | ;; \ 299 | esac 300 | 301 | .PHONY: hosts-vivaldi-user 302 | hosts-vivaldi-user: 303 | @case $(OS) in \ 304 | Linux|*BSD|DragonFly) \ 305 | mkdir -p "$(XDG_CONFIG_HOME)/vivaldi/NativeMessagingHosts/"; \ 306 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/vivaldi/NativeMessagingHosts/$(APP_ID).json"; \ 307 | [ -e "$(XDG_CONFIG_HOME)/vivaldi/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 308 | ;; \ 309 | Darwin) \ 310 | mkdir -p "${HOME}/Library/Application Support/Vivaldi/NativeMessagingHosts/"; \ 311 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Vivaldi/NativeMessagingHosts/$(APP_ID).json"; \ 312 | [ -e "${HOME}/Library/Application Support/Vivaldi/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 313 | ;; \ 314 | *) \ 315 | echo "The operating system $(OS) is not supported"; \ 316 | exit 1; \ 317 | ;; \ 318 | esac 319 | 320 | .PHONY: hosts-yandex 321 | hosts-yandex: 322 | @case $(OS) in \ 323 | Linux) \ 324 | mkdir -p "/etc/opt/yandex-browser/native-messaging-hosts/"; \ 325 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/etc/opt/yandex-browser/native-messaging-hosts/$(APP_ID).json"; \ 326 | [ -e "/etc/opt/yandex-browser/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 327 | ;; \ 328 | Darwin) \ 329 | mkdir -p "/Library/Application Support/Yandex/NativeMessagingHosts/"; \ 330 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/Library/Application Support/Yandex/NativeMessagingHosts/$(APP_ID).json"; \ 331 | [ -e "/Library/Application Support/Yandex/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 332 | ;; \ 333 | *) \ 334 | echo "The operating system $(OS) is not supported"; \ 335 | exit 1; \ 336 | ;; \ 337 | esac 338 | 339 | .PHONY: hosts-yandex-user 340 | hosts-yandex-user: 341 | @case $(OS) in \ 342 | Linux|*BSD|DragonFly) \ 343 | mkdir -p "$(XDG_CONFIG_HOME)/yandex-browser/NativeMessagingHosts/"; \ 344 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/yandex-browser/NativeMessagingHosts/$(APP_ID).json"; \ 345 | [ -e "$(XDG_CONFIG_HOME)/yandex-browser/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 346 | ;; \ 347 | Darwin) \ 348 | mkdir -p "${HOME}/Library/Application Support/Yandex/NativeMessagingHosts/"; \ 349 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Yandex/NativeMessagingHosts/$(APP_ID).json"; \ 350 | [ -e "${HOME}/Library/Application Support/Yandex/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 351 | ;; \ 352 | *) \ 353 | echo "The operating system $(OS) is not supported"; \ 354 | exit 1; \ 355 | ;; \ 356 | esac 357 | 358 | .PHONY: hosts-brave 359 | hosts-brave: 360 | @case $(OS) in \ 361 | Linux) \ 362 | mkdir -p "/etc/opt/chrome/native-messaging-hosts/"; \ 363 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/etc/opt/chrome/native-messaging-hosts/$(APP_ID).json"; \ 364 | [ -e "/etc/opt/chrome/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 365 | ;; \ 366 | Darwin) \ 367 | mkdir -p "/Library/Application Support/Chromium/NativeMessagingHosts/"; \ 368 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/Library/Application Support/Chromium/NativeMessagingHosts/$(APP_ID).json"; \ 369 | [ -e "/Library/Application Support/Chromium/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 370 | ;; \ 371 | *) \ 372 | echo "The operating system $(OS) is not supported"; \ 373 | exit 1; \ 374 | ;; \ 375 | esac 376 | 377 | .PHONY: hosts-brave-user 378 | hosts-brave-user: 379 | @case $(OS) in \ 380 | Linux|*BSD|DragonFly) \ 381 | mkdir -p "$(XDG_CONFIG_HOME)/BraveSoftware/Brave-Browser/NativeMessagingHosts/"; \ 382 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/BraveSoftware/Brave-Browser/NativeMessagingHosts/$(APP_ID).json"; \ 383 | [ -e "$(XDG_CONFIG_HOME)/BraveSoftware/Brave-Browser/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 384 | ;; \ 385 | Darwin) \ 386 | mkdir -p "${HOME}/Library/Application Support/Google/Chrome/NativeMessagingHosts/"; \ 387 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Google/Chrome/NativeMessagingHosts/$(APP_ID).json"; \ 388 | [ -e "${HOME}/Library/Application Support/Google/Chrome/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 389 | ;; \ 390 | *) \ 391 | echo "The operating system $(OS) is not supported"; \ 392 | exit 1; \ 393 | ;; \ 394 | esac 395 | 396 | .PHONY: hosts-iridium 397 | hosts-iridium: 398 | @case $(OS) in \ 399 | Linux) \ 400 | mkdir -p "/etc/iridium-browser/native-messaging-hosts/"; \ 401 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/etc/iridium-browser/native-messaging-hosts/$(APP_ID).json"; \ 402 | [ -e "/etc/iridium-browser/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 403 | ;; \ 404 | Darwin) \ 405 | mkdir -p "/Library/Application Support/Iridium/NativeMessagingHosts/"; \ 406 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/Library/Application Support/Iridium/NativeMessagingHosts/$(APP_ID).json"; \ 407 | [ -e "/Library/Application Support/Iridium/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 408 | ;; \ 409 | *) \ 410 | echo "The operating system $(OS) is not supported"; \ 411 | exit 1; \ 412 | ;; \ 413 | esac 414 | 415 | .PHONY: hosts-iridium-user 416 | hosts-iridium-user: 417 | @case $(OS) in \ 418 | Linux|*BSD|DragonFly) \ 419 | mkdir -p "$(XDG_CONFIG_HOME)/iridium/NativeMessagingHosts/"; \ 420 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/iridium/NativeMessagingHosts/$(APP_ID).json"; \ 421 | [ -e "$(XDG_CONFIG_HOME)/iridium/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 422 | ;; \ 423 | Darwin) \ 424 | mkdir -p "${HOME}/Library/Application Support/Iridium/NativeMessagingHosts/"; \ 425 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Iridium/NativeMessagingHosts/$(APP_ID).json"; \ 426 | [ -e "${HOME}/Library/Application Support/Iridium/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 427 | ;; \ 428 | *) \ 429 | echo "The operating system $(OS) is not supported"; \ 430 | exit 1; \ 431 | ;; \ 432 | esac 433 | 434 | .PHONY: hosts-slimjet 435 | hosts-slimjet: 436 | @case $(OS) in \ 437 | Linux) \ 438 | mkdir -p "/etc/opt/slimjet/native-messaging-hosts/"; \ 439 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/etc/opt/slimjet/native-messaging-hosts/$(APP_ID).json"; \ 440 | [ -e "/etc/opt/slimjet/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 441 | ;; \ 442 | Darwin) \ 443 | mkdir -p "/Library/Application Support/Slimjet/NativeMessagingHosts/"; \ 444 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "/Library/Application Support/Slimjet/NativeMessagingHosts/$(APP_ID).json"; \ 445 | [ -e "/Library/Application Support/Slimjet/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 446 | ;; \ 447 | *) \ 448 | echo "The operating system $(OS) is not supported"; \ 449 | exit 1; \ 450 | ;; \ 451 | esac 452 | 453 | .PHONY: hosts-slimjet-user 454 | hosts-slimjet-user: 455 | @case $(OS) in \ 456 | Linux|*BSD|DragonFly) \ 457 | mkdir -p "${HOME}/.config/slimject/NativeMessagingHosts/"; \ 458 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "${HOME}/.config/slimject/NativeMessagingHosts/$(APP_ID).json"; \ 459 | [ -e "${HOME}/.config/slimject/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 460 | ;; \ 461 | Darwin) \ 462 | mkdir -p "${HOME}/Library/Application Support/Slimjet/NativeMessagingHosts/"; \ 463 | ln -sfv "$(LIB_DIR)/browserpass/hosts/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Slimjet/NativeMessagingHosts/$(APP_ID).json"; \ 464 | [ -e "${HOME}/Library/Application Support/Slimjet/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 465 | ;; \ 466 | *) \ 467 | echo "The operating system $(OS) is not supported"; \ 468 | exit 1; \ 469 | ;; \ 470 | esac 471 | 472 | .PHONY: hosts-firefox 473 | hosts-firefox: 474 | @case $(OS) in \ 475 | Linux) \ 476 | mkdir -p "$(LIB_DIR)/mozilla/native-messaging-hosts/"; \ 477 | ln -sfv "$(LIB_DIR)/browserpass/hosts/firefox/$(APP_ID).json" "/usr/lib/mozilla/native-messaging-hosts/$(APP_ID).json"; \ 478 | [ -e "/usr/lib/mozilla/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 479 | ;; \ 480 | Darwin) \ 481 | mkdir -p "/Library/Application Support/Mozilla/NativeMessagingHosts/"; \ 482 | ln -sfv "$(LIB_DIR)/browserpass/hosts/firefox/$(APP_ID).json" "/Library/Application Support/Mozilla/NativeMessagingHosts/$(APP_ID).json"; \ 483 | [ -e "/Library/Application Support/Mozilla/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 484 | ;; \ 485 | *) \ 486 | echo "The operating system $(OS) is not supported"; \ 487 | exit 1; \ 488 | ;; \ 489 | esac 490 | 491 | .PHONY: hosts-firefox-user 492 | hosts-firefox-user: 493 | @case $(OS) in \ 494 | Linux|*BSD|DragonFly) \ 495 | mkdir -p "${HOME}/.mozilla/native-messaging-hosts/"; \ 496 | ln -sfv "$(LIB_DIR)/browserpass/hosts/firefox/$(APP_ID).json" "${HOME}/.mozilla/native-messaging-hosts/$(APP_ID).json"; \ 497 | [ -e "${HOME}/.mozilla/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 498 | ;; \ 499 | Darwin) \ 500 | mkdir -p "${HOME}/Library/Application Support/Mozilla/NativeMessagingHosts/"; \ 501 | ln -sfv "$(LIB_DIR)/browserpass/hosts/firefox/$(APP_ID).json" "${HOME}/Library/Application Support/Mozilla/NativeMessagingHosts/$(APP_ID).json"; \ 502 | [ -e "${HOME}/Library/Application Support/Mozilla/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 503 | ;; \ 504 | *) \ 505 | echo "The operating system $(OS) is not supported"; \ 506 | exit 1; \ 507 | ;; \ 508 | esac 509 | 510 | .PHONY: hosts-librewolf 511 | hosts-librewolf: 512 | @case $(OS) in \ 513 | Linux) \ 514 | mkdir -p "$(LIB_DIR)/librewolf/native-messaging-hosts/"; \ 515 | ln -sfv "$(LIB_DIR)/browserpass/hosts/firefox/$(APP_ID).json" "/usr/lib/librewolf/native-messaging-hosts/$(APP_ID).json"; \ 516 | [ -e "/usr/lib/librewolf/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 517 | ;; \ 518 | Darwin) \ 519 | mkdir -p "/Library/Application Support/librewolf/NativeMessagingHosts/"; \ 520 | ln -sfv "$(LIB_DIR)/browserpass/hosts/firefox/$(APP_ID).json" "/Library/Application Support/librewolf/NativeMessagingHosts/$(APP_ID).json"; \ 521 | [ -e "/Library/Application Support/librewolf/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 522 | ;; \ 523 | *) \ 524 | echo "The operating system $(OS) is not supported"; \ 525 | exit 1; \ 526 | ;; \ 527 | esac 528 | 529 | .PHONY: hosts-librewolf-user 530 | hosts-librewolf-user: 531 | @case $(OS) in \ 532 | Linux|*BSD|DragonFly) \ 533 | mkdir -p "${HOME}/.librewolf/native-messaging-hosts/"; \ 534 | ln -sfv "$(LIB_DIR)/browserpass/hosts/firefox/$(APP_ID).json" "${HOME}/.librewolf/native-messaging-hosts/$(APP_ID).json"; \ 535 | [ -e "${HOME}/.librewolf/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 536 | ;; \ 537 | Darwin) \ 538 | mkdir -p "${HOME}/Library/Application Support/librewolf/NativeMessagingHosts/"; \ 539 | ln -sfv "$(LIB_DIR)/browserpass/hosts/firefox/$(APP_ID).json" "${HOME}/Library/Application Support/librewolf/NativeMessagingHosts/$(APP_ID).json"; \ 540 | [ -e "${HOME}/Library/Application Support/librewolf/NativeMessagingHosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 541 | ;; \ 542 | *) \ 543 | echo "The operating system $(OS) is not supported"; \ 544 | exit 1; \ 545 | ;; \ 546 | esac 547 | 548 | .PHONY: hosts-waterfox 549 | hosts-waterfox: 550 | @case $(OS) in \ 551 | Linux) \ 552 | mkdir -p "$(LIB_DIR)/waterfox/native-messaging-hosts/"; \ 553 | ln -sfv "$(LIB_DIR)/browserpass/hosts/firefox/$(APP_ID).json" "/usr/lib/waterfox/native-messaging-hosts/$(APP_ID).json"; \ 554 | [ -e "/usr/lib/waterfox/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 555 | ;; \ 556 | *) \ 557 | echo "The operating system $(OS) is not supported"; \ 558 | exit 1; \ 559 | ;; \ 560 | esac 561 | 562 | .PHONY: hosts-waterfox-user 563 | hosts-waterfox-user: 564 | @case $(OS) in \ 565 | Linux|*BSD|DragonFly) \ 566 | mkdir -p "${HOME}/.waterfox/native-messaging-hosts/"; \ 567 | ln -sfv "$(LIB_DIR)/browserpass/hosts/firefox/$(APP_ID).json" "${HOME}/.waterfox/native-messaging-hosts/$(APP_ID).json"; \ 568 | [ -e "${HOME}/.waterfox/native-messaging-hosts/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 569 | ;; \ 570 | *) \ 571 | echo "The operating system $(OS) is not supported"; \ 572 | exit 1; \ 573 | ;; \ 574 | esac 575 | 576 | # Browser-specific policies targets 577 | 578 | .PHONY: policies-chromium 579 | policies-chromium: 580 | @case $(OS) in \ 581 | Linux) \ 582 | mkdir -p "/etc/chromium/policies/managed/"; \ 583 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/etc/chromium/policies/managed/$(APP_ID).json"; \ 584 | [ -e "/etc/chromium/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 585 | ;; \ 586 | Darwin) \ 587 | mkdir -p "/Library/Application Support/Chromium/policies/managed/"; \ 588 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/Library/Application Support/Chromium/policies/managed/$(APP_ID).json"; \ 589 | [ -e "/Library/Application Support/Chromium/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 590 | ;; \ 591 | *) \ 592 | echo "The operating system $(OS) is not supported"; \ 593 | exit 1; \ 594 | ;; \ 595 | esac 596 | 597 | .PHONY: policies-chromium-user 598 | policies-chromium-user: 599 | @case $(OS) in \ 600 | Linux|*BSD|DragonFly) \ 601 | mkdir -p "$(XDG_CONFIG_HOME)/chromium/policies/managed/"; \ 602 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/chromium/policies/managed/$(APP_ID).json"; \ 603 | [ -e "$(XDG_CONFIG_HOME)/chromium/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 604 | ;; \ 605 | Darwin) \ 606 | mkdir -p "${HOME}/Library/Application Support/Chromium/policies/managed/"; \ 607 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Chromium/policies/managed/$(APP_ID).json"; \ 608 | [ -e "${HOME}/Library/Application Support/Chromium/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 609 | ;; \ 610 | *) \ 611 | echo "The operating system $(OS) is not supported"; \ 612 | exit 1; \ 613 | ;; \ 614 | esac 615 | 616 | .PHONY: policies-arc 617 | policies-arc: 618 | @case $(OS) in \ 619 | Darwin) \ 620 | mkdir -p "/Library/Application Support/Arc/User Data/policies/managed/"; \ 621 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/Library/Application Support/Arc/User Data/policies/managed/$(APP_ID).json"; \ 622 | [ -e "/Library/Application Support/Arc/User Data/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 623 | ;; \ 624 | *) \ 625 | echo "The operating system $(OS) is not supported"; \ 626 | exit 1; \ 627 | ;; \ 628 | esac 629 | 630 | .PHONY: policies-arc-user 631 | policies-arc-user: 632 | @case $(OS) in \ 633 | Darwin) \ 634 | mkdir -p "${HOME}/Library/Application Support/Arc/User Data/policies/managed/"; \ 635 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Arc/User Data/policies/managed/$(APP_ID).json"; \ 636 | [ -e "${HOME}/Library/Application Support/Arc/User Data/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 637 | ;; \ 638 | *) \ 639 | echo "The operating system $(OS) is not supported"; \ 640 | exit 1; \ 641 | ;; \ 642 | esac 643 | 644 | .PHONY: policies-chrome 645 | policies-chrome: 646 | @case $(OS) in \ 647 | Linux) \ 648 | mkdir -p "/etc/opt/chrome/policies/managed/"; \ 649 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/etc/opt/chrome/policies/managed/$(APP_ID).json"; \ 650 | [ -e "/etc/opt/chrome/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 651 | ;; \ 652 | Darwin) \ 653 | mkdir -p "/Library/Google/Chrome/policies/managed/"; \ 654 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/Library/Google/Chrome/policies/managed/$(APP_ID).json"; \ 655 | [ -e "/Library/Google/Chrome/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 656 | ;; \ 657 | *) \ 658 | echo "The operating system $(OS) is not supported"; \ 659 | exit 1; \ 660 | ;; \ 661 | esac 662 | 663 | .PHONY: policies-chrome-user 664 | policies-chrome-user: 665 | @case $(OS) in \ 666 | Linux|*BSD|DragonFly) \ 667 | mkdir -p "$(XDG_CONFIG_HOME)/google-chrome/policies/managed/"; \ 668 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/google-chrome/policies/managed/$(APP_ID).json"; \ 669 | [ -e "$(XDG_CONFIG_HOME)/google-chrome/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 670 | ;; \ 671 | Darwin) \ 672 | mkdir -p "${HOME}/Library/Application Support/Google/Chrome/policies/managed/"; \ 673 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Google/Chrome/policies/managed/$(APP_ID).json"; \ 674 | [ -e "${HOME}/Library/Application Support/Google/Chrome/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 675 | ;; \ 676 | *) \ 677 | echo "The operating system $(OS) is not supported"; \ 678 | exit 1; \ 679 | ;; \ 680 | esac 681 | 682 | .PHONY: policies-edge 683 | policies-edge: 684 | @case $(OS) in \ 685 | # Linux) \ 686 | # mkdir -p "/etc/opt/chrome/policies/managed/"; \ 687 | # ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/etc/opt/chrome/policies/managed/$(APP_ID).json"; \ 688 | # [ -e "/etc/opt/chrome/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 689 | # ;; \ 690 | # Darwin) \ 691 | # mkdir -p "/Library/Google/Chrome/policies/managed/"; \ 692 | # ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/Library/Google/Chrome/policies/managed/$(APP_ID).json"; \ 693 | # [ -e "/Library/Google/Chrome/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 694 | # ;; \ 695 | *) \ 696 | echo "The operating system $(OS) is not supported"; \ 697 | exit 1; \ 698 | ;; \ 699 | esac 700 | 701 | .PHONY: policies-edge-user 702 | policies-edge-user: 703 | @case $(OS) in \ 704 | # Linux|*BSD|DragonFly) \ 705 | # mkdir -p "$(XDG_CONFIG_HOME)/microsoft-edge/policies/managed/"; \ 706 | # ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/microsoft-edge/policies/managed/$(APP_ID).json"; \ 707 | # [ -e "$(XDG_CONFIG_HOME)/microsoft-edge/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 708 | # ;; \ 709 | # Darwin) \ 710 | # mkdir -p "${HOME}/Library/Application Support/Google/Chrome/policies/managed/"; \ 711 | # ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Google/Chrome/policies/managed/$(APP_ID).json"; \ 712 | # [ -e "${HOME}/Library/Application Support/Google/Chrome/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 713 | # ;; \ 714 | *) \ 715 | echo "The operating system $(OS) is not supported"; \ 716 | exit 1; \ 717 | ;; \ 718 | esac 719 | 720 | .PHONY: policies-vivaldi 721 | policies-vivaldi: 722 | @case $(OS) in \ 723 | Linux) \ 724 | mkdir -p "/etc/opt/vivaldi/policies/managed/"; \ 725 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/etc/opt/vivaldi/policies/managed/$(APP_ID).json"; \ 726 | [ -e "/etc/opt/vivaldi/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 727 | ;; \ 728 | Darwin) \ 729 | mkdir -p "/Library/Application Support/Vivaldi/policies/managed/"; \ 730 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/Library/Application Support/Vivaldi/policies/managed/$(APP_ID).json"; \ 731 | [ -e "/Library/Application Support/Vivaldi/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 732 | ;; \ 733 | *) \ 734 | echo "The operating system $(OS) is not supported"; \ 735 | exit 1; \ 736 | ;; \ 737 | esac 738 | 739 | .PHONY: policies-vivaldi-user 740 | policies-vivaldi-user: 741 | @case $(OS) in \ 742 | Linux|*BSD|DragonFly) \ 743 | mkdir -p "$(XDG_CONFIG_HOME)/vivaldi/policies/managed/"; \ 744 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/vivaldi/policies/managed/$(APP_ID).json"; \ 745 | [ -e "$(XDG_CONFIG_HOME)/vivaldi/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 746 | ;; \ 747 | Darwin) \ 748 | mkdir -p "${HOME}/Library/Application Support/Vivaldi/policies/managed/"; \ 749 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Vivaldi/policies/managed/$(APP_ID).json"; \ 750 | [ -e "${HOME}/Library/Application Support/Vivaldi/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 751 | ;; \ 752 | *) \ 753 | echo "The operating system $(OS) is not supported"; \ 754 | exit 1; \ 755 | ;; \ 756 | esac 757 | 758 | .PHONY: policies-yandex 759 | policies-yandex: 760 | @case $(OS) in \ 761 | Linux) \ 762 | mkdir -p "/etc/opt/yandex-browser/policies/managed/"; \ 763 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/etc/opt/yandex-browser/policies/managed/$(APP_ID).json"; \ 764 | [ -e "/etc/opt/yandex-browser/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 765 | ;; \ 766 | Darwin) \ 767 | mkdir -p "/Library/Application Support/Yandex/policies/managed/"; \ 768 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/Library/Application Support/Yandex/policies/managed/$(APP_ID).json"; \ 769 | [ -e "/Library/Application Support/Yandex/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 770 | ;; \ 771 | *) \ 772 | echo "The operating system $(OS) is not supported"; \ 773 | exit 1; \ 774 | ;; \ 775 | esac 776 | 777 | .PHONY: policies-yandex-user 778 | policies-yandex-user: 779 | @case $(OS) in \ 780 | Linux|*BSD|DragonFly) \ 781 | mkdir -p "$(XDG_CONFIG_HOME)/yandex-browser/policies/managed/"; \ 782 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/yandex-browser/policies/managed/$(APP_ID).json"; \ 783 | [ -e "$(XDG_CONFIG_HOME)/yandex-browser/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 784 | ;; \ 785 | Darwin) \ 786 | mkdir -p "${HOME}/Library/Application Support/Yandex/policies/managed/"; \ 787 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Yandex/policies/managed/$(APP_ID).json"; \ 788 | [ -e "${HOME}/Library/Application Support/Yandex/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 789 | ;; \ 790 | *) \ 791 | echo "The operating system $(OS) is not supported"; \ 792 | exit 1; \ 793 | ;; \ 794 | esac 795 | 796 | .PHONY: policies-brave 797 | policies-brave: 798 | @case $(OS) in \ 799 | Linux) \ 800 | mkdir -p "/etc/opt/chrome/policies/managed/"; \ 801 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/etc/opt/chrome/policies/managed/$(APP_ID).json"; \ 802 | [ -e "/etc/opt/chrome/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 803 | ;; \ 804 | Darwin) \ 805 | mkdir -p "/Library/Application Support/Chromium/policies/managed/"; \ 806 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/Library/Application Support/Chromium/policies/managed/$(APP_ID).json"; \ 807 | [ -e "/Library/Application Support/Chromium/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 808 | ;; \ 809 | *) \ 810 | echo "The operating system $(OS) is not supported"; \ 811 | exit 1; \ 812 | ;; \ 813 | esac 814 | 815 | .PHONY: policies-brave-user 816 | policies-brave-user: 817 | @case $(OS) in \ 818 | Linux|*BSD|DragonFly) \ 819 | mkdir -p "$(XDG_CONFIG_HOME)/BraveSoftware/Brave-Browser/policies/managed/"; \ 820 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/BraveSoftware/Brave-Browser/policies/managed/$(APP_ID).json"; \ 821 | [ -e "$(XDG_CONFIG_HOME)/BraveSoftware/Brave-Browser/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 822 | ;; \ 823 | Darwin) \ 824 | mkdir -p "${HOME}/Library/Application Support/Google/Chrome/policies/managed/"; \ 825 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Google/Chrome/policies/managed/$(APP_ID).json"; \ 826 | [ -e "${HOME}/Library/Application Support/Google/Chrome/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 827 | ;; \ 828 | *) \ 829 | echo "The operating system $(OS) is not supported"; \ 830 | exit 1; \ 831 | ;; \ 832 | esac 833 | 834 | .PHONY: policies-iridium 835 | policies-iridium: 836 | @case $(OS) in \ 837 | Linux) \ 838 | mkdir -p "/etc/opt/chrome/policies/managed/"; \ 839 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/etc/opt/chrome/policies/managed/$(APP_ID).json"; \ 840 | [ -e "/etc/opt/chrome/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 841 | ;; \ 842 | Darwin) \ 843 | mkdir -p "/Library/Application Support/Chromium/policies/managed/"; \ 844 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/Library/Application Support/Chromium/policies/managed/$(APP_ID).json"; \ 845 | [ -e "/Library/Application Support/Chromium/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 846 | ;; \ 847 | *) \ 848 | echo "The operating system $(OS) is not supported"; \ 849 | exit 1; \ 850 | ;; \ 851 | esac 852 | 853 | .PHONY: policies-iridium-user 854 | policies-iridium-user: 855 | @case $(OS) in \ 856 | Linux|*BSD|DragonFly) \ 857 | mkdir -p "$(XDG_CONFIG_HOME)/iridium/policies/managed/"; \ 858 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "$(XDG_CONFIG_HOME)/iridium/policies/managed/$(APP_ID).json"; \ 859 | [ -e "$(XDG_CONFIG_HOME)/iridium/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 860 | ;; \ 861 | Darwin) \ 862 | mkdir -p "${HOME}/Library/Application Support/Iridium/policies/managed/"; \ 863 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Iridium/policies/managed/$(APP_ID).json"; \ 864 | [ -e "${HOME}/Library/Application Support/Iridium/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 865 | ;; \ 866 | *) \ 867 | echo "The operating system $(OS) is not supported"; \ 868 | exit 1; \ 869 | ;; \ 870 | esac 871 | 872 | .PHONY: policies-slimjet 873 | policies-slimjet: 874 | @case $(OS) in \ 875 | Linux) \ 876 | mkdir -p "/etc/opt/slimjet/policies/managed/"; \ 877 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/etc/opt/slimjet/policies/managed/$(APP_ID).json"; \ 878 | [ -e "/etc/opt/slimjet/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 879 | ;; \ 880 | Darwin) \ 881 | mkdir -p "/Library/Application Support/Slimjet/policies/managed/"; \ 882 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "/Library/Application Support/Slimjet/policies/managed/$(APP_ID).json"; \ 883 | [ -e "/Library/Application Support/Slimjet/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 884 | ;; \ 885 | *) \ 886 | echo "The operating system $(OS) is not supported"; \ 887 | exit 1; \ 888 | ;; \ 889 | esac 890 | 891 | .PHONY: policies-slimjet-user 892 | policies-slimjet-user: 893 | @case $(OS) in \ 894 | Linux|*BSD|DragonFly) \ 895 | mkdir -p "${HOME}/.config/slimjet/policies/managed/"; \ 896 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "${HOME}/.config/slimjet/policies/managed/$(APP_ID).json"; \ 897 | [ -e "${HOME}/.config/slimjet/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 898 | ;; \ 899 | Darwin) \ 900 | mkdir -p "${HOME}/Library/Application Support/Slimjet/policies/managed/"; \ 901 | ln -sfv "$(LIB_DIR)/browserpass/policies/chromium/$(APP_ID).json" "${HOME}/Library/Application Support/Slimjet/policies/managed/$(APP_ID).json"; \ 902 | [ -e "${HOME}/Library/Application Support/Slimjet/policies/managed/$(APP_ID).json" ] || echo "Error: the symlink points to a non-existent location" >&2; \ 903 | ;; \ 904 | *) \ 905 | echo "The operating system $(OS) is not supported"; \ 906 | exit 1; \ 907 | ;; \ 908 | esac 909 | -------------------------------------------------------------------------------- /PROTOCOL.md: -------------------------------------------------------------------------------- 1 | # Browserpass Communication Protocol 2 | 3 | This document describes the protocol used for communication between the browser extension, 4 | and the native host application. 5 | 6 | ## Response Types 7 | 8 | ### OK 9 | 10 | Consists solely of an `ok` status, an integer app version, and a `data` field which 11 | may be of any type. 12 | 13 | The app version is an integer, calculated by `(MAJOR * 1000000) + (MINOR * 1000) + PATCH`. 14 | 15 | ``` 16 | { 17 | "status": "ok", 18 | "version": , 19 | "data": 20 | } 21 | ``` 22 | 23 | ### Error 24 | 25 | Consists solely of an `error` status, a nonzero integer error code, and an optional `params` 26 | object that provides any parameters that should accompany the error. 27 | 28 | ``` 29 | { 30 | "status": "error", 31 | "code": , 32 | "version": , 33 | "params": { 34 | "": 35 | } 36 | } 37 | ``` 38 | 39 | When the error message is relaying a message from a native system component, then that message 40 | should be supplied as a `message` parameter. 41 | 42 | ## List of Error Codes 43 | 44 | | Code | Description | Parameters | 45 | | ---- | ----------------------------------------------------------------------- | ---------------------------------------------------------------- | 46 | | 10 | Unable to parse browser request length | message, error | 47 | | 11 | Unable to parse browser request | message, error | 48 | | 12 | Invalid request action | message, action | 49 | | 13 | Inaccessible user-configured password store | message, action, error, storeId, storePath, storeName | 50 | | 14 | Inaccessible default password store | message, action, error, storePath | 51 | | 15 | Unable to determine the location of the default password store | message, action, error | 52 | | 16 | Unable to read the default settings of a user-configured password store | message, action, error, storeId, storePath, storeName | 53 | | 17 | Unable to read the default settings of the default password store | message, action, error, storePath | 54 | | 18 | Unable to list files in a password store | message, action, error, storeId, storePath, storeName | 55 | | 19 | Unable to determine a relative path for a file in a password store | message, action, error, storeId, storePath, storeName, file | 56 | | 20 | Invalid password store ID | message, action, storeId | 57 | | 21 | Invalid gpg path | message, action, error, gpgPath | 58 | | 22 | Unable to detect the location of the gpg binary | message, action, error | 59 | | 23 | Invalid password file extension | message, action, file | 60 | | 24 | Unable to decrypt the password file | message, action, error, storeId, storePath, storeName, file | 61 | | 25 | Unable to list directories in a password store | message, action, error, storeId, storePath, storeName | 62 | | 26 | Unable to determine a relative path for a directory in a password store | message, action, error, storeId, storePath, storeName, directory | 63 | | 27 | The entry contents is missing | message, action | 64 | | 28 | Unable to determine the recepients for the gpg encryption | message, action, error, storeId, storePath, storeName, file | 65 | | 29 | Unable to encrypt the password file | message, action, error, storeId, storePath, storeName, file | 66 | | 30 | Unable to delete the password file | message, action, error, storeId, storePath, storeName, file | 67 | | 31 | Unable to determine if directory is empty and can be deleted | message, action, error, storeId, storePath, storeName, directory | 68 | | 32 | Unable to delete the empty directory | message, action, error, storeId, storePath, storeName, directory | 69 | 70 | ## Settings 71 | 72 | The `settings` object is a key / value map of individual settings. It's provided by the 73 | browser to the native app as part of every request. 74 | 75 | Settings are saved in browser local storage. Each top-level setting is saved separately, 76 | JSON-encoded and saved by its key. 77 | 78 | Settings may also be supplied via a `.browserpass.json` file in the root of a password store, 79 | and via parameters in individual `*.gpg` files. 80 | 81 | Settings are applied using the following priority, highest first: 82 | 83 | 1. Configured by the user in specific `*.gpg` files (e.g. autosubmit: true) 84 | 1. Configured by the user in `.browserpass.json` file in specific password stores 85 | 1. Configured by the user via the extension options 86 | 1. Defaults shipped with the browser extension 87 | 88 | ### Global Settings 89 | 90 | | Setting | Description | Default | 91 | | ------- | ---------------------------------------------------- | ------- | 92 | | gpgPath | Optional path to gpg binary | `null` | 93 | | stores | List of password stores with store-specific settings | `{}` | 94 | 95 | ### Store-specific Settings 96 | 97 | | Setting | Description | Default | 98 | | ------- | --------------------------------------- | ------- | 99 | | id | Unique store id (same as the store key) | `` | 100 | | name | Store name | `""` | 101 | | path | Path to the password store directory | `""` | 102 | 103 | ## Actions 104 | 105 | ### Configure 106 | 107 | Returns a response containing the per-store config. Used to check that the host app 108 | is alive, determine the version at startup, and provide per-store defaults. 109 | 110 | #### Request 111 | 112 | ``` 113 | { 114 | "settings": , 115 | "defaultStoreSettings": , 116 | "action": "configure" 117 | } 118 | ``` 119 | 120 | #### Response 121 | 122 | ``` 123 | { 124 | 125 | "status": "ok", 126 | "version": , 127 | "data": { 128 | "defaultStore": { 129 | "path": "/path/to/default/store", 130 | "settings": "", 131 | }, 132 | "storeSettings": { 133 | "storeId": "" 134 | } 135 | } 136 | } 137 | ``` 138 | 139 | ### List 140 | 141 | Get a list of all `*.gpg` files for each of a provided array of directory paths. The `storeN` 142 | is the ID of a password store, the key in `"settings.stores"` object. 143 | 144 | #### Request 145 | 146 | ``` 147 | { 148 | "settings": , 149 | "action": "list" 150 | } 151 | ``` 152 | 153 | #### Response 154 | 155 | ``` 156 | { 157 | "status": "ok", 158 | "version": , 159 | "data": { 160 | "files": { 161 | "storeN": ["", "<...>"], 162 | "storeN+1": ["", "<...>"] 163 | } 164 | } 165 | } 166 | ``` 167 | 168 | ### Tree 169 | 170 | Get a list of all nested directories for each of a provided array of directory paths. The `storeN` 171 | is the ID of a password store, the key in `"settings.stores"` object. 172 | 173 | #### Request 174 | 175 | ``` 176 | { 177 | "settings": , 178 | "action": "tree" 179 | } 180 | ``` 181 | 182 | #### Response 183 | 184 | ``` 185 | { 186 | "status": "ok", 187 | "version": , 188 | "data": { 189 | "directories": { 190 | "storeN": ["", "<...>"], 191 | "storeN+1": ["", "<...>"] 192 | } 193 | } 194 | } 195 | ``` 196 | 197 | ### Fetch 198 | 199 | Get the decrypted contents of a specific file. 200 | 201 | #### Request 202 | 203 | ``` 204 | { 205 | "settings": , 206 | "action": "fetch", 207 | "storeId": "", 208 | "file": "relative/path/to/file.gpg" 209 | } 210 | ``` 211 | 212 | #### Response 213 | 214 | ``` 215 | { 216 | "status": "ok", 217 | "version": , 218 | "data": { 219 | "contents": "" 220 | } 221 | } 222 | ``` 223 | 224 | ### Save 225 | 226 | Encrypt the given contents and save to a specific file. 227 | 228 | #### Request 229 | 230 | ``` 231 | { 232 | "settings": , 233 | "action": "save", 234 | "storeId": "", 235 | "file": "relative/path/to/file.gpg", 236 | "contents": "" 237 | } 238 | ``` 239 | 240 | #### Response 241 | 242 | ``` 243 | { 244 | "status": "ok", 245 | "version": 246 | } 247 | ``` 248 | 249 | ### Delete 250 | 251 | Delete a specific file and empty parent directories caused by the deletion, if any. 252 | 253 | #### Request 254 | 255 | ``` 256 | { 257 | "settings": , 258 | "action": "delete", 259 | "storeId": "", 260 | "file": "relative/path/to/file.gpg" 261 | } 262 | ``` 263 | 264 | #### Response 265 | 266 | ``` 267 | { 268 | "status": "ok", 269 | "version": 270 | } 271 | ``` 272 | 273 | ### Echo 274 | 275 | Send the `echoResponse` in the request as a response. 276 | 277 | #### Request 278 | 279 | ``` 280 | { 281 | "action": "echo", 282 | "echoResponse": 283 | } 284 | ``` 285 | 286 | #### Response 287 | 288 | ``` 289 | 290 | ``` 291 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | # Browserpass - native messaging host 4 | 5 | This is a host application for [browserpass](https://github.com/browserpass/browserpass-extension) browser extension providing it access to your password store. The communication is handled through [Native Messaging API](https://developer.chrome.com/extensions/nativeMessaging). 6 | 7 | ## Table of Contents 8 | 9 | - [Installation](#installation) 10 | - [Install via package manager](#install-via-package-manager) 11 | - [Install manually](#install-manually) 12 | - [Install on Nix / NixOS](#install-on-nix--nixos) 13 | - [Install on Windows](#install-on-windows) 14 | - [Install on Windows through WSL](#install-on-windows-through-wsl) 15 | - [Configure browsers](#configure-browsers) 16 | - [Building the app](#building-the-app) 17 | - [Build locally](#build-locally) 18 | - [Build using Docker](#build-using-docker) 19 | - [Updates](#updates) 20 | - [FAQ](#faq) 21 | - [Error: Unable to fetch and parse login fields](#error-unable-to-fetch-and-parse-login-fields) 22 | - [Uninstallation](#uninstallation) 23 | - [Contributing](#contributing) 24 | 25 | ## Installation 26 | 27 | ### Install via package manager 28 | 29 | The following operating systems provide a browserpass package that can be installed using a package manager: 30 | 31 | - Arch Linux: [browserpass](https://www.archlinux.org/packages/extra/x86_64/browserpass/) 32 | - Gentoo Linux: [browserpass](https://packages.gentoo.org/packages/www-plugins/browserpass) 33 | - Debian sid: [webext-browserpass](https://packages.debian.org/sid/webext-browserpass) (note: users report ([#126](https://github.com/browserpass/browserpass-native/issues/126)) that it's not suitable for any browser besides Chromium and Firefox, use manual installation if you plan to use other browsers. Moreover, the latest version in the repos is `3.0.7` and the package seems to have been abandoned, see [browserpass-extension#369](https://github.com/browserpass/browserpass-extension/issues/369). Manual installation is advised). 34 | - openSUSE Tumbleweed: [browserpass-native](https://software.opensuse.org/package/browserpass-native) 35 | - NixOS: [browserpass](https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/security/browserpass/default.nix) - also read [Install on Nix / NixOS](#install-on-nix--nixos) 36 | - macOS (Homebrew): [browserpass](https://github.com/Amar1729/homebrew-formulae/blob/master/Formula/browserpass.rb) in a user-contributed tap [amar1729/formulae](https://github.com/amar1729/homebrew-formulae) 37 | ```sh 38 | brew tap amar1729/formulae 39 | brew install browserpass # this will print further installation instructions, e.g. for Firefox: 40 | PREFIX='/opt/homebrew/opt/browserpass' make hosts-firefox-user -f '/opt/homebrew/opt/browserpass/lib/browserpass/Makefile' 41 | ``` 42 | 43 | Once the package is installed, **refer to the section [Configure browsers](#configure-browsers)**. 44 | 45 | If your OS is not listed above, proceed with the manual installation steps below. 46 | 47 | ### Install manually 48 | 49 | Download [the latest Github release](https://github.com/browserpass/browserpass-native/releases), choose either the source code archive (if you want to compile the app yourself) or an archive for your operating system (it contains a pre-built binary). 50 | 51 | All release files are signed with a PGP key that is available on [maximbaz.com](https://maximbaz.com/), [keybase.io](https://keybase.io/maximbaz) and various OpenPGP key servers. First, import the public key using any of these commands: 52 | 53 | ``` 54 | $ curl https://maximbaz.com/pgp_keys.asc | gpg --import 55 | $ curl https://keybase.io/maximbaz/pgp_keys.asc | gpg --import 56 | $ gpg --recv-keys 56C3E775E72B0C8B1C0C1BD0B5DB77409B11B601 57 | ``` 58 | 59 | To verify the signature of a given file, use `$ gpg --verify .asc`. 60 | 61 | It should report: 62 | 63 | ``` 64 | gpg: Signature made ... 65 | gpg: using EDDSA key 04D7A219B0ABE4C2B62A5E654A2B758631E1FD91 66 | gpg: Good signature from "Maxim Baz <...>" 67 | gpg: aka ... 68 | Primary key fingerprint: 56C3 E775 E72B 0C8B 1C0C 1BD0 B5DB 7740 9B11 B601 69 | Subkey fingerprint: 04D7 A219 B0AB E4C2 B62A 5E65 4A2B 7586 31E1 FD91 70 | ``` 71 | 72 | Unpack the archive. If you decided to compile the application yourself, refer to the [Building the app](#building-the-app) section on how to do so. Once complete, continue with the steps below. 73 | 74 | If you are on macOS, first install the necessary tools: `brew install coreutils gnu-sed`. 75 | 76 | If you are on FreeBSD, first install the GNU tools: `pkg install coreutils gmake gsed'.` Use `gmake` in place of `make` below. 77 | 78 | If you downloaded a release archive with pre-compiled binary, follow these steps to install the app: 79 | 80 | ``` 81 | # IMPORTANT: replace XXXX with OS name depending on the archive you downloaded, e.g. "linux64" 82 | make BIN=browserpass-XXXX configure # Configure the hosts json files 83 | sudo make BIN=browserpass-XXXX install # Install the app 84 | ``` 85 | 86 | In addition, both `configure` and `install` targets respect `PREFIX`, `DESTDIR` parameters if you want to customize the install location (e.g. to install to a `$HOME` dir to avoid using `sudo`). 87 | 88 | For example, if you are on macOS or FreeBSD, you probably want to install Browserpass in `/usr/local/bin`, therefore you need to run: 89 | 90 | ``` 91 | make BIN=browserpass-darwin64 PREFIX=/usr/local configure # Configure the hosts json files 92 | sudo make BIN=browserpass-darwin64 PREFIX=/usr/local install # Install the app 93 | ``` 94 | 95 | If you compiled the app yourself, you can omit `BIN` parameter: 96 | 97 | ``` 98 | make configure # Configure the hosts json files 99 | sudo make install # Install the app 100 | ``` 101 | 102 | Finally proceed to the [Configure browsers](#configure-browsers) section. 103 | 104 | #### Install on Nix / NixOS 105 | 106 | For a declarative NixOS installation, update your channel with `sudo nix-channel --update`, use the following to your `/etc/nixos/configuration.nix` and rebuild your system: 107 | 108 | ```nix 109 | { pkgs, ... }: { 110 | programs.browserpass.enable = true; 111 | environment.systemPackages = with pkgs; [ 112 | # All of these browsers will work 113 | chromium firefox google-chrome vivaldi 114 | # firefox*-bin versions do *not* work with this. If you require such Firefox versions, use the stateful setup described below. 115 | ]; 116 | } 117 | ``` 118 | 119 | For a stateful Nix setup, update your channel, install Browserpass and link the necessary files with the Makefile (see [Configure browsers](#configure-browsers) section), but pass `DESTDIR=~/.nix-profile`: 120 | 121 | ```bash 122 | $ nix-channel --update 123 | $ nix-env -iA nixpkgs.browserpass # Or nix-env -iA nixos.browserpass on NixOS 124 | $ DESTDIR=~/.nix-profile make -f ~/.nix-profile/lib/browserpass/Makefile 125 | ``` 126 | 127 | #### Install on Windows 128 | 129 | Download [the latest Github release](https://github.com/browserpass/browserpass-native/releases/latest) for `windows64`. 130 | 131 | Run the installer, it will install all the necessary files in `C:\Program Files\Browserpass` and it will write in the Windows registry to [configure browsers](#configure-browsers). 132 | Browserpass will look for the password store in `C:\Users\\.password-store` by default. For troubleshooting, the log file can be found in `C:\Users\\AppData\Local\browserpass`. 133 | 134 | #### Install on Windows through WSL 135 | 136 | If you want to use WSL instead 137 | 138 | 1. Follow the [installation](#installation) steps for the WSL distribution you are using. There is no need to configure the browser as your browser does not run in WSL. 139 | 2. Follow the then [installation](#install-on-windows) steps for installing on Windows. 140 | 3. Create `C:\Program Files\Browserpass\browserpass-wsl.bat` with the following contents: 141 | 142 | ``` 143 | @echo off 144 | bash -c "/usr/bin/browserpass-linux64 2>/dev/null" 145 | ``` 146 | 147 | 4. Edit the hosts json files (in our example `C:\Program Files\Browserpass\browser-files\*-host.json`) and replace `browserpass-windows64.exe` with `browserpass-wsl.bat` you've just created. 148 | 149 | Remember to check [Hints for configuring gpg](#error-unable-to-fetch-and-parse-login-fields) on how to configure pinentry to unlock your PGP key. 150 | 151 | ### Configure browsers 152 | 153 | #### Unix-like Systems 154 | 155 | The following operating systems provide packages for certain browsers that can be installed using a package manager: 156 | 157 | - Arch Linux: [browserpass-chromium](https://www.archlinux.org/packages/extra/any/browserpass-chromium/) and [browserpass-firefox](https://www.archlinux.org/packages/extra/any/browserpass-firefox/) 158 | - AUR: [browserpass-chrome](https://aur.archlinux.org/packages/browserpass-chrome/) 159 | 160 | If you installed a distro package above, you are done! 161 | 162 | If something went wrong, if there's no package for your OS and/or a browser of your choice, or for whatever reason you just don't want to use them, proceed with the steps below. 163 | 164 | First, enter the directory with installed Browserpass, by default it is `/usr/lib/browserpass/`, but if you used `PREFIX` or `DESTDIR` when running `make install`, it might be different for you. For example, on macOS the directory is likely to be `/usr/local/lib/browserpass/`. 165 | 166 | See below the list of available `make` goals to configure various browsers. Use `gmake` on FreeBSD in place of `make`. 167 | 168 | **It is recommended to use `*-user` make goals**, as more people had luck with them. But if they don't work as expected, try other available goals. 169 | 170 | If you provided `PREFIX` and/or `DESTDIR` while running `make install`, remember that you must provide the same parameters, for example `make PREFIX=/usr/local hosts-chromium-user`: 171 | 172 | | Command | Description | 173 | | --------------------------- | ------------------------------------------------------------------------------------ | 174 | | `make hosts-chromium-user` | Configure browserpass for Chromium browser, for the current user only | 175 | | `make hosts-firefox-user` | Configure browserpass for Firefox browser, for the current user only | 176 | | `make hosts-librewolf-user` | Configure browserpass for Librewolf browser, for the current user only | 177 | | `make hosts-chrome-user` | Configure browserpass for Google Chrome or Opera browsers, for the current user only | 178 | | `make hosts-edge-user` | Configure browserpass for Microsoft Edge browser, for the current user only | 179 | | `make hosts-brave-user` | Configure browserpass for Brave browser, for the current user only | 180 | | `make hosts-iridium-user` | Configure browserpass for Iridium browser, for the current user only | 181 | | `make hosts-vivaldi-user` | Configure browserpass for Vivaldi browser, for the current user only | 182 | | `make hosts-yandex-user` | Configure browserpass for Yandex browser, for the current user only | 183 | | `make hosts-slimjet-user` | Configure browserpass for Slimjet browser, for the current user only | 184 | | `make hosts-arc-user` | Configure browserpass for Arc browser, for the current user only | 185 | | `sudo make hosts-chromium` | Configure browserpass for Chromium browser, system-wide | 186 | | `sudo make hosts-firefox` | Configure browserpass for Firefox browser, system-wide | 187 | | `sudo make hosts-librewolf` | Configure browserpass for Librewolf browser, system-wide | 188 | | `sudo make hosts-chrome` | Configure browserpass for Google Chrome or Opera browsers, system-wide | 189 | | `sudo make hosts-edge` | Configure browserpass for Microsoft Edge browser, system-wide | 190 | | `sudo make hosts-brave` | Configure browserpass for Brave browser, system-wide | 191 | | `sudo make hosts-iridium` | Configure browserpass for Iridium browser, system-wide | 192 | | `sudo make hosts-vivaldi` | Configure browserpass for Vivaldi browser, system-wide | 193 | | `sudo make hosts-yandex` | Configure browserpass for Yandex browser, system-wide | 194 | | `sudo make hosts-slimjet` | Configure browserpass for Slimjet browser, system-wide | 195 | | `sudo make hosts-arc` | Configure browserpass for Arc browser, system-wide | 196 | 197 | In addition, Chromium-based browsers support the following `make` goals: 198 | 199 | | Command | Description | 200 | | ----------------------------- | ------------------------------------------------------------------------------------------------------------ | 201 | | `make policies-chromium-user` | Automatically install browser extension from Web Store for Chromium browser, for the current user only | 202 | | `make policies-chrome-user` | Automatically install browser extension from Web Store for Google Chrome browser, for the current user only | 203 | | `make policies-edge-user` | Automatically install browser extension from Web Store for Microsoft Edge browser, for the current user only | 204 | | `make policies-brave-user` | Automatically install browser extension from Web Store for Brave browser, for the current user only | 205 | | `make policies-iridium-user` | Automatically install browser extension from Web Store for Iridium browser, for the current user only | 206 | | `make policies-slimjet-user` | Automatically install browser extension from Web Store for Slimjet browser, for the current user only | 207 | | `make policies-vivaldi-user` | Automatically install browser extension from Web Store for Vivaldi browser, for the current user only | 208 | | `make policies-yandex-user` | Automatically install browser extension from Web Store for Yandex browser, for the current user only | 209 | | `make policies-arc-user` | Automatically install browser extension from Web Store for Arc browser, for the current user only | 210 | | `sudo make policies-chromium` | Automatically install browser extension from Web Store for Chromium browser, system-wide | 211 | | `sudo make policies-chrome` | Automatically install browser extension from Web Store for Google Chrome browser, system-wide | 212 | | `sudo make policies-edge` | Automatically install browser extension from Web Store for Microsoft Edge browser, system-wide | 213 | | `sudo make policies-brave` | Automatically install browser extension from Web Store for Brave browser, system-wide | 214 | | `sudo make policies-iridium` | Automatically install browser extension from Web Store for Iridium browser, system-wide | 215 | | `sudo make policies-slimjet` | Automatically install browser extension from Web Store for Slimjet browser, system-wide | 216 | | `sudo make policies-vivaldi` | Automatically install browser extension from Web Store for Vivaldi browser, system-wide | 217 | | `sudo make policies-yandex` | Automatically install browser extension from Web Store for Yandex browser, system-wide | 218 | | `sudo make policies-arc` | Automatically install browser extension from Web Store for Arc browser, system-wide | 219 | 220 | #### Windows 221 | 222 | The browser will fetch the configuration file from the registry, checking for the full path to the manifest file (such as `firefox-host.json`) in the registry in either: 223 | - (Firefox) `Software\Mozilla\NativeMessagingHosts\com.github.browserpass.native\(default)` 224 | - (Chromium-based) `Software\Google\Chrome\NativeMessagingHosts\com.github.browserpass.native\(default)` 225 | 226 | This should be automatically done for you if you ran the released installer. 227 | 228 | ## Building the app 229 | 230 | ### Build locally 231 | 232 | Make sure you have the latest stable Go installed. 233 | 234 | The following `make` goals are available (check Makefile for more details): 235 | 236 | | Command | Description | 237 | | ---------------------------- | ----------------------------------- | 238 | | `make` or `make all` | Compile the app and run tests | 239 | | `make browserpass` | Compile the app for your OS | 240 | | `make browserpass-linux64` | Compile the app for Linux 64-bit | 241 | | `make browserpass-windows64` | Compile the app for Windows 64-bit | 242 | | `make browserpass-darwin64` | Compile the app for Mac OS X 64-bit | 243 | | `make browserpass-openbsd64` | Compile the app for OpenBSD 64-bit | 244 | | `make browserpass-freebsd64` | Compile the app for FreeBSD 64-bit | 245 | | `make test` | Run tests | 246 | 247 | ### Build using Docker 248 | 249 | First build the docker image using the following command in the project root: 250 | 251 | ```shell 252 | docker build -t browserpass-native . 253 | ``` 254 | 255 | The entry point in the docker image is the `make` command. To run it: 256 | 257 | ```shell 258 | docker run --rm -v "$(pwd)":/src browserpass-native 259 | ``` 260 | 261 | Specify `make` goal(s) as the last parameter, for example: 262 | 263 | ```shell 264 | docker run --rm -v "$(pwd)":/src browserpass-native test 265 | ``` 266 | 267 | Refer to the list of available `make` goals above. 268 | 269 | ## Updates 270 | 271 | If you installed the app using a package manager for your OS, you will likely update it in the same way. 272 | 273 | If you installed manually, repeat the steps in the [Install manually](#install-manually) section. 274 | 275 | ## FAQ 276 | 277 | ### Error: Unable to fetch and parse login fields 278 | 279 | If you can see passwords, but unable to fill forms or copy credentials, you likely have issues with your `gpg` setup. 280 | 281 | First things first, make sure that `gpg` and some GUI `pinentry` are installed. 282 | 283 | - on macOS many people succeeded with `pinentry-mac` 284 | - on Linux [users report](https://github.com/browserpass/browserpass-extension/issues/155) that `pinentry-gnome3` does not work well with GNOME 3 and Firefox, use e.g. `pinentry-gtk-2` 285 | - on Windows WSL people succeded with [pinentry-wsl-ps1](https://github.com/diablodale/pinentry-wsl-ps1) 286 | 287 | `pinentry` is the application that asks you your password to unlock PGP key when you for example use `pass`. 288 | 289 | The selected `pinentry` **must have GUI**, console-based (like `pinentry-tty` or `pinentry-curses`) **are not supported** (unless you know what you are doing). 290 | 291 | Ensure that `gpg-agent` process is actually running, if not you need to investigate how to enable it. 292 | 293 | Finally configure a GUI pinentry program in `~/.gnupg/gpg-agent.conf`: 294 | 295 | ``` 296 | pinentry-program /full/path/to/pinentry 297 | ``` 298 | 299 | You will need to restart `gpg-agent` using: `$ gpgconf --kill gpg-agent` 300 | 301 | If Browserpass is unable to locate the proper `gpg` binary, try configuring a full path to your `gpg` in the browser extension settings or in `.browserpass.json` file in the root of your password store: 302 | 303 | ```json 304 | { 305 | "gpgPath": "/full/path/to/gpg" 306 | } 307 | ``` 308 | 309 | ### Setup on NixOS-WSL 310 | 311 | In the case of [NixOS-WSL](https://github.com/nix-community/NixOS-WSL) (or any WSL instance where the `bash` PowerShell command raises a `CreateProcessEntryCommon` error), the content of `C:\Program Files\Browserpass\browserpass-wsl.bat` needs a slight modification since `browserpass` needs to be executed through the [NixOS magic occurring in the `root` login shell](https://github.com/nix-community/NixOS-WSL/issues/284): 312 | 313 | ```powershell 314 | @echo off 315 | wsl -u user -- "/usr/bin/browserpass 2>/dev/null" 316 | ``` 317 | 318 | With the `wsl` command, you can also specify the WSL distribution executing the command: 319 | 320 | ```powershell 321 | wsl -d NixOS -u user -- "/usr/bin/browserpass 2>/dev/null" 322 | ``` 323 | 324 | This is especially handy if your NixOS WSL is not the default distribution. 325 | 326 | It's worth noting that if Browserpass is installed in the user environment, the path should be as follows: 327 | 328 | ```powershell 329 | @echo off 330 | wsl -u user -- "/home/user/.nix-profile/bin/browserpass 2>/dev/null" 331 | ``` 332 | 333 | ## Uninstallation 334 | 335 | If you installed Browserpass via a package manager, uninstalling the package will hopefully do all the necessary cleanup. 336 | 337 | For manual installations, uninstallation procedure basically consists of following the steps you ran in Makefile in reverse. 338 | 339 | ## Contributing 340 | 341 | 1. Fork [the repo](https://github.com/browserpass/browserpass-extension) 342 | 2. Create your feature branch 343 | - `git checkout -b my-new-feature` 344 | 3. Commit your changes 345 | - `git commit -am 'Add some feature'` 346 | 4. Push the branch 347 | - `git push origin my-new-feature` 348 | 5. Create a new pull request 349 | -------------------------------------------------------------------------------- /browser-files/chromium-host.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.github.browserpass.native", 3 | "description": "Browserpass native component for the Chromium extension", 4 | "path": "%%replace%%", 5 | "type": "stdio", 6 | "allowed_origins": [ 7 | "chrome-extension://naepdomgkenhinolocfifgehidddafch/", 8 | "chrome-extension://pjmbgaakjkbhpopmakjoedenlfdmcdgm/", 9 | "chrome-extension://klfoddkbhleoaabpmiigbmpbjfljimgb/" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /browser-files/chromium-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "ExtensionInstallForcelist": [ 3 | "naepdomgkenhinolocfifgehidddafch;https://clients2.google.com/service/update2/crx" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /browser-files/firefox-host.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.github.browserpass.native", 3 | "description": "Browserpass native component for the Firefox extension", 4 | "path": "%%replace%%", 5 | "type": "stdio", 6 | "allowed_extensions": ["browserpass@maximbaz.com"] 7 | } 8 | -------------------------------------------------------------------------------- /debug/README.md: -------------------------------------------------------------------------------- 1 | To test whether the native host is working correctly, run: 2 | `/path/to/browserpass < /path/to/request-configure.hex.txt` 3 | 4 | Note that this is *not* a plain text file; it includes a binary header 5 | that may be damaged if you attempt to edit this file using a standard 6 | text editor. 7 | -------------------------------------------------------------------------------- /debug/request-configure.hex.txt: -------------------------------------------------------------------------------- 1 | {"action": "configure"} 2 | -------------------------------------------------------------------------------- /errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | // Code exit code 8 | type Code int 9 | 10 | // Error codes that are sent to the browser extension and used as exit codes in the app. 11 | // DO NOT MODIFY THE VALUES, always append new error codes to the bottom. 12 | const ( 13 | CodeParseRequestLength Code = 10 14 | CodeParseRequest Code = 11 15 | CodeInvalidRequestAction Code = 12 16 | CodeInaccessiblePasswordStore Code = 13 17 | CodeInaccessibleDefaultPasswordStore Code = 14 18 | CodeUnknownDefaultPasswordStoreLocation Code = 15 19 | CodeUnreadablePasswordStoreDefaultSettings Code = 16 20 | CodeUnreadableDefaultPasswordStoreDefaultSettings Code = 17 21 | CodeUnableToListFilesInPasswordStore Code = 18 22 | CodeUnableToDetermineRelativeFilePathInPasswordStore Code = 19 23 | CodeInvalidPasswordStore Code = 20 24 | CodeInvalidGpgPath Code = 21 25 | CodeUnableToDetectGpgPath Code = 22 26 | CodeInvalidPasswordFileExtension Code = 23 27 | CodeUnableToDecryptPasswordFile Code = 24 28 | CodeUnableToListDirectoriesInPasswordStore Code = 25 29 | CodeUnableToDetermineRelativeDirectoryPathInPasswordStore Code = 26 30 | CodeEmptyContents Code = 27 31 | CodeUnableToDetermineGpgRecipients Code = 28 32 | CodeUnableToEncryptPasswordFile Code = 29 33 | CodeUnableToDeletePasswordFile Code = 30 34 | CodeUnableToDetermineIsDirectoryEmpty Code = 31 35 | CodeUnableToDeleteEmptyDirectory Code = 32 36 | ) 37 | 38 | // Field extra field in the error response params 39 | type Field string 40 | 41 | // Extra fields that can be sent to the browser extension as part of an error response. 42 | // FieldMessage is always present, others are optional. 43 | const ( 44 | FieldMessage Field = "message" 45 | FieldAction Field = "action" 46 | FieldError Field = "error" 47 | FieldStoreID Field = "storeId" 48 | FieldStoreName Field = "storeName" 49 | FieldStorePath Field = "storePath" 50 | FieldFile Field = "file" 51 | FieldDirectory Field = "directory" 52 | FieldGpgPath Field = "gpgPath" 53 | ) 54 | 55 | // ExitWithCode exit with error code 56 | func ExitWithCode(code Code) { 57 | os.Exit(int(code)) 58 | } 59 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/browserpass/browserpass-native 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/mattn/go-zglob v0.0.4 7 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 8 | github.com/sirupsen/logrus v1.9.3 9 | golang.org/x/sys v0.12.0 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM= 5 | github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= 9 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= 10 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 11 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 12 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 13 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 14 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 15 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 | golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 17 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 18 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 20 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /helpers/helpers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | func DetectGpgBinary() (string, error) { 15 | // Look in $PATH first, then check common locations - the first successful result wins 16 | gpgBinaryPriorityList := []string{ 17 | "gpg2", "gpg", 18 | "/bin/gpg2", "/usr/bin/gpg2", "/usr/local/bin/gpg2", 19 | "/bin/gpg", "/usr/bin/gpg", "/usr/local/bin/gpg", 20 | } 21 | 22 | for _, binary := range gpgBinaryPriorityList { 23 | err := ValidateGpgBinary(binary) 24 | if err == nil { 25 | return binary, nil 26 | } 27 | } 28 | return "", fmt.Errorf("Unable to detect the location of the gpg binary to use") 29 | } 30 | 31 | func ValidateGpgBinary(gpgPath string) error { 32 | return exec.Command(gpgPath, "--version").Run() 33 | } 34 | 35 | func GpgDecryptFile(filePath string, gpgPath string) (string, error) { 36 | passwordFile, err := os.Open(filePath) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | var stdout, stderr bytes.Buffer 42 | gpgOptions := []string{"--decrypt", "--yes", "--quiet", "--batch", "-"} 43 | 44 | cmd := exec.Command(gpgPath, gpgOptions...) 45 | cmd.Stdin = passwordFile 46 | cmd.Stdout = &stdout 47 | cmd.Stderr = &stderr 48 | 49 | if err := cmd.Run(); err != nil { 50 | return "", fmt.Errorf("Error: %s, Stderr: %s", err.Error(), stderr.String()) 51 | } 52 | 53 | return stdout.String(), nil 54 | } 55 | 56 | func GpgEncryptFile(filePath string, contents string, recipients []string, gpgPath string) error { 57 | err := os.MkdirAll(filepath.Dir(filePath), 0755) 58 | if err != nil { 59 | return fmt.Errorf("Unable to create directory structure: %s", err.Error()) 60 | } 61 | 62 | var stdout, stderr bytes.Buffer 63 | gpgOptions := []string{"--encrypt", "--yes", "--quiet", "--batch", "--output", filePath} 64 | for _, recipient := range recipients { 65 | gpgOptions = append(gpgOptions, "--recipient", recipient) 66 | } 67 | 68 | cmd := exec.Command(gpgPath, gpgOptions...) 69 | cmd.Stdin = strings.NewReader(contents) 70 | cmd.Stdout = &stdout 71 | cmd.Stderr = &stderr 72 | 73 | if err = cmd.Run(); err != nil { 74 | return fmt.Errorf("Error: %s, Stderr: %s", err.Error(), stderr.String()) 75 | } 76 | 77 | return nil 78 | } 79 | 80 | func DetectGpgRecipients(filePath string) ([]string, error) { 81 | dir := filepath.Dir(filePath) 82 | for { 83 | file, err := ioutil.ReadFile(filepath.Join(dir, ".gpg-id")) 84 | if err == nil { 85 | return strings.Split(strings.ReplaceAll(strings.TrimSpace(string(file)), "\r\n", "\n"), "\n"), nil 86 | } 87 | 88 | if !os.IsNotExist(err) { 89 | return nil, fmt.Errorf("Unable to open `.gpg-id` file: %s", err.Error()) 90 | } 91 | 92 | parentDir := filepath.Dir(dir) 93 | if parentDir == dir { 94 | return nil, fmt.Errorf("Unable to find '.gpg-id' file") 95 | } 96 | 97 | dir = parentDir 98 | } 99 | } 100 | 101 | func IsDirectoryEmpty(dirPath string) (bool, error) { 102 | f, err := os.Open(dirPath) 103 | if err != nil { 104 | return false, err 105 | } 106 | defer f.Close() 107 | 108 | _, err = f.Readdirnames(1) 109 | if err == io.EOF { 110 | return true, nil 111 | } 112 | 113 | return false, err 114 | } 115 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/browserpass/browserpass-native/openbsd" 9 | "github.com/browserpass/browserpass-native/persistentlog" 10 | "github.com/browserpass/browserpass-native/request" 11 | "github.com/browserpass/browserpass-native/version" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func main() { 16 | var isVerbose bool 17 | var isVersion bool 18 | flag.BoolVar(&isVerbose, "v", false, "print verbose output") 19 | flag.BoolVar(&isVersion, "version", false, "print version and exit") 20 | flag.Parse() 21 | 22 | if isVersion { 23 | fmt.Println("Browserpass host app version:", version.String()) 24 | os.Exit(0) 25 | } 26 | 27 | openbsd.Pledge("stdio rpath proc exec getpw unix tty") 28 | 29 | log.SetFormatter(&log.TextFormatter{FullTimestamp: true}) 30 | if isVerbose { 31 | log.SetLevel(log.DebugLevel) 32 | } 33 | 34 | persistentlog.AddPersistentLogHook() 35 | 36 | log.Debugf("Starting browserpass host app v%v", version.String()) 37 | request.Process() 38 | } 39 | -------------------------------------------------------------------------------- /openbsd/generic.go: -------------------------------------------------------------------------------- 1 | // +build !openbsd 2 | 3 | package openbsd 4 | 5 | // Pledge allowed system calls, available only on OpenBSD systems 6 | func Pledge(promises string) { 7 | } 8 | -------------------------------------------------------------------------------- /openbsd/openbsd.go: -------------------------------------------------------------------------------- 1 | // +build openbsd 2 | 3 | package openbsd 4 | 5 | import "golang.org/x/sys/unix" 6 | 7 | // Pledge allowed system calls 8 | func Pledge(promises string) { 9 | unix.PledgePromises(promises) 10 | } 11 | -------------------------------------------------------------------------------- /persistentlog/syslog.go: -------------------------------------------------------------------------------- 1 | // +build !windows,!nacl,!plan9 2 | 3 | package persistentlog 4 | 5 | import ( 6 | "log/syslog" 7 | 8 | log "github.com/sirupsen/logrus" 9 | logSyslog "github.com/sirupsen/logrus/hooks/syslog" 10 | ) 11 | 12 | // AddPersistentLogHook configures persisting logs in syslog 13 | func AddPersistentLogHook() { 14 | if hook, err := logSyslog.NewSyslogHook("", "", syslog.LOG_INFO, "browserpass"); err != nil { 15 | log.Warn("Unable to connect to syslog, logs will NOT be persisted: ", err) 16 | } else { 17 | log.AddHook(hook) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /persistentlog/unsupported.go: -------------------------------------------------------------------------------- 1 | // +build nacl,plan9 2 | 3 | package persistentlog 4 | 5 | import log "github.com/sirupsen/logrus" 6 | 7 | // AddPersistentLogHook configures persisting logs, not supported on these systems 8 | func AddPersistentLogHook() { 9 | log.Warn("Persistent logging is not implemented on this OS") 10 | } 11 | -------------------------------------------------------------------------------- /persistentlog/windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package persistentlog 4 | 5 | import ( 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/rifflock/lfshook" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | // AddPersistentLogHook configures persisting logs in a file 14 | func AddPersistentLogHook() { 15 | appDataPath := os.Getenv("LOCALAPPDATA") 16 | if appDataPath == "" { 17 | log.Warn("Unable to determine %%APPDATA%% folder location, logs will NOT be persisted") 18 | return 19 | } 20 | logFolderPath := filepath.Join(appDataPath, "browserpass") 21 | if err := os.MkdirAll(logFolderPath, os.ModePerm); err != nil { 22 | log.Warn("Unable to create browserpass folder in %%APPDATA%%, logs will NOT be persisted: ", err) 23 | return 24 | } 25 | logFilePath := filepath.Join(logFolderPath, "browserpass.log") 26 | log.Debug("Logs will being written to: ", logFilePath) 27 | log.AddHook(lfshook.NewHook(logFilePath, &log.TextFormatter{FullTimestamp: true})) 28 | } 29 | -------------------------------------------------------------------------------- /request/common.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | ) 9 | 10 | func normalizePasswordStorePath(storePath string) (string, error) { 11 | if storePath == "" { 12 | return "", errors.New("The store path cannot be empty") 13 | } 14 | 15 | if strings.HasPrefix(storePath, "~/") { 16 | storePath = filepath.Join("$HOME", storePath[2:]) 17 | } 18 | storePath = os.ExpandEnv(storePath) 19 | 20 | directStorePath, err := filepath.EvalSymlinks(storePath) 21 | if err != nil { 22 | return "", err 23 | } 24 | storePath = directStorePath 25 | 26 | stat, err := os.Stat(storePath) 27 | if err != nil { 28 | return "", err 29 | } 30 | if !stat.IsDir() { 31 | return "", errors.New("The specified path exists, but is not a directory") 32 | } 33 | return storePath, nil 34 | } 35 | -------------------------------------------------------------------------------- /request/configure.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/browserpass/browserpass-native/errors" 10 | "github.com/browserpass/browserpass-native/helpers" 11 | "github.com/browserpass/browserpass-native/response" 12 | log "github.com/sirupsen/logrus" 13 | ) 14 | 15 | func configure(request *request) { 16 | responseData := response.MakeConfigureResponse() 17 | 18 | // User configured gpgPath in the browser, check if it is a valid binary to use 19 | if request.Settings.GpgPath != "" { 20 | err := helpers.ValidateGpgBinary(request.Settings.GpgPath) 21 | if err != nil { 22 | log.Errorf( 23 | "The provided gpg binary path '%v' is invalid: %+v", 24 | request.Settings.GpgPath, err, 25 | ) 26 | response.SendErrorAndExit( 27 | errors.CodeInvalidGpgPath, 28 | &map[errors.Field]string{ 29 | errors.FieldMessage: "The provided gpg binary path is invalid", 30 | errors.FieldAction: "configure", 31 | errors.FieldError: err.Error(), 32 | errors.FieldGpgPath: request.Settings.GpgPath, 33 | }, 34 | ) 35 | } 36 | } 37 | 38 | // Check that each and every store in the settings exists and is accessible. 39 | // Then read the default configuration for these stores (if available). 40 | for _, store := range request.Settings.Stores { 41 | normalizedStorePath, err := normalizePasswordStorePath(store.Path) 42 | if err != nil { 43 | log.Errorf( 44 | "The password store '%+v' is not accessible at its location: %+v", 45 | store, err, 46 | ) 47 | response.SendErrorAndExit( 48 | errors.CodeInaccessiblePasswordStore, 49 | &map[errors.Field]string{ 50 | errors.FieldMessage: "The password store is not accessible", 51 | errors.FieldAction: "configure", 52 | errors.FieldError: err.Error(), 53 | errors.FieldStoreID: store.ID, 54 | errors.FieldStoreName: store.Name, 55 | errors.FieldStorePath: store.Path, 56 | }, 57 | ) 58 | } 59 | 60 | store.Path = normalizedStorePath 61 | 62 | responseData.StoreSettings[store.ID], err = readDefaultSettings(store.Path) 63 | if err == nil { 64 | var storeSettings StoreSettings 65 | err = json.Unmarshal([]byte(responseData.StoreSettings[store.ID]), &storeSettings) 66 | } 67 | if err != nil { 68 | log.Errorf( 69 | "Unable to read .browserpass.json of the user-configured password store '%+v': %+v", 70 | store, err, 71 | ) 72 | response.SendErrorAndExit( 73 | errors.CodeUnreadablePasswordStoreDefaultSettings, 74 | &map[errors.Field]string{ 75 | errors.FieldMessage: "Unable to read .browserpass.json of the password store", 76 | errors.FieldAction: "configure", 77 | errors.FieldError: err.Error(), 78 | errors.FieldStoreID: store.ID, 79 | errors.FieldStoreName: store.Name, 80 | errors.FieldStorePath: store.Path, 81 | }, 82 | ) 83 | } 84 | } 85 | 86 | // Check whether a store in the default location exists and is accessible. 87 | // If there is at least one store in the settings, user will not use the default store => skip its validation. 88 | // However, if there are no stores in the settings, user expects to use the default password store => return an error if it is not accessible. 89 | if len(request.Settings.Stores) == 0 { 90 | possibleDefaultStorePath, err := getDefaultPasswordStorePath() 91 | if err != nil { 92 | log.Error("Unable to determine the location of the default password store: ", err) 93 | response.SendErrorAndExit( 94 | errors.CodeUnknownDefaultPasswordStoreLocation, 95 | &map[errors.Field]string{ 96 | errors.FieldMessage: "Unable to determine the location of the default password store", 97 | errors.FieldAction: "configure", 98 | errors.FieldError: err.Error(), 99 | }, 100 | ) 101 | } else { 102 | responseData.DefaultStore.Path, err = normalizePasswordStorePath(possibleDefaultStorePath) 103 | if err != nil { 104 | log.Errorf( 105 | "The default password store is not accessible at the location '%v': %+v", 106 | possibleDefaultStorePath, err, 107 | ) 108 | response.SendErrorAndExit( 109 | errors.CodeInaccessibleDefaultPasswordStore, 110 | &map[errors.Field]string{ 111 | errors.FieldMessage: "The default password store is not accessible", 112 | errors.FieldAction: "configure", 113 | errors.FieldError: err.Error(), 114 | errors.FieldStorePath: possibleDefaultStorePath, 115 | }, 116 | ) 117 | } 118 | } 119 | 120 | responseData.DefaultStore.Settings, err = readDefaultSettings(responseData.DefaultStore.Path) 121 | if err == nil { 122 | var storeSettings StoreSettings 123 | err = json.Unmarshal([]byte(responseData.DefaultStore.Settings), &storeSettings) 124 | } 125 | if err != nil { 126 | log.Errorf( 127 | "Unable to read .browserpass.json of the default password store in '%v': %+v", 128 | responseData.DefaultStore.Path, err, 129 | ) 130 | response.SendErrorAndExit( 131 | errors.CodeUnreadableDefaultPasswordStoreDefaultSettings, 132 | &map[errors.Field]string{ 133 | errors.FieldMessage: "Unable to read .browserpass.json of the default password store", 134 | errors.FieldAction: "configure", 135 | errors.FieldError: err.Error(), 136 | errors.FieldStorePath: responseData.DefaultStore.Path, 137 | }, 138 | ) 139 | } 140 | } 141 | 142 | response.SendOk(responseData) 143 | } 144 | 145 | func getDefaultPasswordStorePath() (string, error) { 146 | path := os.Getenv("PASSWORD_STORE_DIR") 147 | if path != "" { 148 | return path, nil 149 | } 150 | 151 | home, err := os.UserHomeDir() 152 | if err != nil { 153 | return "", err 154 | } 155 | 156 | path = filepath.Join(home, ".password-store") 157 | return path, nil 158 | } 159 | 160 | func readDefaultSettings(storePath string) (string, error) { 161 | content, err := ioutil.ReadFile(filepath.Join(storePath, ".browserpass.json")) 162 | if err == nil { 163 | return string(content), nil 164 | } 165 | if os.IsNotExist(err) { 166 | return "{}", nil 167 | } 168 | return "", err 169 | } 170 | -------------------------------------------------------------------------------- /request/delete.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/browserpass/browserpass-native/errors" 9 | "github.com/browserpass/browserpass-native/helpers" 10 | "github.com/browserpass/browserpass-native/response" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func deleteFile(request *request) { 15 | responseData := response.MakeDeleteResponse() 16 | 17 | if !strings.HasSuffix(request.File, ".gpg") { 18 | log.Errorf("The requested password file '%v' does not have the expected '.gpg' extension", request.File) 19 | response.SendErrorAndExit( 20 | errors.CodeInvalidPasswordFileExtension, 21 | &map[errors.Field]string{ 22 | errors.FieldMessage: "The requested password file does not have the expected '.gpg' extension", 23 | errors.FieldAction: "delete", 24 | errors.FieldFile: request.File, 25 | }, 26 | ) 27 | } 28 | 29 | store, ok := request.Settings.Stores[request.StoreID] 30 | if !ok { 31 | log.Errorf( 32 | "The password store with ID '%v' is not present in the list of stores '%+v'", 33 | request.StoreID, request.Settings.Stores, 34 | ) 35 | response.SendErrorAndExit( 36 | errors.CodeInvalidPasswordStore, 37 | &map[errors.Field]string{ 38 | errors.FieldMessage: "The password store is not present in the list of stores", 39 | errors.FieldAction: "delete", 40 | errors.FieldStoreID: request.StoreID, 41 | }, 42 | ) 43 | } 44 | 45 | normalizedStorePath, err := normalizePasswordStorePath(store.Path) 46 | if err != nil { 47 | log.Errorf( 48 | "The password store '%+v' is not accessible at its location: %+v", 49 | store, err, 50 | ) 51 | response.SendErrorAndExit( 52 | errors.CodeInaccessiblePasswordStore, 53 | &map[errors.Field]string{ 54 | errors.FieldMessage: "The password store is not accessible", 55 | errors.FieldAction: "delete", 56 | errors.FieldError: err.Error(), 57 | errors.FieldStoreID: store.ID, 58 | errors.FieldStoreName: store.Name, 59 | errors.FieldStorePath: store.Path, 60 | }, 61 | ) 62 | } 63 | store.Path = normalizedStorePath 64 | 65 | filePath := filepath.Join(store.Path, request.File) 66 | 67 | err = os.Remove(filePath) 68 | if err != nil { 69 | log.Error("Unable to delete the password file: ", err) 70 | response.SendErrorAndExit( 71 | errors.CodeUnableToDeletePasswordFile, 72 | &map[errors.Field]string{ 73 | errors.FieldMessage: "Unable to delete the password file", 74 | errors.FieldAction: "delete", 75 | errors.FieldError: err.Error(), 76 | errors.FieldFile: request.File, 77 | errors.FieldStoreID: store.ID, 78 | errors.FieldStoreName: store.Name, 79 | errors.FieldStorePath: store.Path, 80 | }, 81 | ) 82 | } 83 | 84 | parentDir := filepath.Dir(filePath) 85 | for { 86 | if parentDir == store.Path { 87 | break 88 | } 89 | 90 | isEmpty, err := helpers.IsDirectoryEmpty(parentDir) 91 | if err != nil { 92 | log.Error("Unable to determine if directory is empty and can be deleted: ", err) 93 | response.SendErrorAndExit( 94 | errors.CodeUnableToDetermineIsDirectoryEmpty, 95 | &map[errors.Field]string{ 96 | errors.FieldMessage: "Unable to determine if directory is empty and can be deleted", 97 | errors.FieldAction: "delete", 98 | errors.FieldError: err.Error(), 99 | errors.FieldDirectory: parentDir, 100 | errors.FieldStoreID: store.ID, 101 | errors.FieldStoreName: store.Name, 102 | errors.FieldStorePath: store.Path, 103 | }, 104 | ) 105 | } 106 | 107 | if !isEmpty { 108 | break 109 | } 110 | 111 | err = os.Remove(parentDir) 112 | if err != nil { 113 | log.Error("Unable to delete the empty directory: ", err) 114 | response.SendErrorAndExit( 115 | errors.CodeUnableToDeleteEmptyDirectory, 116 | &map[errors.Field]string{ 117 | errors.FieldMessage: "Unable to delete the empty directory", 118 | errors.FieldAction: "delete", 119 | errors.FieldError: err.Error(), 120 | errors.FieldDirectory: parentDir, 121 | errors.FieldStoreID: store.ID, 122 | errors.FieldStoreName: store.Name, 123 | errors.FieldStorePath: store.Path, 124 | }, 125 | ) 126 | } 127 | 128 | parentDir = filepath.Dir(parentDir) 129 | } 130 | 131 | response.SendOk(responseData) 132 | } 133 | -------------------------------------------------------------------------------- /request/fetch.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/browserpass/browserpass-native/errors" 8 | "github.com/browserpass/browserpass-native/helpers" 9 | "github.com/browserpass/browserpass-native/response" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func fetchDecryptedContents(request *request) { 14 | responseData := response.MakeFetchResponse() 15 | 16 | if !strings.HasSuffix(request.File, ".gpg") { 17 | log.Errorf("The requested password file '%v' does not have the expected '.gpg' extension", request.File) 18 | response.SendErrorAndExit( 19 | errors.CodeInvalidPasswordFileExtension, 20 | &map[errors.Field]string{ 21 | errors.FieldMessage: "The requested password file does not have the expected '.gpg' extension", 22 | errors.FieldAction: "fetch", 23 | errors.FieldFile: request.File, 24 | }, 25 | ) 26 | } 27 | 28 | store, ok := request.Settings.Stores[request.StoreID] 29 | if !ok { 30 | log.Errorf( 31 | "The password store with ID '%v' is not present in the list of stores '%+v'", 32 | request.StoreID, request.Settings.Stores, 33 | ) 34 | response.SendErrorAndExit( 35 | errors.CodeInvalidPasswordStore, 36 | &map[errors.Field]string{ 37 | errors.FieldMessage: "The password store is not present in the list of stores", 38 | errors.FieldAction: "fetch", 39 | errors.FieldStoreID: request.StoreID, 40 | }, 41 | ) 42 | } 43 | 44 | normalizedStorePath, err := normalizePasswordStorePath(store.Path) 45 | if err != nil { 46 | log.Errorf( 47 | "The password store '%+v' is not accessible at its location: %+v", 48 | store, err, 49 | ) 50 | response.SendErrorAndExit( 51 | errors.CodeInaccessiblePasswordStore, 52 | &map[errors.Field]string{ 53 | errors.FieldMessage: "The password store is not accessible", 54 | errors.FieldAction: "fetch", 55 | errors.FieldError: err.Error(), 56 | errors.FieldStoreID: store.ID, 57 | errors.FieldStoreName: store.Name, 58 | errors.FieldStorePath: store.Path, 59 | }, 60 | ) 61 | } 62 | store.Path = normalizedStorePath 63 | 64 | var gpgPath string 65 | if request.Settings.GpgPath != "" || store.Settings.GpgPath != "" { 66 | if request.Settings.GpgPath != "" { 67 | gpgPath = request.Settings.GpgPath 68 | } else { 69 | gpgPath = store.Settings.GpgPath 70 | } 71 | err = helpers.ValidateGpgBinary(gpgPath) 72 | if err != nil { 73 | log.Errorf( 74 | "The provided gpg binary path '%v' is invalid: %+v", 75 | gpgPath, err, 76 | ) 77 | response.SendErrorAndExit( 78 | errors.CodeInvalidGpgPath, 79 | &map[errors.Field]string{ 80 | errors.FieldMessage: "The provided gpg binary path is invalid", 81 | errors.FieldAction: "fetch", 82 | errors.FieldError: err.Error(), 83 | errors.FieldGpgPath: gpgPath, 84 | }, 85 | ) 86 | } 87 | } else { 88 | gpgPath, err = helpers.DetectGpgBinary() 89 | if err != nil { 90 | log.Error("Unable to detect the location of the gpg binary: ", err) 91 | response.SendErrorAndExit( 92 | errors.CodeUnableToDetectGpgPath, 93 | &map[errors.Field]string{ 94 | errors.FieldMessage: "Unable to detect the location of the gpg binary", 95 | errors.FieldAction: "fetch", 96 | errors.FieldError: err.Error(), 97 | }, 98 | ) 99 | } 100 | } 101 | 102 | responseData.Contents, err = helpers.GpgDecryptFile(filepath.Join(store.Path, request.File), gpgPath) 103 | if err != nil { 104 | log.Errorf( 105 | "Unable to decrypt the password file '%v' in the password store '%+v': %+v", 106 | request.File, store, err, 107 | ) 108 | response.SendErrorAndExit( 109 | errors.CodeUnableToDecryptPasswordFile, 110 | &map[errors.Field]string{ 111 | errors.FieldMessage: "Unable to decrypt the password file", 112 | errors.FieldAction: "fetch", 113 | errors.FieldError: err.Error(), 114 | errors.FieldFile: request.File, 115 | errors.FieldStoreID: store.ID, 116 | errors.FieldStoreName: store.Name, 117 | errors.FieldStorePath: store.Path, 118 | }, 119 | ) 120 | } 121 | 122 | response.SendOk(responseData) 123 | } 124 | -------------------------------------------------------------------------------- /request/list.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "path/filepath" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/browserpass/browserpass-native/errors" 9 | "github.com/browserpass/browserpass-native/response" 10 | "github.com/mattn/go-zglob" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func listFiles(request *request) { 15 | responseData := response.MakeListResponse() 16 | 17 | for _, store := range request.Settings.Stores { 18 | normalizedStorePath, err := normalizePasswordStorePath(store.Path) 19 | if err != nil { 20 | log.Errorf( 21 | "The password store '%+v' is not accessible at its location: %+v", 22 | store, err, 23 | ) 24 | response.SendErrorAndExit( 25 | errors.CodeInaccessiblePasswordStore, 26 | &map[errors.Field]string{ 27 | errors.FieldMessage: "The password store is not accessible", 28 | errors.FieldAction: "list", 29 | errors.FieldError: err.Error(), 30 | errors.FieldStoreID: store.ID, 31 | errors.FieldStoreName: store.Name, 32 | errors.FieldStorePath: store.Path, 33 | }, 34 | ) 35 | } 36 | 37 | store.Path = normalizedStorePath 38 | 39 | files, err := zglob.GlobFollowSymlinks(filepath.Join(store.Path, "/**/*.gpg")) 40 | if err != nil { 41 | log.Errorf( 42 | "Unable to list the files in the password store '%+v' at its location: %+v", 43 | store, err, 44 | ) 45 | response.SendErrorAndExit( 46 | errors.CodeUnableToListFilesInPasswordStore, 47 | &map[errors.Field]string{ 48 | errors.FieldMessage: "Unable to list the files in the password store", 49 | errors.FieldAction: "list", 50 | errors.FieldError: err.Error(), 51 | errors.FieldStoreID: store.ID, 52 | errors.FieldStoreName: store.Name, 53 | errors.FieldStorePath: store.Path, 54 | }, 55 | ) 56 | } 57 | 58 | for i, file := range files { 59 | relativePath, err := filepath.Rel(store.Path, file) 60 | if err != nil { 61 | log.Errorf( 62 | "Unable to determine the relative path for a file '%v' in the password store '%+v': %+v", 63 | file, store, err, 64 | ) 65 | response.SendErrorAndExit( 66 | errors.CodeUnableToDetermineRelativeFilePathInPasswordStore, 67 | &map[errors.Field]string{ 68 | errors.FieldMessage: "Unable to determine the relative path for a file in the password store", 69 | errors.FieldAction: "list", 70 | errors.FieldError: err.Error(), 71 | errors.FieldFile: file, 72 | errors.FieldStoreID: store.ID, 73 | errors.FieldStoreName: store.Name, 74 | errors.FieldStorePath: store.Path, 75 | }, 76 | ) 77 | } 78 | files[i] = strings.Replace(relativePath, "\\", "/", -1) // normalize Windows paths 79 | } 80 | 81 | sort.Strings(files) 82 | responseData.Files[store.ID] = files 83 | } 84 | 85 | response.SendOk(responseData) 86 | } 87 | -------------------------------------------------------------------------------- /request/process.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/json" 6 | "io" 7 | "os" 8 | 9 | "github.com/browserpass/browserpass-native/errors" 10 | "github.com/browserpass/browserpass-native/response" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type StoreSettings struct { 15 | GpgPath string `json:"gpgPath"` 16 | } 17 | 18 | type store struct { 19 | ID string `json:"id"` 20 | Name string `json:"name"` 21 | Path string `json:"path"` 22 | Settings StoreSettings `json:"settings"` 23 | } 24 | 25 | type settings struct { 26 | GpgPath string `json:"gpgPath"` 27 | Stores map[string]store `json:"stores"` 28 | } 29 | 30 | type request struct { 31 | Action string `json:"action"` 32 | Settings settings `json:"settings"` 33 | File string `json:"file"` 34 | Contents string `json:"contents"` 35 | StoreID string `json:"storeId"` 36 | EchoResponse interface{} `json:"echoResponse"` 37 | } 38 | 39 | // Process handles browser request 40 | func Process() { 41 | requestLength, err := parseRequestLength(os.Stdin) 42 | if err != nil { 43 | log.Error("Unable to parse the length of the browser request: ", err) 44 | response.SendErrorAndExit( 45 | errors.CodeParseRequestLength, 46 | &map[errors.Field]string{ 47 | errors.FieldMessage: "Unable to parse the length of the browser request", 48 | errors.FieldError: err.Error(), 49 | }, 50 | ) 51 | } 52 | 53 | request, err := parseRequest(requestLength, os.Stdin) 54 | if err != nil { 55 | log.Error("Unable to parse the browser request: ", err) 56 | response.SendErrorAndExit( 57 | errors.CodeParseRequest, 58 | &map[errors.Field]string{ 59 | errors.FieldMessage: "Unable to parse the browser request", 60 | errors.FieldError: err.Error(), 61 | }, 62 | ) 63 | } 64 | 65 | switch request.Action { 66 | case "configure": 67 | configure(request) 68 | case "list": 69 | listFiles(request) 70 | case "tree": 71 | listDirectories(request) 72 | case "fetch": 73 | fetchDecryptedContents(request) 74 | case "save": 75 | saveEncryptedContents(request) 76 | case "delete": 77 | deleteFile(request) 78 | case "echo": 79 | response.SendRaw(request.EchoResponse) 80 | default: 81 | log.Errorf("Received a browser request with an unknown action: %+v", request) 82 | response.SendErrorAndExit( 83 | errors.CodeInvalidRequestAction, 84 | &map[errors.Field]string{ 85 | errors.FieldMessage: "Invalid request action", 86 | errors.FieldAction: request.Action, 87 | }, 88 | ) 89 | } 90 | } 91 | 92 | // Request length is the first 4 bytes in LittleEndian encoding 93 | func parseRequestLength(input io.Reader) (uint32, error) { 94 | var length uint32 95 | if err := binary.Read(input, binary.LittleEndian, &length); err != nil { 96 | return 0, err 97 | } 98 | return length, nil 99 | } 100 | 101 | // Request is a json with a predefined structure 102 | func parseRequest(messageLength uint32, input io.Reader) (*request, error) { 103 | var parsed request 104 | reader := &io.LimitedReader{R: input, N: int64(messageLength)} 105 | if err := json.NewDecoder(reader).Decode(&parsed); err != nil { 106 | return nil, err 107 | } 108 | return &parsed, nil 109 | } 110 | -------------------------------------------------------------------------------- /request/process_test.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func Test_ParseRequestLength_ConsidersFirstFourBytes(t *testing.T) { 12 | // Arrange 13 | expected := uint32(201334791) // 0x0c002007 14 | 15 | // The first 4 bytes represent the value of `expected` in Little Endian format, 16 | // the rest should be completely ignored during the parsing. 17 | input := bytes.NewReader([]byte{7, 32, 0, 12, 13, 13, 13}) 18 | 19 | // Act 20 | actual, err := parseRequestLength(input) 21 | 22 | // Assert 23 | if err != nil { 24 | t.Fatalf("Error parsing request length: %v", err) 25 | } 26 | 27 | if expected != actual { 28 | t.Fatalf("The actual length '%v' does not match the expected value of '%v'", actual, expected) 29 | } 30 | } 31 | 32 | func Test_ParseRequestLength_ConnectionAborted(t *testing.T) { 33 | // Arrange 34 | expectedErr := io.ErrUnexpectedEOF 35 | input := bytes.NewReader([]byte{7}) 36 | 37 | // Act 38 | _, err := parseRequestLength(input) 39 | 40 | // Assert 41 | if expectedErr != err { 42 | t.Fatalf("The expected error is '%v', but got '%v'", expectedErr, err) 43 | } 44 | } 45 | 46 | func Test_ParseRequest_CanParse(t *testing.T) { 47 | // Arrange 48 | expected := &request{ 49 | Action: "list", 50 | Settings: settings{ 51 | Stores: map[string]store{ 52 | "id1": store{ 53 | ID: "id1", 54 | Name: "default", 55 | Path: "~/.password-store", 56 | }, 57 | }, 58 | }, 59 | } 60 | 61 | jsonBytes, err := json.Marshal(expected) 62 | if err != nil { 63 | t.Fatal("Unable to marshal the expected object to initialize the test") 64 | } 65 | 66 | inputLength := uint32(len(jsonBytes)) 67 | input := bytes.NewReader(jsonBytes) 68 | 69 | // Act 70 | actual, err := parseRequest(inputLength, input) 71 | 72 | // Assert 73 | if err != nil { 74 | t.Fatalf("Error parsing request: %v", err) 75 | } 76 | 77 | if !reflect.DeepEqual(expected, actual) { 78 | t.Fatalf("The request was parsed incorrectly.\nExpected: %+v\nActual: %+v", expected, actual) 79 | } 80 | } 81 | 82 | func Test_ParseRequest_WrongLength(t *testing.T) { 83 | // Arrange 84 | expectedErr := io.ErrUnexpectedEOF 85 | 86 | jsonBytes, err := json.Marshal(&request{Action: "list"}) 87 | if err != nil { 88 | t.Fatal("Unable to marshal the expected object to initialize the test") 89 | } 90 | 91 | wrongInputLength := uint32(len(jsonBytes)) - 1 92 | input := bytes.NewReader(jsonBytes) 93 | 94 | // Act 95 | _, err = parseRequest(wrongInputLength, input) 96 | 97 | // Assert 98 | if expectedErr != err { 99 | t.Fatalf("The expected error is '%v', but got '%v'", expectedErr, err) 100 | } 101 | } 102 | 103 | func Test_ParseRequest_InvalidJson(t *testing.T) { 104 | // Arrange 105 | jsonBytes := []byte("not_a_json") 106 | inputLength := uint32(len(jsonBytes)) 107 | input := bytes.NewReader(jsonBytes) 108 | 109 | // Act 110 | _, err := parseRequest(inputLength, input) 111 | 112 | // Assert 113 | if err == nil { 114 | t.Fatalf("Expected a parsing error, but didn't get it") 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /request/save.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "path/filepath" 5 | "strings" 6 | 7 | "github.com/browserpass/browserpass-native/errors" 8 | "github.com/browserpass/browserpass-native/helpers" 9 | "github.com/browserpass/browserpass-native/response" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | func saveEncryptedContents(request *request) { 14 | responseData := response.MakeSaveResponse() 15 | 16 | if !strings.HasSuffix(request.File, ".gpg") { 17 | log.Errorf("The requested password file '%v' does not have the expected '.gpg' extension", request.File) 18 | response.SendErrorAndExit( 19 | errors.CodeInvalidPasswordFileExtension, 20 | &map[errors.Field]string{ 21 | errors.FieldMessage: "The requested password file does not have the expected '.gpg' extension", 22 | errors.FieldAction: "save", 23 | errors.FieldFile: request.File, 24 | }, 25 | ) 26 | } 27 | 28 | if request.Contents == "" { 29 | log.Errorf("The entry contents is missing") 30 | response.SendErrorAndExit( 31 | errors.CodeEmptyContents, 32 | &map[errors.Field]string{ 33 | errors.FieldMessage: "The entry contents is missing", 34 | errors.FieldAction: "save", 35 | }, 36 | ) 37 | } 38 | 39 | store, ok := request.Settings.Stores[request.StoreID] 40 | if !ok { 41 | log.Errorf( 42 | "The password store with ID '%v' is not present in the list of stores '%+v'", 43 | request.StoreID, request.Settings.Stores, 44 | ) 45 | response.SendErrorAndExit( 46 | errors.CodeInvalidPasswordStore, 47 | &map[errors.Field]string{ 48 | errors.FieldMessage: "The password store is not present in the list of stores", 49 | errors.FieldAction: "save", 50 | errors.FieldStoreID: request.StoreID, 51 | }, 52 | ) 53 | } 54 | 55 | normalizedStorePath, err := normalizePasswordStorePath(store.Path) 56 | if err != nil { 57 | log.Errorf( 58 | "The password store '%+v' is not accessible at its location: %+v", 59 | store, err, 60 | ) 61 | response.SendErrorAndExit( 62 | errors.CodeInaccessiblePasswordStore, 63 | &map[errors.Field]string{ 64 | errors.FieldMessage: "The password store is not accessible", 65 | errors.FieldAction: "save", 66 | errors.FieldError: err.Error(), 67 | errors.FieldStoreID: store.ID, 68 | errors.FieldStoreName: store.Name, 69 | errors.FieldStorePath: store.Path, 70 | }, 71 | ) 72 | } 73 | store.Path = normalizedStorePath 74 | 75 | var gpgPath string 76 | if request.Settings.GpgPath != "" || store.Settings.GpgPath != "" { 77 | if request.Settings.GpgPath != "" { 78 | gpgPath = request.Settings.GpgPath 79 | } else { 80 | gpgPath = store.Settings.GpgPath 81 | } 82 | err = helpers.ValidateGpgBinary(gpgPath) 83 | if err != nil { 84 | log.Errorf( 85 | "The provided gpg binary path '%v' is invalid: %+v", 86 | gpgPath, err, 87 | ) 88 | response.SendErrorAndExit( 89 | errors.CodeInvalidGpgPath, 90 | &map[errors.Field]string{ 91 | errors.FieldMessage: "The provided gpg binary path is invalid", 92 | errors.FieldAction: "save", 93 | errors.FieldError: err.Error(), 94 | errors.FieldGpgPath: gpgPath, 95 | }, 96 | ) 97 | } 98 | } else { 99 | gpgPath, err = helpers.DetectGpgBinary() 100 | if err != nil { 101 | log.Error("Unable to detect the location of the gpg binary: ", err) 102 | response.SendErrorAndExit( 103 | errors.CodeUnableToDetectGpgPath, 104 | &map[errors.Field]string{ 105 | errors.FieldMessage: "Unable to detect the location of the gpg binary", 106 | errors.FieldAction: "save", 107 | errors.FieldError: err.Error(), 108 | }, 109 | ) 110 | } 111 | } 112 | 113 | filePath := filepath.Join(store.Path, request.File) 114 | 115 | recipients, err := helpers.DetectGpgRecipients(filePath) 116 | if err != nil { 117 | log.Error("Unable to determine recipients for the gpg encryption: ", err) 118 | response.SendErrorAndExit( 119 | errors.CodeUnableToDetermineGpgRecipients, 120 | &map[errors.Field]string{ 121 | errors.FieldMessage: "Unable to determine recipients for the gpg encryption", 122 | errors.FieldAction: "save", 123 | errors.FieldError: err.Error(), 124 | errors.FieldFile: request.File, 125 | errors.FieldStoreID: store.ID, 126 | errors.FieldStoreName: store.Name, 127 | errors.FieldStorePath: store.Path, 128 | }, 129 | ) 130 | } 131 | 132 | err = helpers.GpgEncryptFile(filePath, request.Contents, recipients, gpgPath) 133 | if err != nil { 134 | log.Errorf( 135 | "Unable to encrypt the password file '%v' in the password store '%+v': %+v", 136 | request.File, store, err, 137 | ) 138 | response.SendErrorAndExit( 139 | errors.CodeUnableToEncryptPasswordFile, 140 | &map[errors.Field]string{ 141 | errors.FieldMessage: "Unable to encrypt the password file", 142 | errors.FieldAction: "save", 143 | errors.FieldError: err.Error(), 144 | errors.FieldFile: request.File, 145 | errors.FieldStoreID: store.ID, 146 | errors.FieldStoreName: store.Name, 147 | errors.FieldStorePath: store.Path, 148 | }, 149 | ) 150 | } 151 | 152 | response.SendOk(responseData) 153 | } 154 | -------------------------------------------------------------------------------- /request/tree.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "sort" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/browserpass/browserpass-native/errors" 11 | "github.com/browserpass/browserpass-native/response" 12 | "github.com/mattn/go-zglob/fastwalk" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | func listDirectories(request *request) { 17 | responseData := response.MakeTreeResponse() 18 | 19 | for _, store := range request.Settings.Stores { 20 | normalizedStorePath, err := normalizePasswordStorePath(store.Path) 21 | if err != nil { 22 | log.Errorf( 23 | "The password store '%+v' is not accessible at its location: %+v", 24 | store, err, 25 | ) 26 | response.SendErrorAndExit( 27 | errors.CodeInaccessiblePasswordStore, 28 | &map[errors.Field]string{ 29 | errors.FieldMessage: "The password store is not accessible", 30 | errors.FieldAction: "tree", 31 | errors.FieldError: err.Error(), 32 | errors.FieldStoreID: store.ID, 33 | errors.FieldStoreName: store.Name, 34 | errors.FieldStorePath: store.Path, 35 | }, 36 | ) 37 | } 38 | 39 | store.Path = normalizedStorePath 40 | 41 | var mu sync.Mutex 42 | directories := []string{} 43 | err = fastwalk.FastWalk(store.Path, func(path string, typ os.FileMode) error { 44 | if typ == os.ModeSymlink { 45 | followedPath, err := filepath.EvalSymlinks(path) 46 | if err == nil { 47 | fi, err := os.Lstat(followedPath) 48 | if err == nil && fi.IsDir() { 49 | return fastwalk.TraverseLink 50 | } 51 | } 52 | } 53 | 54 | if typ.IsDir() && path != store.Path { 55 | if filepath.Base(path) == ".git" { 56 | return filepath.SkipDir 57 | } 58 | mu.Lock() 59 | directories = append(directories, path) 60 | mu.Unlock() 61 | } 62 | 63 | return nil 64 | }) 65 | 66 | if err != nil { 67 | log.Errorf( 68 | "Unable to list the directory tree in the password store '%+v' at its location: %+v", 69 | store, err, 70 | ) 71 | response.SendErrorAndExit( 72 | errors.CodeUnableToListDirectoriesInPasswordStore, 73 | &map[errors.Field]string{ 74 | errors.FieldMessage: "Unable to list the directory tree in the password store", 75 | errors.FieldAction: "tree", 76 | errors.FieldError: err.Error(), 77 | errors.FieldStoreID: store.ID, 78 | errors.FieldStoreName: store.Name, 79 | errors.FieldStorePath: store.Path, 80 | }, 81 | ) 82 | } 83 | 84 | for i, directory := range directories { 85 | relativePath, err := filepath.Rel(store.Path, directory) 86 | if err != nil { 87 | log.Errorf( 88 | "Unable to determine the relative path for a file '%v' in the password store '%+v': %+v", 89 | directory, store, err, 90 | ) 91 | response.SendErrorAndExit( 92 | errors.CodeUnableToDetermineRelativeDirectoryPathInPasswordStore, 93 | &map[errors.Field]string{ 94 | errors.FieldMessage: "Unable to determine the relative path for a directory in the password store", 95 | errors.FieldAction: "tree", 96 | errors.FieldError: err.Error(), 97 | errors.FieldDirectory: directory, 98 | errors.FieldStoreID: store.ID, 99 | errors.FieldStoreName: store.Name, 100 | errors.FieldStorePath: store.Path, 101 | }, 102 | ) 103 | } 104 | directories[i] = strings.Replace(relativePath, "\\", "/", -1) // normalize Windows paths 105 | } 106 | 107 | sort.Strings(directories) 108 | responseData.Directories[store.ID] = directories 109 | } 110 | 111 | response.SendOk(responseData) 112 | } 113 | -------------------------------------------------------------------------------- /response/response.go: -------------------------------------------------------------------------------- 1 | package response 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/json" 7 | "os" 8 | 9 | "github.com/browserpass/browserpass-native/errors" 10 | "github.com/browserpass/browserpass-native/version" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | type okResponse struct { 15 | Status string `json:"status"` 16 | Version int `json:"version"` 17 | Data interface{} `json:"data"` 18 | } 19 | 20 | type errorResponse struct { 21 | Status string `json:"status"` 22 | Code errors.Code `json:"code"` 23 | Version int `json:"version"` 24 | Params interface{} `json:"params"` 25 | } 26 | 27 | // ConfigureResponse a response format for the "configure" request 28 | type ConfigureResponse struct { 29 | DefaultStore struct { 30 | Path string `json:"path"` 31 | Settings string `json:"settings"` 32 | } `json:"defaultStore"` 33 | StoreSettings map[string]string `json:"storeSettings"` 34 | } 35 | 36 | // MakeConfigureResponse initializes an empty configure response 37 | func MakeConfigureResponse() *ConfigureResponse { 38 | return &ConfigureResponse{ 39 | StoreSettings: make(map[string]string), 40 | } 41 | } 42 | 43 | // ListResponse a response format for the "list" request 44 | type ListResponse struct { 45 | Files map[string][]string `json:"files"` 46 | } 47 | 48 | // MakeListResponse initializes an empty list response 49 | func MakeListResponse() *ListResponse { 50 | return &ListResponse{ 51 | Files: make(map[string][]string), 52 | } 53 | } 54 | 55 | // TreeResponse a response format for the "tree" request 56 | type TreeResponse struct { 57 | Directories map[string][]string `json:"directories"` 58 | } 59 | 60 | // MakeTreeResponse initializes an empty tree response 61 | func MakeTreeResponse() *TreeResponse { 62 | return &TreeResponse{ 63 | Directories: make(map[string][]string), 64 | } 65 | } 66 | 67 | // FetchResponse a response format for the "fetch" request 68 | type FetchResponse struct { 69 | Contents string `json:"contents"` 70 | } 71 | 72 | // MakeFetchResponse initializes an empty fetch response 73 | func MakeFetchResponse() *FetchResponse { 74 | return &FetchResponse{} 75 | } 76 | 77 | // SaveResponse a response format for the "save" request 78 | type SaveResponse struct { 79 | } 80 | 81 | // MakeSaveResponse initializes an empty save response 82 | func MakeSaveResponse() *SaveResponse { 83 | return &SaveResponse{} 84 | } 85 | 86 | // DeleteResponse a response format for the "delete" request 87 | type DeleteResponse struct { 88 | } 89 | 90 | // MakeDeleteResponse initializes an empty delete response 91 | func MakeDeleteResponse() *DeleteResponse { 92 | return &DeleteResponse{} 93 | } 94 | 95 | // SendOk sends a success response to the browser extension in the predefined json format 96 | func SendOk(data interface{}) { 97 | SendRaw(&okResponse{ 98 | Status: "ok", 99 | Version: version.Code, 100 | Data: data, 101 | }) 102 | } 103 | 104 | // SendErrorAndExit sends an error response to the browser extension in the predefined json format and exits with the specified exit code 105 | func SendErrorAndExit(errorCode errors.Code, params *map[errors.Field]string) { 106 | SendRaw(&errorResponse{ 107 | Status: "error", 108 | Code: errorCode, 109 | Version: version.Code, 110 | Params: params, 111 | }) 112 | 113 | errors.ExitWithCode(errorCode) 114 | } 115 | 116 | // SendRaw sends a raw data to the browser extension 117 | func SendRaw(response interface{}) { 118 | var bytesBuffer bytes.Buffer 119 | if err := json.NewEncoder(&bytesBuffer).Encode(response); err != nil { 120 | log.Fatal("Unable to encode response for sending: ", err) 121 | } 122 | 123 | if err := binary.Write(os.Stdout, binary.LittleEndian, uint32(bytesBuffer.Len())); err != nil { 124 | log.Fatal("Unable to send the length of the response: ", err) 125 | } 126 | if _, err := bytesBuffer.WriteTo(os.Stdout); err != nil { 127 | log.Fatal("Unable to send the response: ", err) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "fmt" 4 | 5 | const major = 3 6 | const minor = 1 7 | const patch = 0 8 | 9 | // Code version as integer 10 | const Code = major*1000000 + minor*1000 + patch 11 | 12 | // String version as string 13 | func String() string { 14 | return fmt.Sprintf("%d.%d.%d", major, minor, patch) 15 | } 16 | -------------------------------------------------------------------------------- /windows-setup.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | --------------------------------------------------------------------------------