Skip to content

Commit b22361c

Browse files
committed
Use AES encryption when not on windows
1 parent 46aa98e commit b22361c

6 files changed

Lines changed: 188 additions & 40 deletions

File tree

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
namespace Paket.Core.Common
2+
3+
open System
4+
open System.IO
5+
open System.Net.Http
6+
open System.Security.Cryptography
7+
open System.Text
8+
open Paket
9+
open Paket.Logging
10+
11+
type private AesKey<'T> = private AesKey of 'T
12+
type private AesIV<'T> = private AesIV of 'T
13+
14+
type PlainTextPassword = PlainTextPassword of string
15+
type AesEncryptedPassword = private AesEncryptedPassword of string
16+
type DPApiEncryptedPassword = private DPApiEncryptedPassword of string
17+
18+
[<RequireQualifiedAccess>]
19+
type EncryptedPassword =
20+
| Aes of AesEncryptedPassword
21+
| DPApi of DPApiEncryptedPassword
22+
with
23+
override x.ToString() =
24+
match x with
25+
| Aes (AesEncryptedPassword password) | DPApi (DPApiEncryptedPassword password) -> password
26+
27+
type AesSalt = private AesSalt of string
28+
type DPApiSalt = private DPApiSalt of string
29+
30+
[<RequireQualifiedAccess>]
31+
type Salt =
32+
| Aes of AesSalt
33+
| DPApi of DPApiSalt
34+
with
35+
override x.ToString() =
36+
match x with
37+
| Aes (AesSalt salt) | DPApi (DPApiSalt salt) -> salt
38+
39+
[<RequireQualifiedAccess>]
40+
module private AesSalt =
41+
let [<Literal>] saltSeparator = "SALT_SEPARATOR"
42+
43+
let private toBytes (AesKey keyString, AesIV ivString) =
44+
(AesKey << Convert.FromBase64String) keyString,
45+
(AesIV << Convert.FromBase64String) ivString
46+
47+
let encode ((AesKey key): AesKey<byte[]>,(AesIV iv): AesIV<byte[]>) =
48+
seq {key;iv}
49+
|> Seq.map Convert.ToBase64String
50+
|> String.concat saltSeparator
51+
|> AesSalt
52+
53+
let decode (AesSalt saltString) =
54+
match saltString.Split([|saltSeparator|], StringSplitOptions.None) with
55+
| [|keyString;ivString|] -> (AesKey keyString, AesIV ivString)
56+
| _ -> failwith "Should never happen"
57+
58+
let (|IsAesSalt|_|) (str: string) =
59+
if str.Contains saltSeparator then
60+
Some <| AesSalt str
61+
else
62+
None
63+
64+
[<RequireQualifiedAccess>]
65+
module Aes =
66+
let private encryptString (password: string) =
67+
let writePassword (cryptoStream: CryptoStream) =
68+
use streamWriter = new StreamWriter(cryptoStream)
69+
streamWriter.Write(password)
70+
71+
use aes = Aes.Create()
72+
let encryptor = aes.CreateEncryptor()
73+
use memoryStream = new MemoryStream()
74+
use cryptoStream = new CryptoStream(memoryStream,encryptor,CryptoStreamMode.Write)
75+
writePassword cryptoStream
76+
let encryptedPassword = memoryStream.ToArray()
77+
encryptedPassword,aes.Key,aes.IV
78+
79+
let private decryptBytes (encryptedBytes: byte[]) key iv =
80+
use aes = Aes.Create()
81+
aes.Key <- key
82+
aes.IV <- iv
83+
let decryptor = aes.CreateDecryptor()
84+
use memoryStream = new MemoryStream(encryptedBytes)
85+
use cryptoStream = new CryptoStream(memoryStream,decryptor,CryptoStreamMode.Read)
86+
use streamReader = new StreamReader(cryptoStream)
87+
let password = streamReader.ReadToEnd()
88+
password
89+
90+
let private serialize password (key: byte[]) (iv: byte[]) =
91+
(AesEncryptedPassword << Convert.ToBase64String) password,
92+
AesSalt.encode (AesKey key, AesIV iv)
93+
94+
let private deserialize (AesEncryptedPassword password) aesSalt =
95+
let (AesKey key, AesIV iv) = AesSalt.decode aesSalt
96+
97+
Convert.FromBase64String password,
98+
Convert.FromBase64String key,
99+
Convert.FromBase64String iv
100+
101+
let encrypt (PlainTextPassword password) =
102+
encryptString password
103+
|||> serialize
104+
105+
let decrypt encryptedPassword aesSalt =
106+
deserialize encryptedPassword aesSalt
107+
|||> decryptBytes
108+
|> PlainTextPassword
109+
110+
[<RequireQualifiedAccess>]
111+
module private DPApiSalt =
112+
let private fillRandomBytes =
113+
let provider = RandomNumberGenerator.Create()
114+
(fun (b:byte[]) -> provider.GetBytes(b))
115+
116+
let encode bytes =
117+
bytes
118+
|> Convert.ToBase64String
119+
|> DPApiSalt
120+
121+
let getRandomSalt() =
122+
let saltSize = 8
123+
let saltBytes = Array.create saltSize ( new Byte() )
124+
fillRandomBytes(saltBytes)
125+
saltBytes
126+
127+
[<RequireQualifiedAccess>]
128+
module DPApi =
129+
let encrypt (PlainTextPassword password) =
130+
let salt = DPApiSalt.getRandomSalt()
131+
let encryptedPassword =
132+
try
133+
ProtectedData.Protect(Encoding.UTF8.GetBytes password, salt, DataProtectionScope.CurrentUser)
134+
with | :? CryptographicException as e ->
135+
if verbose then
136+
verbosefn "could not protect password: %s\n for current user" e.Message
137+
ProtectedData.Protect(Encoding.UTF8.GetBytes password, salt, DataProtectionScope.LocalMachine)
138+
encryptedPassword |> Convert.ToBase64String |> DPApiEncryptedPassword,
139+
salt |> DPApiSalt.encode
140+
141+
let decrypt (encryptedPassword : string) (salt : string) =
142+
ProtectedData.Unprotect(Convert.FromBase64String encryptedPassword, Convert.FromBase64String salt, DataProtectionScope.CurrentUser)
143+
|> Encoding.UTF8.GetString
144+
|> PlainTextPassword
145+
146+
[<RequireQualifiedAccess>]
147+
module Crypto =
148+
let encrypt plainTextPassword =
149+
if Utils.isWindows then
150+
let (dpApiPassword, dpApiSalt) = DPApi.encrypt plainTextPassword
151+
(EncryptedPassword.DPApi dpApiPassword, Salt.DPApi dpApiSalt)
152+
else
153+
let (aesPassword, aesSalt) = Aes.encrypt plainTextPassword
154+
(EncryptedPassword.Aes aesPassword, Salt.Aes aesSalt)
155+
156+
let decrypt password salt =
157+
match salt with
158+
| AesSalt.IsAesSalt salt ->
159+
Aes.decrypt (AesEncryptedPassword password) salt
160+
| _ ->
161+
DPApi.decrypt password salt

