1
0
mirror of https://github.com/vwxyzjn/portwarden synced 2025-12-06 01:33:18 +00:00
Files
portwarden/core.go
guoguangwu 05a2cfde16 fix: typo
2023-09-18 15:24:51 +08:00

476 lines
12 KiB
Go

package portwarden
import (
"archive/zip"
"bufio"
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
b64 "encoding/base64"
"github.com/davecgh/go-spew/spew"
"github.com/mholt/archiver"
"github.com/tidwall/pretty"
)
const (
BackupFolderName = "./portwarden_backup/"
ErrVaultIsLocked = "vault is locked"
ErrNoPhassPhraseProvided = "no passphrase provided"
ErrNoFilenameProvided = "no filename provided"
ErrSessionKeyExtractionFailed = "session key extraction failed"
ErrVaultNotEmptyForRestore = "account's valut not empty! you have to restore the backup to an empty Bitwarden account"
BWErrNotLoggedIn = "You are not logged in."
BWErrInvalidMasterPassword = "Invalid master password."
BWEnterEmailAddress = "? Email address:"
BWEnterMasterPassword = "? Master password:"
LoginCredentialMethodNone = 100
LoginCredentialMethodAuthenticator = 0
LoginCredentialMethodEmail = 1
LoginCredentialMethodYubikey = 3
ItemsJsonFileName = "items.json"
FoldersJSONFileName = "folders.json"
)
// LoginCredentials is used to login to the `bw` cli. See documentation
// https://help.bitwarden.com/article/cli/
// The possible `Method` values are
// None 100
// Authenticator 0
// Email 1
// Yubikey 3
type LoginCredentials struct {
Email string `json:"email"`
Password string `json:"password"`
Method int `json:"method"`
Code string `json:"code"`
}
func CreateBackupBytesUsingBitwardenLocalJSON(dataJson []byte, BITWARDENCLI_APPDATA_DIR, passphrase, sessionKey string, sleepMilliseconds int) ([]byte, error) {
// Put data.json in the BITWARDENCLI_APPDATA_DIR
defer BWDelete(BITWARDENCLI_APPDATA_DIR)
if err := ioutil.WriteFile(filepath.Join(BITWARDENCLI_APPDATA_DIR, "data.json"), dataJson, 0644); err != nil {
return nil, err
}
return CreateBackupBytes(passphrase, sessionKey, sleepMilliseconds)
}
func CreateBackupFile(fileName, passphrase, sessionKey string, sleepMilliseconds int, noLogout bool) error {
if !noLogout {
defer BWLogout()
}
if !strings.HasSuffix(fileName, ".portwarden") {
fileName += ".portwarden"
}
f, err := os.Create(fileName)
if err != nil {
return err
}
defer f.Close()
encryptedData, err := CreateBackupBytes(passphrase, sessionKey, sleepMilliseconds)
if err != nil {
return err
}
f.Write(encryptedData)
return nil
}
func CreateBackupBytes(passphrase, sessionKey string, sleepMilliseconds int) ([]byte, error) {
if err := os.MkdirAll(BackupFolderName, os.ModePerm); err != nil {
return nil, err
}
defer os.RemoveAll(BackupFolderName)
// save formmated json to FoldersJSONFileName
rawByte, err := BWListFoldersRawBytes(sessionKey)
if err != nil {
return nil, err
}
formattedByte := pretty.Pretty(rawByte)
if err := ioutil.WriteFile(BackupFolderName+FoldersJSONFileName, formattedByte, 0644); err != nil {
return nil, err
}
// save formmated json to ItemsJsonFileName
rawByte, err = BWListItemsRawBytes(sessionKey)
if err != nil {
return nil, err
}
formattedByte = pretty.Pretty(rawByte)
if err := ioutil.WriteFile(BackupFolderName+ItemsJsonFileName, formattedByte, 0644); err != nil {
return nil, err
}
// download attachments
pwes := []PortWardenElement{}
if err := json.Unmarshal(rawByte, &pwes); err != nil {
return nil, err
}
err = BWGetAllAttachments(BackupFolderName, sessionKey, pwes, sleepMilliseconds)
if err != nil {
return nil, err
}
var b bytes.Buffer
writer := bufio.NewWriter(&b)
err = archiver.Zip.Write(writer, []string{BackupFolderName})
if err != nil {
return nil, err
}
// derive a key from the master password
encryptedBytes, err := EncryptBytes(b.Bytes(), passphrase)
if err != nil {
return nil, err
}
return encryptedBytes, nil
}
func DecryptBackupFile(fileName, passphrase string) error {
rawBytes, err := ioutil.ReadFile(fileName)
if err != nil {
return err
}
tb, err := DecryptBytes(rawBytes, passphrase)
if err != nil {
fmt.Println("decryption failed: " + err.Error())
return err
}
if err := ioutil.WriteFile(fileName+".decrypted"+".zip", tb, 0644); err != nil {
fmt.Println("decryption failed: " + err.Error())
return err
}
return nil
}
func RestoreBackupFile(fileName, passphrase, sessionKey string, sleepMilliseconds int, noLogout bool) error {
// dummy check if the account is not empty, don't restore
var err error
var rawByte []byte
var file []byte
err = DecryptBackupFile(fileName, passphrase)
if err != nil {
return err
}
err = Unzip(fileName+".decrypted"+".zip", "")
if err != nil {
return err
}
defer os.RemoveAll(BackupFolderName)
defer os.Remove(fileName + ".decrypted" + ".zip")
rawByte, err = BWListItemsRawBytes(sessionKey)
if err != nil {
return err
}
pwes := []PortWardenElement{}
if err := json.Unmarshal(rawByte, &pwes); err != nil {
return err
}
if len(pwes) != 0 {
return errors.New(ErrVaultNotEmptyForRestore)
}
// restore folders
if file, err = ioutil.ReadFile(BackupFolderName + FoldersJSONFileName); err != nil {
return err
}
folderData := PortWardenFolder{}
err = json.Unmarshal([]byte(file), &folderData)
if err != nil {
return err
}
oldToNewFolderID := make(map[string]string)
var itemBytes []byte
for _, item := range folderData {
time.Sleep(time.Millisecond * time.Duration(sleepMilliseconds))
if item.ID != nil {
itemBytes, err = json.Marshal(item)
cmd := exec.Command("bw", "create", "folder", "--session", sessionKey, b64.StdEncoding.EncodeToString(itemBytes))
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
fmt.Println("An error occurred: ", err)
spew.Dump(stdout, stderr)
}
fmt.Println("restoring folder", item.Name)
newItem := PortWardenFolderElement{}
err = json.Unmarshal(stdout.Bytes(), &newItem)
if err != nil {
return err
}
oldToNewFolderID[*item.ID] = *newItem.ID
}
}
// restore items
if file, err = ioutil.ReadFile(BackupFolderName + ItemsJsonFileName); err != nil {
return err
}
itemData := PortWarden{}
err = json.Unmarshal([]byte(file), &itemData)
if err != nil {
return err
}
oldToNewItemID := make(map[string]string)
for _, item := range itemData {
time.Sleep(time.Millisecond * time.Duration(sleepMilliseconds))
// deal with attachments separately
item.Attachments = nil
if item.FolderID != nil {
*item.FolderID = oldToNewFolderID[*item.FolderID]
}
item.OrganizationID = nil
item.CollectionIDS = nil
itemBytes, err = json.Marshal(item)
cmd := exec.Command("bw", "create", "item", "--session", sessionKey, b64.StdEncoding.EncodeToString(itemBytes))
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
fmt.Println("An error occurred: ", err)
spew.Dump(stdout, stderr)
}
fmt.Println("restoring item", item.Name)
newItem := PortWardenElement{}
err = json.Unmarshal(stdout.Bytes(), &newItem)
if err != nil {
return err
}
oldToNewItemID[item.ID] = newItem.ID
}
fmt.Println("restoring item finished")
// restore item's attachments
if file, err = ioutil.ReadFile(BackupFolderName + ItemsJsonFileName); err != nil {
return err
}
itemData = PortWarden{}
err = json.Unmarshal([]byte(file), &itemData)
if err != nil {
return err
}
for _, item := range itemData {
if len(item.Attachments) > 0 {
time.Sleep(time.Millisecond * time.Duration(sleepMilliseconds))
for _, innerItem := range item.Attachments {
itemBytes, err = json.Marshal(item)
cmd := exec.Command("bw", "create", "attachment", "--itemid", oldToNewItemID[item.ID], "--session", sessionKey, "--file", BackupFolderName+item.Name+"/"+innerItem.FileName)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
fmt.Println("An error occurred: ", err)
spew.Dump(stdout, stderr)
}
fmt.Println("restoring item's attachment", item.Name, innerItem.FileName)
}
}
}
return nil
}
func ExtractSessionKey(stdout string) (string, error) {
r := regexp.MustCompile(`BW_SESSION=".+"`)
matches := r.FindAllString(stdout, 1)
if len(matches) == 0 {
return "", errors.New(ErrSessionKeyExtractionFailed)
}
sessionKeyRawString := r.FindAllString(stdout, 1)[0]
sessionKey := strings.TrimPrefix(sessionKeyRawString, `BW_SESSION="`)
sessionKey = sessionKey[:len(sessionKey)-1]
return sessionKey, nil
}
func BWListItemsRawBytes(sessionKey string) ([]byte, error) {
var stdout, stderr bytes.Buffer
cmd := exec.Command("bw", "list", "items", "--session", sessionKey)
cmd.Stdin = os.Stdin
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return nil, err
}
return stdout.Bytes(), nil
}
func BWListFoldersRawBytes(sessionKey string) ([]byte, error) {
var stdout, stderr bytes.Buffer
cmd := exec.Command("bw", "list", "folders", "--session", sessionKey)
cmd.Stdin = os.Stdin
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return nil, err
}
return stdout.Bytes(), nil
}
func BWGetAttachment(outputDir, itemID, attachmentID, sessionKey string) error {
cmd := exec.Command("bw", "get", "attachment", attachmentID, "--itemid", itemID,
"--session", sessionKey, "--output", outputDir)
err := cmd.Run()
if err != nil {
return err
}
return nil
}
func BWGetAllAttachments(outputDir, sessionKey string, pws []PortWardenElement, sleepMilliseconds int) error {
for _, item := range pws {
if len(item.Attachments) > 0 {
for _, innerItem := range item.Attachments {
ourputDir := strings.TrimSpace(outputDir + item.Name) // Keep this line. See https://github.com/vwxyzjn/portwarden/issues/10
err := BWGetAttachment(ourputDir+"/", item.ID, innerItem.ID, sessionKey)
time.Sleep(time.Millisecond * time.Duration(sleepMilliseconds))
if err != nil {
spew.Dump(err, "failed item ids are ", item.ID, innerItem.ID, item.Name)
return err
}
}
}
}
return nil
}
func BWLoginGetSessionKey(lc *LoginCredentials) (string, error) {
var cmd *exec.Cmd
if lc.Method != LoginCredentialMethodNone {
cmd = exec.Command("bw", "login", lc.Email, lc.Password, "--method", strconv.Itoa(lc.Method), "--code", lc.Code, "--raw")
} else {
cmd = exec.Command("bw", "login", lc.Email, lc.Password, "--raw")
}
var stdout bytes.Buffer
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return stdout.String(), err
}
sessionKey := stdout.String()
return sessionKey, nil
}
func BWLoginGetSessionKeyAndDataJSON(lc *LoginCredentials, BITWARDENCLI_APPDATA_DIR string) (string, []byte, error) {
sessionKey, err := BWLoginGetSessionKey(lc)
if err != nil {
return "", nil, err
}
defer BWDelete(BITWARDENCLI_APPDATA_DIR)
dataJSONPath := filepath.Join(BITWARDENCLI_APPDATA_DIR, "data.json")
dat, err := ioutil.ReadFile(dataJSONPath)
if err != nil {
return "", nil, err
}
err = os.Remove(dataJSONPath)
if err != nil {
return "", nil, err
}
return sessionKey, dat, nil
}
func BWLogout() error {
var stdout, stderr bytes.Buffer
cmd := exec.Command("bw", "logout")
cmd.Stdin = os.Stdin
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
return errors.New(string(stderr.Bytes()))
}
return nil
}
func BWDelete(BITWARDENCLI_APPDATA_DIR string) error {
dataJSONPath := filepath.Join(BITWARDENCLI_APPDATA_DIR, "data.json")
err := os.Remove(dataJSONPath)
if err != nil {
return err
}
return nil
}
func Unzip(src, dest string) error {
r, err := zip.OpenReader(src)
if err != nil {
return err
}
defer func() {
if err := r.Close(); err != nil {
panic(err)
}
}()
os.MkdirAll(dest, 0755)
// Closure to address file descriptors issue with all the deferred .Close() methods
extractAndWriteFile := func(f *zip.File) error {
rc, err := f.Open()
if err != nil {
return err
}
defer func() {
if err := rc.Close(); err != nil {
panic(err)
}
}()
path := filepath.Join(dest, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(path, f.Mode())
} else {
os.MkdirAll(filepath.Dir(path), f.Mode())
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer func() {
if err := f.Close(); err != nil {
panic(err)
}
}()
_, err = io.Copy(f, rc)
if err != nil {
return err
}
}
return nil
}
for _, f := range r.File {
err := extractAndWriteFile(f)
if err != nil {
return err
}
}
return nil
}