diff --git a/AMAZON_S3.md b/AMAZON_S3.md index fce698d..0c1c455 100644 --- a/AMAZON_S3.md +++ b/AMAZON_S3.md @@ -1,8 +1,46 @@ -# Amazon S3 Setup +# Amazon S3 (or compatible) Setup + + +- [Non Amazon, S3 Compatible services](#non-amazon-s3-compatible-services) +- [Setup for Cloudflare R2](#setup-for-cloudflare-r2) +- [Setup for Backblaze B2](#setup-for-backblaze-b2) +- [Standard Amazon s3 setup](#standard-amazon-s3-setup) + - [Generate an access key user and get users ARN:](#generate-an-access-key-user-and-get-users-arn) + - [Add the users permissions to a new \(or existing\) S3 storage bucket](#add-the-users-permissions-to-a-new-or-existing-s3-storage-bucket) + + + + +## Non Amazon, S3 Compatible services +For Amazon S3 compatible storage services you will need to see that company's documentation for how to create a storage bucket and setup an access key. A few examples are below. NOTE: Not all providers support ACL/checksums so you may need to disable these features in the keepass options to use it. For a partial list of S3 compatible providers see: https://rclone.org/s3/ + +## Setup for Cloudflare R2 +- Go to: https://dash.cloudflare.com/sign-up/r2 and create an account +- Go to the R2 buckets page: https://dash.cloudflare.com/?to=/:account/r2 and click "Create bucket" +- Name the bucket whatever you want, just copy this name down. You can leave location on automatic. Then click "Create Bucket". +- Go to the R2 API Token page https://dash.cloudflare.com/?to=/:account/r2/api-tokens and click "Create API token" +- Name the token anything you like, for permissions you can just do " +Object Read & Write", under "Specify bucket(s)" click "Apply to specific buckets only +" and select the bucket you just created. Leave the other items at default and click "Create API Token" +- Copy the access key id, secret access key, and the "jurisdiction-specific endpoints for S3 Client" url (this is the service url). +- Enter these 3 items in KeePassSync options. + + +## Setup for Backblaze B2 + +- Go to: https://www.backblaze.com/cloud-storage and signup (make sure to choose a region from the dropdown below and on the right side of the "Sign Up Now" button (or you will get an odd error). +- Once signed in go to: https://secure.backblaze.com/b2_buckets.htm and click "Create a Bucket". Name it whatever you would like, leave "Files in Bucket are" set to private, set the encryption to enabled if you like (not required) do NOT enable object lock. click "Create a Bucket" +- One the bucket is created you will be taken back to your bucket list page. From here copy the endpoint url (ie: s3.us-west-007.backblazeb2.com). +- Next click the "How to connect to this bucket" in the box showing that bucket on the bucket list. click "Create an app key". +- On the app key page click "Add a New Application Key" give the key whatever name you want. You just need to copy the "Key ID" and the "Application key" and put these into keepass sync (along with the endpoint URL from above). +- Enter these 3 items in KeePassSync options. + + +## Standard Amazon s3 setup You can give the user the ability to create buckets then you can just create the user and KPSync will do the rest, but a more secure way is to create the bucket yourself. There may be an easier way to do this but this will work: -## Generate an access key user and get users ARN: +### Generate an access key user and get users ARN: - Login to AWS on your account click your name in the upper right, go to Security Credentials. - Click users under Access Management on the left. - Click "Create User" button on the right. @@ -17,7 +55,7 @@ You can give the user the ability to create buckets then you can just create the - On the next page Copy the access key ID and secret to paste into keepass later, copy their ARN from the upper left on the page (should start with "arn:aws....") -## Add the users permissions to a new (or existing) S3 storage bucket +### Add the users permissions to a new (or existing) S3 storage bucket - Next on the top left click "Services" go to storage and S3. - Click "Create Bucket", git it a name under bucket name, leave ACL's disabled, block all public access (checked by default) diff --git a/KeePassSync/Properties/AssemblyInfo.cs b/KeePassSync/Properties/AssemblyInfo.cs index 53435ed..f90c4ed 100644 --- a/KeePassSync/Properties/AssemblyInfo.cs +++ b/KeePassSync/Properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -31,5 +31,5 @@ // // You can specify all the values or you can default the Revision and Build Numbers // by using the '*' as shown below: -[assembly: AssemblyVersion("7.2.0.0")] -[assembly: AssemblyFileVersion("7.2.0.0")] +[assembly: AssemblyVersion("8.0.0.0")] +[assembly: AssemblyFileVersion("8.0.0.0")] diff --git a/KeePassSync/Providers/S3/AccountDetails.cs b/KeePassSync/Providers/S3/AccountDetails.cs index 4841a41..e196fb1 100644 --- a/KeePassSync/Providers/S3/AccountDetails.cs +++ b/KeePassSync/Providers/S3/AccountDetails.cs @@ -1,38 +1,50 @@ -using System; -using System.Windows.Forms; - -namespace KeePassSync.Providers.S3 { - public partial class AccountDetails : UserControl { - public string AccessKey { - get { return txtAccessKey.Text; } - set { txtAccessKey.Text = value; } - } - - public string SecretAccessKey { - get { return txtSecretAccessKey.Text; } - set { txtSecretAccessKey.Text = value; } - } - - public string BucketName { - get { return txtBucketName.Text; } - set { txtBucketName.Text = value; } - } - public bool CreateBackups { - get { return cbxDailyBackups.Checked; } - set { cbxDailyBackups.Checked = value; } - - } - - public AccountDetails() { - InitializeComponent(); - } - - private void AccountDetails_Load(object sender, EventArgs e) { - - } - - private void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { - System.Diagnostics.Process.Start("https://github.com/mitchcapper/KeePassSync/blob/master/AMAZON_S3.md"); - } - } -} +using System; +using System.Windows.Forms; + +namespace KeePassSync.Providers.S3 { + public partial class AccountDetails : UserControl { + public string AccessKey { + get { return txtAccessKey.Text; } + set { txtAccessKey.Text = value; } + } + + public string SecretAccessKey { + get { return txtSecretAccessKey.Text; } + set { txtSecretAccessKey.Text = value; } + } + + public string BucketName { + get { return txtBucketName.Text; } + set { txtBucketName.Text = value; } + } + public bool CreateBackups { + get { return cbxDailyBackups.Checked; } + set { cbxDailyBackups.Checked = value; } + + } + public string ServiceURL { + get { return txtServiceUrl.Text; } + set { txtServiceUrl.Text = value; } + } + public bool UseChecksums { + get { return cbxTransferChecksums.Checked; } + set { cbxTransferChecksums.Checked = value; } + } + public bool UseACLs { + get { return cbxCopyACLS.Checked; } + set { cbxCopyACLS.Checked = value; } + } + + public AccountDetails() { + InitializeComponent(); + } + + private void AccountDetails_Load(object sender, EventArgs e) { + + } + + private void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { + System.Diagnostics.Process.Start("https://github.com/mitchcapper/KeePassSync/blob/master/AMAZON_S3.md"); + } + } +} diff --git a/KeePassSync/Providers/S3/AccountDetails.designer.cs b/KeePassSync/Providers/S3/AccountDetails.designer.cs index a8e9820..c122a23 100644 --- a/KeePassSync/Providers/S3/AccountDetails.designer.cs +++ b/KeePassSync/Providers/S3/AccountDetails.designer.cs @@ -33,12 +33,18 @@ private void InitializeComponent() { this.label2 = new System.Windows.Forms.Label(); this.cbxDailyBackups = new System.Windows.Forms.CheckBox(); this.linkLabel1 = new System.Windows.Forms.LinkLabel(); + this.cbxTransferChecksums = new System.Windows.Forms.CheckBox(); + this.label5 = new System.Windows.Forms.Label(); + this.cbxCopyACLS = new System.Windows.Forms.CheckBox(); + this.label7 = new System.Windows.Forms.Label(); + this.txtServiceUrl = new System.Windows.Forms.TextBox(); + this.label8 = new System.Windows.Forms.Label(); this.SuspendLayout(); // // label1 // this.label1.AutoSize = true; - this.label1.Location = new System.Drawing.Point(6, 25); + this.label1.Location = new System.Drawing.Point(6, 53); this.label1.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0); this.label1.Name = "label1"; this.label1.Size = new System.Drawing.Size(157, 25); @@ -47,7 +53,7 @@ private void InitializeComponent() { // // txtAccessKey // - this.txtAccessKey.Location = new System.Drawing.Point(212, 19); + this.txtAccessKey.Location = new System.Drawing.Point(212, 47); this.txtAccessKey.Margin = new System.Windows.Forms.Padding(26, 6, 6, 6); this.txtAccessKey.Name = "txtAccessKey"; this.txtAccessKey.Size = new System.Drawing.Size(390, 31); @@ -56,7 +62,7 @@ private void InitializeComponent() { // label3 // this.label3.AutoSize = true; - this.label3.Location = new System.Drawing.Point(6, 125); + this.label3.Location = new System.Drawing.Point(6, 153); this.label3.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0); this.label3.Name = "label3"; this.label3.Size = new System.Drawing.Size(146, 25); @@ -65,7 +71,7 @@ private void InitializeComponent() { // // txtBucketName // - this.txtBucketName.Location = new System.Drawing.Point(162, 119); + this.txtBucketName.Location = new System.Drawing.Point(162, 147); this.txtBucketName.Margin = new System.Windows.Forms.Padding(6); this.txtBucketName.Name = "txtBucketName"; this.txtBucketName.Size = new System.Drawing.Size(440, 31); @@ -74,7 +80,7 @@ private void InitializeComponent() { // label4 // this.label4.AutoSize = true; - this.label4.Location = new System.Drawing.Point(6, 75); + this.label4.Location = new System.Drawing.Point(6, 103); this.label4.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0); this.label4.Name = "label4"; this.label4.Size = new System.Drawing.Size(199, 25); @@ -83,7 +89,7 @@ private void InitializeComponent() { // // txtSecretAccessKey // - this.txtSecretAccessKey.Location = new System.Drawing.Point(212, 69); + this.txtSecretAccessKey.Location = new System.Drawing.Point(212, 97); this.txtSecretAccessKey.Margin = new System.Windows.Forms.Padding(6); this.txtSecretAccessKey.Name = "txtSecretAccessKey"; this.txtSecretAccessKey.PasswordChar = '*'; @@ -93,7 +99,7 @@ private void InitializeComponent() { // label6 // this.label6.AutoSize = true; - this.label6.Location = new System.Drawing.Point(16, 232); + this.label6.Location = new System.Drawing.Point(16, 388); this.label6.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0); this.label6.MaximumSize = new System.Drawing.Size(600, 0); this.label6.Name = "label6"; @@ -105,7 +111,7 @@ private void InitializeComponent() { // label2 // this.label2.AutoSize = true; - this.label2.Location = new System.Drawing.Point(6, 170); + this.label2.Location = new System.Drawing.Point(6, 268); this.label2.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0); this.label2.Name = "label2"; this.label2.Size = new System.Drawing.Size(520, 25); @@ -115,7 +121,7 @@ private void InitializeComponent() { // cbxDailyBackups // this.cbxDailyBackups.AutoSize = true; - this.cbxDailyBackups.Location = new System.Drawing.Point(574, 169); + this.cbxDailyBackups.Location = new System.Drawing.Point(574, 267); this.cbxDailyBackups.Margin = new System.Windows.Forms.Padding(6); this.cbxDailyBackups.Name = "cbxDailyBackups"; this.cbxDailyBackups.Size = new System.Drawing.Size(28, 27); @@ -125,7 +131,7 @@ private void InitializeComponent() { // linkLabel1 // this.linkLabel1.AutoSize = true; - this.linkLabel1.Location = new System.Drawing.Point(6, 199); + this.linkLabel1.Location = new System.Drawing.Point(6, 13); this.linkLabel1.Name = "linkLabel1"; this.linkLabel1.Size = new System.Drawing.Size(184, 25); this.linkLabel1.TabIndex = 13; @@ -133,10 +139,73 @@ private void InitializeComponent() { this.linkLabel1.Text = "Setup Instructions"; this.linkLabel1.LinkClicked += new System.Windows.Forms.LinkLabelLinkClickedEventHandler(this.linkLabel1_LinkClicked); // + // cbxTransferChecksums + // + this.cbxTransferChecksums.AutoSize = true; + this.cbxTransferChecksums.Location = new System.Drawing.Point(574, 308); + this.cbxTransferChecksums.Margin = new System.Windows.Forms.Padding(6); + this.cbxTransferChecksums.Name = "cbxTransferChecksums"; + this.cbxTransferChecksums.Size = new System.Drawing.Size(28, 27); + this.cbxTransferChecksums.TabIndex = 15; + this.cbxTransferChecksums.UseVisualStyleBackColor = true; + // + // label5 + // + this.label5.AutoSize = true; + this.label5.Location = new System.Drawing.Point(6, 309); + this.label5.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0); + this.label5.Name = "label5"; + this.label5.Size = new System.Drawing.Size(502, 25); + this.label5.TabIndex = 14; + this.label5.Text = "Use Transfer Checksums (not all services support):"; + // + // cbxCopyACLS + // + this.cbxCopyACLS.AutoSize = true; + this.cbxCopyACLS.Location = new System.Drawing.Point(574, 350); + this.cbxCopyACLS.Margin = new System.Windows.Forms.Padding(6); + this.cbxCopyACLS.Name = "cbxCopyACLS"; + this.cbxCopyACLS.Size = new System.Drawing.Size(28, 27); + this.cbxCopyACLS.TabIndex = 17; + this.cbxCopyACLS.UseVisualStyleBackColor = true; + // + // label7 + // + this.label7.AutoSize = true; + this.label7.Location = new System.Drawing.Point(6, 351); + this.label7.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0); + this.label7.Name = "label7"; + this.label7.Size = new System.Drawing.Size(514, 25); + this.label7.TabIndex = 16; + this.label7.Text = "If existing entry exists, copy its ACL (not all support):"; + // + // txtServiceUrl + // + this.txtServiceUrl.Location = new System.Drawing.Point(162, 205); + this.txtServiceUrl.Margin = new System.Windows.Forms.Padding(6); + this.txtServiceUrl.Name = "txtServiceUrl"; + this.txtServiceUrl.Size = new System.Drawing.Size(440, 31); + this.txtServiceUrl.TabIndex = 18; + // + // label8 + // + this.label8.Location = new System.Drawing.Point(6, 184); + this.label8.Margin = new System.Windows.Forms.Padding(6, 0, 6, 0); + this.label8.Name = "label8"; + this.label8.Size = new System.Drawing.Size(157, 77); + this.label8.TabIndex = 19; + this.label8.Text = "Service URL (leave blank for Amazon):"; + // // AccountDetails // this.AutoScaleDimensions = new System.Drawing.SizeF(12F, 25F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.txtServiceUrl); + this.Controls.Add(this.label8); + this.Controls.Add(this.cbxCopyACLS); + this.Controls.Add(this.label7); + this.Controls.Add(this.cbxTransferChecksums); + this.Controls.Add(this.label5); this.Controls.Add(this.linkLabel1); this.Controls.Add(this.cbxDailyBackups); this.Controls.Add(this.label2); @@ -149,7 +218,7 @@ private void InitializeComponent() { this.Controls.Add(this.label1); this.Margin = new System.Windows.Forms.Padding(6); this.Name = "AccountDetails"; - this.Size = new System.Drawing.Size(624, 296); + this.Size = new System.Drawing.Size(624, 451); this.Load += new System.EventHandler(this.AccountDetails_Load); this.ResumeLayout(false); this.PerformLayout(); @@ -167,6 +236,12 @@ private void InitializeComponent() { private System.Windows.Forms.Label label6; private System.Windows.Forms.Label label2; private System.Windows.Forms.CheckBox cbxDailyBackups; - private System.Windows.Forms.LinkLabel linkLabel1; + private System.Windows.Forms.LinkLabel linkLabel1; + private System.Windows.Forms.CheckBox cbxTransferChecksums; + private System.Windows.Forms.Label label5; + private System.Windows.Forms.CheckBox cbxCopyACLS; + private System.Windows.Forms.Label label7; + private System.Windows.Forms.TextBox txtServiceUrl; + private System.Windows.Forms.Label label8; } } diff --git a/KeePassSync/Providers/S3/OurAmazonS3Client.cs b/KeePassSync/Providers/S3/OurAmazonS3Client.cs new file mode 100644 index 0000000..aece2f8 --- /dev/null +++ b/KeePassSync/Providers/S3/OurAmazonS3Client.cs @@ -0,0 +1,66 @@ +using System.Collections.Generic; +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.Runtime.Internal; +using Amazon.S3.Model.Internal.MarshallTransformations; +using System.Threading.Tasks; +using Amazon.Runtime; +using Amazon.Runtime.Internal.Util; + + +namespace KeePassSync.Providers.S3 { + + //just used to work around some 3rd party client oddities + internal class OurAmazonS3Client : AmazonS3Client { + public OurAmazonS3Client(string awsAccessKeyId, string awsSecretAccessKey, AmazonS3Config clientConfig, List HeadersToStrip = null) : base(awsAccessKeyId, awsSecretAccessKey, clientConfig) { + if (HeadersToStrip != null) + RuntimePipeline.AddHandlerBefore(new HeaderStripHandler(HeadersToStrip)); + + + } + + public class HeaderStripHandler : IPipelineHandler { + private List headersToStrip; + + public HeaderStripHandler(List headersToStrip) { + this.headersToStrip = headersToStrip; + } + + public ILogger Logger { get; set; } + public IPipelineHandler InnerHandler { get; set; } + public IPipelineHandler OuterHandler { get; set; } + + public async Task InvokeAsync(IExecutionContext executionContext) where T : AmazonWebServiceResponse, new() { + RemoveBadHeaders(executionContext); + if (InnerHandler == null) + return default(T); + return await InnerHandler.InvokeAsync(executionContext); + } + + private void RemoveBadHeaders(IExecutionContext executionContext) { + if (headersToStrip != null) { + foreach (var header in headersToStrip) + executionContext.RequestContext.Request.Headers.Remove(header); + } + } + + public void InvokeSync(IExecutionContext executionContext) { + RemoveBadHeaders(executionContext); + if (InnerHandler == null) + return; + InnerHandler.InvokeSync(executionContext); + } + } + + public override CopyObjectResponse CopyObject(CopyObjectRequest request) { + var invokeOptions = new InvokeOptions(); + invokeOptions.RequestMarshaller = CopyObjectRequestMarshaller.Instance; + invokeOptions.ResponseUnmarshaller = CopyObjectResponseUnmarshaller.Instance; + + return Invoke(request, invokeOptions); + } + + } + +} + diff --git a/KeePassSync/Providers/S3/S3Provider.cs b/KeePassSync/Providers/S3/S3Provider.cs index bbcac0b..220f335 100644 --- a/KeePassSync/Providers/S3/S3Provider.cs +++ b/KeePassSync/Providers/S3/S3Provider.cs @@ -1,252 +1,294 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Security.Cryptography; -using System.Windows.Forms; -using KeePassLib; -using KeePassLib.Security; -using Amazon.S3; -using Amazon.S3.Model; -using Amazon.S3.Util; - - -namespace KeePassSync.Providers.S3 { - +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Security.Cryptography; +using System.Windows.Forms; +using KeePassLib; +using KeePassLib.Security; +using Amazon.S3; +using Amazon.S3.Model; +using Amazon.S3.Util; + + +namespace KeePassSync.Providers.S3 { + public class S3Provider : IOnlineProvider { - #region -- Private data -- - private const string m_Name = "S3"; - private string[] m_AcceptedNames = { "S3" }; - private AccountDetails m_UserControl; - #endregion - - private const string access_key_id_field = PwDefs.UserNameField; - private const string bucket_name_field = PwDefs.UrlField; - private const string create_backups_field = "create_backups"; - + #region -- Private data -- + private const string m_Name = "S3"; + private string[] m_AcceptedNames = { "S3" }; + private AccountDetails m_UserControl; + #endregion + + private const string access_key_id_field = PwDefs.UserNameField; + private const string bucket_name_field = PwDefs.UrlField; + private const string use_checksums_field = "use_checksums"; + private const string use_acls_field = "use_acls"; + private const string custom_service_url_field = "service_url"; + private const string create_backups_field = "create_backups"; + private const string secret_access_key_field = PwDefs.PasswordField; private bool memprotect_secret_access_key = true; - - public override KeePassSyncErr Initialize(KeePassSyncExt mainInterface) { - KeePassSyncErr ret = base.Initialize(mainInterface); - - m_UserControl = new AccountDetails(); - - m_IsInitialized = (ret == KeePassSyncErr.None); - - return ret; - } - public override void DecodeEntry(PwEntry entry) { - m_UserControl.AccessKey = read_PwEntry_string(entry, access_key_id_field); - m_UserControl.SecretAccessKey = read_PwEntry_string(entry, secret_access_key_field); - m_UserControl.BucketName = read_PwEntry_string(entry, bucket_name_field); - String backup_str = read_PwEntry_string(entry, create_backups_field); - - m_UserControl.CreateBackups = false; - if (backup_str == "true") - m_UserControl.CreateBackups = true; - } - - public override void EncodeEntry(PwEntry entry) { - write_PwEntry_string(entry, access_key_id_field, m_UserControl.AccessKey, false); - write_PwEntry_string(entry, secret_access_key_field, m_UserControl.SecretAccessKey, memprotect_secret_access_key); - write_PwEntry_string(entry, bucket_name_field, m_UserControl.BucketName, false); - write_PwEntry_string(entry, create_backups_field, m_UserControl.CreateBackups ? "true" : "false", false); - } - public void write_PwEntry_string(PwEntry entry, String key, String value, bool in_memory_encrypt) { - entry.Strings.Set(key, new ProtectedString(in_memory_encrypt, value)); - } - public string read_PwEntry_string(PwEntry entry, String key) { - ProtectedString str = entry.Strings.Get(key); - if (str == null) - return ""; - return str.ReadString(); - } - public override KeePassSyncErr ValidateOptions(OptionsData options) { - KeePassSyncErr ret = KeePassSyncErr.None; - PwEntry entry = m_OptionData.PasswordEntry; - AccountDetails old_details = m_UserControl; - m_UserControl = new AccountDetails(); - DecodeEntry(entry); - ret = verify_bucket_or_create(BucketName); - m_UserControl = old_details; - return ret; - } - - public override string CreateAccountLink { - get { return "http://aws.amazon.com/s3/"; } - } - public override string Name { - get { return m_Name; } - } - - public override string[] AcceptedNames { - get { return m_AcceptedNames; } - } - - public override string[] Databases { - get { - return GetDatabases(m_OptionData.PasswordEntry); - } - } - private AmazonS3Client GetClient() { - - return new AmazonS3Client(m_UserControl.AccessKey, m_UserControl.SecretAccessKey, new AmazonS3Config { ServiceURL = "https://s3.amazonaws.com" }); - } - public override KeePassSyncErr PutFile(PwEntry entry, string remoteFilename, string localFilename) { - try { - DecodeEntry(entry); - using (var fs = File.OpenRead(localFilename)) { - KeePassSyncErr err = verify_bucket_or_create(BucketName); - if (err != KeePassSyncErr.None) - return err; - - var client = GetClient(); - S3AccessControlList acl = null; - if (FileExists(remoteFilename)) { - string backupFilename = remoteFilename + ".bkup_day" + DateTime.Today.Day; - acl = client.GetACL(new GetACLRequest() { BucketName = BucketName, Key = remoteFilename }).AccessControlList; - if (m_UserControl.CreateBackups) - client.CopyObject(BucketName, remoteFilename, BucketName, backupFilename); - - - } - var hash = GetSHA1(fs); //s3 will auto seek to start on stream after - client.PutObject(new PutObjectRequest { BucketName = BucketName, Key = remoteFilename, InputStream = fs, AutoCloseStream = false, ChecksumSHA1 = hash }); - if (acl != null) { - try { - client.PutACL(new PutACLRequest { BucketName = BucketName, Key = remoteFilename, AccessControlList = acl }); - } catch (AmazonS3Exception s3e) { - if (s3e.ErrorCode != "AccessControlListNotSupported") - throw s3e; - } - } - - - } - } catch (Exception e) { - return convert_exception(e); - } - return KeePassSyncErr.None; - } - private string GetSHA1(Stream stream) { - using (var sha1 = SHA1.Create()) { - return Convert.ToBase64String(sha1.ComputeHash(stream)); - } - - } - public override UserControl GetUserControl() { - return m_UserControl; - } - public override string[] GetDatabases(PwEntry entry) { - var client = GetClient(); - DecodeEntry(entry); - List databases = new List(); - var files = client.ListObjectsV2(new ListObjectsV2Request { BucketName = BucketName }); - foreach (var file in files.S3Objects) { - if (file.Key.EndsWith(".kdbx")) - databases.Add(file.Key); - } - return databases.ToArray(); - } - - private bool FileExists(string filename) { - - var client = GetClient(); - try { - var meta = client.GetObjectMetadata(BucketName, filename); - return true; - } catch (AmazonS3Exception s3) { - if (s3.StatusCode != HttpStatusCode.NotFound) - throw s3; - return false; - } - } - //public string BucketName => m_UserControl.BucketName?.ToLower(); - //public string BucketName { get { return m_UserControl.BucketName == null ? null : m_UserControl.BucketName.ToLower(); } } - public string BucketName { get { return m_UserControl.BucketName == null ? null : m_UserControl.BucketName; } } - public override KeePassSyncErr GetFile(PwEntry entry, string remoteFilename, string localFilename) { - DecodeEntry(entry); - var client = GetClient(); - try { - verify_bucket_or_create(BucketName); - var obj = client.GetObject(new GetObjectRequest {BucketName = BucketName, Key = remoteFilename,ChecksumMode = ChecksumMode.ENABLED }); - using (var memStream = new MemoryStream()) { - using (var resp = obj.ResponseStream) { - resp.CopyTo(memStream); - } - memStream.Seek(0, SeekOrigin.Begin); - var hash = GetSHA1(memStream); - - //the alg must have been set to sha1 during upload for this to work, prior to 2024 we didn't use sha, in addition if a user manually copied it might lose the SHA checksum - if (obj.ResponseMetadata.ChecksumAlgorithm == Amazon.Runtime.CoreChecksumAlgorithm.SHA1 && !obj.ChecksumSHA1.Equals(hash, StringComparison.OrdinalIgnoreCase)) - //throw new Exception($"File downloaded but our hash of: {hash} does not match server hash of: {obj.ChecksumSHA1}"); - throw new Exception("File downloaded but our hash of: " + hash +" does not match server hash of: " + obj.ChecksumSHA1); - - using (var fs = File.OpenWrite(localFilename)) { - memStream.Seek(0, SeekOrigin.Begin); - memStream.CopyTo(fs); - } - } - } catch (Exception e) { - if (e.Message == "The specified key does not exist.") - return KeePassSyncErr.FileNotFound; - return convert_exception(e); - } - return KeePassSyncErr.None; - - } - private KeePassSyncErr convert_exception(Exception e) { - if (e.GetType() == typeof(WebException)) { - WebException w_exp = (WebException)e; - if (w_exp.Status == WebExceptionStatus.ConnectFailure || w_exp.Status == WebExceptionStatus.NameResolutionFailure) - return KeePassSyncErr.NotConnected; - - } - KeePassSyncErr ret; - string msg; - StatusPriority priority = StatusPriority.eMessageBoxFatal; - switch (e.Message) { - case "The specified bucket is not valid.": - ret = KeePassSyncErr.Error; - msg = "Unable to access or create the bucket, make sure bucketname is only lowercase characters and numbers and that you own it (if it exists) min 3 chars max 63"; - break; - case "Access Denied": - ret = KeePassSyncErr.Error; - msg = "If the bucket exists, does the access key you are using have read/write access to it? if it doesn't exist do you have permissions with this access key to create? If not create yourself"; - break; - case "The specified key does not exist.": - ret = KeePassSyncErr.FileNotFound; - msg = "Tried to get file we could not find"; - priority = StatusPriority.eStatusBar; - break; - case "The request signature we calculated does not match the signature you provided. Check your key and signing method.": - case "The AWS Access Key Id you provided does not exist in our records.": - ret = KeePassSyncErr.InvalidCredentials; - msg = "Invalid Credentials"; - break; - default: - msg = e.Message + "\r\n" + e.StackTrace; - ret = KeePassSyncErr.Error; - break; - } - m_MainInterface.SetStatus(priority, "KeyPassSync_S3: " + msg); - return ret; - } - - - private KeePassSyncErr verify_bucket_or_create(String bucket_name) { - try { - var client = GetClient(); - if (!AmazonS3Util.DoesS3BucketExistV2(client, bucket_name)) - client.PutBucket(bucket_name); - return KeePassSyncErr.None; - - } catch (Exception e) { - return convert_exception(e); - } - } - - } -} + + public override KeePassSyncErr Initialize(KeePassSyncExt mainInterface) { + KeePassSyncErr ret = base.Initialize(mainInterface); + + m_UserControl = new AccountDetails(); + + m_IsInitialized = (ret == KeePassSyncErr.None); + + return ret; + } + public override void DecodeEntry(PwEntry entry) { + m_UserControl.AccessKey = read_PwEntry_string(entry, access_key_id_field); + m_UserControl.SecretAccessKey = read_PwEntry_string(entry, secret_access_key_field); + m_UserControl.BucketName = read_PwEntry_string(entry, bucket_name_field); + + m_UserControl.CreateBackups = GetBoolField(entry, create_backups_field); + m_UserControl.UseACLs = GetBoolField(entry, use_acls_field); + m_UserControl.UseChecksums = GetBoolField(entry, use_checksums_field); + m_UserControl.ServiceURL = read_PwEntry_string(entry, custom_service_url_field); + } + private bool GetBoolField(PwEntry entry, String fieldname) { + var str = read_PwEntry_string(entry, fieldname); + return str == "true"; + } + private void SetBoolField(PwEntry entry, String fieldname, bool value) { + write_PwEntry_string(entry, fieldname, value ? "true" : "false", false); + } + + public override void EncodeEntry(PwEntry entry) { + write_PwEntry_string(entry, access_key_id_field, m_UserControl.AccessKey, false); + write_PwEntry_string(entry, secret_access_key_field, m_UserControl.SecretAccessKey, memprotect_secret_access_key); + write_PwEntry_string(entry, bucket_name_field, m_UserControl.BucketName, false); + write_PwEntry_string(entry, custom_service_url_field, m_UserControl.ServiceURL, false); + SetBoolField(entry, create_backups_field, m_UserControl.CreateBackups); + SetBoolField(entry, use_acls_field, m_UserControl.UseACLs); + SetBoolField(entry, use_checksums_field, m_UserControl.UseChecksums); + } + public void write_PwEntry_string(PwEntry entry, String key, String value, bool in_memory_encrypt) { + entry.Strings.Set(key, new ProtectedString(in_memory_encrypt, value)); + } + public string read_PwEntry_string(PwEntry entry, String key) { + ProtectedString str = entry.Strings.Get(key); + if (str == null) + return ""; + return str.ReadString(); + } + public override KeePassSyncErr ValidateOptions(OptionsData options) { + KeePassSyncErr ret = KeePassSyncErr.None; + PwEntry entry = m_OptionData.PasswordEntry; + AccountDetails old_details = m_UserControl; + m_UserControl = new AccountDetails(); + DecodeEntry(entry); + ret = verify_bucket_or_create(BucketName); + m_UserControl = old_details; + return ret; + } + + public override string CreateAccountLink { + get { return "http://aws.amazon.com/s3/"; } + } + public override string Name { + get { return m_Name; } + } + + public override string[] AcceptedNames { + get { return m_AcceptedNames; } + } + + public override string[] Databases { + get { + return GetDatabases(m_OptionData.PasswordEntry); + } + } + private bool UsePayloadSigning { get { return String.IsNullOrWhiteSpace(m_UserControl.ServiceURL); } } + private AmazonS3Client GetClient() { + var isS3Official = String.IsNullOrWhiteSpace(m_UserControl.ServiceURL); + if (!isS3Official && m_UserControl.ServiceURL.StartsWith("http", StringComparison.CurrentCultureIgnoreCase) == false) + m_UserControl.ServiceURL = "https://" + m_UserControl.ServiceURL; + + var url = isS3Official ? "https://s3.amazonaws.com" : m_UserControl.ServiceURL; + var cfg = new AmazonS3Config { ServiceURL = url }; + //cfg.ProxyHost = "127.0.0.1";cfg.ProxyPort = 1234; + List headerRemove= null; + if (!isS3Official) { + headerRemove = new List {"x-amz-tagging-directive"}; + } + + + var client = new OurAmazonS3Client(m_UserControl.AccessKey, m_UserControl.SecretAccessKey, cfg,headerRemove); + + return client; + + } + public override KeePassSyncErr PutFile(PwEntry entry, string remoteFilename, string localFilename) { + try { + DecodeEntry(entry); + using (var fs = File.OpenRead(localFilename)) { + KeePassSyncErr err = verify_bucket_or_create(BucketName); + if (err != KeePassSyncErr.None) + return err; + + var client = GetClient(); + S3AccessControlList acl = null; + if (FileExists(remoteFilename)) { + string backupFilename = remoteFilename + ".bkup_day" + DateTime.Today.Day; + if (m_UserControl.UseACLs) + acl = client.GetACL(new GetACLRequest() { BucketName = BucketName, Key = remoteFilename }).AccessControlList; + if (m_UserControl.CreateBackups) { + var copy = new CopyObjectRequest { DestinationBucket = BucketName, SourceBucket = BucketName, SourceKey = remoteFilename, DestinationKey = backupFilename }; + + + client.CopyObject(copy); + } + + + } + var hash = GetSHA1(fs); //s3 will auto seek to start on stream after + var req = new PutObjectRequest { BucketName = BucketName, Key = remoteFilename, InputStream = fs, AutoCloseStream = false, DisablePayloadSigning = !UsePayloadSigning }; + if (m_UserControl.UseChecksums) + req.ChecksumSHA1 = hash; + client.PutObject(req); + if (acl != null) { + try { + client.PutACL(new PutACLRequest { BucketName = BucketName, Key = remoteFilename, AccessControlList = acl }); + } catch (AmazonS3Exception s3e) { + if (s3e.ErrorCode != "AccessControlListNotSupported") + throw s3e; + } + } + + + } + } catch (Exception e) { + return convert_exception(e); + } + return KeePassSyncErr.None; + } + private string GetSHA1(Stream stream) { + using (var sha1 = SHA1.Create()) { + return Convert.ToBase64String(sha1.ComputeHash(stream)); + } + + } + public override UserControl GetUserControl() { + return m_UserControl; + } + public override string[] GetDatabases(PwEntry entry) { + var client = GetClient(); + DecodeEntry(entry); + List databases = new List(); + var files = client.ListObjectsV2(new ListObjectsV2Request { BucketName = BucketName }); + foreach (var file in files.S3Objects) { + if (file.Key.EndsWith(".kdbx")) + databases.Add(file.Key); + } + return databases.ToArray(); + } + + private bool FileExists(string filename) { + + var client = GetClient(); + try { + var meta = client.GetObjectMetadata(BucketName, filename); + return true; + } catch (AmazonS3Exception s3) { + if (s3.StatusCode != HttpStatusCode.NotFound) + throw s3; + return false; + } + } + //public string BucketName => m_UserControl.BucketName?.ToLower(); + //public string BucketName { get { return m_UserControl.BucketName == null ? null : m_UserControl.BucketName.ToLower(); } } + public string BucketName { get { return m_UserControl.BucketName == null ? null : m_UserControl.BucketName; } } + public override KeePassSyncErr GetFile(PwEntry entry, string remoteFilename, string localFilename) { + DecodeEntry(entry); + var client = GetClient(); + try { + verify_bucket_or_create(BucketName); + var req = new GetObjectRequest { BucketName = BucketName, Key = remoteFilename }; + if (m_UserControl.UseChecksums) + req.ChecksumMode = ChecksumMode.ENABLED; + + var obj = client.GetObject(req); + using (var memStream = new MemoryStream()) { + using (var resp = obj.ResponseStream) { + resp.CopyTo(memStream); + } + memStream.Seek(0, SeekOrigin.Begin); + var hash = GetSHA1(memStream); + + //the alg must have been set to sha1 during upload for this to work, prior to 2024 we didn't use sha, in addition if a user manually copied it might lose the SHA checksum + if (m_UserControl.UseChecksums && obj.ResponseMetadata.ChecksumAlgorithm == Amazon.Runtime.CoreChecksumAlgorithm.SHA1 && !obj.ChecksumSHA1.Equals(hash, StringComparison.OrdinalIgnoreCase)) + //throw new Exception($"File downloaded but our hash of: {hash} does not match server hash of: {obj.ChecksumSHA1}"); + throw new Exception("File downloaded but our hash of: " + hash + " does not match server hash of: " + obj.ChecksumSHA1); + + using (var fs = File.OpenWrite(localFilename)) { + memStream.Seek(0, SeekOrigin.Begin); + memStream.CopyTo(fs); + } + } + } catch (Exception e) { + var ae = e as AmazonS3Exception; + if (e.Message == "The specified key does not exist." || (ae != null && ae.ErrorCode=="NoSuchKey") )//errorcode check for 3rd parties that may not conform + return KeePassSyncErr.FileNotFound; + return convert_exception(e); + } + return KeePassSyncErr.None; + + } + private KeePassSyncErr convert_exception(Exception e) { + if (e.GetType() == typeof(WebException)) { + WebException w_exp = (WebException)e; + if (w_exp.Status == WebExceptionStatus.ConnectFailure || w_exp.Status == WebExceptionStatus.NameResolutionFailure) + return KeePassSyncErr.NotConnected; + + } + KeePassSyncErr ret; + string msg; + StatusPriority priority = StatusPriority.eMessageBoxFatal; + switch (e.Message) { + case "The specified bucket is not valid.": + ret = KeePassSyncErr.Error; + msg = "Unable to access or create the bucket, make sure bucketname is only lowercase characters and numbers and that you own it (if it exists) min 3 chars max 63"; + break; + case "Access Denied": + ret = KeePassSyncErr.Error; + msg = "If the bucket exists, does the access key you are using have read/write access to it? if it doesn't exist do you have permissions with this access key to create? If not create yourself"; + break; + case "The specified key does not exist.": + ret = KeePassSyncErr.FileNotFound; + msg = "Tried to get file we could not find"; + priority = StatusPriority.eStatusBar; + break; + case "The request signature we calculated does not match the signature you provided. Check your key and signing method.": + case "The AWS Access Key Id you provided does not exist in our records.": + ret = KeePassSyncErr.InvalidCredentials; + msg = "Invalid Credentials"; + break; + default: + msg = e.Message + "\r\n" + e.StackTrace; + ret = KeePassSyncErr.Error; + break; + } + m_MainInterface.SetStatus(priority, "KeyPassSync_S3: " + msg); + return ret; + } + + + private KeePassSyncErr verify_bucket_or_create(String bucket_name) { + try { + var client = GetClient(); + if (!AmazonS3Util.DoesS3BucketExistV2(client, bucket_name)) + client.PutBucket(bucket_name); + return KeePassSyncErr.None; + + } catch (Exception e) { + return convert_exception(e); + } + } + + } +} diff --git a/README.md b/README.md index b3c4e36..132ff39 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,26 @@ What is KeePassSync? --------------- -KeePassSync is a KeePass plugin that synchronizes your database using various online storage providers. This allows two or more computers to easily keep their data in sync. It was originally created in 2008 by Shawn Casey, shawn.casey@gmail.com and is currently maintained in this GitHub repository. +KeePassSync is a KeePass plugin that synchronizes your database using an online storage provider of your choice, any Rclone/Rsync compat storage, or FTP/SFTP. This allows two or more computers to easily keep their data in sync. It was originally created in 2008 by Shawn Casey, shawn.casey@gmail.com and is currently maintained in this GitHub repository. If you are looking for a provider recommendation consider CloudFlare R2. It is free for KeePass databases up to 10GB. This plugin will NOT work with KeePass v1.x. Thanks to https://github.com/walterpg/plgx-build-tasks for modernizing the PLGX build process. + + +- [Requirements:](#requirements) +- [Changes:](#changes) +- [How to install:](#how-to-install) +- [How to use:](#how-to-use) +- [Services:](#services) +- [How to compile:](#how-to-compile) +- [Service Specific Notes](#service-specific-notes) + - [Amazon S3 \(and other compatible providers\)](#amazon-s3-and-other-compatible-providers) + - [SFTP/FTP](#sftpftp) +- [Help!?!](#help) + + + + Requirements: ------------- @@ -47,7 +63,21 @@ KeePassSync supports the following services natively (although other users can a - FTP/SFTP (through plink/psftp from PUTTY) -- Amazon S3 +- Amazon S3 or S3 Compatible services, this includes: + - [Cloudflare R2](https://www.cloudflare.com/developer-platform/r2/) - free forever basically, 10GB a month which is way more than any keepass DB + - [Rclone Serve S3](https://rclone.org/commands/rclone_serve_s3/) - Free open source software to use as an interface to anything RClone can interface with + - [Backblaze B2](https://www.backblaze.com/cloud-storage) - Very cheap + - Alibaba Cloud + - DigitalOcean + - Dreamhost + - [Synology C2](https://c2.synology.com/en-us) + - Linode Object Storage + - IDrive E2 + - Huawei OBS + - IBM COS S3 + - [Google GCS](https://cloud.google.com/storage) + - Any of the many other compat providers, for several more see https://rclone.org/s3/ + How to compile: --------------- @@ -58,7 +88,7 @@ For more information: http://keepass.info/help/v2_dev/plg_index.html ## Service Specific Notes -### Amazon S3 +### Amazon S3 (and other compatible providers) Please see the dedicated [Amazon S3](AMAZON_S3.md) instructions.