‎src/Paket.Core/Paket.Core.fsproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<Compile Include="Common\Constants.fs" />
4040
<Compile Include="Common\Profile.fs" />
4141
<Compile Include="Common\Utils.fs" />
42+
<Compile Include="Common\Encryption.fs" />
4243
<Compile Include="Common\Xml.fs" />
4344
<Compile Include="Common\ProcessHelper.fs" />
4445
<Compile Include="Common\SymlinkUtils.fs" />

‎src/Paket.Core/Versioning/ConfigFile.fs‎

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ open System.Text
77
open System.IO
88

99
open Chessie.ErrorHandling
10+
open Paket.Core.Common
1011
open Paket.Domain
1112
open Paket.Xml
1213
open Paket.Logging
@@ -38,42 +39,12 @@ let private getConfigNode (nodeName : string) =
3839
return node
3940
}
4041

41-
4242
let private saveConfigNode (node : XmlNode) =
4343
trial {
4444
do! createDir Constants.PaketConfigFolder
4545
do! saveNormalizedXml Constants.PaketConfigFile node.OwnerDocument
4646
}
4747

48-
49-
let private fillRandomBytes =
50-
let provider = RandomNumberGenerator.Create()
51-
(fun (b:byte[]) -> provider.GetBytes(b))
52-
53-
let private getRandomSalt() =
54-
let saltSize = 8
55-
let saltBytes = Array.create saltSize ( new Byte() )
56-
fillRandomBytes(saltBytes)
57-
saltBytes
58-
59-
/// Encrypts a string with a user specific keys
60-
let Encrypt (password : string) =
61-
let salt = getRandomSalt()
62-
let encryptedPassword =
63-
try
64-
ProtectedData.Protect(Encoding.UTF8.GetBytes password, salt, DataProtectionScope.CurrentUser)
65-
with | :? CryptographicException as e ->
66-
if verbose then
67-
verbosefn "could not protect password: %s\n for current user" e.Message
68-
ProtectedData.Protect(Encoding.UTF8.GetBytes password, salt, DataProtectionScope.LocalMachine)
69-
salt |> Convert.ToBase64String ,
70-
encryptedPassword |> Convert.ToBase64String
71-
72-
/// Decrypt a encrypted string with a user specific keys
73-
let Decrypt (salt : string) (encrypted : string) =
74-
ProtectedData.Unprotect(Convert.FromBase64String encrypted, Convert.FromBase64String salt, DataProtectionScope.CurrentUser)
75-
|> Encoding.UTF8.GetString
76-
7748
let DecryptNuget (encrypted : string) =
7849
ProtectedData.Unprotect(Convert.FromBase64String encrypted, Encoding.UTF8.GetBytes "NuGet", DataProtectionScope.CurrentUser)
7950
|> Encoding.UTF8.GetString
@@ -108,7 +79,8 @@ let getAuthFromNode (node : XmlNode) =
10879
| n -> n.Value |> NetUtils.parseAuthTypeString
10980

