Port bw-bio-handler for browser biometrics

This commit is contained in:
Bernd Schoolmann 2023-07-17 05:02:29 +02:00
parent fbf843d090
commit 350124c7e6
No known key found for this signature in database
15 changed files with 596 additions and 6 deletions

View 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))
}

View file

@ -71,6 +71,7 @@ func handleLogin(msg ipc.IPCMessage, cfg *config.Config, vault *vault.Vault, cal
cfg.SetUserSymmetricKey(vault.Keyring.AccountKey.Bytes())
cfg.SetMasterPasswordHash([]byte(masterpasswordHash))
cfg.SetMasterKey([]byte(masterKey.GetBytes()))
protectedUserSymetricKey, err := crypto.SymmetricEncryptionKeyFromBytes(vault.Keyring.AccountKey.Bytes())
if err != nil {
var payload = ipc.ActionResponse{

View file

@ -21,7 +21,7 @@ func Sync(ctx context.Context, config *config.Config) (models.SyncData, error) {
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...")
sync, err := Sync(ctx, config)
@ -29,13 +29,13 @@ func SyncToVault(ctx context.Context, vault *vault.Vault, config *config.Config,
return err
}
if masterKey != nil {
if userSymmetricKey != nil {
var orgKeys map[string]string = make(map[string]string)
for _, org := range sync.Profile.Organizations {
orgId := org.Id.String()
orgKeys[orgId] = org.Key
}
crypto.InitKeyringFromUserSymmetricKey(vault.Keyring, *masterKey, sync.Profile.PrivateKey, orgKeys)
crypto.InitKeyringFromUserSymmetricKey(vault.Keyring, *userSymmetricKey, sync.Profile.PrivateKey, orgKeys)
}
vault.Clear()

View file

@ -35,6 +35,7 @@ type ConfigFile struct {
EncryptedToken string
EncryptedUserSymmetricKey string
EncryptedMasterPasswordHash string
EncryptedMasterKey string
}
type LoginToken struct {
@ -63,6 +64,7 @@ func DefaultConfig() Config {
EncryptedToken: "",
EncryptedUserSymmetricKey: "",
EncryptedMasterPasswordHash: "",
EncryptedMasterKey: "",
},
sync.Mutex{},
}
@ -111,6 +113,7 @@ func (c *Config) Purge() {
c.ConfigFile.EncryptedToken = ""
c.ConfigFile.EncryptedUserSymmetricKey = ""
c.ConfigFile.ConfigKeyHash = ""
c.ConfigFile.EncryptedMasterKey = ""
c.key = memguard.NewBuffer(32)
}
@ -131,6 +134,7 @@ func (c *Config) UpdatePin(password string, write bool) {
plaintextToken, err1 := c.decryptString(c.ConfigFile.EncryptedToken)
plaintextUserSymmetricKey, err3 := c.decryptString(c.ConfigFile.EncryptedUserSymmetricKey)
plaintextEncryptedMasterPasswordHash, err4 := c.decryptString(c.ConfigFile.EncryptedMasterPasswordHash)
plaintextMasterKey, err5 := c.decryptString(c.ConfigFile.EncryptedMasterKey)
c.key = memguard.NewBufferFromBytes(newKey)
@ -143,6 +147,9 @@ func (c *Config) UpdatePin(password string, write bool) {
if err4 == nil {
c.ConfigFile.EncryptedMasterPasswordHash, err4 = c.encryptString(plaintextEncryptedMasterPasswordHash)
}
if err5 == nil {
c.ConfigFile.EncryptedMasterKey, err5 = c.encryptString(plaintextMasterKey)
}
if write {
c.WriteConfig()
@ -240,6 +247,32 @@ func (c *Config) SetMasterPasswordHash(hash []byte) error {
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) {
if c.IsLocked() {
return "", errors.New("config is locked")

View file

@ -10,9 +10,11 @@ var log = llamalog.NewLogger("Goldwarden", "Systemauth")
type Approval string
const (
AccessCredential Approval = "com.quexten.goldwarden.accesscredential"
ChangePin Approval = "com.quexten.goldwarden.changepin"
SSHKey Approval = "com.quexten.goldwarden.usesshkey"
AccessCredential Approval = "com.quexten.goldwarden.accesscredential"
ChangePin Approval = "com.quexten.goldwarden.changepin"
SSHKey Approval = "com.quexten.goldwarden.usesshkey"
ModifyVault Approval = "com.quexten.goldwarden.modifyvault"
BrowserBiometrics Approval = "com.quexten.goldwarden.browserbiometrics"
)
func (a Approval) String() string {

View 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
View 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
}

View 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
View 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
}

View 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/"
]
}`

View 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"`
}

View 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
}
}

View file

@ -47,6 +47,9 @@ const (
IPCMessageTypeSetAPIUrlRequest IPCMessageType = 30
IPCMessageTypeSetIdentityURLRequest IPCMessageType = 31
IPCMessageTypeGetBiometricsKeyRequest IPCMessageType = 8
IPCMessageTypeGetBiometricsKeyResponse IPCMessageType = 9
)
type IPCMessage struct {
@ -194,6 +197,20 @@ func (m IPCMessage) ParsedPayload() interface{} {
panic("Unmarshal: " + err.Error())
}
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:
return nil
}
@ -326,6 +343,16 @@ func IPCMessageFromPayload(payload interface{}) (IPCMessage, error) {
Type: IPCMessageListLoginsRequest,
Payload: jsonBytes,
}, nil
case GetBiometricsKeyRequest:
return IPCMessage{
Type: IPCMessageTypeGetBiometricsKeyRequest,
Payload: jsonBytes,
}, nil
case GetBiometricsKeyResponse:
return IPCMessage{
Type: IPCMessageTypeGetBiometricsKeyResponse,
Payload: jsonBytes,
}, nil
default:
payloadBytes, err := json.Marshal(payload)
if err != nil {
@ -454,3 +481,10 @@ type SetIdentityURLRequest struct {
type ListLoginsRequest struct {
}
type GetBiometricsKeyRequest struct {
}
type GetBiometricsKeyResponse struct {
Key string
}

View file

@ -1,9 +1,18 @@
package main
import (
"os"
"strings"
"github.com/quexten/goldwarden/browserbiometrics"
"github.com/quexten/goldwarden/cmd"
)
func main() {
if len(os.Args) > 1 && strings.Contains(os.Args[1], "com.8bit.bitwarden.json") {
browserbiometrics.Main()
return
}
cmd.Execute()
}

View file

@ -40,4 +40,13 @@
<allow_active>auth_self</allow_active>
</defaults>
</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>