Skip to content

Commit

Permalink
Support encrypted database keys on Windows
Browse files Browse the repository at this point in the history
  • Loading branch information
Tim van der Molen committed Jul 29, 2024
1 parent 1c31ed4 commit 2680580
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 4 deletions.
39 changes: 39 additions & 0 deletions safestorage/decrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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")
}
Expand All @@ -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")
Expand Down
27 changes: 27 additions & 0 deletions safestorage/decrypt_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) 2024 Tim van der Molen <tim@kariliq.nl>
//
// 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")
}
89 changes: 89 additions & 0 deletions safestorage/decrypt_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) 2024 Tim van der Molen <tim@kariliq.nl>
//
// 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
}
7 changes: 4 additions & 3 deletions signal/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 7 additions & 1 deletion signal/open.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"

"github.com/tbvdm/sigtop/safestorage"
"github.com/tbvdm/sigtop/sqlcipher"
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 2680580

Please sign in to comment.