11081
let salt = node.Attributes.["salt"].Value
111-
Credentials ({Username = username; Password = Decrypt salt password; Type = authType})
82+
let (PlainTextPassword password) = Crypto.decrypt password salt
83+
Credentials ({Username = username; Password = password; Type = authType})
11284
| "token" -> Token node.Attributes.["value"].Value
11385
| _ -> failwith "unknown node"
11486

@@ -119,11 +91,11 @@ let private createSourceNode (credentialsNode : XmlNode) source nodeName =
11991
node
12092

12193
let private setCredentials (username : string) (password : string) (authType : string) (node : XmlElement) =
122-
let salt, encrypedPassword = Encrypt password
94+
let encryptedPassword, salt = Crypto.encrypt (PlainTextPassword password)
12395
node.SetAttribute ("username", username)
124-
node.SetAttribute ("password", encrypedPassword)
96+
node.SetAttribute ("password", encryptedPassword.ToString())
12597
node.SetAttribute ("authType", authType)
126-
node.SetAttribute ("salt", salt)
98+
node.SetAttribute ("salt", salt.ToString())
12799
node
128100

129101
let private setToken (token : string) (node : XmlElement) =

‎tests/Paket.Tests/AesSpecs.fs‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module Paket.AesSpecs
2+
3+
open FsUnit
4+
open NUnit.Framework
5+
open Paket.Core.Common
6+
7+
[<Test>]
8+
let ``should be able to decrypt encrypted password`` () =
9+
let password = (PlainTextPassword "Super Secret 123!")
10+
Aes.encrypt password
11+
||> Aes.decrypt
12+
|> shouldEqual password

‎tests/Paket.Tests/Paket.Tests.fsproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<Compile Include="AssemblyInfo.fs" />
1919
<Compile Include="TestHelpers.fs" />
2020
<Compile Include="UtilsSpecs.fs" />
21+
<Compile Include="AesSpecs.fs" />
2122
<Compile Include="Versioning\PackageSourceSpecs.fs" />
2223
<Compile Include="Versioning\PlatformMatchingSpecs.fs" />
2324
<Compile Include="Versioning\SemVerSpecs.fs" />

‎tests/Paket.Tests/Versioning/ConfigFileSpecs.fs‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module Paket.ConfigFileSpecs
22

33
open Paket
4+
open Paket.Core.Common
45
open Paket.ConfigFile
56
open NUnit.Framework
67
open System.Xml
@@ -31,9 +32,9 @@ let ``get username, password, and auth type from node``() =
3132
let doc = sampleDoc()
3233
let node = doc.CreateElement("credential")
3334
node.SetAttribute("username", "demo-user")
34-
let salt, password = Encrypt "demopassword"
35-
node.SetAttribute("password", password)
36-
node.SetAttribute("salt", salt)
35+
let password, salt = Crypto.encrypt (PlainTextPassword "demopassword")
36+
node.SetAttribute("password", password.ToString())
37+
node.SetAttribute("salt", salt.ToString())
3738
node.SetAttribute("authType", "ntlm")
3839
// Act
3940
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``() =
5354
let doc = sampleDoc()
5455
let node = doc.CreateElement("credential")
5556
node.SetAttribute("username", "demo-user")
56-
let salt, password = Encrypt "demopassword"
57-
node.SetAttribute("password", password)
58-
node.SetAttribute("salt", salt)
57+
let password, salt = Crypto.encrypt (PlainTextPassword "demopassword")
58+
node.SetAttribute("password", password.ToString())
59+
node.SetAttribute("salt", salt.ToString())
5960
// Act
6061
let (Credentials{Username = username; Password = password; Type = NetUtils.AuthType.Basic}) = getAuthFromNode node
6162

0 commit comments

Comments
 (0)