From 6d6fd622500699747aa0c3f898f6d588853460e8 Mon Sep 17 00:00:00 2001 From: Benjamin Gobeil Date: Wed, 22 Jul 2020 19:27:55 -0400 Subject: [PATCH 1/2] Use AES encryption when not on windows (cherry picked from commit b22361ce510ab530c8f8886f012d617893e61a8d) --- src/Paket.Core/Common/Encryption.fs | 161 ++++++++++++++++++ src/Paket.Core/Paket.Core.fsproj | 1 + src/Paket.Core/Versioning/ConfigFile.fs | 40 +---- tests/Paket.Tests/AesSpecs.fs | 12 ++ tests/Paket.Tests/Paket.Tests.fsproj | 1 + .../Paket.Tests/Versioning/ConfigFileSpecs.fs | 13 +- 6 files changed, 188 insertions(+), 40 deletions(-) create mode 100644 src/Paket.Core/Common/Encryption.fs create mode 100644 tests/Paket.Tests/AesSpecs.fs diff --git a/src/Paket.Core/Common/Encryption.fs b/src/Paket.Core/Common/Encryption.fs new file mode 100644 index 0000000000..d0f2c0059f --- /dev/null +++ b/src/Paket.Core/Common/Encryption.fs @@ -0,0 +1,161 @@ +namespace Paket.Core.Common + +open System +open System.IO +open System.Net.Http +open System.Security.Cryptography +open System.Text +open Paket +open Paket.Logging + +type private AesKey<'T> = private AesKey of 'T +type private AesIV<'T> = private AesIV of 'T + +type PlainTextPassword = PlainTextPassword of string +type AesEncryptedPassword = private AesEncryptedPassword of string +type DPApiEncryptedPassword = private DPApiEncryptedPassword of string + +[] +type EncryptedPassword = + | Aes of AesEncryptedPassword + | DPApi of DPApiEncryptedPassword + with + override x.ToString() = + match x with + | Aes (AesEncryptedPassword password) | DPApi (DPApiEncryptedPassword password) -> password + +type AesSalt = private AesSalt of string +type DPApiSalt = private DPApiSalt of string + +[] +type Salt = + | Aes of AesSalt + | DPApi of DPApiSalt + with + override x.ToString() = + match x with + | Aes (AesSalt salt) | DPApi (DPApiSalt salt) -> salt + +[] +module private AesSalt = + let [] saltSeparator = "SALT_SEPARATOR" + + let private toBytes (AesKey keyString, AesIV ivString) = + (AesKey << Convert.FromBase64String) keyString, + (AesIV << Convert.FromBase64String) ivString + + let encode ((AesKey key): AesKey,(AesIV iv): AesIV) = + seq {key;iv} + |> Seq.map Convert.ToBase64String + |> String.concat saltSeparator + |> AesSalt + + let decode (AesSalt saltString) = + match saltString.Split([|saltSeparator|], StringSplitOptions.None) with + | [|keyString;ivString|] -> (AesKey keyString, AesIV ivString) + | _ -> failwith "Should never happen" + + let (|IsAesSalt|_|) (str: string) = + if str.Contains saltSeparator then + Some <| AesSalt str + else + None + +[] +module Aes = + let private encryptString (password: string) = + let writePassword (cryptoStream: CryptoStream) = + use streamWriter = new StreamWriter(cryptoStream) + streamWriter.Write(password) + + use aes = Aes.Create() + let encryptor = aes.CreateEncryptor() + use memoryStream = new MemoryStream() + use cryptoStream = new CryptoStream(memoryStream,encryptor,CryptoStreamMode.Write) + writePassword cryptoStream + let encryptedPassword = memoryStream.ToArray() + encryptedPassword,aes.Key,aes.IV + + let private decryptBytes (encryptedBytes: byte[]) key iv = + use aes = Aes.Create() + aes.Key <- key + aes.IV <- iv + let decryptor = aes.CreateDecryptor() + use memoryStream = new MemoryStream(encryptedBytes) + use cryptoStream = new CryptoStream(memoryStream,decryptor,CryptoStreamMode.Read) + use streamReader = new StreamReader(cryptoStream) + let password = streamReader.ReadToEnd() + password + + let private serialize password (key: byte[]) (iv: byte[]) = + (AesEncryptedPassword << Convert.ToBase64String) password, + AesSalt.encode (AesKey key, AesIV iv) + + let private deserialize (AesEncryptedPassword password) aesSalt = + let (AesKey key, AesIV iv) = AesSalt.decode aesSalt + + Convert.FromBase64String password, + Convert.FromBase64String key, + Convert.FromBase64String iv + + let encrypt (PlainTextPassword password) = + encryptString password + |||> serialize + + let decrypt encryptedPassword aesSalt = + deserialize encryptedPassword aesSalt + |||> decryptBytes + |> PlainTextPassword + +[] +module private DPApiSalt = + let private fillRandomBytes = + let provider = RandomNumberGenerator.Create() + (fun (b:byte[]) -> provider.GetBytes(b)) + + let encode bytes = + bytes + |> Convert.ToBase64String + |> DPApiSalt + + let getRandomSalt() = + let saltSize = 8 + let saltBytes = Array.create saltSize ( new Byte() ) + fillRandomBytes(saltBytes) + saltBytes + +[] +module DPApi = + let encrypt (PlainTextPassword password) = + let salt = DPApiSalt.getRandomSalt() + let encryptedPassword = + try + ProtectedData.Protect(Encoding.UTF8.GetBytes password, salt, DataProtectionScope.CurrentUser) + with | :? CryptographicException as e -> + if verbose then + verbosefn "could not protect password: %s\n for current user" e.Message + ProtectedData.Protect(Encoding.UTF8.GetBytes password, salt, DataProtectionScope.LocalMachine) + encryptedPassword |> Convert.ToBase64String |> DPApiEncryptedPassword, + salt |> DPApiSalt.encode + + let decrypt (encryptedPassword : string) (salt : string) = + ProtectedData.Unprotect(Convert.FromBase64String encryptedPassword, Convert.FromBase64String salt, DataProtectionScope.CurrentUser) + |> Encoding.UTF8.GetString + |> PlainTextPassword + +[] +module Crypto = + let encrypt plainTextPassword = + if Utils.isWindows then + let (dpApiPassword, dpApiSalt) = DPApi.encrypt plainTextPassword + (EncryptedPassword.DPApi dpApiPassword, Salt.DPApi dpApiSalt) + else + let (aesPassword, aesSalt) = Aes.encrypt plainTextPassword + (EncryptedPassword.Aes aesPassword, Salt.Aes aesSalt) + + let decrypt password salt = + match salt with + | AesSalt.IsAesSalt salt -> + Aes.decrypt (AesEncryptedPassword password) salt + | _ -> + DPApi.decrypt password salt \ No newline at end of file diff --git a/src/Paket.Core/Paket.Core.fsproj b/src/Paket.Core/Paket.Core.fsproj index 088569c046..a297baa284 100644 --- a/src/Paket.Core/Paket.Core.fsproj +++ b/src/Paket.Core/Paket.Core.fsproj @@ -80,6 +80,7 @@ + diff --git a/src/Paket.Core/Versioning/ConfigFile.fs b/src/Paket.Core/Versioning/ConfigFile.fs index a9556175f8..8d879c6de9 100644 --- a/src/Paket.Core/Versioning/ConfigFile.fs +++ b/src/Paket.Core/Versioning/ConfigFile.fs @@ -7,6 +7,7 @@ open System.Text open System.IO open Chessie.ErrorHandling +open Paket.Core.Common open Paket.Domain open Paket.Xml open Paket.Logging @@ -38,42 +39,12 @@ let private getConfigNode (nodeName : string) = return node } - let private saveConfigNode (node : XmlNode) = trial { do! createDir Constants.PaketConfigFolder do! saveNormalizedXml Constants.PaketConfigFile node.OwnerDocument } - -let private fillRandomBytes = - let provider = RandomNumberGenerator.Create() - (fun (b:byte[]) -> provider.GetBytes(b)) - -let private getRandomSalt() = - let saltSize = 8 - let saltBytes = Array.create saltSize ( new Byte() ) - fillRandomBytes(saltBytes) - saltBytes - -/// Encrypts a string with a user specific keys -let Encrypt (password : string) = - let salt = getRandomSalt() - let encryptedPassword = - try - ProtectedData.Protect(Encoding.UTF8.GetBytes password, salt, DataProtectionScope.CurrentUser) - with | :? CryptographicException as e -> - if verbose then - verbosefn "could not protect password: %s\n for current user" e.Message - ProtectedData.Protect(Encoding.UTF8.GetBytes password, salt, DataProtectionScope.LocalMachine) - salt |> Convert.ToBase64String , - encryptedPassword |> Convert.ToBase64String - -/// Decrypt a encrypted string with a user specific keys -let Decrypt (salt : string) (encrypted : string) = - ProtectedData.Unprotect(Convert.FromBase64String encrypted, Convert.FromBase64String salt, DataProtectionScope.CurrentUser) - |> Encoding.UTF8.GetString - let DecryptNuget (encrypted : string) = ProtectedData.Unprotect(Convert.FromBase64String encrypted, Encoding.UTF8.GetBytes "NuGet", DataProtectionScope.CurrentUser) |> Encoding.UTF8.GetString @@ -108,7 +79,8 @@ let getAuthFromNode (node : XmlNode) = | n -> n.Value |> NetUtils.parseAuthTypeString let salt = node.Attributes.["salt"].Value - Credentials ({Username = username; Password = Decrypt salt password; Type = authType}) + let (PlainTextPassword password) = Crypto.decrypt password salt + Credentials ({Username = username; Password = password; Type = authType}) | "token" -> Token node.Attributes.["value"].Value | _ -> failwith "unknown node" @@ -119,11 +91,11 @@ let private createSourceNode (credentialsNode : XmlNode) source nodeName = node let private setCredentials (username : string) (password : string) (authType : string) (node : XmlElement) = - let salt, encrypedPassword = Encrypt password + let encryptedPassword, salt = Crypto.encrypt (PlainTextPassword password) node.SetAttribute ("username", username) - node.SetAttribute ("password", encrypedPassword) + node.SetAttribute ("password", encryptedPassword.ToString()) node.SetAttribute ("authType", authType) - node.SetAttribute ("salt", salt) + node.SetAttribute ("salt", salt.ToString()) node let private setToken (token : string) (node : XmlElement) = diff --git a/tests/Paket.Tests/AesSpecs.fs b/tests/Paket.Tests/AesSpecs.fs new file mode 100644 index 0000000000..7ac01eec07 --- /dev/null +++ b/tests/Paket.Tests/AesSpecs.fs @@ -0,0 +1,12 @@ +module Paket.AesSpecs + +open FsUnit +open NUnit.Framework +open Paket.Core.Common + +[] +let ``should be able to decrypt encrypted password`` () = + let password = (PlainTextPassword "Super Secret 123!") + Aes.encrypt password + ||> Aes.decrypt + |> shouldEqual password \ No newline at end of file diff --git a/tests/Paket.Tests/Paket.Tests.fsproj b/tests/Paket.Tests/Paket.Tests.fsproj index d5494a6577..cdf730d456 100644 --- a/tests/Paket.Tests/Paket.Tests.fsproj +++ b/tests/Paket.Tests/Paket.Tests.fsproj @@ -81,6 +81,7 @@ + diff --git a/tests/Paket.Tests/Versioning/ConfigFileSpecs.fs b/tests/Paket.Tests/Versioning/ConfigFileSpecs.fs index 3dc1d711ca..e14901b76d 100644 --- a/tests/Paket.Tests/Versioning/ConfigFileSpecs.fs +++ b/tests/Paket.Tests/Versioning/ConfigFileSpecs.fs @@ -1,6 +1,7 @@ module Paket.ConfigFileSpecs open Paket +open Paket.Core.Common open Paket.ConfigFile open NUnit.Framework open System.Xml @@ -31,9 +32,9 @@ let ``get username, password, and auth type from node``() = let doc = sampleDoc() let node = doc.CreateElement("credential") node.SetAttribute("username", "demo-user") - let salt, password = Encrypt "demopassword" - node.SetAttribute("password", password) - node.SetAttribute("salt", salt) + let password, salt = Crypto.encrypt (PlainTextPassword "demopassword") + node.SetAttribute("password", password.ToString()) + node.SetAttribute("salt", salt.ToString()) node.SetAttribute("authType", "ntlm") // Act let (Credentials{Username = username; Password = password; Type = NetUtils.AuthType.NTLM}) = getAuthFromNode node @@ -53,9 +54,9 @@ let ``get username and password from node without auth type``() = let doc = sampleDoc() let node = doc.CreateElement("credential") node.SetAttribute("username", "demo-user") - let salt, password = Encrypt "demopassword" - node.SetAttribute("password", password) - node.SetAttribute("salt", salt) + let password, salt = Crypto.encrypt (PlainTextPassword "demopassword") + node.SetAttribute("password", password.ToString()) + node.SetAttribute("salt", salt.ToString()) // Act let (Credentials{Username = username; Password = password; Type = NetUtils.AuthType.Basic}) = getAuthFromNode node From cbd659dd53f71db31c8eb27739644281a8c223ce Mon Sep 17 00:00:00 2001 From: Benjamin Gobeil Date: Thu, 23 Jul 2020 08:44:47 -0400 Subject: [PATCH 2/2] MAke it build --- src/Paket.Core.preview3/Paket.Core.fsproj | 1 + src/Paket.Core/Common/Encryption.fs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Paket.Core.preview3/Paket.Core.fsproj b/src/Paket.Core.preview3/Paket.Core.fsproj index 4816582da8..5847502fdc 100644 --- a/src/Paket.Core.preview3/Paket.Core.fsproj +++ b/src/Paket.Core.preview3/Paket.Core.fsproj @@ -30,6 +30,7 @@ + diff --git a/src/Paket.Core/Common/Encryption.fs b/src/Paket.Core/Common/Encryption.fs index d0f2c0059f..f1962a6668 100644 --- a/src/Paket.Core/Common/Encryption.fs +++ b/src/Paket.Core/Common/Encryption.fs @@ -45,7 +45,7 @@ module private AesSalt = (AesIV << Convert.FromBase64String) ivString let encode ((AesKey key): AesKey,(AesIV iv): AesIV) = - seq {key;iv} + seq {yield key; yield iv} |> Seq.map Convert.ToBase64String |> String.concat saltSeparator |> AesSalt @@ -146,7 +146,7 @@ module DPApi = [] module Crypto = let encrypt plainTextPassword = - if Utils.isWindows then + if isWindows then let (dpApiPassword, dpApiSalt) = DPApi.encrypt plainTextPassword (EncryptedPassword.DPApi dpApiPassword, Salt.DPApi dpApiSalt) else