430 lines
18 KiB
C#
430 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore;
|
|
using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisPwd;
|
|
|
|
namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisFile
|
|
{
|
|
/// <summary>
|
|
/// Password file operations
|
|
/// </summary>
|
|
public class MeterPwdFile
|
|
{
|
|
// The FwUpdateSw references this string
|
|
public const String StrPasswordFileName = "0\\password";
|
|
private readonly IGenesisMeter _genesisMeter;
|
|
private readonly MeterFile _meterFile;
|
|
//seven passwords are needed for Level 1, 2, 4, 5, 6, 7, Level 3 will be generated
|
|
//out of the ProcessorUID.
|
|
|
|
/// <summary>
|
|
/// Hashed password file return
|
|
/// </summary>
|
|
public Byte[] HashedPasswordFile
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ctor for usage of build of password file without writing it
|
|
/// </summary>
|
|
/// <remarks date="2023-Oct-17" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
public MeterPwdFile()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ctor
|
|
/// </summary>
|
|
/// <param name="genesisMeter"></param>
|
|
/// <remarks date="2019-May-02" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
public MeterPwdFile(IGenesisMeter genesisMeter)
|
|
{
|
|
_genesisMeter = genesisMeter;
|
|
_meterFile = new MeterFile(_genesisMeter);
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the already hashed password file if this is already pre-generated.
|
|
/// This is the preparation to the <see cref="WriteMeterPwdFile"/>.
|
|
/// The password file has a unique length of <see cref="MeterPwdDb.PwdFileLength"/>.
|
|
/// </summary>
|
|
/// <param name="hashedPwdFile">hashed password file with constant length as byte array
|
|
/// </param>
|
|
/// <returns>true if length matches the expectations</returns>
|
|
/// <remarks date="2020-Dec-08" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
private Boolean SetHashedPwdFile(Byte[] hashedPwdFile)
|
|
{
|
|
if (MeterPwdDb.PwdFileLength != hashedPwdFile.Length) return false;
|
|
HashedPasswordFile = new Byte[hashedPwdFile.Length];
|
|
HashedPasswordFile = hashedPwdFile;
|
|
return true;
|
|
}
|
|
|
|
///// <summary>
|
|
///// Build the password file out of the clear text passwords and the processor UID,
|
|
///// password Level 3 has to be build out of the Processor UID.
|
|
///// </summary>
|
|
///// <param name="pwdLevels">7 sorted passwords levels: 1, 2, 4, 5, 6, 7, 8</param>
|
|
///// <param name="processorUid">unique ID of processor used for password level 3</param>
|
|
///// <param name="dbSkeletonKey">SkeletonKey from data base for comparison </param>
|
|
///// <returns></returns>
|
|
///// <remarks date="2019-Mai-15" author="Thomas Wiedebusch">
|
|
///// - Initial.
|
|
///// </remarks>
|
|
///// <remarks date="2020-Mai-15" author="Roland Drabesch">
|
|
///// - Insert of Level 3 password with skeletonKey,
|
|
///// - SHA 1 of passwords.
|
|
///// </remarks>
|
|
///// <remarks date="2020-Dec-08" author="Thomas Wiedebusch">
|
|
///// - .
|
|
///// </remarks>
|
|
///// <remarks date="2021-Jan-29" author="Thomas Wiedebusch">
|
|
///// - Set Level 3 password to modified <see cref="BuildLevel3Pwd"/> from processor UID,
|
|
///// - Removed skeletonKey as this cannot be used here!
|
|
///// </remarks>
|
|
///// <remarks date="2021-Feb-01" author="Thomas Wiedebusch">
|
|
///// - Set Level 3 password to dbSkeletonKey if processorUid is unknown (processorUid == 1)!
|
|
///// </remarks>
|
|
//public Boolean BuildPwdFile(List<Byte[]> pwdLevels, UInt64 processorUid, String dbSkeletonKey)
|
|
//{
|
|
// if (pwdLevels.Count != PwdLevels || processorUid == 0 ||
|
|
// string.IsNullOrEmpty(dbSkeletonKey) && processorUid == 1)
|
|
// {
|
|
// return false;
|
|
// }
|
|
|
|
// // selective Level 3 password
|
|
// if (processorUid == 1 && !string.IsNullOrEmpty(dbSkeletonKey))
|
|
// {
|
|
// pwdLevels.Insert(2, Encoding.UTF8.GetBytes(dbSkeletonKey));
|
|
// }
|
|
// else
|
|
// {
|
|
// pwdLevels.Insert(2,Encoding.UTF8.GetBytes(BuildLevel3Pwd(processorUid)));
|
|
// }
|
|
|
|
|
|
// var ret = new List<Byte>();
|
|
|
|
// foreach (var keyLvl in pwdLevels)
|
|
// {
|
|
// using (var sha1 = new System.Security.Cryptography.SHA1Managed())
|
|
// {
|
|
// var hash = sha1.ComputeHash(keyLvl);
|
|
// ret.AddRange(hash.ToList());
|
|
// }
|
|
// }
|
|
// _hashedPasswordFile = ret.ToArray();
|
|
// return true;
|
|
//}
|
|
|
|
/// <summary>
|
|
/// Build the password file out of the clear text passwords.
|
|
/// The Level 3 password is already preset with skeletonKey.
|
|
///
|
|
/// The skeleton key has to be unequal to the level 8 password, otherwise the skeleton key has been
|
|
/// overwritten with this password level 8 locking out all applications which need this key at the
|
|
/// level 3 to login in the configuration of the Cordonel.
|
|
///
|
|
/// ATTENTION - PRECONDITIONS:
|
|
/// All passwords have to be preset in the list of passwords:
|
|
/// Level 1: random from password service
|
|
/// Level 2: random from password service
|
|
/// Level 3: skeletonKey, this is the initial password used in production, used by applications to
|
|
/// login to the configuration system of the Cordonel
|
|
/// Level 4: random from password service
|
|
/// Level 5: random from password service
|
|
/// Level 6: random from password service
|
|
/// Level 7: random from password service
|
|
/// Level 8: random from password service, this is the passwordLvl8 used in production after
|
|
/// password file installation
|
|
///
|
|
/// Checks ( each failed check is going to throw an exception):
|
|
/// - Checks for individual clear text password length,
|
|
/// - Checks if list of clear text passwords are containing 8 levels,
|
|
/// - Checks that password level 3 is set to skeleton key,
|
|
/// - Checks that skeleton key is unequal to password level 8,
|
|
/// - Check file size of generated password file.
|
|
///
|
|
/// </summary>
|
|
/// <param name="passwords">8 sorted passwords levels: 1, 2, skeletonKey, 4, 5, 6, 7, 8</param>
|
|
/// <param name="processorUid">unique ID of processor used for password level 3</param>
|
|
/// <param name="dbSkeletonKey">SkeletonKey for comparison </param>
|
|
/// <returns></returns>
|
|
/// <remarks date="2019-Mai-15" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
/// <remarks date="2020-Mai-15" author="Roland Drabesch">
|
|
/// - All 8 password levels will be passed,
|
|
/// - Password Level 3 will be compared with data base skeletonKey,
|
|
/// - SHA 1 of passwords.
|
|
/// </remarks>
|
|
/// <remarks date="2023-Oct-11" author="Thomas Wiedebusch">
|
|
/// - Throw exception if clear text password length is out of range.
|
|
/// - Throw exception if password list is out of range.
|
|
/// - Throw exception if password level3 does not contain the skeleton key.
|
|
/// - Throw exception if skeletonKey is overwritten with passwordLvl8.
|
|
/// - Throw exception if password file is out of range.
|
|
/// </remarks>
|
|
public Boolean BuildPwdFile(List<Byte[]> passwords, UInt64 processorUid, String dbSkeletonKey)
|
|
{
|
|
// The length of each password has to be 12 bytes
|
|
if (passwords.Any( x => x.Length != MeterPwdDb.ClearTextPwdLength))
|
|
{
|
|
throw new ApplicationException("Individual password length is out of range! " +
|
|
$"Expected length for each password: {MeterPwdDb.ClearTextPwdLength}");
|
|
}
|
|
// The password list has to contain 8 passwords
|
|
if (passwords.Count != MeterPwdDb.PwdLevels)
|
|
{
|
|
throw new ApplicationException("Number of passwords is out of range! " +
|
|
$"Expected: {MeterPwdDb.PwdLevels}, " +
|
|
$"Transmitted: {passwords.Count }");
|
|
}
|
|
// The skeleton key has to be preset on level 3
|
|
if (!Encoding.UTF8.GetBytes(dbSkeletonKey).SequenceEqual(passwords[MeterPwdDb.SkeletonKeyIdx]))
|
|
{
|
|
throw new ApplicationException("SkeletonKey is not set on password file level 3!");
|
|
}
|
|
// The skeleton key has to be unequal to the level 8 password
|
|
if (Encoding.UTF8.GetBytes(dbSkeletonKey).SequenceEqual(passwords[MeterPwdDb.PwdLevel8Idx]))
|
|
{
|
|
throw new ApplicationException("SkeletonKey is overwritten with password level 8!");
|
|
}
|
|
var hashedPasswords = new List<Byte>();
|
|
|
|
foreach (var password in passwords)
|
|
{
|
|
using (var sha1 = new System.Security.Cryptography.SHA1Managed())
|
|
{
|
|
var passwordHash = sha1.ComputeHash(password);
|
|
hashedPasswords.AddRange(passwordHash.ToList());
|
|
}
|
|
}
|
|
// The password file size has to be 160 bytes
|
|
if (hashedPasswords.ToArray().Length != MeterPwdDb.PwdFileLength)
|
|
{
|
|
throw new ApplicationException("Generated password file is out of range! " +
|
|
$"Expected: {MeterPwdDb.PwdFileLength}, " +
|
|
$"Transmitted: {hashedPasswords.ToArray().Length }");
|
|
}
|
|
HashedPasswordFile = hashedPasswords.ToArray();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check the hashed password file:
|
|
/// - Checks file size,
|
|
/// - Checks if password level 8 is unequal to skeleton key
|
|
/// - Checks if password level 3 is unequal to password level 8 (level 3 may be overwritten by level 8
|
|
/// password hash)
|
|
/// - Builds skeleton key password hash and checks if this is placed at password level 3,
|
|
/// - Builds password level 8 hash and checks if this is placed at level 8.
|
|
/// </summary>
|
|
/// <param name="pwdFile"></param>
|
|
/// <param name="skeletonKey">SkeletonKey for comparison </param>
|
|
/// <param name="pwdLevel8"></param>
|
|
/// <returns></returns>
|
|
/// <remarks date="2023-Oct-26" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
/// <remarks date="2023-Oct-27" author="Thomas Wiedebusch">
|
|
/// - Check password file length.
|
|
/// </remarks>
|
|
public Boolean CheckHashedPwdFile(Byte[] pwdFile, String skeletonKey, String pwdLevel8)
|
|
{
|
|
|
|
var retVal = false;
|
|
// check the file size
|
|
if (pwdFile.Length != MeterPwdDb.PwdFileLength)
|
|
{
|
|
return false;
|
|
}
|
|
// the skeletonKey has to be unequally to the passwordLvl8
|
|
if (skeletonKey.Equals(pwdLevel8))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// compare hashed skeletonKey and hashed passwordLvl8
|
|
for (var c = 0; c < MeterPwdDb.HashedPwdLength; c++)
|
|
{
|
|
// hashed level 3 and level 8 have to be unequal on at least one byte position
|
|
if (pwdFile[MeterPwdDb.SkeletonKeyIdx * MeterPwdDb.HashedPwdLength + c] !=
|
|
pwdFile[MeterPwdDb.PwdLevel8Idx * MeterPwdDb.HashedPwdLength + c])
|
|
{
|
|
retVal = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// hash the skeleton and check if equal to hashed level 3
|
|
// hash the passwordLvl8 and check if equal to hashed level 8
|
|
using (var sha1 = new System.Security.Cryptography.SHA1Managed())
|
|
{
|
|
var skeletonHash = sha1.ComputeHash(Encoding.UTF8.GetBytes(skeletonKey));
|
|
var passwordLvl8Hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(pwdLevel8));
|
|
|
|
for (var c = 0; c < MeterPwdDb.HashedPwdLength; c++)
|
|
{
|
|
// if any byte is different the passwords do not match
|
|
if (pwdFile[MeterPwdDb.SkeletonKeyIdx * MeterPwdDb.HashedPwdLength + c] != skeletonHash[c] ||
|
|
pwdFile[MeterPwdDb.PwdLevel8Idx * MeterPwdDb.HashedPwdLength + c] != passwordLvl8Hash[c])
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return retVal;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build skeleton key out of unique processor Id
|
|
/// </summary>
|
|
/// <param name="processorUid"></param>
|
|
/// <returns></returns>
|
|
/// <remarks date="2019-Mai-03" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
public static String BuildSkeletonKey(UInt64 processorUid)
|
|
{
|
|
return $"{(UInt16)((processorUid >> 48) ^ (processorUid >> 32)):X4}" +
|
|
$"{(UInt32)((processorUid >> 16) ^ processorUid):X8}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build level 3 password out of unique processor Id
|
|
/// </summary>
|
|
/// <param name="processorUid"></param>
|
|
/// <returns></returns>
|
|
/// <remarks date="2019-Mai-03" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
public static String BuildLevel3Pwd(UInt64 processorUid)
|
|
{
|
|
//password level 3 algorithm
|
|
return $"{(UInt16)((processorUid >> 48) ^ processorUid):X4}" +
|
|
$"{(UInt32)((processorUid >> 32) ^ processorUid):X8}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// This function combines the write, read back and verification of the password file.
|
|
/// Write password file <see cref="WriteMeterPwdFile"/> and compare it <see cref="VerifyMeterPwdFile"/>
|
|
/// The hashed password file can be set or has been preprocessed using the <see cref="BuildPwdFile"/>
|
|
/// </summary>
|
|
/// <param name="hashedPwdFile">optional hashed password file with constant length as byte array </param>
|
|
/// <returns>true if password file could be written, read back byte by byte
|
|
/// and compared being identical </returns>
|
|
/// <remarks date="2023-Oct-11" author="Thomas Wiedebusch">
|
|
/// - Initial, imported from ProductionProcessPasswordFile.
|
|
/// </remarks>
|
|
public Boolean WriteAndVerifyMeterPwdFile(Byte[] hashedPwdFile = null)
|
|
{
|
|
if (UnlockEraseWriteMeterPwdFile())
|
|
{
|
|
if (WriteMeterPwdFile(hashedPwdFile))
|
|
{
|
|
return VerifyMeterPwdFile();
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/// <summary>
|
|
/// Write the meter password file which was generated at <see cref="BuildPwdFile"/> or
|
|
/// use the already hashed password file if this is already pre-generated.
|
|
/// The password file has a unique length of <see cref="MeterPwdDb.PwdFileLength"/>.
|
|
/// </summary>
|
|
/// <param name="hashedPwdFile">optional hashed password file with constant length as byte array </param>
|
|
/// <returns>true if length matches the expectations and password file could be written
|
|
/// </returns>
|
|
/// <remarks date="2019-Mai-02" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
/// <remarks date="2020-Dec-08" author="Thomas Wiedebusch">
|
|
/// - Modified to take optional hashed password file as input.
|
|
/// </remarks>
|
|
public Boolean WriteMeterPwdFile(Byte[] hashedPwdFile = null)
|
|
{
|
|
if (hashedPwdFile != null)
|
|
{
|
|
if (!SetHashedPwdFile(hashedPwdFile)) return false;
|
|
}
|
|
|
|
if (HashedPasswordFile != null && _meterFile != null && _genesisMeter != null
|
|
&& _genesisMeter.IsLoggedOn && HashedPasswordFile.Length == MeterPwdDb.PwdFileLength)
|
|
{
|
|
return _meterFile.WriteMeterFile(StrPasswordFileName, HashedPasswordFile);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Verify Meter password file.
|
|
/// <param name="hashedPwdFile">optional hashed password file with constant length as byte array </param>
|
|
/// <returns>true if the read value is identical with the preset or given hashed value</returns>
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
/// <remarks date="2023-Jan-24" author="Thomas Wiedebusch/Roland Drabesch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
|
|
public Boolean VerifyMeterPwdFile(Byte[] hashedPwdFile = null)
|
|
{
|
|
if (hashedPwdFile != null)
|
|
{
|
|
if (!SetHashedPwdFile(hashedPwdFile)) return false;
|
|
}
|
|
if (HashedPasswordFile == null)
|
|
{
|
|
return false;
|
|
}
|
|
return _meterFile.VerifyMeterFile(StrPasswordFileName, HashedPasswordFile);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Erase the meter password file
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
/// <remarks date="2019-Mai-02" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
public Boolean EraseMeterPwdFile()
|
|
{
|
|
if (_meterFile != null && _genesisMeter != null && _genesisMeter.IsLoggedOn)
|
|
{
|
|
return _meterFile.EraseMeterFile(StrPasswordFileName);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Unlock access to password file for write and erase
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
/// <remarks date="2019-Mai-02" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
public Boolean UnlockEraseWriteMeterPwdFile()
|
|
{
|
|
if (_meterFile != null && _genesisMeter != null && _genesisMeter.IsLoggedOn)
|
|
{
|
|
return _meterFile.UnlockEraseWriteMeterFile(StrPasswordFileName);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|