using System;
using System.IO;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Security.AccessControl;
using System.Security.Principal;
namespace ACLencoder
{
///
/// A class that allows you to encode and decode files as ACL entries
///
public class AclEncoder
{
///
/// This combination of FileSystemRights flags identifies an ACL entry as part of the AclEncoding scheme.
///
private const FileSystemRights CANARY_RIGHTS = FileSystemRights.Synchronize | FileSystemRights.ReadPermissions;
private const int CANARY_VALUE = (int)CANARY_RIGHTS; // FileSystemRights.Synchronize + FileSystemRights.ReadPermissions
private const int SID_LENGTH = 68;
private const int SID_HEADER_LENGTH = 8;
private const int MAX_CHUNK_LENGTH = 60;
private const int MAX_ROUNDS = 512;
private ArrayList _fileList;
///
/// Instantiates a new AclEncoder that can be used to encode/decode files as ACL entries
///
/// A path to the list of files to use for encoding/decoding
public AclEncoder(string fileList)
{
this.LoadFileList(fileList);
}
~AclEncoder()
{
_fileList = null;
}
///
/// Loads the list of files identified at the
///
/// A path to the list of files to use for encoding/decoding
private void LoadFileList(string filePath)
{
// Scan the list of drives available to the system and remember NTFS drives
ArrayList NTFSDrives = new ArrayList();
DriveInfo[] DriveList = DriveInfo.GetDrives();
foreach (DriveInfo Info in DriveList)
{
if (Info.IsReady == true)
{
if (Info.DriveFormat.Equals("NTFS"))
NTFSDrives.Add(Info.Name);
}
}
// Open the file for reading
_fileList = new ArrayList();
StreamReader FileListReader = new StreamReader(filePath, true);
while (!FileListReader.EndOfStream)
{
string EntryPath = FileListReader.ReadLine();
// Make sure the filepath is fully qualified
if (!EntryPath.StartsWith(Path.GetPathRoot(EntryPath)))
{
if (FileListReader != null)
FileListReader.Close();
throw new ApplicationException("The following file referenced in your filelist is not fully qualified: " + EntryPath);
}
// Make sure this file exists...
if (!File.Exists(EntryPath))
{
if (FileListReader != null)
FileListReader.Close();
throw new ApplicationException("The following file referenced in your filelist does not exist: " + EntryPath);
}
// Make sure this file exists on an NTFS volume..
if (!NTFSDrives.Contains(Path.GetPathRoot(EntryPath)))
{
if (FileListReader != null)
FileListReader.Close();
throw new ApplicationException("The following file referenced in your filelist is not on an NTFS volume: " + EntryPath);
}
_fileList.Add(EntryPath);
}
if (FileListReader != null)
FileListReader.Close();
if (_fileList.Count == 0)
throw new ApplicationException("No files were loaded from the specified file list!");
}
///
/// Encodes the file specified in as ACL entries on files listed in the FileList
///
/// The path of the file to read and encode as ACL entries
public void Encode(string filePath)
{
if (_fileList == null)
throw new ApplicationException("No FileList loaded.");
// 68-bytes per S-ID
// 1st byte is always 1 (revision)
// 2nd byte is a length of sub-identifiers (maximum 15 subidentifiers)
// 3rd-8th byte (6 bytes) is the identifier authority. This is set to 'unused' or 000000000004.
// After that there are 15x4byte chunks (each sub-identifier as counted in the 2nd byte)
//
// This has a maximum S-ID size of 1 + 1 + 6 + 4x15 = 68 bytes
// usable size is 60 bytes
byte[] Buffer = new byte[SID_LENGTH];
int FileOffset = 0;
int BytesRead = 0;
int ChunkCount = 0;
int ChunkIndex = 0;
int RoundNumber = 0;
int RawFileRights = CANARY_VALUE;
// Open the file to be encoded for reading
FileStream InputStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
// If the file's too large to be encoded with this list, abort
if (InputStream.Length > (MAX_CHUNK_LENGTH * _fileList.Count * MAX_ROUNDS))
{
InputStream.Close();
throw new ApplicationException("The input file is larger than the file list can hold.\nUse a longer list.");
}
// Loop through the bytes in the file so we can encode it
while (FileOffset < InputStream.Length && RoundNumber < MAX_ROUNDS)
{
// Read 60 bytes from the input file into bytes 9-68 of the Chunk array
WipeSIDBytes(ref Buffer);
BytesRead = InputStream.Read(Buffer, SID_HEADER_LENGTH, Buffer.Length - SID_HEADER_LENGTH);
// The last chunk may be less than 60 bytes long.
// Pad the remaining bytes with the length of the chunk (i.e. if the chunk is 16 bytes long, pad all remaining 44 bytes with the value 0x10 (16))
if (BytesRead < MAX_CHUNK_LENGTH)
{
for (int i = (BytesRead + SID_HEADER_LENGTH); i < Buffer.Length; i++)
{
Buffer[i] = (byte)BytesRead;
}
}
// Create a new S-ID from this chunk. The 1st and 2nd bytes should be static "0x01 0x0F"
SecurityIdentifier ID = new SecurityIdentifier(Buffer, 0);
string CurrentFilePath = (string)_fileList[ChunkIndex];
// Read the current ACL Entries from the next file in our filelist
FileSecurity Security = File.GetAccessControl(CurrentFilePath);
// Create the FileRights necessary to identify this file AND know its round position
RawFileRights = CANARY_VALUE + RoundNumber;
FileSystemRights FileRights = (FileSystemRights)RawFileRights;
// Add the SID to the file's security list
Security.AddAccessRule(new FileSystemAccessRule(ID, FileRights, AccessControlType.Allow));
// Update the ACL on the file
File.SetAccessControl(CurrentFilePath, Security);
// Keep track of where we are in the file
FileOffset += BytesRead;
ChunkCount += 1;
// Increment/wrap the ListIndex. Keep track of the #wraps...
RoundNumber = ChunkCount / _fileList.Count;
ChunkIndex = ChunkCount % _fileList.Count;
}
// Close the file
if (InputStream != null)
InputStream.Close();
}
///
/// Decodes an ACLEncoded file into the path specified
///
/// The path to a file where the ACLEncoded file will be written
public void Decode(string outputFilePath)
{
int ChunkCount = 0;
int RoundNumber = 0;
int ChunkIndex = 0;
bool LookForMoreChunks = true;
byte[] Buffer = new byte[SID_LENGTH];
FileStream OutputStream = new FileStream(outputFilePath, FileMode.OpenOrCreate, FileAccess.Write);
// Keep looping until we don't detect any more Access Control Entries
while (LookForMoreChunks == true)
{
LookForMoreChunks = false; // Abort by default. Continue if we found one.
string FilePath = (string)_fileList[ChunkIndex];
// Retrieve ACL entries from the file
FileSecurity Security = File.GetAccessControl(FilePath);
// Loop through ACL entries to find encoded entries with the current round number
foreach (FileSystemAccessRule Rule in Security.GetAccessRules(true, false, typeof(SecurityIdentifier)))
{
// If it's an encoded entry...
if (IsEncodedEntry(Rule))
{
// Parse out the RoundNumber from the FileSystemRights
int ParsedRoundNumber = (int)(Rule.FileSystemRights ^ CANARY_RIGHTS);
// If it's the current Round#, read the bytes from the SID
if (ParsedRoundNumber == RoundNumber)
{
SecurityIdentifier Identifier = new SecurityIdentifier(Rule.IdentityReference.Value);
Buffer.Initialize();
Identifier.GetBinaryForm(Buffer, 0);
OutputStream.Write(Buffer, 8, SID_LENGTH - 8);
// We found our entry. Continue to the next file/chunk
ChunkCount += 1;
LookForMoreChunks = true;
break;
}
}
}
// If we didn't find a chunk
if (LookForMoreChunks == false)
{
// ...and we've found chunks before
if (ChunkCount >= 1)
{
// The chunk from the last ACL entry was the last one.
// We need to find the true length of the last chunk and trim it down
int ChunkLength = MAX_CHUNK_LENGTH;
byte LastValue = Buffer[Buffer.Length - 1];
if (Buffer[LastValue + SID_HEADER_LENGTH] == LastValue)
{
ChunkLength = LastValue;
// Trim the file to the true chunk length
long NewFileLength = OutputStream.Length - (MAX_CHUNK_LENGTH - ChunkLength);
OutputStream.SetLength(NewFileLength);
}
}
else
{
// We didn't find a chunk, and we haven't found any before
if (OutputStream != null)
OutputStream.Close();
throw new ApplicationException("No ACLEncoded entries were found on any of the targeted files.");
}
}
RoundNumber = ChunkCount / _fileList.Count;
ChunkIndex = ChunkCount % _fileList.Count;
}
// Close the file
if (OutputStream != null)
OutputStream.Close();
}
///
/// Wipes all ACL entries from the files listed that appear to be chunks of ACLEncoded files
///
public void RemoveEncodedFile()
{
// Loop through all files in the filelist
foreach (string FilePath in _fileList)
{
// Retrieve ACL entries from the file
FileSecurity Security = File.GetAccessControl(FilePath);
// Loop through ACL entries
foreach (FileSystemAccessRule Rule in Security.GetAccessRules(true, false, typeof(SecurityIdentifier)))
{
// If the entry has the canary value, remove it
if (IsEncodedEntry(Rule))
{
Security.RemoveAccessRule(Rule);
}
}
// Write ACL entries back to the file
File.SetAccessControl(FilePath, Security);
}
}
///
/// Identifies whether or not a particular FileSystemAccessRule represents a piece of an ACLEncoded file
///
/// An ACL rule to test
/// true if the ACL rule is an ACLEncoded file. false if it isn't.
private bool IsEncodedEntry(FileSystemAccessRule rule)
{
FileSystemRights Rights = rule.FileSystemRights;
AccessControlType ACType = rule.AccessControlType;
return (((rule.FileSystemRights & CANARY_RIGHTS) == CANARY_RIGHTS) && (rule.AccessControlType == AccessControlType.Allow));
}
///
/// Zeroes out the specified byte array and adds the SID header
///
/// A reference to a byte array that represents an SID
private void WipeSIDBytes(ref byte[] sid)
{
sid.Initialize();
sid[0] = 0x01; // Revision Byte
sid[1] = 0x0F; // #Sub-Identifiers (Maximum 15)
sid[2] = 0x00; // These six bytes
sid[3] = 0x00; // are the Identifier Authority
sid[4] = 0x00; //
sid[5] = 0x00; // The value "00 00 00 00 00 04"
sid[6] = 0x00; // means "Not used"
sid[7] = 0x04; //
}
}
}