diff --git a/KeeAgent/KeeAgent.csproj b/KeeAgent/KeeAgent.csproj
index f9a4cb8..d18a99d 100644
--- a/KeeAgent/KeeAgent.csproj
+++ b/KeeAgent/KeeAgent.csproj
@@ -56,6 +56,7 @@
+
diff --git a/KeeAgent/SshKeyGenerator.cs b/KeeAgent/SshKeyGenerator.cs
new file mode 100644
index 0000000..cbe43af
--- /dev/null
+++ b/KeeAgent/SshKeyGenerator.cs
@@ -0,0 +1,300 @@
+// SPDX-License-Identifier: GPL-2.0-only
+using System;
+using System.IO;
+using System.Text;
+using KeePassLib.Cryptography;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Security;
+using SshAgentLib.Keys;
+
+namespace KeeAgent
+{
+ internal static class SshKeyGenerator
+ {
+ internal static void Generate(string comment,
+ out byte[] privateKeyBytes, out byte[] publicKeyBytes)
+ {
+ if (comment == null) comment = string.Empty;
+ if (comment.IndexOfAny(new char[] { '\r', '\n', '\0' }) >= 0) {
+ throw new ArgumentException("comment must not contain newlines or NUL", "comment");
+ }
+ // No-arg constructor seeds from RNGCryptoServiceProvider.
+ // SetSeed() ADDS entropy — it does not replace existing seeding.
+ var secureRandom = new SecureRandom();
+ secureRandom.SetSeed(CryptoRandom.Instance.GetRandomBytes(64));
+
+ var privateParams = new Ed25519PrivateKeyParameters(secureRandom);
+ var seed = privateParams.GetEncoded(); // 32 bytes
+ var pub = privateParams.GeneratePublicKey().GetEncoded(); // 32 bytes
+
+ try {
+ privateKeyBytes = BuildPrivateKey(seed, pub, comment);
+ publicKeyBytes = BuildPublicKey(pub, comment);
+ }
+ finally {
+ Array.Clear(seed, 0, seed.Length);
+ }
+ }
+
+ static byte[] BuildPrivateKey(byte[] seed, byte[] pub, string comment)
+ {
+ var commentBytes = Encoding.UTF8.GetBytes(comment);
+ var checkInt = CryptoRandom.Instance.GetRandomBytes(4);
+
+ using (var blob = new MemoryStream()) {
+ // magic
+ blob.Write(Encoding.ASCII.GetBytes("openssh-key-v1"), 0, 14);
+ blob.WriteByte(0);
+
+ WriteString(blob, "none"); // cipher
+ WriteString(blob, "none"); // kdf
+ WriteString(blob, new byte[0]); // kdf options (empty)
+
+ WriteUInt32(blob, 1); // number of keys
+
+ // public key blob
+ using (var pubBlob = new MemoryStream()) {
+ WriteString(pubBlob, "ssh-ed25519");
+ WriteString(pubBlob, pub);
+ WriteString(blob, pubBlob.ToArray());
+ }
+
+ // private key blob
+ using (var privBlob = new MemoryStream()) {
+ privBlob.Write(checkInt, 0, 4); // check_int repeated twice
+ privBlob.Write(checkInt, 0, 4);
+ WriteString(privBlob, "ssh-ed25519");
+ WriteString(privBlob, pub);
+ var privMaterial = new byte[64]; // seed (32) || pubkey (32)
+ try {
+ Buffer.BlockCopy(seed, 0, privMaterial, 0, 32);
+ Buffer.BlockCopy(pub, 0, privMaterial, 32, 32);
+ WriteString(privBlob, privMaterial);
+ }
+ finally {
+ Array.Clear(privMaterial, 0, privMaterial.Length);
+ }
+ WriteString(privBlob, commentBytes);
+ for (int pad = 1; privBlob.Length % 8 != 0; pad++) {
+ privBlob.WriteByte((byte)pad);
+ }
+ WriteString(blob, privBlob.ToArray());
+ }
+
+ var b64 = Convert.ToBase64String(blob.ToArray());
+ var sb = new StringBuilder();
+ sb.Append("-----BEGIN OPENSSH PRIVATE KEY-----\n");
+ for (int i = 0; i < b64.Length; i += 70) {
+ sb.Append(b64.Substring(i, Math.Min(70, b64.Length - i))).Append('\n');
+ }
+ sb.Append("-----END OPENSSH PRIVATE KEY-----\n");
+ return Encoding.ASCII.GetBytes(sb.ToString());
+ }
+ }
+
+ static byte[] BuildPublicKey(byte[] pub, string comment)
+ {
+ using (var keyBlob = new MemoryStream()) {
+ WriteString(keyBlob, "ssh-ed25519");
+ WriteString(keyBlob, pub);
+ var b64 = Convert.ToBase64String(keyBlob.ToArray());
+ return Encoding.UTF8.GetBytes("ssh-ed25519 " + b64 + " " + comment);
+ }
+ }
+
+ static void WriteString(Stream s, string value)
+ {
+ WriteString(s, Encoding.UTF8.GetBytes(value));
+ }
+
+ static void WriteString(Stream s, byte[] value)
+ {
+ WriteUInt32(s, (uint)value.Length);
+ s.Write(value, 0, value.Length);
+ }
+
+ static void WriteUInt32(Stream s, uint value)
+ {
+ s.WriteByte((byte)(value >> 24));
+ s.WriteByte((byte)(value >> 16));
+ s.WriteByte((byte)(value >> 8));
+ s.WriteByte((byte)value);
+ }
+
+ internal static void ChangeComment(
+ byte[] attachmentBytes,
+ string newComment,
+ out byte[] privateKeyBytes,
+ out byte[] publicKeyBytes)
+ {
+ if (newComment == null) newComment = string.Empty;
+
+ SshPrivateKey key;
+ using (var ms = new MemoryStream(attachmentBytes)) {
+ key = SshPrivateKey.Read(ms);
+ }
+
+ if (key.IsEncrypted) {
+ throw new InvalidOperationException(
+ "Key is encrypted. Decrypt it first to change the comment.");
+ }
+
+ string oldComment;
+ var privateParams = key.Decrypt(() => new byte[0], null, out oldComment);
+
+ var authParts = key.PublicKey.AuthorizedKeysString.Split(' ');
+ var algo = authParts[0];
+ var b64Pub = authParts[1];
+ var pubKeyBlob = Convert.FromBase64String(b64Pub);
+
+ privateKeyBytes = BuildDecryptedPrivateKey(privateParams, pubKeyBlob, newComment);
+
+ publicKeyBytes = Encoding.UTF8.GetBytes(
+ string.IsNullOrEmpty(newComment)
+ ? algo + " " + b64Pub
+ : algo + " " + b64Pub + " " + newComment);
+ }
+
+ internal static void Decrypt(
+ byte[] attachmentBytes,
+ string passphrase,
+ out byte[] privateKeyBytes,
+ out byte[] publicKeyBytes)
+ {
+ var pass = Encoding.UTF8.GetBytes(passphrase ?? string.Empty);
+ try {
+ SshPrivateKey key;
+ using (var ms = new MemoryStream(attachmentBytes)) {
+ key = SshPrivateKey.Read(ms);
+ }
+
+ if (!key.IsEncrypted) {
+ throw new InvalidOperationException("Key is not encrypted.");
+ }
+
+ string comment;
+ var capturedPass = pass;
+ var privateParams = key.Decrypt(
+ () => capturedPass, null, out comment);
+
+ // AuthorizedKeysString = "{algo} {base64(KeyBlob)} [{comment}]"
+ // KeyBlob is internal, so decode via AuthorizedKeysString.
+ var authParts = key.PublicKey.AuthorizedKeysString.Split(' ');
+ var algo = authParts[0];
+ var b64Pub = authParts[1];
+ var pubKeyBlob = Convert.FromBase64String(b64Pub);
+
+ privateKeyBytes = BuildDecryptedPrivateKey(privateParams, pubKeyBlob, comment);
+
+ publicKeyBytes = Encoding.UTF8.GetBytes(
+ string.IsNullOrEmpty(comment)
+ ? algo + " " + b64Pub
+ : algo + " " + b64Pub + " " + comment);
+ }
+ finally {
+ Array.Clear(pass, 0, pass.Length);
+ }
+ }
+
+ static byte[] BuildDecryptedPrivateKey(
+ AsymmetricKeyParameter privateParams, byte[] pubKeyBlob, string comment)
+ {
+ var commentBytes = Encoding.UTF8.GetBytes(comment ?? string.Empty);
+ var checkInt = CryptoRandom.Instance.GetRandomBytes(4);
+
+ using (var blob = new MemoryStream()) {
+ blob.Write(Encoding.ASCII.GetBytes("openssh-key-v1"), 0, 14);
+ blob.WriteByte(0);
+
+ WriteString(blob, "none");
+ WriteString(blob, "none");
+ WriteString(blob, new byte[0]);
+ WriteUInt32(blob, 1);
+ WriteString(blob, pubKeyBlob);
+
+ using (var privBlob = new MemoryStream()) {
+ privBlob.Write(checkInt, 0, 4);
+ privBlob.Write(checkInt, 0, 4);
+ privBlob.Write(pubKeyBlob, 0, pubKeyBlob.Length);
+ WriteDecryptedKeyMaterial(privBlob, privateParams);
+ WriteString(privBlob, commentBytes);
+ for (int pad = 1; privBlob.Length % 8 != 0; pad++) {
+ privBlob.WriteByte((byte)pad);
+ }
+ WriteString(blob, privBlob.ToArray());
+ }
+
+ var b64 = Convert.ToBase64String(blob.ToArray());
+ var sb = new StringBuilder();
+ sb.Append("-----BEGIN OPENSSH PRIVATE KEY-----\n");
+ for (int i = 0; i < b64.Length; i += 70) {
+ sb.Append(b64.Substring(i, Math.Min(70, b64.Length - i))).Append('\n');
+ }
+ sb.Append("-----END OPENSSH PRIVATE KEY-----\n");
+ return Encoding.ASCII.GetBytes(sb.ToString());
+ }
+ }
+
+ static void WriteDecryptedKeyMaterial(Stream s, AsymmetricKeyParameter key)
+ {
+ var ed25519 = key as Ed25519PrivateKeyParameters;
+ if (ed25519 != null) {
+ var seed = ed25519.GetEncoded();
+ var pub = ed25519.GeneratePublicKey().GetEncoded();
+ var combined = new byte[64];
+ try {
+ Buffer.BlockCopy(seed, 0, combined, 0, 32);
+ Buffer.BlockCopy(pub, 0, combined, 32, 32);
+ WriteString(s, combined);
+ }
+ finally {
+ Array.Clear(seed, 0, seed.Length);
+ Array.Clear(combined, 0, combined.Length);
+ }
+ return;
+ }
+
+ var rsa = key as RsaPrivateCrtKeyParameters;
+ if (rsa != null) {
+ // order: d, iqmp, p, q (matches OpenSSH wire format)
+ WriteString(s, rsa.Exponent.ToByteArrayUnsigned());
+ WriteString(s, rsa.QInv.ToByteArrayUnsigned());
+ WriteString(s, rsa.P.ToByteArrayUnsigned());
+ WriteString(s, rsa.Q.ToByteArrayUnsigned());
+ return;
+ }
+
+ var ecdsa = key as ECPrivateKeyParameters;
+ if (ecdsa != null) {
+ WriteString(s, ecdsa.D.ToByteArrayUnsigned());
+ return;
+ }
+
+ var dsa = key as DsaPrivateKeyParameters;
+ if (dsa != null) {
+ WriteString(s, dsa.X.ToByteArrayUnsigned());
+ return;
+ }
+
+ var ed448 = key as Ed448PrivateKeyParameters;
+ if (ed448 != null) {
+ var seed = ed448.GetEncoded();
+ var pub = ed448.GeneratePublicKey().GetEncoded();
+ var combined = new byte[seed.Length + pub.Length];
+ try {
+ Buffer.BlockCopy(seed, 0, combined, 0, seed.Length);
+ Buffer.BlockCopy(pub, 0, combined, seed.Length, pub.Length);
+ WriteString(s, combined);
+ }
+ finally {
+ Array.Clear(seed, 0, seed.Length);
+ Array.Clear(combined, 0, combined.Length);
+ }
+ return;
+ }
+
+ throw new NotSupportedException("Unsupported key type: " + key.GetType().Name);
+ }
+ }
+}
diff --git a/KeeAgent/UI/EntryPanel.Designer.cs b/KeeAgent/UI/EntryPanel.Designer.cs
index 36a145f..c1ccca5 100644
--- a/KeeAgent/UI/EntryPanel.Designer.cs
+++ b/KeeAgent/UI/EntryPanel.Designer.cs
@@ -129,6 +129,7 @@ private void InitializeComponent()
resources.ApplyResources(this.commentTextBox, "commentTextBox");
this.commentTextBox.Name = "commentTextBox";
this.commentTextBox.ReadOnly = true;
+ this.commentTextBox.Leave += new System.EventHandler(this.commentTextBox_Leave);
//
// fingerprintTextBox
//
diff --git a/KeeAgent/UI/EntryPanel.cs b/KeeAgent/UI/EntryPanel.cs
index 8ffa85d..e8b5892 100644
--- a/KeeAgent/UI/EntryPanel.cs
+++ b/KeeAgent/UI/EntryPanel.cs
@@ -5,11 +5,16 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
+using System.IO;
using System.Linq;
using System.Windows.Forms;
using KeePass.Forms;
using KeePass.Util;
+using KeePass.Util.Spr;
+using KeePassLib;
using KeePassLib.Security;
+using KeePassLib.Utility;
+using dlech.SshAgentLib;
using SshAgentLib.Extension;
using SshAgentLib.Keys;
@@ -19,6 +24,10 @@ public partial class EntryPanel : UserControl
{
private PwEntryForm pwEntryForm;
private readonly KeeAgentExt ext;
+ private string editableCommentOriginal;
+ private ISshKey pendingOldAgentKey;
+ private byte[] pendingNewPrivKeyBytes;
+ private byte[] pendingNewPubKeyBytes;
public EntrySettings InitialSettings {
get;
@@ -79,10 +88,13 @@ protected override void OnLoad(EventArgs e)
};
}
- pwEntryForm.FormClosing += delegate {
+ pwEntryForm.FormClosing += (sender2, e2) => {
while (delayedUpdateKeyInfoTimer.Enabled) {
Application.DoEvents();
}
+ if (!e2.Cancel && pwEntryForm.DialogResult == DialogResult.OK) {
+ ApplyPendingAgentReload();
+ }
};
}
else {
@@ -133,6 +145,7 @@ void UpdateKeyInfo()
invalidKeyWarningIcon.Visible = !isLocationValid;
}
+ var commentEditable = false;
try {
var key = CurrentSettings.TryGetSshPublicKey(pwEntryForm.EntryBinaries);
@@ -140,6 +153,18 @@ void UpdateKeyInfo()
fingerprintTextBox.Text = key.Sha256Fingerprint;
publicKeyTextBox.Text = key.AuthorizedKeysString;
copyPublicKeyButton.Enabled = true;
+
+ // Comment is editable when the key is an unencrypted attachment.
+ if (CurrentSettings.Location != null &&
+ CurrentSettings.Location.SelectedType == EntrySettings.LocationType.Attachment &&
+ !string.IsNullOrEmpty(CurrentSettings.Location.AttachmentName)) {
+ var attach = pwEntryForm.EntryBinaries.Get(CurrentSettings.Location.AttachmentName);
+ if (attach != null) {
+ using (var ms = new MemoryStream(attach.ReadData())) {
+ commentEditable = !SshPrivateKey.Read(ms).IsEncrypted;
+ }
+ }
+ }
}
catch (Exception) {
commentTextBox.Text = string.Empty;
@@ -147,6 +172,108 @@ void UpdateKeyInfo()
publicKeyTextBox.Text = string.Empty;
copyPublicKeyButton.Enabled = false;
}
+
+ commentTextBox.ReadOnly = !commentEditable;
+ editableCommentOriginal = commentEditable ? commentTextBox.Text : null;
+ }
+
+ private void commentTextBox_Leave(object sender, EventArgs e)
+ {
+ if (commentTextBox.ReadOnly) return;
+ if (commentTextBox.Text == editableCommentOriginal) return;
+ ApplyCommentChange();
+ }
+
+ private void ApplyCommentChange()
+ {
+ var loc = CurrentSettings.Location;
+ if (loc == null ||
+ loc.SelectedType != EntrySettings.LocationType.Attachment ||
+ string.IsNullOrEmpty(loc.AttachmentName)) {
+ return;
+ }
+
+ var attachName = loc.AttachmentName;
+ var attachment = pwEntryForm.EntryBinaries.Get(attachName);
+ if (attachment == null) return;
+
+ // On the first comment edit, capture the currently-loaded agent key so we
+ // can replace it when the entry dialog closes with OK. We defer the live
+ // agent swap to FormClosing so a Cancel roll-back doesn't leave the agent
+ // in an inconsistent state.
+ if (pendingOldAgentKey == null && ext.agent != null) {
+ try {
+ var pubKey = CurrentSettings.TryGetSshPublicKey(pwEntryForm.EntryBinaries);
+ if (pubKey != null) {
+ pendingOldAgentKey = ext.agent.ListKeys()
+ .FirstOrDefault(k => pubKey.Matches(k.GetPublicKeyBlob()));
+ }
+ }
+ catch (Exception) { }
+ }
+
+ byte[] privateKeyBytes, publicKeyBytes;
+ try {
+ SshKeyGenerator.ChangeComment(
+ attachment.ReadData(),
+ commentTextBox.Text,
+ out privateKeyBytes,
+ out publicKeyBytes);
+ }
+ catch (Exception ex) {
+ MessageService.ShowWarning("KeeAgent: Failed to update comment:", ex.Message);
+ return;
+ }
+
+ pwEntryForm.EntryBinaries.Set(attachName, new ProtectedBinary(false, privateKeyBytes));
+ pwEntryForm.EntryBinaries.Set(attachName + ".pub", new ProtectedBinary(false, publicKeyBytes));
+ pwEntryForm.UpdateEntryBinaries(false, true);
+
+ editableCommentOriginal = commentTextBox.Text;
+
+ // Track the latest bytes; the FormClosing handler will use the final state.
+ pendingNewPrivKeyBytes = privateKeyBytes;
+ pendingNewPubKeyBytes = publicKeyBytes;
+
+ UpdateKeyInfoDelayed();
+ }
+
+ private void ApplyPendingAgentReload()
+ {
+ if (pendingOldAgentKey == null || pendingNewPrivKeyBytes == null) return;
+ try {
+ SshPrivateKey newPrivKey;
+ using (var ms = new MemoryStream(pendingNewPrivKeyBytes)) {
+ newPrivKey = SshPrivateKey.Read(ms);
+ }
+ string newComment;
+ var privateParam = newPrivKey.Decrypt(() => new byte[0], null, out newComment);
+
+ SshPublicKey newPubKey;
+ using (var ms = new MemoryStream(pendingNewPubKeyBytes)) {
+ newPubKey = SshPublicKey.Read(ms);
+ }
+
+ var newKey = new SshKey(
+ newPubKey.Parameter, privateParam, newComment,
+ newPubKey.Nonce, newPubKey.Certificate);
+ newKey.Source = pendingOldAgentKey.Source;
+ newKey.DestinationConstraint = pendingOldAgentKey.DestinationConstraint;
+ foreach (var c in pendingOldAgentKey.Constraints) {
+ newKey.AddConstraint(c);
+ }
+
+ ext.RemoveKey(pendingOldAgentKey);
+ ext.agent.AddKey(newKey);
+ }
+ catch (Exception) {
+ // Not critical — old key stays loaded if reload fails.
+ }
+ finally {
+ pendingOldAgentKey = null;
+ pendingNewPrivKeyBytes = null;
+ pendingNewPubKeyBytes = null;
+ }
}
private void hasSshKeyCheckBox_CheckedChanged(object sender, EventArgs e)
@@ -180,6 +307,10 @@ private void openManageFilesDialogButton_Click(object sender, EventArgs e)
pwEntryForm.UpdateEntryBinaries(true, false);
var dialog = new ManageKeyFileDialog {
+ DefaultComment = pwEntryForm.EntryRef.Strings.ReadSafe(PwDefs.TitleField),
+ Passphrase = SprEngine.Compile(
+ pwEntryForm.EntryRef.Strings.ReadSafe(PwDefs.PasswordField),
+ new SprContext(pwEntryForm.EntryRef, pwEntryForm.EntryRef.GetDatabase(), SprCompileFlags.Deref)),
Attachments = new AttachmentBindingList(pwEntryForm.EntryBinaries),
KeyLocation = CurrentSettings.Location.DeepCopy(),
};
diff --git a/KeeAgent/UI/EntryPanel.resx b/KeeAgent/UI/EntryPanel.resx
index 1e8795d..739ffd1 100644
--- a/KeeAgent/UI/EntryPanel.resx
+++ b/KeeAgent/UI/EntryPanel.resx
@@ -844,7 +844,7 @@
NoControl
- 312, 358
+ 306, 358
6, 6, 6, 6
diff --git a/KeeAgent/UI/KeyLocationPanel.Designer.cs b/KeeAgent/UI/KeyLocationPanel.Designer.cs
index 692e2c7..1d3a336 100644
--- a/KeeAgent/UI/KeyLocationPanel.Designer.cs
+++ b/KeeAgent/UI/KeyLocationPanel.Designer.cs
@@ -40,6 +40,10 @@ private void InitializeComponent()
this.fileRadioButton = new System.Windows.Forms.RadioButton();
this.attachmentRadioButton = new System.Windows.Forms.RadioButton();
this.attachButton = new System.Windows.Forms.Button();
+ this.generateRadioButton = new System.Windows.Forms.RadioButton();
+ this.generateCommentLabel = new System.Windows.Forms.Label();
+ this.generateCommentTextBox = new System.Windows.Forms.TextBox();
+ this.decryptRadioButton = new System.Windows.Forms.RadioButton();
((System.ComponentModel.ISupportInitialize)(this.locationSettingsBindingSource)).BeginInit();
this.locationGroupBox.SuspendLayout();
this.SuspendLayout();
@@ -59,6 +63,10 @@ private void InitializeComponent()
this.locationGroupBox.Controls.Add(this.attachmentComboBox);
this.locationGroupBox.Controls.Add(this.fileRadioButton);
this.locationGroupBox.Controls.Add(this.attachmentRadioButton);
+ this.locationGroupBox.Controls.Add(this.generateRadioButton);
+ this.locationGroupBox.Controls.Add(this.generateCommentLabel);
+ this.locationGroupBox.Controls.Add(this.generateCommentTextBox);
+ this.locationGroupBox.Controls.Add(this.decryptRadioButton);
this.locationGroupBox.DataBindings.Add(new System.Windows.Forms.Binding("SelectedRadioButton", this.locationSettingsBindingSource, "SelectedType", true, System.Windows.Forms.DataSourceUpdateMode.OnPropertyChanged));
resources.ApplyResources(this.locationGroupBox, "locationGroupBox");
this.locationGroupBox.Name = "locationGroupBox";
@@ -119,14 +127,40 @@ private void InitializeComponent()
this.attachmentRadioButton.CheckedChanged += new System.EventHandler(this.radioButton_CheckedChanged);
//
// attachButton
- //
+ //
resources.ApplyResources(this.attachButton, "attachButton");
this.attachButton.Name = "attachButton";
this.attachButton.UseVisualStyleBackColor = true;
this.attachButton.Click += new System.EventHandler(this.attachButton_Click);
- //
+ //
+ // generateRadioButton
+ //
+ resources.ApplyResources(this.generateRadioButton, "generateRadioButton");
+ this.generateRadioButton.Name = "generateRadioButton";
+ this.generateRadioButton.TabStop = true;
+ this.generateRadioButton.UseVisualStyleBackColor = true;
+ this.generateRadioButton.CheckedChanged += new System.EventHandler(this.radioButton_CheckedChanged);
+ //
+ // generateCommentLabel
+ //
+ resources.ApplyResources(this.generateCommentLabel, "generateCommentLabel");
+ this.generateCommentLabel.Name = "generateCommentLabel";
+ //
+ // generateCommentTextBox
+ //
+ resources.ApplyResources(this.generateCommentTextBox, "generateCommentTextBox");
+ this.generateCommentTextBox.Name = "generateCommentTextBox";
+ //
+ // decryptRadioButton
+ //
+ resources.ApplyResources(this.decryptRadioButton, "decryptRadioButton");
+ this.decryptRadioButton.Name = "decryptRadioButton";
+ this.decryptRadioButton.TabStop = true;
+ this.decryptRadioButton.UseVisualStyleBackColor = true;
+ this.decryptRadioButton.CheckedChanged += new System.EventHandler(this.radioButton_CheckedChanged);
+ //
// KeyLocationPanel
- //
+ //
resources.ApplyResources(this, "$this");
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.locationGroupBox);
@@ -151,5 +185,9 @@ private void InitializeComponent()
private System.Windows.Forms.CheckBox saveKeyToTempFileCheckBox;
private InPlaceMessage errorMessage;
private System.Windows.Forms.Button attachButton;
+ private System.Windows.Forms.RadioButton generateRadioButton;
+ private System.Windows.Forms.Label generateCommentLabel;
+ private System.Windows.Forms.TextBox generateCommentTextBox;
+ private System.Windows.Forms.RadioButton decryptRadioButton;
}
}
diff --git a/KeeAgent/UI/KeyLocationPanel.cs b/KeeAgent/UI/KeyLocationPanel.cs
index c2dcaf8..7d08b08 100644
--- a/KeeAgent/UI/KeyLocationPanel.cs
+++ b/KeeAgent/UI/KeyLocationPanel.cs
@@ -182,6 +182,28 @@ protected override void OnLoad(EventArgs e)
UpdateControlStates();
}
+ public bool IsGenerateSelected {
+ get { return generateRadioButton.Checked; }
+ }
+
+ public string GenerateComment {
+ get { return generateCommentTextBox.Text; }
+ set { generateCommentTextBox.Text = value; }
+ }
+
+ public bool IsDecryptSelected {
+ get { return decryptRadioButton.Checked; }
+ }
+
+ public string DecryptAttachmentName {
+ get { return attachmentComboBox.SelectedValue as string; }
+ }
+
+ public void SetDecryptEnabled(bool enabled)
+ {
+ decryptRadioButton.Enabled = enabled;
+ }
+
private void UpdateControlStates()
{
attachmentComboBox.Enabled = attachmentRadioButton.Checked;
@@ -189,6 +211,8 @@ private void UpdateControlStates()
saveKeyToTempFileCheckBox.Enabled = attachmentRadioButton.Checked;
fileNameTextBox.Enabled = fileRadioButton.Checked;
browseButton.Enabled = fileRadioButton.Checked;
+ generateCommentLabel.Enabled = generateRadioButton.Checked;
+ generateCommentTextBox.Enabled = generateRadioButton.Checked;
}
private void radioButton_CheckedChanged(object sender, EventArgs e)
diff --git a/KeeAgent/UI/KeyLocationPanel.resx b/KeeAgent/UI/KeyLocationPanel.resx
index 69cf34d..6462c0e 100644
--- a/KeeAgent/UI/KeyLocationPanel.resx
+++ b/KeeAgent/UI/KeyLocationPanel.resx
@@ -156,8 +156,122 @@
0
+
+ True
+
+
+ NoControl
+
+
+ 12, 95
+
+
+ 168, 17
+
+
+ 8
+
+
+ Generate new Ed25519 key
+
+
+ generateRadioButton
+
+
+ System.Windows.Forms.RadioButton, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ locationGroupBox
+
+
+ 8
+
+
+ True
+
+
+ NoControl
+
+
+ 97, 120
+
+
+ 55, 13
+
+
+ 9
+
+
+ Comment:
+
+
+ generateCommentLabel
+
+
+ System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ locationGroupBox
+
+
+ 9
+
+
+ Top, Left, Right
+
+
+ 155, 117
+
+
+ 284, 20
+
+
+ 10
+
+
+ generateCommentTextBox
+
+
+ System.Windows.Forms.TextBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ locationGroupBox
+
+
+ 10
+
+
+ True
+
+
+ NoControl
+
+
+ 12, 143
+
+
+ 200, 17
+
+
+ 11
+
+
+ Decrypt existing key (uses entry password)
+
+
+ decryptRadioButton
+
+
+ System.Windows.Forms.RadioButton, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ locationGroupBox
+
+
+ 11
+
- 12, 93
+ 12, 167
427, 19
@@ -355,7 +469,7 @@
0, 0
- 445, 115
+ 445, 192
3
@@ -385,7 +499,7 @@
6, 13
- 445, 115
+ 445, 192
locationSettingsBindingSource
diff --git a/KeeAgent/UI/ManageKeyFileDialog.Designer.cs b/KeeAgent/UI/ManageKeyFileDialog.Designer.cs
index 86decc6..c8bee68 100644
--- a/KeeAgent/UI/ManageKeyFileDialog.Designer.cs
+++ b/KeeAgent/UI/ManageKeyFileDialog.Designer.cs
@@ -36,25 +36,25 @@ private void InitializeComponent()
//
this.cancelButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
this.cancelButton.DialogResult = System.Windows.Forms.DialogResult.Cancel;
- this.cancelButton.Location = new System.Drawing.Point(456, 143);
+ this.cancelButton.Location = new System.Drawing.Point(456, 217);
this.cancelButton.Margin = new System.Windows.Forms.Padding(2);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(75, 23);
this.cancelButton.TabIndex = 2;
this.cancelButton.Text = "&Cancel";
this.cancelButton.UseVisualStyleBackColor = true;
- //
+ //
// okButton
- //
+ //
this.okButton.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right)));
- this.okButton.DialogResult = System.Windows.Forms.DialogResult.OK;
- this.okButton.Location = new System.Drawing.Point(377, 143);
+ this.okButton.Location = new System.Drawing.Point(377, 217);
this.okButton.Margin = new System.Windows.Forms.Padding(2);
this.okButton.Name = "okButton";
this.okButton.Size = new System.Drawing.Size(75, 23);
this.okButton.TabIndex = 3;
this.okButton.Text = "&OK";
this.okButton.UseVisualStyleBackColor = true;
+ this.okButton.Click += new System.EventHandler(this.okButton_Click);
//
// privateKeyLocationPanel
//
@@ -67,7 +67,7 @@ private void InitializeComponent()
this.privateKeyLocationPanel.Location = new System.Drawing.Point(15, 15);
this.privateKeyLocationPanel.Margin = new System.Windows.Forms.Padding(6);
this.privateKeyLocationPanel.Name = "privateKeyLocationPanel";
- this.privateKeyLocationPanel.Size = new System.Drawing.Size(512, 118);
+ this.privateKeyLocationPanel.Size = new System.Drawing.Size(512, 192);
this.privateKeyLocationPanel.TabIndex = 0;
this.privateKeyLocationPanel.Title = "Private Key File Location";
//
@@ -77,7 +77,7 @@ private void InitializeComponent()
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.CancelButton = this.cancelButton;
- this.ClientSize = new System.Drawing.Size(542, 177);
+ this.ClientSize = new System.Drawing.Size(542, 251);
this.Controls.Add(this.okButton);
this.Controls.Add(this.cancelButton);
this.Controls.Add(this.privateKeyLocationPanel);
diff --git a/KeeAgent/UI/ManageKeyFileDialog.cs b/KeeAgent/UI/ManageKeyFileDialog.cs
index 29704ef..6a03769 100644
--- a/KeeAgent/UI/ManageKeyFileDialog.cs
+++ b/KeeAgent/UI/ManageKeyFileDialog.cs
@@ -2,9 +2,13 @@
// Copyright (c) 2022 David Lechner
using System;
+using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Windows.Forms;
using KeePassLib.Security;
+using KeePassLib.Utility;
+using SshAgentLib.Keys;
namespace KeeAgent.UI
{
@@ -13,6 +17,8 @@ namespace KeeAgent.UI
///
public partial class ManageKeyFileDialog : Form
{
+ private string passphrase;
+
///
/// Creates a new key file dialog.
///
@@ -41,11 +47,59 @@ public ManageKeyFileDialog()
});
privateKeyLocationPanel.KeyLocationChanged += (s, e) => {
- privateKeyLocationPanel.ErrorMessage = UI.Validate.Location(
- privateKeyLocationPanel.KeyLocation, getAttachment);
+ UpdateDecryptEnabled();
+ if (privateKeyLocationPanel.IsGenerateSelected ||
+ privateKeyLocationPanel.IsDecryptSelected) {
+ privateKeyLocationPanel.ErrorMessage = null;
+ }
+ else {
+ privateKeyLocationPanel.ErrorMessage = UI.Validate.Location(
+ privateKeyLocationPanel.KeyLocation, getAttachment);
+ }
};
}
+ protected override void OnLoad(EventArgs e)
+ {
+ base.OnLoad(e);
+ UpdateDecryptEnabled();
+ }
+
+ private void UpdateDecryptEnabled()
+ {
+ var canDecrypt = false;
+
+ if (!string.IsNullOrEmpty(passphrase)) {
+ try {
+ var loc = privateKeyLocationPanel.KeyLocation;
+ if (loc != null &&
+ loc.SelectedType == EntrySettings.LocationType.Attachment &&
+ !string.IsNullOrEmpty(loc.AttachmentName) &&
+ Attachments != null) {
+ var item = Attachments.FirstOrDefault(a => a.Key == loc.AttachmentName);
+ if (item.Value != null) {
+ using (var ms = new MemoryStream(item.Value.ReadData())) {
+ var key = SshPrivateKey.Read(ms);
+ canDecrypt = key.IsEncrypted;
+ }
+ }
+ }
+ }
+ catch {
+ // If we can't read the key, decrypt option stays disabled.
+ }
+ }
+
+ privateKeyLocationPanel.SetDecryptEnabled(canDecrypt);
+ }
+
+ ///
+ /// Sets the passphrase used to decrypt an encrypted key attachment.
+ ///
+ public string Passphrase {
+ set { passphrase = value; }
+ }
+
///
/// Gets and sets the attachments data source.
///
@@ -58,6 +112,13 @@ public AttachmentBindingList Attachments {
}
}
+ ///
+ /// Sets the default comment pre-filled in the Generate option.
+ ///
+ public string DefaultComment {
+ set { privateKeyLocationPanel.GenerateComment = value; }
+ }
+
///
/// Gets and sets the private key location data source.
///
@@ -69,5 +130,110 @@ public EntrySettings.LocationData KeyLocation {
privateKeyLocationPanel.KeyLocation = value;
}
}
+
+ private void okButton_Click(object sender, EventArgs e)
+ {
+ if (privateKeyLocationPanel.IsDecryptSelected) {
+ var attachName = privateKeyLocationPanel.DecryptAttachmentName;
+
+ if (string.IsNullOrEmpty(attachName)) {
+ MessageService.ShowWarning("KeeAgent: No key attachment selected.");
+ return;
+ }
+
+ var item = Attachments == null
+ ? default(KeyValuePair)
+ : Attachments.FirstOrDefault(a => a.Key == attachName);
+
+ if (item.Value == null) {
+ MessageService.ShowWarning("KeeAgent: Selected attachment not found.");
+ return;
+ }
+
+ byte[] privateKeyBytes, publicKeyBytes;
+
+ try {
+ SshKeyGenerator.Decrypt(
+ item.Value.ReadData(),
+ passphrase,
+ out privateKeyBytes,
+ out publicKeyBytes);
+ }
+ catch (Exception ex) {
+ MessageService.ShowWarning(
+ "KeeAgent: Failed to decrypt SSH key:", ex.Message);
+ return;
+ }
+
+ var pubAttachName = attachName + ".pub";
+ var snapshot = new List>(Attachments);
+ snapshot.RemoveAll(a => a.Key == attachName || a.Key == pubAttachName);
+ snapshot.Add(new KeyValuePair(
+ attachName, new ProtectedBinary(false, privateKeyBytes)));
+ snapshot.Add(new KeyValuePair(
+ pubAttachName, new ProtectedBinary(false, publicKeyBytes)));
+ Attachments.Clear();
+ foreach (var entry in snapshot) {
+ Attachments.Add(entry);
+ }
+
+ KeyLocation = new EntrySettings.LocationData {
+ SelectedType = EntrySettings.LocationType.Attachment,
+ AttachmentName = attachName,
+ };
+
+ DialogResult = DialogResult.OK;
+ Close();
+ return;
+ }
+
+ if (privateKeyLocationPanel.IsGenerateSelected) {
+ if (Attachments != null && Attachments.Any(a => a.Key == "id_ed25519")) {
+ var confirm = MessageBox.Show(
+ this,
+ "An SSH key attachment already exists. Replacing it will invalidate any " +
+ "servers already configured with the current public key. Continue?",
+ "Replace Existing Key?",
+ MessageBoxButtons.YesNo,
+ MessageBoxIcon.Warning);
+ if (confirm != DialogResult.Yes) {
+ return;
+ }
+ }
+
+ byte[] privateKeyBytes, publicKeyBytes;
+
+ try {
+ SshKeyGenerator.Generate(
+ privateKeyLocationPanel.GenerateComment,
+ out privateKeyBytes,
+ out publicKeyBytes);
+ }
+ catch (Exception ex) {
+ MessageService.ShowWarning("KeeAgent: Failed to generate SSH key:", ex.Message);
+ return;
+ }
+
+ // Rebuild Attachments, replacing any existing id_ed25519 pair
+ var snapshot = new List>(Attachments);
+ snapshot.RemoveAll(a => a.Key == "id_ed25519" || a.Key == "id_ed25519.pub");
+ snapshot.Add(new KeyValuePair(
+ "id_ed25519", new ProtectedBinary(false, privateKeyBytes)));
+ snapshot.Add(new KeyValuePair(
+ "id_ed25519.pub", new ProtectedBinary(false, publicKeyBytes)));
+ Attachments.Clear();
+ foreach (var item in snapshot) {
+ Attachments.Add(item);
+ }
+
+ KeyLocation = new EntrySettings.LocationData {
+ SelectedType = EntrySettings.LocationType.Attachment,
+ AttachmentName = "id_ed25519",
+ };
+ }
+
+ DialogResult = DialogResult.OK;
+ Close();
+ }
}
}