diff --git a/duplicacy/duplicacy_main.go b/duplicacy/duplicacy_main.go index beeee3f..1878772 100644 --- a/duplicacy/duplicacy_main.go +++ b/duplicacy/duplicacy_main.go @@ -208,6 +208,17 @@ func runScript(context *cli.Context, storageName string, phase string) bool { return true } +func loadRSAPrivateKey(keyFile string, preference *duplicacy.Preference, backupManager *duplicacy.BackupManager, resetPasswords bool) { + if keyFile == "" { + return + } + + prompt := fmt.Sprintf("Enter the passphrase for %s:", keyFile) + passphrase := duplicacy.GetPassword(*preference, "rsa_passphrase", prompt, false, resetPasswords) + backupManager.LoadRSAPrivateKey(keyFile, passphrase) + duplicacy.SavePassword(*preference, "rsa_passphrase", passphrase) +} + func initRepository(context *cli.Context) { configRepository(context, true) } @@ -319,6 +330,11 @@ func configRepository(context *cli.Context, init bool) { if preference.Encrypted { prompt := fmt.Sprintf("Enter storage password for %s:", preference.StorageURL) storagePassword = duplicacy.GetPassword(preference, "password", prompt, false, true) + } else { + if context.String("key") != "" { + duplicacy.LOG_ERROR("STORAGE_CONFIG", "RSA encryption can't be enabled with an unencrypted storage") + return + } } existingConfig, _, err := duplicacy.DownloadConfig(storage, storagePassword) @@ -434,7 +450,7 @@ func configRepository(context *cli.Context, init bool) { iterations = duplicacy.CONFIG_DEFAULT_ITERATIONS } duplicacy.ConfigStorage(storage, iterations, compressionLevel, averageChunkSize, maximumChunkSize, - minimumChunkSize, storagePassword, otherConfig, bitCopy) + minimumChunkSize, storagePassword, otherConfig, bitCopy, context.String("key")) } duplicacy.Preferences = append(duplicacy.Preferences, preference) @@ -718,10 +734,6 @@ func backupRepository(context *cli.Context) { backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) - if context.String("key") != "" { - backupManager.SetupRSAPublicKey(context.String("key")) - } - backupManager.SetupSnapshotCache(preference.Name) backupManager.SetDryRun(dryRun) backupManager.Backup(repository, quickMode, threads, context.String("t"), showStatistics, enableVSS, vssTimeout, enumOnly) @@ -799,12 +811,7 @@ func restoreRepository(context *cli.Context) { backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) - if context.String("key") != "" { - prompt := fmt.Sprintf("Enter the passphrase for %s:", context.String("key")) - passphrase := duplicacy.GetPassword(*preference, "rsa_passphrase", prompt, false, false) - backupManager.SetupRSAPrivateKey(context.String("key"), passphrase) - duplicacy.SavePassword(*preference, "rsa_passphrase", passphrase) - } + loadRSAPrivateKey(context.String("key"), preference, backupManager, false) backupManager.SetupSnapshotCache(preference.Name) backupManager.Restore(repository, revision, true, quickMode, threads, overwrite, deleteMode, setOwner, showStatistics, patterns) @@ -857,12 +864,7 @@ func listSnapshots(context *cli.Context) { showChunks := context.Bool("chunks") // list doesn't need to decrypt file chunks; but we need -key here so we can reset the passphrase for the private key - if context.String("key") != "" { - prompt := fmt.Sprintf("Enter the passphrase for %s:", context.String("key")) - passphrase := duplicacy.GetPassword(*preference, "rsa_passphrase", prompt, false, resetPassword) - backupManager.SetupRSAPrivateKey(context.String("key"), passphrase) - duplicacy.SavePassword(*preference, "rsa_passphrase", passphrase) - } + loadRSAPrivateKey(context.String("key"), preference, backupManager, resetPassword) backupManager.SetupSnapshotCache(preference.Name) backupManager.SnapshotManager.ListSnapshots(id, revisions, tag, showFiles, showChunks) @@ -902,12 +904,7 @@ func checkSnapshots(context *cli.Context) { backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) - if context.String("key") != "" { - prompt := fmt.Sprintf("Enter the passphrase for %s:", context.String("key")) - passphrase := duplicacy.GetPassword(*preference, "rsa_passphrase", prompt, false, false) - backupManager.SetupRSAPrivateKey(context.String("key"), passphrase) - duplicacy.SavePassword(*preference, "rsa_passphrase", passphrase) - } + loadRSAPrivateKey(context.String("key"), preference, backupManager, false) id := preference.SnapshotID if context.Bool("all") { @@ -964,12 +961,7 @@ func printFile(context *cli.Context) { backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) - if context.String("key") != "" { - prompt := fmt.Sprintf("Enter the passphrase for %s:", context.String("key")) - passphrase := duplicacy.GetPassword(*preference, "rsa_passphrase", prompt, false, false) - backupManager.SetupRSAPrivateKey(context.String("key"), passphrase) - duplicacy.SavePassword(*preference, "rsa_passphrase", passphrase) - } + loadRSAPrivateKey(context.String("key"), preference, backupManager, false) backupManager.SetupSnapshotCache(preference.Name) @@ -1027,12 +1019,7 @@ func diff(context *cli.Context) { backupManager := duplicacy.CreateBackupManager(preference.SnapshotID, storage, repository, password, preference.NobackupFile) duplicacy.SavePassword(*preference, "password", password) - if context.String("key") != "" { - prompt := fmt.Sprintf("Enter the passphrase for %s:", context.String("key")) - passphrase := duplicacy.GetPassword(*preference, "rsa_passphrase", prompt, false, false) - backupManager.SetupRSAPrivateKey(context.String("key"), passphrase) - duplicacy.SavePassword(*preference, "rsa_passphrase", passphrase) - } + loadRSAPrivateKey(context.String("key"), preference, backupManager, false) backupManager.SetupSnapshotCache(preference.Name) backupManager.SnapshotManager.Diff(repository, snapshotID, revisions, path, compareByHash, preference.NobackupFile) @@ -1181,6 +1168,8 @@ func copySnapshots(context *cli.Context) { sourceManager.SetupSnapshotCache(source.Name) duplicacy.SavePassword(*source, "password", sourcePassword) + loadRSAPrivateKey(context.String("key"), source, sourceManager, false) + _, destination := getRepositoryPreference(context, context.String("to")) if destination.Name == source.Name { @@ -1388,6 +1377,11 @@ func main() { Usage: "initialize a new repository at the specified path rather than the current working directory", Argument: "", }, + cli.StringFlag{ + Name: "key", + Usage: "the RSA public key to encrypt file chunks", + Argument: "", + }, }, Usage: "Initialize the storage if necessary and the current directory as the repository", ArgsUsage: " ", @@ -1444,11 +1438,6 @@ func main() { Name: "enum-only", Usage: "enumerate the repository recursively and then exit", }, - cli.StringFlag{ - Name: "key", - Usage: "the RSA public key to encrypt file chunks", - Argument: "", - }, }, Usage: "Save a snapshot of the repository to the storage", ArgsUsage: " ", @@ -1837,6 +1826,11 @@ func main() { Usage: "specify the path of the repository (instead of the current working directory)", Argument: "", }, + cli.StringFlag{ + Name: "key", + Usage: "the RSA public key to encrypt file chunks", + Argument: "", + }, }, Usage: "Add an additional storage to be used for the existing repository", ArgsUsage: " ", @@ -1935,6 +1929,11 @@ func main() { Usage: "number of uploading threads", Argument: "", }, + cli.StringFlag{ + Name: "key", + Usage: "the RSA private key to decrypt file chunks from the source storage", + Argument: "", + }, }, Usage: "Copy snapshots between compatible storages", ArgsUsage: " ", diff --git a/src/duplicacy_backupmanager.go b/src/duplicacy_backupmanager.go index 8643f01..ff98339 100644 --- a/src/duplicacy_backupmanager.go +++ b/src/duplicacy_backupmanager.go @@ -20,11 +20,6 @@ import ( "sync" "sync/atomic" "time" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "io/ioutil" - "reflect" ) // BackupManager performs the two major operations, backup and restore, and passes other operations, mostly related to @@ -81,6 +76,11 @@ func CreateBackupManager(snapshotID string, storage Storage, top string, passwor return backupManager } +// loadRSAPrivateKey loads the specifed private key file for decrypting file chunks +func (manager *BackupManager) LoadRSAPrivateKey(keyFile string, passphrase string) { + manager.config.loadRSAPrivateKey(keyFile, passphrase) +} + // SetupSnapshotCache creates the snapshot cache, which is merely a local storage under the default .duplicacy // directory func (manager *BackupManager) SetupSnapshotCache(storageName string) bool { @@ -108,80 +108,6 @@ func (manager *BackupManager) SetupSnapshotCache(storageName string) bool { return true } -// SetupRSAPublicKey loads the specifed public key file for encrypting file chunks -func (manager *BackupManager) SetupRSAPublicKey(keyFile string) { - encodedKey, err := ioutil.ReadFile(keyFile) - if err != nil { - LOG_ERROR("BACKUP_KEY", "Failed to read the public key file: %v", err) - return - } - - decodedKey, _ := pem.Decode(encodedKey) - if decodedKey == nil { - LOG_ERROR("RESTORE_KEY", "unrecognized public key in %s", keyFile) - return - } - if decodedKey.Type != "PUBLIC KEY" { - LOG_ERROR("BACKUP_KEY", "Unsupported public key type %s in %s", decodedKey.Type, keyFile) - return - } - - parsedKey, err := x509.ParsePKIXPublicKey(decodedKey.Bytes) - if err != nil { - LOG_ERROR("BACKUP_KEY", "Failed to parse the public key in %s: %v", keyFile, err) - return - } - - key, ok := parsedKey.(*rsa.PublicKey) - if !ok { - LOG_ERROR("BACKUP_KEY", "Unsupported public key type %s in %s", reflect.TypeOf(parsedKey), keyFile) - return - } - - manager.config.rsaPublicKey = key -} - -// SetupRSAPrivateKey loads the specifed private key file for decrypting file chunks -func (manager *BackupManager) SetupRSAPrivateKey(keyFile string, passphrase string) { - - encodedKey, err := ioutil.ReadFile(keyFile) - if err != nil { - LOG_ERROR("RESTORE_KEY", "Failed to read the private key file: %v", err) - } - - decodedKey, _ := pem.Decode(encodedKey) - if decodedKey == nil { - LOG_ERROR("RESTORE_KEY", "unrecognized private key in %s", keyFile) - return - } - if decodedKey.Type != "RSA PRIVATE KEY" { - LOG_ERROR("RESTORE_KEY", "Unsupported private key type %s in %s", decodedKey.Type, keyFile) - return - } - - var decodedKeyBytes []byte - if passphrase != "" { - decodedKeyBytes, err = x509.DecryptPEMBlock(decodedKey, []byte(passphrase)) - } else { - decodedKeyBytes = decodedKey.Bytes - } - - var parsedKey interface{} - if parsedKey, err = x509.ParsePKCS1PrivateKey(decodedKeyBytes); err != nil { - if parsedKey, err = x509.ParsePKCS8PrivateKey(decodedKeyBytes); err != nil { - LOG_ERROR("RESTORE_KEY", "Failed to parse the private key in %s: %v", keyFile, err) - return - } - } - - key, ok := parsedKey.(*rsa.PrivateKey) - if !ok { - LOG_ERROR("RESTORE_KEY", "Unsupported private key type %s in %s", reflect.TypeOf(parsedKey), keyFile) - return - } - - manager.config.rsaPrivateKey = key -} // setEntryContent sets the 4 content pointers for each entry in 'entries'. 'offset' indicates the value // to be added to the StartChunk and EndChunk points, used when intending to append 'entries' to the @@ -256,6 +182,10 @@ func (manager *BackupManager) Backup(top string, quickMode bool, threads int, ta LOG_DEBUG("BACKUP_PARAMETERS", "top: %s, quick: %t, tag: %s", top, quickMode, tag) + if manager.config.rsaPublicKey != nil && len(manager.config.FileKey) > 0 { + LOG_INFO("BACKUP_KEY", "RSA encryption is enabled" ) + } + remoteSnapshot := manager.SnapshotManager.downloadLatestSnapshot(manager.snapshotID) if remoteSnapshot == nil { remoteSnapshot = CreateEmptySnapshot(manager.snapshotID) @@ -1796,6 +1726,7 @@ func (manager *BackupManager) CopySnapshots(otherManager *BackupManager, snapsho newChunk := otherManager.config.GetChunk() newChunk.Reset(true) newChunk.Write(chunk.GetBytes()) + newChunk.encryptionVersion = chunk.encryptionVersion chunkUploader.StartChunk(newChunk, chunkIndex) totalCopied++ } else { diff --git a/src/duplicacy_chunk.go b/src/duplicacy_chunk.go index df5d664..37d7488 100644 --- a/src/duplicacy_chunk.go +++ b/src/duplicacy_chunk.go @@ -62,6 +62,8 @@ type Chunk struct { config *Config // Every chunk is associated with a Config object. Which hashing algorithm to use is determined // by the config + + encryptionVersion byte // The version type in the encrytion header } // Magic word to identify a duplicacy format encrypted file, plus a version number. @@ -191,7 +193,7 @@ func (chunk *Chunk) Encrypt(encryptionKey []byte, derivationKey string, isSnapsh key := encryptionKey usingRSA := false - if !isSnapshot && chunk.config.rsaPublicKey != nil { + if chunk.config.rsaPublicKey != nil && (!isSnapshot || chunk.encryptionVersion == ENCRYPTION_VERSION_RSA) { // If the chunk is not a snpashot chunk, we attempt to encrypt it with the RSA publick key if there is one randomKey := make([]byte, 32) _, err := rand.Read(randomKey) @@ -319,6 +321,8 @@ func (chunk *Chunk) Decrypt(encryptionKey []byte, derivationKey string) (err err chunk.buffer, encryptedBuffer = encryptedBuffer, chunk.buffer headerLength := len(ENCRYPTION_HEADER) + chunk.encryptionVersion = 0 + if len(encryptionKey) > 0 { key := encryptionKey @@ -343,12 +347,12 @@ func (chunk *Chunk) Decrypt(encryptionKey []byte, derivationKey string) (err err return fmt.Errorf("The storage doesn't seem to be encrypted") } - version := encryptedBuffer.Bytes()[headerLength-1] - if version != 0 && version != ENCRYPTION_VERSION_RSA { - return fmt.Errorf("Unsupported encryption version %d", version) + chunk.encryptionVersion = encryptedBuffer.Bytes()[headerLength-1] + if chunk.encryptionVersion != 0 && chunk.encryptionVersion != ENCRYPTION_VERSION_RSA { + return fmt.Errorf("Unsupported encryption version %d", chunk.encryptionVersion) } - if version == ENCRYPTION_VERSION_RSA { + if chunk.encryptionVersion == ENCRYPTION_VERSION_RSA { if chunk.config.rsaPrivateKey == nil { LOG_ERROR("CHUNK_DECRYPT", "An RSA private key is required to decrypt the chunk") return fmt.Errorf("An RSA private key is required to decrypt the chunk") diff --git a/src/duplicacy_config.go b/src/duplicacy_config.go index 00fc13e..217bd4a 100644 --- a/src/duplicacy_config.go +++ b/src/duplicacy_config.go @@ -10,15 +10,19 @@ import ( "crypto/rand" "crypto/sha256" "crypto/rsa" + "crypto/x509" "encoding/binary" "encoding/hex" "encoding/json" + "encoding/pem" "fmt" "hash" "os" "runtime" "runtime/debug" "sync/atomic" + "io/ioutil" + "reflect" blake2 "github.com/minio/blake2b-simd" ) @@ -85,10 +89,15 @@ type jsonableConfig struct { IDKey string `json:"id-key"` ChunkKey string `json:"chunk-key"` FileKey string `json:"file-key"` + RSAPublicKey string `json:"rsa-public-key"` } func (config *Config) MarshalJSON() ([]byte, error) { + publicKey := []byte {} + if config.rsaPublicKey != nil { + publicKey, _ = x509.MarshalPKIXPublicKey(config.rsaPublicKey) + } return json.Marshal(&jsonableConfig{ aliasedConfig: (*aliasedConfig)(config), ChunkSeed: hex.EncodeToString(config.ChunkSeed), @@ -96,6 +105,7 @@ func (config *Config) MarshalJSON() ([]byte, error) { IDKey: hex.EncodeToString(config.IDKey), ChunkKey: hex.EncodeToString(config.ChunkKey), FileKey: hex.EncodeToString(config.FileKey), + RSAPublicKey: hex.EncodeToString(publicKey), }) } @@ -125,6 +135,19 @@ func (config *Config) UnmarshalJSON(description []byte) (err error) { return fmt.Errorf("Invalid representation of the file key in the config") } + if publicKey, err := hex.DecodeString(aliased.RSAPublicKey); err != nil { + return fmt.Errorf("Invalid hex encoding of the RSA public key in the config") + } else if len(publicKey) > 0 { + parsedKey, err := x509.ParsePKIXPublicKey(publicKey) + if err != nil { + return fmt.Errorf("Invalid RSA public key in the config: %v", err) + } + config.rsaPublicKey = parsedKey.(*rsa.PublicKey) + if config.rsaPublicKey == nil { + return fmt.Errorf("Unsupported public key type %s in the config", reflect.TypeOf(parsedKey)) + } + } + return nil } @@ -145,6 +168,29 @@ func (config *Config) Print() { LOG_INFO("CONFIG_INFO", "Maximum chunk size: %d", config.MaximumChunkSize) LOG_INFO("CONFIG_INFO", "Minimum chunk size: %d", config.MinimumChunkSize) LOG_INFO("CONFIG_INFO", "Chunk seed: %x", config.ChunkSeed) + + LOG_TRACE("CONFIG_INFO", "Hash key: %x", config.HashKey) + LOG_TRACE("CONFIG_INFO", "ID key: %x", config.IDKey) + + if len(config.ChunkKey) >= 0 { + LOG_TRACE("CONFIG_INFO", "File chunks are encrypted") + } + + if len(config.FileKey) >= 0 { + LOG_TRACE("CONFIG_INFO", "Metadata chunks are encrypted") + } + + if config.rsaPublicKey != nil { + pkisPublicKey, _ := x509.MarshalPKIXPublicKey(config.rsaPublicKey) + + publicKey := pem.EncodeToMemory(&pem.Block{ + Type: "PUBLIC KEY", + Bytes: pkisPublicKey, + }) + + LOG_TRACE("CONFIG_INFO", "RSA public key: %s", publicKey) + } + } func CreateConfigFromParameters(compressionLevel int, averageChunkSize int, maximumChunkSize int, mininumChunkSize int, @@ -482,7 +528,7 @@ func UploadConfig(storage Storage, config *Config, password string, iterations i // it simply creates a file named 'config' that stores various parameters as well as a set of keys if encryption // is enabled. func ConfigStorage(storage Storage, iterations int, compressionLevel int, averageChunkSize int, maximumChunkSize int, - minimumChunkSize int, password string, copyFrom *Config, bitCopy bool) bool { + minimumChunkSize int, password string, copyFrom *Config, bitCopy bool, keyFile string) bool { exist, _, _, err := storage.GetFileInfo(0, "config") if err != nil { @@ -501,5 +547,108 @@ func ConfigStorage(storage Storage, iterations int, compressionLevel int, averag return false } + if keyFile != "" { + config.loadRSAPublicKey(keyFile) + } return UploadConfig(storage, config, password, iterations) } + +func (config *Config) loadRSAPublicKey(keyFile string) { + encodedKey, err := ioutil.ReadFile(keyFile) + if err != nil { + LOG_ERROR("BACKUP_KEY", "Failed to read the public key file: %v", err) + return + } + + decodedKey, _ := pem.Decode(encodedKey) + if decodedKey == nil { + LOG_ERROR("RSA_PUBLIC", "unrecognized public key in %s", keyFile) + return + } + if decodedKey.Type != "PUBLIC KEY" { + LOG_ERROR("RSA_PUBLIC", "Unsupported public key type %s in %s", decodedKey.Type, keyFile) + return + } + + parsedKey, err := x509.ParsePKIXPublicKey(decodedKey.Bytes) + if err != nil { + LOG_ERROR("RSA_PUBLIC", "Failed to parse the public key in %s: %v", keyFile, err) + return + } + + key, ok := parsedKey.(*rsa.PublicKey) + if !ok { + LOG_ERROR("RSA_PUBLIC", "Unsupported public key type %s in %s", reflect.TypeOf(parsedKey), keyFile) + return + } + + config.rsaPublicKey = key +} + +// loadRSAPrivateKey loads the specifed private key file for decrypting file chunks +func (config *Config) loadRSAPrivateKey(keyFile string, passphrase string) { + + encodedKey, err := ioutil.ReadFile(keyFile) + if err != nil { + LOG_ERROR("RSA_PRIVATE", "Failed to read the private key file: %v", err) + return + } + + decodedKey, _ := pem.Decode(encodedKey) + if decodedKey == nil { + LOG_ERROR("RSA_PRIVATE", "unrecognized private key in %s", keyFile) + return + } + if decodedKey.Type != "RSA PRIVATE KEY" { + LOG_ERROR("RSA_PRIVATE", "Unsupported private key type %s in %s", decodedKey.Type, keyFile) + return + } + + var decodedKeyBytes []byte + if passphrase != "" { + decodedKeyBytes, err = x509.DecryptPEMBlock(decodedKey, []byte(passphrase)) + } else { + decodedKeyBytes = decodedKey.Bytes + } + + var parsedKey interface{} + if parsedKey, err = x509.ParsePKCS1PrivateKey(decodedKeyBytes); err != nil { + if parsedKey, err = x509.ParsePKCS8PrivateKey(decodedKeyBytes); err != nil { + LOG_ERROR("RSA_PRIVATE", "Failed to parse the private key in %s: %v", keyFile, err) + return + } + } + + key, ok := parsedKey.(*rsa.PrivateKey) + if !ok { + LOG_ERROR("RSA_PRIVATE", "Unsupported private key type %s in %s", reflect.TypeOf(parsedKey), keyFile) + return + } + + data := make([]byte, 32) + _, err = rand.Read(data) + if err != nil { + LOG_ERROR("RSA_PRIVATE", "Failed to generate random data for testing the private key: %v", err) + return + } + + // Now test if the private key matches the public key + encryptedData, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, config.rsaPublicKey, data, nil) + if err != nil { + LOG_ERROR("RSA_PRIVATE", "Failed to encrypt random data with the public key: %v", err) + return + } + + decryptedData, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, key, encryptedData, nil) + if err != nil { + LOG_ERROR("RSA_PRIVATE", "Incorrect private key: %v", err) + return + } + + if !bytes.Equal(data, decryptedData) { + LOG_ERROR("RSA_PRIVATE", "Decrypted data do not match the original data") + return + } + + config.rsaPrivateKey = key +}