// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Security.Cryptography; using Duplicati.Library.Interface; using Duplicati.Library.Utility; using System.Text; using System.Linq; namespace Duplicati.Library.Backend { public class KeyGenerator : IWebModule { private const string KEY_TYPE_NAME = "key-type"; private const string KEY_USERNAME = "key-username"; private const string KEY_KEYLEN = "key-bits"; private const string KEY_TEMPLATE_DSA = "-----BEGIN DSA PRIVATE KEY-----\n{0}\n-----END DSA PRIVATE KEY----- \n"; private const string KEY_TEMPLATE_RSA = "-----BEGIN RSA PRIVATE KEY-----\n{0}\n-----END RSA PRIVATE KEY----- \n"; private const string PUB_KEY_FORMAT_DSA = "ssh-dss"; private const string PUB_KEY_FORMAT_RSA = "ssh-rsa"; private const string KEYTYPE_DSA = "dsa"; private const string KEYTYPE_RSA = "rsa"; private readonly string DEFAULT_USERNAME = "backup-user@" + System.Environment.MachineName; private const string DEFAULT_KEYTYPE = KEYTYPE_DSA; private const int DEFAULT_KEYLEN = 1024; private static void EncodePEMLength(System.IO.Stream s, uint length) { s.WriteByte((byte)((length >> 24) & 0xff)); s.WriteByte((byte)((length >> 16) & 0xff)); s.WriteByte((byte)((length >> 8) & 0xff)); s.WriteByte((byte)(length & 0xff)); } private static void EncodeDERLength(System.IO.Stream s, uint length) { if (length < 0x7f) { s.WriteByte((byte)length); } else if (length <= 0x7fff) { s.WriteByte(0x82); s.WriteByte((byte)((length >> 8) & 0xff)); s.WriteByte((byte)(length & 0xff)); } else { s.WriteByte(0x84); s.WriteByte((byte)((length >> 24) & 0xff)); s.WriteByte((byte)((length >> 16) & 0xff)); s.WriteByte((byte)((length >> 8) & 0xff)); s.WriteByte((byte)(length & 0xff)); } } private static byte[] EncodeDER(byte[][] data) { byte[] payload; using(var ms = new System.IO.MemoryStream()) { foreach(var b in data) { ms.WriteByte(0x02); var isNegative = (b[0] & 0x80) != 0; EncodeDERLength(ms, (uint)(b.Length + (isNegative ? 1 : 0))); if (isNegative) ms.WriteByte(0); ms.Write(b, 0, b.Length); } payload = ms.ToArray(); } using(var ms = new System.IO.MemoryStream()) { ms.WriteByte(0x30); EncodeDERLength(ms, (uint)payload.Length); ms.Write(payload, 0, payload.Length); return ms.ToArray(); } } private static byte[] EncodePEM(byte[][] data) { using(var ms = new System.IO.MemoryStream()) { foreach(var n in data) { var isNegative = (n[0] & 0x80) != 0; EncodePEMLength(ms, (uint)(n.Length + (isNegative ? 1 : 0))); if (isNegative) ms.WriteByte(0); ms.Write(n, 0, n.Length); } return ms.ToArray(); } } private static IDictionary OutputKey(byte[] private_key, byte[] public_key, string pem_template, string key_name, string username) { var res = new Dictionary(); var b64_raw = Convert.ToBase64String(private_key); var sb = new StringBuilder(); var lw = 64; for(var i = 0; i < b64_raw.Length; i += lw) { sb.Append(b64_raw.Substring(i, Math.Min(lw, b64_raw.Length - i))); sb.Append("\n"); } var b64 = sb.ToString().Trim(); var pem = string.Format(pem_template, b64); var uri = SSHv2.KEYFILE_URI + Duplicati.Library.Utility.Uri.UrlEncode(pem); var pub = key_name + " " + Convert.ToBase64String(public_key) + " " + username; res["privkey"] = b64_raw; res["privkeyfile"] = pem; res["privkeyuri"] = uri; res["pubkey"] = pub; return res; } #region IWebModule implementation public IDictionary Execute(IDictionary options) { string keytype; if (!options.TryGetValue(KEY_TYPE_NAME, out keytype)) keytype = DEFAULT_KEYTYPE; string username; if (!options.TryGetValue(KEY_USERNAME, out username)) username = DEFAULT_USERNAME; string keylen_s; if (!options.TryGetValue(KEY_KEYLEN, out keylen_s)) keylen_s = "0"; int keylen; if (!int.TryParse(keylen_s, out keylen)) keylen = DEFAULT_KEYLEN; if (KEYTYPE_RSA.Equals(keytype, StringComparison.OrdinalIgnoreCase)) { var rsa = RSACryptoServiceProvider.Create(); if (keylen > 0) rsa.KeySize = keylen; else rsa.KeySize = DEFAULT_KEYLEN; var key = rsa.ExportParameters(true); var privateEntries = new byte[][] { new byte[]{ 0x0 }, key.Modulus, key.Exponent, key.D, key.P, key.Q, key.DP, key.DQ, key.InverseQ }; var publicEntries = new byte[][] { System.Text.Encoding.ASCII.GetBytes(PUB_KEY_FORMAT_RSA), key.Exponent, key.Modulus }; return OutputKey(EncodeDER(privateEntries), EncodePEM(publicEntries), KEY_TEMPLATE_RSA, PUB_KEY_FORMAT_RSA, username); } else if (KEYTYPE_DSA.Equals(keytype, StringComparison.OrdinalIgnoreCase)) { var dsa = DSACryptoServiceProvider.Create(); if (keylen > 0) dsa.KeySize = keylen; else dsa.KeySize = DEFAULT_KEYLEN; var key = dsa.ExportParameters(true); var privateEntries = new byte[][] { new byte[] { 0x0 }, key.P, key.Q, key.G, key.Y, key.X }; var publicEntries = new byte[][] { System.Text.Encoding.ASCII.GetBytes(PUB_KEY_FORMAT_DSA), key.P, key.Q, key.G, key.Y }; return OutputKey(EncodeDER(privateEntries), EncodePEM(publicEntries), KEY_TEMPLATE_DSA, PUB_KEY_FORMAT_DSA, username); } else { throw new UserInformationException(string.Format("Unsupported key type: {0}", keytype), "SSHUnsupportedKey"); } } public string Key { get { return "ssh-keygen"; } } public string DisplayName { get { return Strings.KeyGenerator.DisplayName; } } public string Description { get { return Strings.KeyGenerator.Description; } } public IList SupportedCommands { get { return new List(new ICommandLineArgument[] { new CommandLineArgument(KEY_KEYLEN, CommandLineArgument.ArgumentType.Integer, Strings.KeyGenerator.KeyLenShort, Strings.KeyGenerator.KeyLenLong, DEFAULT_KEYLEN.ToString(), null, new string[] {"1024", "2048"}), new CommandLineArgument(KEY_TYPE_NAME, CommandLineArgument.ArgumentType.Enumeration, Strings.KeyGenerator.KeyTypeShort, Strings.KeyGenerator.KeyTypeLong, DEFAULT_KEYTYPE, null, new string[] {KEYTYPE_DSA, KEYTYPE_RSA}), new CommandLineArgument(KEY_USERNAME, CommandLineArgument.ArgumentType.Integer, Strings.KeyGenerator.KeyUsernameShort, Strings.KeyGenerator.KeyUsernameLong, DEFAULT_USERNAME), }); } } #endregion } }