/// <summary> /// Removes a file from the archive. /// </summary> /// <param name="filePath">The virtual path to the file.</param> /// <remarks>If no file exists at that path, this operation is a silent no-op.</remarks> public void RemoveFile(AegisVirtualFilePath filePath) { ArgCheck.NotNull(filePath, nameof(filePath)); this.ThrowIfLocked(); var fileInfo = this.GetFileInfo(filePath); if (fileInfo != null) { this.RemoveFile(fileInfo.FileId); } }
/// <summary> /// Unlocks (i.e. decrypts) the archive with the given <see cref="UserKey"/>. /// </summary> /// <param name="userKey">The <see cref="UserKey"/> to use to unlock the archive.</param> public void Unlock(UserKey userKey) { ArgCheck.NotNull(userKey, nameof(userKey)); // Setting the ArchiveKey property puts the archive into the "unlocked" state. // Wait to set the property until after everything is properly unlocked. var archiveKey = this.DecryptArchiveKey(userKey); this.FileIndex = this.ArchiveMetadata.EncryptedFileIndex is null || this.ArchiveMetadata.EncryptedFileIndex.IsEmpty() ? new FileIndex() : FileIndex.DecryptFrom(this.ArchiveMetadata.EncryptedFileIndex, archiveKey, this.ArchiveMetadata.SecuritySettings); this.ArchiveKey = archiveKey; }
/// <summary> /// Creates a new instance of the <see cref="EncryptedPacket"/> class. /// </summary> /// <param name="cipherText">The encrypted data.</param> /// <param name="iv">The initialization vector for the encryption.</param> /// <param name="authTag">The authentication tag, for when authenticated encryption algorithms are used.</param> /// <returns></returns> public static EncryptedPacket CreateNewEncryptedPacket( Span <byte> cipherText, Span <byte> iv, Span <byte> authTag = default) { ArgCheck.NotEmpty(cipherText, nameof(cipherText)); ArgCheck.NotEmpty(iv, nameof(iv)); return(new EncryptedPacket { CipherText = new List <byte>(cipherText.ToArray()), IV = new List <byte>(iv.ToArray()), AuthTag = authTag.IsEmpty ? new List <byte>() : new List <byte>(authTag.ToArray()), }); }
/// <summary> /// Loads a <see cref="AegisBondArchive"/> from disk. This operation does not unlock the archive. /// </summary> /// <param name="fileSettings">Settings for where the archive and related files are stored.</param> /// <returns>The loaded <see cref="AegisBondArchive"/>.</returns> public static AegisArchive Load(SecureArchiveFileSettings fileSettings) { ArgCheck.IsValid(fileSettings, nameof(fileSettings)); ZipArchive tempSecureArchive = null; AegisArchive archive = null; try { // Open the secure archive and read the metadata entry. tempSecureArchive = OpenSecureArchiveFile(fileSettings); var metadataEntry = tempSecureArchive.GetEntry(AegisConstants.SecureArchiveMetadataEntryName); if (metadataEntry is null) { throw new ArchiveCorruptedException("The secure archive does not contain any internal metadata!"); } using var metadataReader = new StreamReader(metadataEntry.Open()); var metadataJson = metadataReader.ReadToEnd(); var metadata = JsonSerializer.Deserialize <SecureArchiveMetadata>(metadataJson); if (metadata is null) { throw new ArchiveCorruptedException("Unable to parse the archive internal metadata!"); } archive = new AegisArchive { ArchiveMetadata = metadata, FileSettings = fileSettings, SecureArchive = tempSecureArchive, }; // We've transfered dispose ownership of tempSecureArchive to the archive. tempSecureArchive = null; } finally { tempSecureArchive?.Dispose(); } return(archive); }
/// <summary> /// Creates a new instance of the <see cref="SecuritySettings"/> class. /// </summary> /// <param name="encryptionAlgo">The algorithm used to encrypt data in the archive.</param> /// <param name="keyDerivationFunction">The key derivation function (KDF) used to generate user keys.</param> /// <param name="keyDerivationWorkFactor">The work factor parameter (e.g. iteration count) for the KDF.</param> /// <param name="KeyIdSizeInBytes">The size (in bytes) of KeyIds generated for user keys.</param> public static SecuritySettings Create( EncryptionAlgo encryptionAlgo, KeyDerivationFunction keyDerivationFunction, int keyDerivationWorkFactor, int keyIdSizeInBytes) { ArgCheck.IsNot(EncryptionAlgo.Unknown, encryptionAlgo, nameof(encryptionAlgo)); ArgCheck.IsNot(KeyDerivationFunction.Unknown, keyDerivationFunction, nameof(keyDerivationFunction)); ArgCheck.GreaterThanZero(keyDerivationWorkFactor, nameof(keyDerivationWorkFactor)); ArgCheck.GreaterThanZero(keyIdSizeInBytes, nameof(keyIdSizeInBytes)); return(new SecuritySettings { EncryptionAlgo = encryptionAlgo, KeyDerivationFunction = keyDerivationFunction, KeyDerivationWorkFactor = keyDerivationWorkFactor, KeyIdSizeInBytes = keyIdSizeInBytes, }); }
/// <summary> /// Decrypts and extract a file from the archive. /// </summary> /// <param name="fileInfo">The file to extract.</param> /// <param name="outputStream">The stream to extract the file to.</param> public void ExtractFile(AegisFileInfo fileInfo, Stream outputStream) { ArgCheck.NotNull(fileInfo, nameof(fileInfo)); ArgCheck.NotNull(outputStream, nameof(outputStream)); this.ThrowIfLocked(); var archiveEntry = this.SecureArchive.GetEntry(fileInfo.ArchiveEntryName); if (archiveEntry is null) { throw new ArchiveCorruptedException($"Unable to find archived entry for file ID {fileInfo.FileId}"); } using var archiveStream = archiveEntry.Open(); var encryptedFileData = EncryptedPacketExtensions.ReadFromBinaryStream( archiveStream, this.ArchiveMetadata.SecuritySettings.EncryptionAlgo); var plainTextFileBytes = this.ArchiveKey.Decrypt(this.CryptoStrategy, encryptedFileData); outputStream.Write(plainTextFileBytes); }
public static bool TryDecryptArchiveKey( this UserKeyAuthorization authorization, UserKey userKey, SecuritySettings securitySettings, out ArchiveKey archiveKey) { ArgCheck.NotNull(authorization, nameof(authorization)); ArgCheck.NotNull(userKey, nameof(userKey)); ArgCheck.IsValid(securitySettings, nameof(securitySettings)); archiveKey = null; if (!CryptoHelpers.SecureEquals(userKey.KeyId, authorization.KeyId)) { return(false); } try { // The SecureArchive file format requires that the friendly name and keyId be // checked for tampering when using authenticated cyphers. var additionalData = Encoding.UTF8.GetBytes(authorization.FriendlyName + authorization.KeyId); var cryptoStrategy = CryptoHelpers.GetCryptoStrategy(securitySettings.EncryptionAlgo); var decryptedArchiveKey = userKey.Decrypt(cryptoStrategy, authorization.EncryptedArchiveKey, additionalData); if (!decryptedArchiveKey.IsEmpty) { archiveKey = new ArchiveKey(decryptedArchiveKey.ToArray()); } } catch { return(false); } return(archiveKey != null); }
/// <summary> /// Initializes a new instance of the <see cref="SecureArchiveCreationParameters"/> class. /// </summary> /// <param name="firstKeyAuthorizationParam">The parameters to authorize the first user key.</param> public SecureArchiveCreationParameters(UserKeyAuthorizationParameters firstKeyAuthorizationParam) { ArgCheck.IsValid(firstKeyAuthorizationParam, nameof(firstKeyAuthorizationParam)); this.FirstKeyAuthorizationParams = firstKeyAuthorizationParam; }
/// <summary> /// Adds a file to the archive. /// </summary> /// <param name="virtualPath">The virtual path to add the file at.</param> /// <param name="fileStream">The file data stream.</param> /// <param name="overwriteIfExists"> /// Whether or not to overwrite existing files at that path. /// If false, an <see cref="InvalidOperationException"/> is thrown if a file already exists at the path. /// </param> /// <returns>Metadata about the file added to the archive.</returns> public AegisFileInfo PutFile(AegisVirtualFilePath virtualPath, Stream fileStream, bool overwriteIfExists = false) { ArgCheck.NotNull(virtualPath, nameof(virtualPath)); ArgCheck.NotNull(fileStream, nameof(fileStream)); this.ThrowIfLocked(); // Remove any existing files stored at the path, if we're allowed to. var existingFileInfo = this.FileIndex.GetFileInfo(virtualPath); if (existingFileInfo != null) { if (!overwriteIfExists) { throw new InvalidOperationException( $"A file with ID '{existingFileInfo.FileId}' already exists at virtual path '{virtualPath}'!"); } this.RemoveFile(existingFileInfo.FileId); } var currentTime = DateTimeOffset.UtcNow; var fileInfo = existingFileInfo ?? new AegisFileInfo( virtualPath, new FileIndexEntry { FileId = Guid.NewGuid(), FilePath = virtualPath.ToString(), AddedTime = currentTime, LastModifiedTime = currentTime, }); fileInfo.IndexEntry.LastModifiedTime = currentTime; // Encrypt the file. // This mechanism won't work for very large files since it loads the whole thing into memory. // For now, this is an acceptable limit for Aegis file archiving. using var memoryStream = new MemoryStream(); fileStream.CopyTo(memoryStream); var encryptedFile = this.ArchiveKey.Encrypt( this.CryptoStrategy, memoryStream.ToArray()); encryptedFile.WriteToBinaryStream(this.SecureArchive.GetEntryWriteStream(fileInfo.ArchiveEntryName)); try { this.FileIndex.Add(fileInfo); this.PersistMetadata(); } catch { // Creating file metadata failed. // Revert adding file to keep the archive to keep it consistent. this.SecureArchive.DeleteEntryIfExists(fileInfo.ArchiveEntryName); throw; } this.FlushArchive(); return(fileInfo); }
/// <summary> /// Creates a new <see cref="AegisArchive"/> that contains no files. /// </summary> /// <param name="fileSettings">Settings for where the archive and related files are stored.</param> /// <param name="creationParameters">The <see cref="SecureArchiveCreationParameters"/> to use when creating the archive.</param> /// <returns>A new <see cref="AegisArchive"/> that is opened but not yet persisted to disk.</returns> public static AegisArchive CreateNew( SecureArchiveFileSettings fileSettings, SecureArchiveCreationParameters creationParameters) { ArgCheck.IsValid(fileSettings, nameof(fileSettings)); ArgCheck.IsValid(creationParameters, nameof(creationParameters)); var currentTime = DateTimeOffset.UtcNow; var archiveId = Guid.NewGuid(); ArchiveKey tempArchiveKey = null; AegisArchive tempArchive = null; AegisArchive archive = null; try { tempArchiveKey = ArchiveKey.CreateNew(creationParameters.SecuritySettings); // Derive and authorize the first user key. var keyDerivationSalt = CryptoHelpers.GetRandomBytes(creationParameters.KeyDerivationSaltSizeInBytes); var firstUserKeyAuthorization = UserKeyAuthorizationExtensions.CreateNewAuthorization( creationParameters.FirstKeyAuthorizationParams, keyDerivationSalt, tempArchiveKey, creationParameters.SecuritySettings); var authCanary = tempArchiveKey.Encrypt( CryptoHelpers.GetCryptoStrategy(creationParameters.SecuritySettings.EncryptionAlgo), archiveId.ToByteArray()); var archiveMetadata = new SecureArchiveMetadata { Id = archiveId, SecuritySettings = creationParameters.SecuritySettings, CreateTime = currentTime, LastModifiedTime = currentTime, KeyDerivationSalt = new List <byte>(keyDerivationSalt), AuthCanary = authCanary, UserKeyAuthorizations = new List <UserKeyAuthorization> { firstUserKeyAuthorization }, }; tempArchive = new AegisArchive { ArchiveMetadata = archiveMetadata, ArchiveKey = tempArchiveKey, FileSettings = fileSettings, FileIndex = new FileIndex(), SecureArchive = OpenSecureArchiveFile(fileSettings, createNewArchive: true), }; tempArchive.PersistMetadata(); // Transfer the archive reference to the return variable. archive = tempArchive; tempArchive = null; // Dispose ownership of the archive key now belongs to the archive. tempArchiveKey = null; } finally { tempArchive?.Dispose(); tempArchiveKey?.Dispose(); } return(archive); }