2016-08-23 16:38:53 +02:00
using Newtonsoft.Json ;
2016-08-22 01:30:49 +02:00
using SteamAuth ;
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Text ;
using System.Threading.Tasks ;
2016-08-22 20:13:36 +02:00
using System.Security.Cryptography ;
2016-08-22 01:30:49 +02:00
public class Manifest
{
2016-08-22 20:13:36 +02:00
private const int PBKDF2_ITERATIONS = 50000 ; //Set to 50k to make program not unbearably slow. May increase in future.
private const int SALT_LENGTH = 8 ;
private const int KEY_SIZE_BYTES = 32 ;
private const int IV_LENGTH = 16 ;
2016-08-22 01:30:49 +02:00
[JsonProperty("encrypted")]
public bool Encrypted { get ; set ; }
[JsonProperty("first_run")]
public bool FirstRun { get ; set ; } = true ;
[JsonProperty("entries")]
public List < ManifestEntry > Entries { get ; set ; }
[JsonProperty("periodic_checking")]
public bool PeriodicChecking { get ; set ; } = false ;
[JsonProperty("periodic_checking_interval")]
public int PeriodicCheckingInterval { get ; set ; } = 5 ;
[JsonProperty("periodic_checking_checkall")]
public bool CheckAllAccounts { get ; set ; } = false ;
[JsonProperty("auto_confirm_market_transactions")]
public bool AutoConfirmMarketTransactions { get ; set ; } = false ;
[JsonProperty("auto_confirm_trades")]
public bool AutoConfirmTrades { get ; set ; } = false ;
private static Manifest _manifest { get ; set ; }
public static Manifest GetManifest ( bool forceLoad = false )
{
// Check if already staticly loaded
if ( _manifest ! = null & & ! forceLoad )
{
return _manifest ;
}
// Find config dir and manifest file
2016-08-23 00:54:14 +02:00
string maFile = Path . Combine ( Program . SteamGuardPath , "manifest.json" ) ;
2016-08-22 01:30:49 +02:00
// If there's no config dir, create it
if ( ! Directory . Exists ( Program . SteamGuardPath ) )
{
_manifest = _generateNewManifest ( ) ;
return _manifest ;
}
// If there's no manifest, create it
if ( ! File . Exists ( maFile ) )
{
if ( Program . Verbose ) Console . WriteLine ( "warn: No manifest file found at {0}" , maFile ) ;
_manifest = _generateNewManifest ( true ) ;
return _manifest ;
}
try
{
string manifestContents = File . ReadAllText ( maFile ) ;
_manifest = JsonConvert . DeserializeObject < Manifest > ( manifestContents ) ;
if ( _manifest . Encrypted & & _manifest . Entries . Count = = 0 )
{
_manifest . Encrypted = false ;
_manifest . Save ( ) ;
}
_manifest . RecomputeExistingEntries ( ) ;
return _manifest ;
}
catch ( Exception ex )
{
Console . WriteLine ( "error: Could not open manifest file: {0}" , ex . ToString ( ) ) ;
return null ;
}
}
private static Manifest _generateNewManifest ( bool scanDir = false )
{
if ( Program . Verbose ) Console . WriteLine ( "Generating new manifest..." ) ;
// No directory means no manifest file anyways.
Manifest newManifest = new Manifest ( ) ;
newManifest . Encrypted = false ;
newManifest . PeriodicCheckingInterval = 5 ;
newManifest . PeriodicChecking = false ;
newManifest . AutoConfirmMarketTransactions = false ;
newManifest . AutoConfirmTrades = false ;
newManifest . Entries = new List < ManifestEntry > ( ) ;
newManifest . FirstRun = true ;
// Take a pre-manifest version and generate a manifest for it.
if ( scanDir )
{
if ( Directory . Exists ( Program . SteamGuardPath ) )
{
DirectoryInfo dir = new DirectoryInfo ( Program . SteamGuardPath ) ;
var files = dir . GetFiles ( ) ;
foreach ( var file in files )
{
if ( file . Extension ! = ".maFile" ) continue ;
string contents = File . ReadAllText ( file . FullName ) ;
try
{
SteamGuardAccount account = JsonConvert . DeserializeObject < SteamGuardAccount > ( contents ) ;
ManifestEntry newEntry = new ManifestEntry ( )
{
Filename = file . Name ,
SteamID = account . Session . SteamID
} ;
newManifest . Entries . Add ( newEntry ) ;
}
2016-08-23 03:12:48 +02:00
catch ( Exception ex )
2016-08-22 01:30:49 +02:00
{
2016-08-23 03:12:48 +02:00
if ( Program . Verbose ) Console . WriteLine ( "warn: {0}" , ex . Message ) ;
2016-08-22 01:30:49 +02:00
}
}
if ( newManifest . Entries . Count > 0 )
{
newManifest . Save ( ) ;
2016-08-23 03:12:48 +02:00
newManifest . PromptSetupPassKey ( true ) ;
2016-08-22 01:30:49 +02:00
}
}
}
if ( newManifest . Save ( ) )
{
return newManifest ;
}
return null ;
}
public class IncorrectPassKeyException : Exception { }
public class ManifestNotEncryptedException : Exception { }
2016-08-23 00:52:02 +02:00
// TODO: move PromptForPassKey to Program.cs
// TODO: make PromptForPassKey more secure
2016-08-22 01:30:49 +02:00
public string PromptForPassKey ( )
{
if ( ! this . Encrypted )
{
throw new ManifestNotEncryptedException ( ) ;
}
bool passKeyValid = false ;
string passKey = "" ;
while ( ! passKeyValid )
{
Console . WriteLine ( "Please enter encryption password: " ) ;
passKey = Console . ReadLine ( ) ;
if ( passKey = = "" )
continue ;
passKeyValid = this . VerifyPasskey ( passKey ) ;
if ( ! passKeyValid )
{
Console . WriteLine ( "Incorrect." ) ;
}
}
return passKey ;
}
2016-08-23 00:52:02 +02:00
// TODO: move PromptSetupPassKey to Program.cs
2016-08-23 03:12:48 +02:00
public string PromptSetupPassKey ( bool inAccountSetupProcess = false )
2016-08-22 01:30:49 +02:00
{
2016-08-23 03:12:48 +02:00
if ( inAccountSetupProcess )
2016-08-22 01:30:49 +02:00
{
2016-08-23 03:12:48 +02:00
Console . Write ( "Would you like to use encryption? [Y/n] " ) ;
string doEncryptAnswer = Console . ReadLine ( ) ;
if ( doEncryptAnswer = = "n" | | doEncryptAnswer = = "N" )
{
Console . WriteLine ( "WARNING: You chose to not encrypt your files. Doing so imposes a security risk for yourself. If an attacker were to gain access to your computer, they could completely lock you out of your account and steal all your items." ) ;
return null ;
}
2016-08-22 01:30:49 +02:00
}
string newPassKey = "" ;
string confirmPassKey = "" ;
do
{
2016-08-23 03:12:48 +02:00
Console . Write ( "Enter" + ( inAccountSetupProcess ? " " : " new " ) + "passkey: " ) ;
2016-08-22 01:30:49 +02:00
newPassKey = Console . ReadLine ( ) ;
2016-08-23 03:12:48 +02:00
Console . Write ( "Confirm" + ( inAccountSetupProcess ? " " : " new " ) + "passkey: " ) ;
2016-08-22 01:30:49 +02:00
confirmPassKey = Console . ReadLine ( ) ;
if ( newPassKey ! = confirmPassKey )
{
Console . WriteLine ( "Passkeys do not match." ) ;
}
2016-08-23 03:12:48 +02:00
} while ( newPassKey ! = confirmPassKey | | newPassKey = = "" ) ;
2016-08-22 01:30:49 +02:00
return newPassKey ;
}
public SteamAuth . SteamGuardAccount [ ] GetAllAccounts ( string passKey = null , int limit = - 1 )
{
if ( passKey = = null & & this . Encrypted ) return new SteamGuardAccount [ 0 ] ;
List < SteamAuth . SteamGuardAccount > accounts = new List < SteamAuth . SteamGuardAccount > ( ) ;
foreach ( var entry in this . Entries )
{
2016-08-23 00:42:44 +02:00
var account = GetAccount ( entry , passKey ) ;
2016-08-22 01:30:49 +02:00
if ( account = = null ) continue ;
accounts . Add ( account ) ;
if ( limit ! = - 1 & & limit > = accounts . Count )
break ;
}
return accounts . ToArray ( ) ;
}
2016-08-23 00:42:44 +02:00
public SteamGuardAccount GetAccount ( ManifestEntry entry , string passKey = null )
2016-08-22 01:30:49 +02:00
{
2016-08-23 00:42:44 +02:00
string fileText = "" ;
Stream stream = null ;
RijndaelManaged aes256 ;
if ( this . Encrypted )
{
MemoryStream ms = new MemoryStream ( Convert . FromBase64String ( File . ReadAllText ( Path . Combine ( Program . SteamGuardPath , entry . Filename ) ) ) ) ;
byte [ ] key = GetEncryptionKey ( passKey , entry . Salt ) ;
aes256 = new RijndaelManaged
{
IV = Convert . FromBase64String ( entry . IV ) ,
Key = key ,
Padding = PaddingMode . PKCS7 ,
Mode = CipherMode . CBC
} ;
ICryptoTransform decryptor = aes256 . CreateDecryptor ( aes256 . Key , aes256 . IV ) ;
stream = new CryptoStream ( ms , decryptor , CryptoStreamMode . Read ) ;
}
else
{
FileStream fileStream = File . OpenRead ( Path . Combine ( Program . SteamGuardPath , entry . Filename ) ) ;
stream = fileStream ;
}
if ( Program . Verbose ) Console . WriteLine ( "Decrypting..." ) ;
using ( StreamReader reader = new StreamReader ( stream ) )
{
fileText = reader . ReadToEnd ( ) ;
}
stream . Close ( ) ;
return JsonConvert . DeserializeObject < SteamAuth . SteamGuardAccount > ( fileText ) ;
2016-08-22 01:30:49 +02:00
}
public bool VerifyPasskey ( string passkey )
{
if ( ! this . Encrypted | | this . Entries . Count = = 0 ) return true ;
var accounts = this . GetAllAccounts ( passkey , 1 ) ;
return accounts ! = null & & accounts . Length = = 1 ;
}
public bool RemoveAccount ( SteamGuardAccount account , bool deleteMaFile = true )
{
ManifestEntry entry = ( from e in this . Entries where e . SteamID = = account . Session . SteamID select e ) . FirstOrDefault ( ) ;
if ( entry = = null ) return true ; // If something never existed, did you do what they asked?
string filename = Path . Combine ( Program . SteamGuardPath , entry . Filename ) ;
this . Entries . Remove ( entry ) ;
if ( this . Entries . Count = = 0 )
{
this . Encrypted = false ;
}
if ( this . Save ( ) & & deleteMaFile )
{
try
{
File . Delete ( filename ) ;
return true ;
}
catch ( Exception )
{
return false ;
}
}
return false ;
}
2016-08-23 03:12:48 +02:00
public bool SaveAccount ( SteamGuardAccount account , bool encrypt , string passKey = null , string salt = null , string iV = null )
2016-08-22 01:30:49 +02:00
{
2016-08-23 19:49:52 +02:00
if ( encrypt & & ( String . IsNullOrEmpty ( passKey ) | | String . IsNullOrEmpty ( salt ) | | String . IsNullOrEmpty ( iV ) ) ) return false ;
2016-08-22 01:30:49 +02:00
string jsonAccount = JsonConvert . SerializeObject ( account ) ;
string filename = account . Session . SteamID . ToString ( ) + ".maFile" ;
2016-08-23 19:29:49 +02:00
if ( Program . Verbose ) Console . WriteLine ( $"Saving account {account.AccountName} to {filename}..." ) ;
2016-08-22 01:30:49 +02:00
ManifestEntry newEntry = new ManifestEntry ( )
{
SteamID = account . Session . SteamID ,
IV = iV ,
Salt = salt ,
Filename = filename
} ;
bool foundExistingEntry = false ;
for ( int i = 0 ; i < this . Entries . Count ; i + + )
{
if ( this . Entries [ i ] . SteamID = = account . Session . SteamID )
{
this . Entries [ i ] = newEntry ;
foundExistingEntry = true ;
break ;
}
}
if ( ! foundExistingEntry )
{
this . Entries . Add ( newEntry ) ;
}
bool wasEncrypted = this . Encrypted ;
2016-08-23 19:59:10 +02:00
this . Encrypted = encrypt ;
2016-08-22 01:30:49 +02:00
if ( ! this . Save ( ) )
{
this . Encrypted = wasEncrypted ;
return false ;
}
try
{
2016-08-22 21:51:37 +02:00
Stream stream = null ;
2016-08-23 00:42:44 +02:00
MemoryStream ms = null ;
2016-08-22 21:51:37 +02:00
RijndaelManaged aes256 ;
2016-08-23 19:49:52 +02:00
if ( encrypt )
2016-08-22 21:51:37 +02:00
{
2016-08-23 00:42:44 +02:00
ms = new MemoryStream ( ) ;
2016-08-22 21:51:37 +02:00
byte [ ] key = GetEncryptionKey ( passKey , newEntry . Salt ) ;
aes256 = new RijndaelManaged
{
IV = Convert . FromBase64String ( newEntry . IV ) ,
Key = key ,
Padding = PaddingMode . PKCS7 ,
Mode = CipherMode . CBC
} ;
2016-08-23 03:12:48 +02:00
ICryptoTransform encryptor = aes256 . CreateEncryptor ( aes256 . Key , aes256 . IV ) ;
stream = new CryptoStream ( ms , encryptor , CryptoStreamMode . Write ) ;
2016-08-22 21:51:37 +02:00
}
else
{
2016-08-23 20:49:07 +02:00
// An unencrypted maFile is shorter than the encrypted version,
// so when an unencrypted maFile gets written this way, the file does not get wiped
// leaving encrypted text after the final } bracket. Deleting and recreating the file fixes this.
File . Delete ( Path . Combine ( Program . SteamGuardPath , newEntry . Filename ) ) ;
stream = File . OpenWrite ( Path . Combine ( Program . SteamGuardPath , newEntry . Filename ) ) ; // open or create
2016-08-22 21:51:37 +02:00
}
using ( StreamWriter writer = new StreamWriter ( stream ) )
{
writer . Write ( jsonAccount ) ;
}
2016-08-23 00:42:44 +02:00
2016-08-23 19:49:52 +02:00
if ( encrypt )
2016-08-23 00:42:44 +02:00
{
2016-08-23 03:12:48 +02:00
File . WriteAllText ( Path . Combine ( Program . SteamGuardPath , newEntry . Filename ) , Convert . ToBase64String ( ms . ToArray ( ) ) ) ;
2016-08-23 00:42:44 +02:00
}
2016-08-22 21:51:37 +02:00
stream . Close ( ) ;
2016-08-22 01:30:49 +02:00
return true ;
}
2016-08-23 03:12:48 +02:00
catch ( Exception ex )
2016-08-22 01:30:49 +02:00
{
2016-08-23 03:12:48 +02:00
if ( Program . Verbose ) Console . WriteLine ( "error: {0}" , ex . ToString ( ) ) ;
2016-08-22 01:30:49 +02:00
return false ;
}
}
public bool Save ( )
{
2016-08-23 19:29:49 +02:00
string filename = Path . Combine ( Program . SteamGuardPath , "manifest.json" ) ;
2016-08-22 01:30:49 +02:00
if ( ! Directory . Exists ( Program . SteamGuardPath ) )
{
try
{
2016-08-23 19:29:49 +02:00
if ( Program . Verbose ) Console . WriteLine ( "Creating {0}" , Program . SteamGuardPath ) ;
2016-08-22 01:30:49 +02:00
Directory . CreateDirectory ( Program . SteamGuardPath ) ;
}
2016-08-23 19:29:49 +02:00
catch ( Exception ex )
2016-08-22 01:30:49 +02:00
{
2016-08-23 19:29:49 +02:00
if ( Program . Verbose ) Console . WriteLine ( $"error: {ex.Message}" ) ;
2016-08-22 01:30:49 +02:00
return false ;
}
}
try
{
string contents = JsonConvert . SerializeObject ( this ) ;
File . WriteAllText ( filename , contents ) ;
return true ;
}
2016-08-23 19:29:49 +02:00
catch ( Exception ex )
2016-08-22 01:30:49 +02:00
{
2016-08-23 19:29:49 +02:00
if ( Program . Verbose ) Console . WriteLine ( $"error: {ex.Message}" ) ;
2016-08-22 01:30:49 +02:00
return false ;
}
}
private void RecomputeExistingEntries ( )
{
List < ManifestEntry > newEntries = new List < ManifestEntry > ( ) ;
foreach ( var entry in this . Entries )
{
string filename = Path . Combine ( Program . SteamGuardPath , entry . Filename ) ;
if ( File . Exists ( filename ) )
{
newEntries . Add ( entry ) ;
}
}
this . Entries = newEntries ;
if ( this . Entries . Count = = 0 )
{
this . Encrypted = false ;
}
}
public void MoveEntry ( int from , int to )
{
if ( from < 0 | | to < 0 | | from > Entries . Count | | to > Entries . Count - 1 ) return ;
ManifestEntry sel = Entries [ from ] ;
Entries . RemoveAt ( from ) ;
Entries . Insert ( to , sel ) ;
Save ( ) ;
}
public class ManifestEntry
{
[JsonProperty("encryption_iv")]
public string IV { get ; set ; }
[JsonProperty("encryption_salt")]
public string Salt { get ; set ; }
[JsonProperty("filename")]
public string Filename { get ; set ; }
[JsonProperty("steamid")]
public ulong SteamID { get ; set ; }
}
2016-08-22 20:13:36 +02:00
/ *
Crypto Functions
* /
/// <summary>
/// Returns an 8-byte cryptographically random salt in base64 encoding
/// </summary>
/// <returns></returns>
public static string GetRandomSalt ( )
{
byte [ ] salt = new byte [ SALT_LENGTH ] ;
using ( RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider ( ) )
{
rng . GetBytes ( salt ) ;
}
return Convert . ToBase64String ( salt ) ;
}
/// <summary>
/// Returns a 16-byte cryptographically random initialization vector (IV) in base64 encoding
/// </summary>
/// <returns></returns>
public static string GetInitializationVector ( )
{
byte [ ] IV = new byte [ IV_LENGTH ] ;
using ( RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider ( ) )
{
rng . GetBytes ( IV ) ;
}
return Convert . ToBase64String ( IV ) ;
}
/// <summary>
/// Generates an encryption key derived using a password, a random salt, and specified number of rounds of PBKDF2
/// </summary>
/// <param name="password"></param>
/// <param name="salt"></param>
/// <returns></returns>
private static byte [ ] GetEncryptionKey ( string password , string salt )
{
if ( string . IsNullOrEmpty ( password ) )
{
throw new ArgumentException ( "Password is empty" ) ;
}
if ( string . IsNullOrEmpty ( salt ) )
{
throw new ArgumentException ( "Salt is empty" ) ;
}
using ( Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes ( password , Convert . FromBase64String ( salt ) , PBKDF2_ITERATIONS ) )
{
return pbkdf2 . GetBytes ( KEY_SIZE_BYTES ) ;
}
}
2016-08-22 01:30:49 +02:00
}