From 26805801aa6ef91fbe524ea51379ec7a52024d44 Mon Sep 17 00:00:00 2001 From: Tim van der Molen Date: Mon, 29 Jul 2024 11:09:48 +0200 Subject: [PATCH] Support encrypted database keys on Windows #48 --- safestorage/decrypt.go | 39 +++++++++++++++ safestorage/decrypt_other.go | 27 +++++++++++ safestorage/decrypt_windows.go | 89 ++++++++++++++++++++++++++++++++++ signal/const.go | 7 +-- signal/open.go | 8 ++- 5 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 safestorage/decrypt_other.go create mode 100644 safestorage/decrypt_windows.go diff --git a/safestorage/decrypt.go b/safestorage/decrypt.go index ebc0a65..283fe1a 100644 --- a/safestorage/decrypt.go +++ b/safestorage/decrypt.go @@ -35,6 +35,10 @@ const ( linuxPrefixV10 = "v10" linuxPrefixV11 = "v11" linuxIterations = 1 + + windowsPrefix = "v10" + windowsKeySize = 32 // AES-256 + windowsNonceSize = 12 ) func DecryptWithPassword(ciphertext, password []byte) ([]byte, error) { @@ -43,6 +47,8 @@ func DecryptWithPassword(ciphertext, password []byte) ([]byte, error) { return decryptWithMacosPassword(ciphertext, password) case "linux", "openbsd": return decryptWithLinuxPassword(ciphertext, password) + case "windows": + return decryptWithWindowsPassword(ciphertext, password) default: return nil, errors.New("not yet supported") } @@ -67,6 +73,39 @@ func decryptWithLinuxPassword(ciphertext, password []byte) ([]byte, error) { return decryptWithPassword(ciphertext, password, linuxIterations) } +func decryptWithWindowsPassword(ciphertext, password []byte) ([]byte, error) { + if !bytes.HasPrefix(ciphertext, []byte(windowsPrefix)) { + return nil, errors.New("unsupported ciphertext format") + } + ciphertext = bytes.TrimPrefix(ciphertext, []byte(windowsPrefix)) + + if len(ciphertext) < windowsNonceSize { + return nil, errors.New("invalid ciphertext length") + } + nonce := ciphertext[:windowsNonceSize] + ciphertext = ciphertext[windowsNonceSize:] + + if len(password) != windowsKeySize { + return nil, errors.New("invalid password length") + } + + c, err := aes.NewCipher(password) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCMWithNonceSize(c, windowsNonceSize) + if err != nil { + return nil, err + } + + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, err + } + + return plaintext, nil +} + func decryptWithPassword(ciphertext, password []byte, iters int) ([]byte, error) { if len(ciphertext)%aes.BlockSize != 0 { return nil, errors.New("invalid ciphertext length") diff --git a/safestorage/decrypt_other.go b/safestorage/decrypt_other.go new file mode 100644 index 0000000..e357d29 --- /dev/null +++ b/safestorage/decrypt_other.go @@ -0,0 +1,27 @@ +// Copyright (c) 2024 Tim van der Molen +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +//go:build !windows + +package safestorage + +import "errors" + +func Decrypt(ciphertext []byte) ([]byte, error) { + return nil, errors.New("not yet supported") +} + +func DecryptWithLocalState(ciphertext []byte, localStateFile string) ([]byte, error) { + return nil, errors.New("not supported") +} diff --git a/safestorage/decrypt_windows.go b/safestorage/decrypt_windows.go new file mode 100644 index 0000000..83d5909 --- /dev/null +++ b/safestorage/decrypt_windows.go @@ -0,0 +1,89 @@ +// Copyright (c) 2024 Tim van der Molen +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package safestorage + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "os" + "unsafe" + + "golang.org/x/sys/windows" +) + +const dpapiKeyPrefix = "DPAPI" + +func Decrypt(ciphertext []byte) ([]byte, error) { + return nil, fmt.Errorf("not supported") +} + +func DecryptWithLocalState(ciphertext []byte, localStateFile string) ([]byte, error) { + key, err := encryptionKey(localStateFile) + if err != nil { + return nil, err + } + return decryptWithWindowsPassword(ciphertext, key) +} + +func encryptionKey(localStateFile string) ([]byte, error) { + data, err := os.ReadFile(localStateFile) + if err != nil { + return nil, err + } + + var localState struct { + OSCrypt struct { + EncryptedKey *string `json:"encrypted_key"` + } `json:"os_crypt"` + } + if err := json.Unmarshal(data, &localState); err != nil { + return nil, fmt.Errorf("cannot parse %s: %w", localStateFile, err) + } + + if localState.OSCrypt.EncryptedKey == nil { + return nil, fmt.Errorf("encryption key not found") + } + + key, err := base64.StdEncoding.DecodeString(*localState.OSCrypt.EncryptedKey) + if err != nil { + return nil, fmt.Errorf("cannot decode encryption key: %w", err) + } + + if !bytes.HasPrefix(key, []byte(dpapiKeyPrefix)) { + return nil, fmt.Errorf("invalid encryption key format") + } + key = bytes.TrimPrefix(key, []byte(dpapiKeyPrefix)) + + return decryptWithDPAPI(key) +} + +func decryptWithDPAPI(ciphertext []byte) ([]byte, error) { + in := windows.DataBlob{ + Data: &ciphertext[0], + Size: uint32(len(ciphertext)), + } + var out windows.DataBlob + if err := windows.CryptUnprotectData(&in, nil, nil, 0, nil, 0, &out); err != nil { + return nil, err + } + + plaintext := make([]byte, out.Size) + copy(plaintext, unsafe.Slice(out.Data, out.Size)) + windows.LocalFree(windows.Handle(unsafe.Pointer(out.Data))) + + return plaintext, nil +} diff --git a/signal/const.go b/signal/const.go index 9e71f75..b06c82d 100644 --- a/signal/const.go +++ b/signal/const.go @@ -17,9 +17,10 @@ package signal import "os" const ( - DatabaseFile = "sql" + string(os.PathSeparator) + "db.sqlite" - ConfigFile = "config.json" - AttachmentDir = "attachments.noindex" + DatabaseFile = "sql" + string(os.PathSeparator) + "db.sqlite" + ConfigFile = "config.json" + LocalStateFile = "Local State" + AttachmentDir = "attachments.noindex" // Content type of the long-text attachment of a long message LongTextType = "text/x-signal-plain" diff --git a/signal/open.go b/signal/open.go index a305eb5..eace364 100644 --- a/signal/open.go +++ b/signal/open.go @@ -20,6 +20,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "github.com/tbvdm/sigtop/safestorage" "github.com/tbvdm/sigtop/sqlcipher" @@ -129,7 +130,12 @@ func databaseKey(dir string, password []byte) ([]byte, error) { if password != nil { dbKey, err = safestorage.DecryptWithPassword(key, password) } else { - err = fmt.Errorf("not yet supported") + if runtime.GOOS == "windows" { + localStateFile := filepath.Join(dir, LocalStateFile) + dbKey, err = safestorage.DecryptWithLocalState(key, localStateFile) + } else { + dbKey, err = safestorage.Decrypt(key) + } } if err != nil { return nil, fmt.Errorf("cannot decrypt database key: %w", err)