Port bw-bio-handler for browser biometrics
This commit is contained in:
parent
fbf843d090
commit
350124c7e6
15 changed files with 596 additions and 6 deletions
36
agent/actions/browserbiometrics.go
Normal file
36
agent/actions/browserbiometrics.go
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
package actions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/quexten/goldwarden/agent/config"
|
||||||
|
"github.com/quexten/goldwarden/agent/sockets"
|
||||||
|
"github.com/quexten/goldwarden/agent/systemauth"
|
||||||
|
"github.com/quexten/goldwarden/agent/vault"
|
||||||
|
"github.com/quexten/goldwarden/ipc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handleGetBiometricsKey(request ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, ctx sockets.CallingContext) (response interface{}, err error) {
|
||||||
|
if approved, err := systemauth.GetApproval("Approve Credential Access", fmt.Sprintf("%s on %s>%s>%s is trying to access your vault encryption key for browser biometric unlock.", ctx.UserName, ctx.GrandParentProcessName, ctx.ParentProcessName, ctx.ProcessName)); err != nil || !approved {
|
||||||
|
response, err = ipc.IPCMessageFromPayload(ipc.ActionResponse{
|
||||||
|
Success: false,
|
||||||
|
Message: "not approved",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
masterKey, err := cfg.GetMasterKey()
|
||||||
|
masterKeyB64 := base64.StdEncoding.EncodeToString(masterKey)
|
||||||
|
response, err = ipc.IPCMessageFromPayload(ipc.GetBiometricsKeyResponse{
|
||||||
|
Key: masterKeyB64,
|
||||||
|
})
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
AgentActionsRegistry.Register(ipc.IPCMessageTypeGetBiometricsKeyRequest, ensureEverything(systemauth.BrowserBiometrics, handleGetBiometricsKey))
|
||||||
|
}
|
||||||
|
|
@ -71,6 +71,7 @@ func handleLogin(msg ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, cal
|
||||||
|
|
||||||
cfg.SetUserSymmetricKey(vault.Keyring.AccountKey.Bytes())
|
cfg.SetUserSymmetricKey(vault.Keyring.AccountKey.Bytes())
|
||||||
cfg.SetMasterPasswordHash([]byte(masterpasswordHash))
|
cfg.SetMasterPasswordHash([]byte(masterpasswordHash))
|
||||||
|
cfg.SetMasterKey([]byte(masterKey.GetBytes()))
|
||||||
protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(vault.Keyring.AccountKey.Bytes())
|
protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(vault.Keyring.AccountKey.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var payload = ipc.ActionResponse{
|
var payload = ipc.ActionResponse{
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ func Sync(ctx context.Context, config *config.Config) (models.SyncData, error) {
|
||||||
return sync, nil
|
return sync, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SyncToVault(ctx context.Context, vault *vault.Vault, config *config.Config, masterKey *crypto.SymmetricEncryptionKey) error {
|
func SyncToVault(ctx context.Context, vault *vault.Vault, config *config.Config, userSymmetricKey *crypto.SymmetricEncryptionKey) error {
|
||||||
log.Info("Performing full sync...")
|
log.Info("Performing full sync...")
|
||||||
|
|
||||||
sync, err := Sync(ctx, config)
|
sync, err := Sync(ctx, config)
|
||||||
|
|
@ -29,13 +29,13 @@ func SyncToVault(ctx context.Context, vault *vault.Vault, config *config.Config,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if masterKey != nil {
|
if userSymmetricKey != nil {
|
||||||
var orgKeys map[string]string = make(map[string]string)
|
var orgKeys map[string]string = make(map[string]string)
|
||||||
for _, org := range sync.Profile.Organizations {
|
for _, org := range sync.Profile.Organizations {
|
||||||
orgId := org.Id.String()
|
orgId := org.Id.String()
|
||||||
orgKeys[orgId] = org.Key
|
orgKeys[orgId] = org.Key
|
||||||
}
|
}
|
||||||
crypto.InitKeyringFromUserSymmetricKey(vault.Keyring, *masterKey, sync.Profile.PrivateKey, orgKeys)
|
crypto.InitKeyringFromUserSymmetricKey(vault.Keyring, *userSymmetricKey, sync.Profile.PrivateKey, orgKeys)
|
||||||
}
|
}
|
||||||
|
|
||||||
vault.Clear()
|
vault.Clear()
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ type ConfigFile struct {
|
||||||
EncryptedToken string
|
EncryptedToken string
|
||||||
EncryptedUserSymmetricKey string
|
EncryptedUserSymmetricKey string
|
||||||
EncryptedMasterPasswordHash string
|
EncryptedMasterPasswordHash string
|
||||||
|
EncryptedMasterKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginToken struct {
|
type LoginToken struct {
|
||||||
|
|
@ -63,6 +64,7 @@ func DefaultConfig() Config {
|
||||||
EncryptedToken: "",
|
EncryptedToken: "",
|
||||||
EncryptedUserSymmetricKey: "",
|
EncryptedUserSymmetricKey: "",
|
||||||
EncryptedMasterPasswordHash: "",
|
EncryptedMasterPasswordHash: "",
|
||||||
|
EncryptedMasterKey: "",
|
||||||
},
|
},
|
||||||
sync.Mutex{},
|
sync.Mutex{},
|
||||||
}
|
}
|
||||||
|
|
@ -111,6 +113,7 @@ func (c *Config) Purge() {
|
||||||
c.ConfigFile.EncryptedToken = ""
|
c.ConfigFile.EncryptedToken = ""
|
||||||
c.ConfigFile.EncryptedUserSymmetricKey = ""
|
c.ConfigFile.EncryptedUserSymmetricKey = ""
|
||||||
c.ConfigFile.ConfigKeyHash = ""
|
c.ConfigFile.ConfigKeyHash = ""
|
||||||
|
c.ConfigFile.EncryptedMasterKey = ""
|
||||||
c.key = memguard.NewBuffer(32)
|
c.key = memguard.NewBuffer(32)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -131,6 +134,7 @@ func (c *Config) UpdatePin(password string, write bool) {
|
||||||
plaintextToken, err1 := c.decryptString(c.ConfigFile.EncryptedToken)
|
plaintextToken, err1 := c.decryptString(c.ConfigFile.EncryptedToken)
|
||||||
plaintextUserSymmetricKey, err3 := c.decryptString(c.ConfigFile.EncryptedUserSymmetricKey)
|
plaintextUserSymmetricKey, err3 := c.decryptString(c.ConfigFile.EncryptedUserSymmetricKey)
|
||||||
plaintextEncryptedMasterPasswordHash, err4 := c.decryptString(c.ConfigFile.EncryptedMasterPasswordHash)
|
plaintextEncryptedMasterPasswordHash, err4 := c.decryptString(c.ConfigFile.EncryptedMasterPasswordHash)
|
||||||
|
plaintextMasterKey, err5 := c.decryptString(c.ConfigFile.EncryptedMasterKey)
|
||||||
|
|
||||||
c.key = memguard.NewBufferFromBytes(newKey)
|
c.key = memguard.NewBufferFromBytes(newKey)
|
||||||
|
|
||||||
|
|
@ -143,6 +147,9 @@ func (c *Config) UpdatePin(password string, write bool) {
|
||||||
if err4 == nil {
|
if err4 == nil {
|
||||||
c.ConfigFile.EncryptedMasterPasswordHash, err4 = c.encryptString(plaintextEncryptedMasterPasswordHash)
|
c.ConfigFile.EncryptedMasterPasswordHash, err4 = c.encryptString(plaintextEncryptedMasterPasswordHash)
|
||||||
}
|
}
|
||||||
|
if err5 == nil {
|
||||||
|
c.ConfigFile.EncryptedMasterKey, err5 = c.encryptString(plaintextMasterKey)
|
||||||
|
}
|
||||||
|
|
||||||
if write {
|
if write {
|
||||||
c.WriteConfig()
|
c.WriteConfig()
|
||||||
|
|
@ -240,6 +247,32 @@ func (c *Config) SetMasterPasswordHash(hash []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Config) GetMasterKey() ([]byte, error) {
|
||||||
|
if c.IsLocked() {
|
||||||
|
return []byte{}, errors.New("config is locked")
|
||||||
|
}
|
||||||
|
decrypted, err := c.decryptString(c.ConfigFile.EncryptedMasterKey)
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
return []byte(decrypted), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) SetMasterKey(key []byte) error {
|
||||||
|
if c.IsLocked() {
|
||||||
|
return errors.New("config is locked")
|
||||||
|
}
|
||||||
|
encryptedKey, err := c.encryptString(string(key))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// c.mu.Lock()
|
||||||
|
c.ConfigFile.EncryptedMasterKey = encryptedKey
|
||||||
|
// c.mu.Unlock()
|
||||||
|
c.WriteConfig()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Config) encryptString(data string) (string, error) {
|
func (c *Config) encryptString(data string) (string, error) {
|
||||||
if c.IsLocked() {
|
if c.IsLocked() {
|
||||||
return "", errors.New("config is locked")
|
return "", errors.New("config is locked")
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,11 @@ var log = llamalog.NewLogger("Goldwarden", "Systemauth")
|
||||||
type Approval string
|
type Approval string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
AccessCredential Approval = "com.quexten.goldwarden.accesscredential"
|
AccessCredential Approval = "com.quexten.goldwarden.accesscredential"
|
||||||
ChangePin Approval = "com.quexten.goldwarden.changepin"
|
ChangePin Approval = "com.quexten.goldwarden.changepin"
|
||||||
SSHKey Approval = "com.quexten.goldwarden.usesshkey"
|
SSHKey Approval = "com.quexten.goldwarden.usesshkey"
|
||||||
|
ModifyVault Approval = "com.quexten.goldwarden.modifyvault"
|
||||||
|
BrowserBiometrics Approval = "com.quexten.goldwarden.browserbiometrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (a Approval) String() string {
|
func (a Approval) String() string {
|
||||||
|
|
|
||||||
68
browserbiometrics/communication.go
Normal file
68
browserbiometrics/communication.go
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
package browserbiometrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/quexten/goldwarden/browserbiometrics/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
const bufferSize = 8192 * 8
|
||||||
|
|
||||||
|
var nativeEndian binary.ByteOrder
|
||||||
|
|
||||||
|
func setupCommunication() {
|
||||||
|
// determine native endianess
|
||||||
|
var one int16 = 1
|
||||||
|
b := (*byte)(unsafe.Pointer(&one))
|
||||||
|
if *b == 0 {
|
||||||
|
nativeEndian = binary.BigEndian
|
||||||
|
} else {
|
||||||
|
nativeEndian = binary.LittleEndian
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dataToBytes(msg SendMessage) []byte {
|
||||||
|
byteMsg, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf("Unable to marshal OutgoingMessage struct to slice of bytes: " + err.Error())
|
||||||
|
}
|
||||||
|
return byteMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeMessageLength(msg []byte) {
|
||||||
|
err := binary.Write(os.Stdout, nativeEndian, uint32(len(msg)))
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf("Unable to write message length to Stdout: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readMessageLength(msg []byte) int {
|
||||||
|
var length uint32
|
||||||
|
buf := bytes.NewBuffer(msg)
|
||||||
|
err := binary.Read(buf, nativeEndian, &length)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf("Unable to read bytes representing message length:" + err.Error())
|
||||||
|
}
|
||||||
|
return int(length)
|
||||||
|
}
|
||||||
|
|
||||||
|
func send(msg SendMessage) {
|
||||||
|
byteMsg := dataToBytes(msg)
|
||||||
|
logging.Debugf("Sending message: " + string(byteMsg))
|
||||||
|
writeMessageLength(byteMsg)
|
||||||
|
|
||||||
|
var msgBuf bytes.Buffer
|
||||||
|
_, err := msgBuf.Write(byteMsg)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = msgBuf.WriteTo(os.Stdout)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf(err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
118
browserbiometrics/crypto.go
Normal file
118
browserbiometrics/crypto.go
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
package browserbiometrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidBlockSize = errors.New("invalid blocksize")
|
||||||
|
ErrInvalidPKCS7Data = errors.New("invalid PKCS7 data (empty or not padded)")
|
||||||
|
ErrInvalidPKCS7Padding = errors.New("invalid padding on input")
|
||||||
|
)
|
||||||
|
|
||||||
|
func pkcs7Pad(b []byte, blocksize int) ([]byte, error) {
|
||||||
|
if blocksize <= 0 {
|
||||||
|
return nil, ErrInvalidBlockSize
|
||||||
|
}
|
||||||
|
if b == nil || len(b) == 0 {
|
||||||
|
return nil, ErrInvalidPKCS7Data
|
||||||
|
}
|
||||||
|
n := blocksize - (len(b) % blocksize)
|
||||||
|
pb := make([]byte, len(b)+n)
|
||||||
|
copy(pb, b)
|
||||||
|
copy(pb[len(b):], bytes.Repeat([]byte{byte(n)}, n))
|
||||||
|
return pb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func pkcs7Unpad(b []byte, blocksize int) ([]byte, error) {
|
||||||
|
if blocksize <= 0 {
|
||||||
|
return nil, ErrInvalidBlockSize
|
||||||
|
}
|
||||||
|
if b == nil || len(b) == 0 {
|
||||||
|
return nil, ErrInvalidPKCS7Data
|
||||||
|
}
|
||||||
|
if len(b)%blocksize != 0 {
|
||||||
|
return nil, ErrInvalidPKCS7Padding
|
||||||
|
}
|
||||||
|
c := b[len(b)-1]
|
||||||
|
n := int(c)
|
||||||
|
if n == 0 || n > len(b) {
|
||||||
|
return nil, ErrInvalidPKCS7Padding
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if b[len(b)-n+i] != c {
|
||||||
|
return nil, ErrInvalidPKCS7Padding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b[:len(b)-n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptStringSymmetric(key []byte, ivb64 string, data string) string {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
iv, _ := base64.StdEncoding.DecodeString(ivb64)
|
||||||
|
ciphertext, _ := base64.StdEncoding.DecodeString(data)
|
||||||
|
bm := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
bm.CryptBlocks(ciphertext, ciphertext)
|
||||||
|
ciphertext, _ = pkcs7Unpad(ciphertext, aes.BlockSize)
|
||||||
|
|
||||||
|
return string(ciphertext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encryptStringSymmetric(key []byte, data []byte) EncryptedString {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
data, _ = pkcs7Pad(data, block.BlockSize())
|
||||||
|
ciphertext := make([]byte, aes.BlockSize+len(data))
|
||||||
|
iv := ciphertext[:aes.BlockSize]
|
||||||
|
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bm := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
bm.CryptBlocks(ciphertext[aes.BlockSize:], data)
|
||||||
|
|
||||||
|
return EncryptedString{
|
||||||
|
IV: base64.StdEncoding.EncodeToString(ciphertext[:aes.BlockSize]),
|
||||||
|
Data: base64.StdEncoding.EncodeToString(ciphertext[aes.BlockSize:]),
|
||||||
|
EncType: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateTransportKey() []byte {
|
||||||
|
key := make([]byte, 32)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func rsaEncrypt(keyB64 string, message []byte) (string, error) {
|
||||||
|
publicKey, err := base64.StdEncoding.DecodeString(keyB64)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
test, err := x509.ParsePKIXPublicKey(publicKey)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
rsatest := test.(*rsa.PublicKey)
|
||||||
|
oaepDigest := sha1.New()
|
||||||
|
ciphertext, _ := rsa.EncryptOAEP(oaepDigest, rand.Reader, rsatest, message, []byte{})
|
||||||
|
b64cipherText := base64.StdEncoding.EncodeToString(ciphertext)
|
||||||
|
|
||||||
|
return b64cipherText, nil
|
||||||
|
}
|
||||||
12
browserbiometrics/logging/nooplogger.go
Normal file
12
browserbiometrics/logging/nooplogger.go
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
//go:build !logging
|
||||||
|
|
||||||
|
package logging
|
||||||
|
|
||||||
|
func Debugf(format string, args ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func Errorf(format string, args ...interface{}) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func Panicf(format string, args ...interface{}) {
|
||||||
|
}
|
||||||
64
browserbiometrics/main.go
Normal file
64
browserbiometrics/main.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package browserbiometrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const appID = "com.quexten.bw-bio-handler"
|
||||||
|
|
||||||
|
var transportKey []byte
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
if os.Args[1] == "install" {
|
||||||
|
var err error
|
||||||
|
err = detectAndInstallBrowsers(".config")
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to detect browsers: " + err.Error())
|
||||||
|
}
|
||||||
|
err = detectAndInstallBrowsers(".mozilla")
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to detect browsers: " + err.Error())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transportKey = generateTransportKey()
|
||||||
|
|
||||||
|
setupCommunication()
|
||||||
|
readLoop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectAndInstallBrowsers(startPath string) error {
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
err := filepath.Walk(home+"/"+startPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var tempPath string
|
||||||
|
if !strings.HasPrefix(path, home) {
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
tempPath = strings.TrimPrefix(path, home)
|
||||||
|
}
|
||||||
|
if strings.Count(tempPath, "/") > 3 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() && info.Name() == "native-messaging-hosts" {
|
||||||
|
fmt.Printf("Found mozilla-like browser: %s\n", path)
|
||||||
|
manifest := strings.Replace(templateMozilla, "PATH", os.Getenv("PWD")+"/bw-bio-handler", 1)
|
||||||
|
err = os.WriteFile(path+"/com.8bit.bitwarden.json", []byte(manifest), 0644)
|
||||||
|
} else if info.IsDir() && info.Name() == "NativeMessagingHosts" {
|
||||||
|
fmt.Printf("Found chrome-like browser: %s\n", path)
|
||||||
|
manifest := strings.Replace(templateChrome, "PATH", os.Getenv("PWD")+"/bw-bio-handler", 1)
|
||||||
|
err = os.WriteFile(path+"/com.8bit.bitwarden.json", []byte(manifest), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
23
browserbiometrics/manifests.go
Normal file
23
browserbiometrics/manifests.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package browserbiometrics
|
||||||
|
|
||||||
|
const templateMozilla = `{
|
||||||
|
"name": "com.8bit.bitwarden",
|
||||||
|
"description": "Bitwarden desktop <-> browser bridge",
|
||||||
|
"path": "PATH",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_extensions": [
|
||||||
|
"{446900e4-71c2-419f-a6a7-df9c091e268b}"
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
const templateChrome = `{
|
||||||
|
"name": "com.8bit.bitwarden",
|
||||||
|
"description": "Bitwarden desktop <-> browser bridge",
|
||||||
|
"path": "PATH",
|
||||||
|
"type": "stdio",
|
||||||
|
"allowed_origins": [
|
||||||
|
"chrome-extension://nngceckbapebfimnlniiiahkandclblb/",
|
||||||
|
"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/",
|
||||||
|
"chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/"
|
||||||
|
]
|
||||||
|
}`
|
||||||
45
browserbiometrics/messages.go
Normal file
45
browserbiometrics/messages.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package browserbiometrics
|
||||||
|
|
||||||
|
// top level messages
|
||||||
|
type GenericRecvMessage struct {
|
||||||
|
AppID string `json:"appId"`
|
||||||
|
Message interface{} `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnencryptedRecvMessage struct {
|
||||||
|
AppID string `json:"appId"`
|
||||||
|
Message PayloadMessage `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EncryptedRecvMessage struct {
|
||||||
|
AppID string `json:"appId"`
|
||||||
|
Message EncryptedString `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReceiveMessage struct {
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
Command string `json:"command"`
|
||||||
|
Response string `json:"response"`
|
||||||
|
KeyB64 string `json:"keyB64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SendMessage struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
AppID string `json:"appId"`
|
||||||
|
SharedSecret string `json:"sharedSecret"`
|
||||||
|
Message EncryptedString `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type EncryptedString struct {
|
||||||
|
IV string `json:"iv"`
|
||||||
|
Mac string `json:"mac"`
|
||||||
|
Data string `json:"data"`
|
||||||
|
EncType int `json:"encryptionType"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PayloadMessage struct {
|
||||||
|
Command string `json:"command"`
|
||||||
|
UserId string `json:"userId"`
|
||||||
|
Timestamp int64 `json:"timestamp"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
}
|
||||||
136
browserbiometrics/protocol.go
Normal file
136
browserbiometrics/protocol.go
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
package browserbiometrics
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/quexten/goldwarden/browserbiometrics/logging"
|
||||||
|
"github.com/quexten/goldwarden/client"
|
||||||
|
"github.com/quexten/goldwarden/ipc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readLoop() {
|
||||||
|
v := bufio.NewReader(os.Stdin)
|
||||||
|
s := bufio.NewReaderSize(v, bufferSize)
|
||||||
|
|
||||||
|
lengthBytes := make([]byte, 4)
|
||||||
|
lengthNum := int(0)
|
||||||
|
|
||||||
|
send(SendMessage{
|
||||||
|
Command: "connected",
|
||||||
|
AppID: appID,
|
||||||
|
})
|
||||||
|
|
||||||
|
for b, err := s.Read(lengthBytes); b > 0 && err == nil; b, err = s.Read(lengthBytes) {
|
||||||
|
lengthNum = readMessageLength(lengthBytes)
|
||||||
|
|
||||||
|
content := make([]byte, lengthNum)
|
||||||
|
_, err := s.Read(content)
|
||||||
|
if err != nil && err != io.EOF {
|
||||||
|
logging.Panicf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
parseMessage(content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseMessage(msg []byte) {
|
||||||
|
logging.Debugf("Received message: " + string(msg))
|
||||||
|
|
||||||
|
var genericMessage GenericRecvMessage
|
||||||
|
err := json.Unmarshal(msg, &genericMessage)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf("Unable to unmarshal json to struct: " + err.Error())
|
||||||
|
}
|
||||||
|
if _, ok := (genericMessage.Message.(map[string]interface{})["command"]); ok {
|
||||||
|
logging.Debugf("Message is unencrypted")
|
||||||
|
|
||||||
|
var unmsg UnencryptedRecvMessage
|
||||||
|
err := json.Unmarshal(msg, &unmsg)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf("Unable to unmarshal json to struct: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUnencryptedMessage(unmsg)
|
||||||
|
} else {
|
||||||
|
logging.Debugf("Message is encrypted")
|
||||||
|
|
||||||
|
var encmsg EncryptedRecvMessage
|
||||||
|
err := json.Unmarshal(msg, &encmsg)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf("Unable to unmarshal json to struct: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedMessage := decryptStringSymmetric(transportKey, encmsg.Message.IV, encmsg.Message.Data)
|
||||||
|
var payloadMsg PayloadMessage
|
||||||
|
err = json.Unmarshal([]byte(decryptedMessage), &payloadMsg)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf("Unable to unmarshal json to struct: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePayloadMessage(payloadMsg, genericMessage.AppID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleUnencryptedMessage(msg UnencryptedRecvMessage) {
|
||||||
|
logging.Debugf("Received unencrypted message: %+v", msg.Message)
|
||||||
|
logging.Debugf(" with command: %s", msg.Message.Command)
|
||||||
|
|
||||||
|
switch msg.Message.Command {
|
||||||
|
case "setupEncryption":
|
||||||
|
sharedSecret, err := rsaEncrypt(msg.Message.PublicKey, transportKey)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf(err.Error())
|
||||||
|
}
|
||||||
|
send(SendMessage{
|
||||||
|
Command: "setupEncryption",
|
||||||
|
AppID: msg.AppID,
|
||||||
|
SharedSecret: sharedSecret,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func handlePayloadMessage(msg PayloadMessage, appID string) {
|
||||||
|
logging.Debugf("Received unencrypted message: %+v", msg)
|
||||||
|
|
||||||
|
switch msg.Command {
|
||||||
|
case "biometricUnlock":
|
||||||
|
logging.Debugf("Biometric unlock requested")
|
||||||
|
// logging.Debugf("Biometrics authorized: %t", isAuthorized)
|
||||||
|
result, err := client.SendToAgent(ipc.GetBiometricsKeyRequest{})
|
||||||
|
if err != nil {
|
||||||
|
logging.Errorf("Unable to send message to agent: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch result.(type) {
|
||||||
|
case ipc.GetBiometricsKeyResponse:
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = result.(ipc.GetBiometricsKeyResponse).Key
|
||||||
|
var payloadMsg ReceiveMessage = ReceiveMessage{
|
||||||
|
Command: "biometricUnlock",
|
||||||
|
Response: "unlocked",
|
||||||
|
Timestamp: msg.Timestamp,
|
||||||
|
KeyB64: key,
|
||||||
|
}
|
||||||
|
payloadStr, err := json.Marshal(payloadMsg)
|
||||||
|
if err != nil {
|
||||||
|
logging.Panicf(err.Error())
|
||||||
|
}
|
||||||
|
logging.Debugf("Payload: %s", payloadStr)
|
||||||
|
|
||||||
|
encStr := encryptStringSymmetric(transportKey, payloadStr)
|
||||||
|
send(SendMessage{
|
||||||
|
AppID: appID,
|
||||||
|
Message: encStr,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
34
ipc/ipc.go
34
ipc/ipc.go
|
|
@ -47,6 +47,9 @@ const (
|
||||||
|
|
||||||
IPCMessageTypeSetAPIUrlRequest IPCMessageType = 30
|
IPCMessageTypeSetAPIUrlRequest IPCMessageType = 30
|
||||||
IPCMessageTypeSetIdentityURLRequest IPCMessageType = 31
|
IPCMessageTypeSetIdentityURLRequest IPCMessageType = 31
|
||||||
|
|
||||||
|
IPCMessageTypeGetBiometricsKeyRequest IPCMessageType = 8
|
||||||
|
IPCMessageTypeGetBiometricsKeyResponse IPCMessageType = 9
|
||||||
)
|
)
|
||||||
|
|
||||||
type IPCMessage struct {
|
type IPCMessage struct {
|
||||||
|
|
@ -194,6 +197,20 @@ func (m IPCMessage) ParsedPayload() interface{} {
|
||||||
panic("Unmarshal: " + err.Error())
|
panic("Unmarshal: " + err.Error())
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
case IPCMessageTypeGetBiometricsKeyRequest:
|
||||||
|
var res GetBiometricsKeyRequest
|
||||||
|
err := json.Unmarshal(m.Payload, &res)
|
||||||
|
if err != nil {
|
||||||
|
panic("Unmarshal: " + err.Error())
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
case IPCMessageTypeGetBiometricsKeyResponse:
|
||||||
|
var res GetBiometricsKeyResponse
|
||||||
|
err := json.Unmarshal(m.Payload, &res)
|
||||||
|
if err != nil {
|
||||||
|
panic("Unmarshal: " + err.Error())
|
||||||
|
}
|
||||||
|
return res
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -326,6 +343,16 @@ func IPCMessageFromPayload(payload interface{}) (IPCMessage, error) {
|
||||||
Type: IPCMessageListLoginsRequest,
|
Type: IPCMessageListLoginsRequest,
|
||||||
Payload: jsonBytes,
|
Payload: jsonBytes,
|
||||||
}, nil
|
}, nil
|
||||||
|
case GetBiometricsKeyRequest:
|
||||||
|
return IPCMessage{
|
||||||
|
Type: IPCMessageTypeGetBiometricsKeyRequest,
|
||||||
|
Payload: jsonBytes,
|
||||||
|
}, nil
|
||||||
|
case GetBiometricsKeyResponse:
|
||||||
|
return IPCMessage{
|
||||||
|
Type: IPCMessageTypeGetBiometricsKeyResponse,
|
||||||
|
Payload: jsonBytes,
|
||||||
|
}, nil
|
||||||
default:
|
default:
|
||||||
payloadBytes, err := json.Marshal(payload)
|
payloadBytes, err := json.Marshal(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -454,3 +481,10 @@ type SetIdentityURLRequest struct {
|
||||||
|
|
||||||
type ListLoginsRequest struct {
|
type ListLoginsRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetBiometricsKeyRequest struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetBiometricsKeyResponse struct {
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
|
||||||
9
main.go
9
main.go
|
|
@ -1,9 +1,18 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/quexten/goldwarden/browserbiometrics"
|
||||||
"github.com/quexten/goldwarden/cmd"
|
"github.com/quexten/goldwarden/cmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if len(os.Args) > 1 && strings.Contains(os.Args[1], "com.8bit.bitwarden.json") {
|
||||||
|
browserbiometrics.Main()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
cmd.Execute()
|
cmd.Execute()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,4 +40,13 @@
|
||||||
<allow_active>auth_self</allow_active>
|
<allow_active>auth_self</allow_active>
|
||||||
</defaults>
|
</defaults>
|
||||||
</action>
|
</action>
|
||||||
|
<action id="com.quexten.goldwarden.browserbiometrics">
|
||||||
|
<description>Browser Biometrics</description>
|
||||||
|
<message>Authenticate to allow Goldwarden to unlock your browser.</message>
|
||||||
|
<defaults>
|
||||||
|
<allow_any>auth_self</allow_any>
|
||||||
|
<allow_inactive>auth_self</allow_inactive>
|
||||||
|
<allow_active>auth_self</allow_active>
|
||||||
|
</defaults>
|
||||||
|
</action>
|
||||||
</policyconfig>
|
</policyconfig>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue