common/Hardware/WaterMeter/Genesis/GenesisFile/MeterPwdFile.cs
2026-04-23 17:50:07 +02:00

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;
}
}
}