Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Paket.Core.preview3/Paket.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<Compile Include="$(PaketCoreSourcesDir)\Common\Constants.fs" />
<Compile Include="$(PaketCoreSourcesDir)\Common\Profile.fs" />
<Compile Include="$(PaketCoreSourcesDir)\Common\Utils.fs" />
<Compile Include="$(PaketCoreSourcesDir)\Common\Encryption.fs" />
<Compile Include="$(PaketCoreSourcesDir)\Common\Xml.fs" />
<Compile Include="$(PaketCoreSourcesDir)\Common\ProcessHelper.fs" />
<Compile Include="$(PaketCoreSourcesDir)\Common\SymlinkUtils.fs" />
Expand Down
161 changes: 161 additions & 0 deletions src/Paket.Core/Common/Encryption.fs
Original file line number Diff line number Diff line change
@@ -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

[<RequireQualifiedAccess>]
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

[<RequireQualifiedAccess>]
type Salt =
| Aes of AesSalt
| DPApi of DPApiSalt
with
override x.ToString() =
match x with
| Aes (AesSalt salt) | DPApi (DPApiSalt salt) -> salt

[<RequireQualifiedAccess>]
module private AesSalt =
let [<Literal>] saltSeparator = "SALT_SEPARATOR"

let private toBytes (AesKey keyString, AesIV ivString) =
(AesKey << Convert.FromBase64String) keyString,
(AesIV << Convert.FromBase64String) ivString

let encode ((AesKey key): AesKey<byte[]>,(AesIV iv): AesIV<byte[]>) =
seq {yield key; yield 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

[<RequireQualifiedAccess>]
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

[<RequireQualifiedAccess>]
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

[<RequireQualifiedAccess>]
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

[<RequireQualifiedAccess>]
module Crypto =
let encrypt plainTextPassword =
if 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
1 change: 1 addition & 0 deletions src/Paket.Core/Paket.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
<Compile Include="Common\Constants.fs" />
<Compile Include="Common\Profile.fs" />
<Compile Include="Common\Utils.fs" />
<Compile Include="Common\Encryption.fs" />
<Compile Include="Common\Xml.fs" />
<Compile Include="Common\ProcessHelper.fs" />
<Compile Include="Common\SymlinkUtils.fs" />
Expand Down
40 changes: 6 additions & 34 deletions src/Paket.Core/Versioning/ConfigFile.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"

Expand All @@ -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) =
Expand Down
12 changes: 12 additions & 0 deletions tests/Paket.Tests/AesSpecs.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Paket.AesSpecs

open FsUnit
open NUnit.Framework
open Paket.Core.Common

[<Test>]
let ``should be able to decrypt encrypted password`` () =
let password = (PlainTextPassword "Super Secret 123!")
Aes.encrypt password
||> Aes.decrypt
|> shouldEqual password
1 change: 1 addition & 0 deletions tests/Paket.Tests/Paket.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
<Compile Include="AssemblyInfo.fs" />
<Compile Include="TestHelpers.fs" />
<Compile Include="UtilsSpecs.fs" />
<Compile Include="AesSpecs.fs" />
<Compile Include="Versioning\PackageSourceSpecs.fs" />
<Compile Include="Versioning\PlatformMatchingSpecs.fs" />
<Compile Include="Versioning\SemVerSpecs.fs" />
Expand Down
13 changes: 7 additions & 6 deletions tests/Paket.Tests/Versioning/ConfigFileSpecs.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Paket.ConfigFileSpecs

open Paket
open Paket.Core.Common
open Paket.ConfigFile
open NUnit.Framework
open System.Xml
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand Down