Skip to content

Commit 4f9b981

Browse files
authored
Merge pull request #71 from ChangemakerStudios/feature/encryption-options
Add PDF encryption with userPassword and ownerPassword
2 parents c9521c8 + 11311ae commit 4f9b981

9 files changed

Lines changed: 408 additions & 0 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,23 @@ public async Task<Stream> ConvertToAccessiblePdfA(string pdfPath)
481481
}
482482
```
483483

484+
### PDF Encryption
485+
*Password-protect PDFs with user and owner passwords:*
486+
487+
```csharp
488+
public async Task<Stream> CreateEncryptedPdf()
489+
{
490+
var builder = new HtmlRequestBuilder()
491+
.AddDocument(doc => doc.SetBody("<html><body><h1>Confidential</h1></body></html>"))
492+
.SetPdfOutputOptions(o => o
493+
.SetEncryption(userPassword: "reader123", ownerPassword: "admin456"))
494+
.WithPageProperties(pp => pp.UseChromeDefaults());
495+
496+
var request = builder.Build();
497+
return await _sharpClient.HtmlToPdfAsync(request);
498+
}
499+
```
500+
484501
### Flatten PDFs
485502
*Flatten PDF forms and annotations (v2.8+):*
486503

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
</Project>

examples/EncryptPdf/Program.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using Gotenberg.Sharp.API.Client;
2+
using Gotenberg.Sharp.API.Client.Domain.Builders;
3+
using Gotenberg.Sharp.API.Client.Domain.Settings;
4+
using Gotenberg.Sharp.API.Client.Infrastructure.Pipeline;
5+
6+
using Microsoft.Extensions.Configuration;
7+
8+
var config = new ConfigurationBuilder()
9+
.SetBasePath(AppContext.BaseDirectory)
10+
.AddJsonFile("appsettings.json")
11+
.Build();
12+
13+
var options = new GotenbergSharpClientOptions();
14+
config.GetSection(nameof(GotenbergSharpClient)).Bind(options);
15+
16+
var destinationDirectory = args.Length > 0 ? args[0] : Path.Combine(Directory.GetCurrentDirectory(), "output");
17+
Directory.CreateDirectory(destinationDirectory);
18+
19+
var path = await CreateEncryptedPdf(destinationDirectory, options);
20+
Console.WriteLine($"Encrypted PDF created: {path}");
21+
22+
static async Task<string> CreateEncryptedPdf(string destinationDirectory, GotenbergSharpClientOptions options)
23+
{
24+
var handler = new HttpClientHandler();
25+
HttpMessageHandler effectiveHandler = handler;
26+
if (string.IsNullOrWhiteSpace(options.BasicAuthUsername) != string.IsNullOrWhiteSpace(options.BasicAuthPassword))
27+
throw new InvalidOperationException("Both BasicAuthUsername and BasicAuthPassword must be provided, or neither.");
28+
if (!string.IsNullOrWhiteSpace(options.BasicAuthUsername) && !string.IsNullOrWhiteSpace(options.BasicAuthPassword))
29+
effectiveHandler = new BasicAuthHandler(options.BasicAuthUsername, options.BasicAuthPassword) { InnerHandler = handler };
30+
31+
using var httpClient = new HttpClient(effectiveHandler, disposeHandler: true)
32+
{
33+
BaseAddress = options.ServiceUrl,
34+
Timeout = options.TimeOut
35+
};
36+
37+
var sharpClient = new GotenbergSharpClient(httpClient);
38+
39+
// Create a password-protected PDF
40+
var builder = new HtmlRequestBuilder()
41+
.AddDocument(doc => doc.SetBody(@"
42+
<html><body>
43+
<h1>Confidential Report</h1>
44+
<p>This document is password protected.</p>
45+
</body></html>"))
46+
.SetPdfOutputOptions(o => o
47+
.SetEncryption(
48+
userPassword: "reader123", // Required to open the PDF
49+
ownerPassword: "admin456")) // Required to change permissions
50+
.WithPageProperties(pp => pp.UseChromeDefaults());
51+
52+
var request = builder.Build();
53+
var response = await sharpClient.HtmlToPdfAsync(request);
54+
55+
var resultPath = Path.Combine(destinationDirectory, $"Encrypted-{DateTime.Now:yyyyMMddHHmmss}.pdf");
56+
57+
await using var destinationStream = File.Create(resultPath);
58+
await response.CopyToAsync(destinationStream, CancellationToken.None);
59+
60+
return resultPath;
61+
}

src/Gotenberg.Sharp.Api.Client/Domain/Builders/Faceted/PdfOutputOptionsBuilder.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
using Gotenberg.Sharp.API.Client.Domain.ValueObjects;
17+
1618
using Newtonsoft.Json.Linq;
1719

1820
namespace Gotenberg.Sharp.API.Client.Domain.Builders.Faceted;
@@ -112,4 +114,66 @@ public PdfOutputOptionsBuilder SetMetadata(JObject metadata)
112114

113115
return this;
114116
}
117+
118+
/// <summary>
119+
/// Sets the password required to open the resulting PDF.
120+
/// </summary>
121+
/// <param name="password">A validated PDF password.</param>
122+
/// <returns>The builder instance for method chaining.</returns>
123+
public PdfOutputOptionsBuilder SetUserPassword(PdfPassword password)
124+
{
125+
_options.UserPassword = password ?? throw new ArgumentNullException(nameof(password));
126+
127+
return this;
128+
}
129+
130+
/// <summary>
131+
/// Sets the password required to open the resulting PDF.
132+
/// </summary>
133+
/// <param name="password">A non-empty password string.</param>
134+
/// <returns>The builder instance for method chaining.</returns>
135+
public PdfOutputOptionsBuilder SetUserPassword(string password)
136+
{
137+
return SetUserPassword(PdfPassword.Create(password));
138+
}
139+
140+
/// <summary>
141+
/// Sets the password required to change permissions or edit the resulting PDF.
142+
/// </summary>
143+
/// <param name="password">A validated PDF password.</param>
144+
/// <returns>The builder instance for method chaining.</returns>
145+
public PdfOutputOptionsBuilder SetOwnerPassword(PdfPassword password)
146+
{
147+
_options.OwnerPassword = password ?? throw new ArgumentNullException(nameof(password));
148+
149+
return this;
150+
}
151+
152+
/// <summary>
153+
/// Sets the password required to change permissions or edit the resulting PDF.
154+
/// </summary>
155+
/// <param name="password">A non-empty password string.</param>
156+
/// <returns>The builder instance for method chaining.</returns>
157+
public PdfOutputOptionsBuilder SetOwnerPassword(string password)
158+
{
159+
return SetOwnerPassword(PdfPassword.Create(password));
160+
}
161+
162+
/// <summary>
163+
/// Sets both user and owner passwords for the resulting PDF.
164+
/// </summary>
165+
/// <param name="userPassword">Password required to open the PDF.</param>
166+
/// <param name="ownerPassword">Password required to change permissions.</param>
167+
/// <returns>The builder instance for method chaining.</returns>
168+
public PdfOutputOptionsBuilder SetEncryption(string userPassword, string ownerPassword)
169+
{
170+
// Validate both passwords first before mutating any state
171+
var validatedUser = PdfPassword.Create(userPassword);
172+
var validatedOwner = PdfPassword.Create(ownerPassword);
173+
174+
_options.UserPassword = validatedUser;
175+
_options.OwnerPassword = validatedOwner;
176+
177+
return this;
178+
}
115179
}

src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/FacetBase.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
using System.Globalization;
1717

18+
using Gotenberg.Sharp.API.Client.Domain.ValueObjects;
19+
1820
namespace Gotenberg.Sharp.API.Client.Domain.Requests.Facets;
1921

2022
public abstract class FacetBase : IConvertToHttpContent
@@ -77,6 +79,7 @@ public virtual IEnumerable<HttpContent> ToHttpContent()
7779
PdfFormat format => format.ToFormDataValue(),
7880
LibrePdfFormats format => format.ToFormDataValue(),
7981
ConversionPdfFormats format => format.ToFormDataValue(),
82+
PdfPassword password => password.Value,
8083
List<Cookie> cookies => JsonConvert.SerializeObject(cookies),
8184
float f => f.ToString(cultureInfo),
8285
double d => d.ToString(cultureInfo),

src/Gotenberg.Sharp.Api.Client/Domain/Requests/Facets/PdfOutputOptions.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515

16+
using Gotenberg.Sharp.API.Client.Domain.ValueObjects;
17+
1618
using Newtonsoft.Json.Linq;
1719

1820
namespace Gotenberg.Sharp.API.Client.Domain.Requests.Facets;
@@ -54,4 +56,16 @@ public class PdfOutputOptions : FacetBase
5456
/// </summary>
5557
[MultiFormHeader(Constants.Gotenberg.PdfOutput.MetaData)]
5658
public JObject? MetaData { get; set; }
59+
60+
/// <summary>
61+
/// The password required to open the PDF.
62+
/// </summary>
63+
[MultiFormHeader(Constants.Gotenberg.PdfOutput.UserPassword)]
64+
public PdfPassword? UserPassword { get; set; }
65+
66+
/// <summary>
67+
/// The password required to change permissions or edit the PDF.
68+
/// </summary>
69+
[MultiFormHeader(Constants.Gotenberg.PdfOutput.OwnerPassword)]
70+
public PdfPassword? OwnerPassword { get; set; }
5771
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
// Copyright 2019-2026 Chris Mohan, Jaben Cargman
2+
// and GotenbergSharpApiClient Contributors
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
namespace Gotenberg.Sharp.API.Client.Domain.ValueObjects;
17+
18+
/// <summary>
19+
/// Represents a validated PDF password used for encryption.
20+
/// Used for both userPassword (required to open) and ownerPassword (required to change permissions).
21+
/// </summary>
22+
public sealed class PdfPassword : IEquatable<PdfPassword>
23+
{
24+
public string Value { get; }
25+
26+
private PdfPassword(string value)
27+
{
28+
Value = value;
29+
}
30+
31+
/// <summary>
32+
/// Creates a validated PDF password.
33+
/// </summary>
34+
/// <param name="password">A non-empty password string.</param>
35+
/// <exception cref="ArgumentException">Thrown when the password is null or whitespace.</exception>
36+
public static PdfPassword Create(string password)
37+
{
38+
if (string.IsNullOrWhiteSpace(password))
39+
throw new ArgumentException("PDF password must not be null or empty.", nameof(password));
40+
41+
return new PdfPassword(password);
42+
}
43+
44+
public override string ToString() => "****";
45+
46+
public bool Equals(PdfPassword? other) => other is not null && Value == other.Value;
47+
48+
public override bool Equals(object? obj) => Equals(obj as PdfPassword);
49+
50+
public override int GetHashCode() => Value.GetHashCode();
51+
52+
public static bool operator ==(PdfPassword? left, PdfPassword? right) => Equals(left, right);
53+
54+
public static bool operator !=(PdfPassword? left, PdfPassword? right) => !Equals(left, right);
55+
}

src/Gotenberg.Sharp.Api.Client/Infrastructure/Constants.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ private static class CrossCutting
9090

9191
internal const string MetaData = "metadata";
9292

93+
internal const string UserPassword = "userPassword";
94+
95+
internal const string OwnerPassword = "ownerPassword";
96+
9397
internal static class FileNames
9498
{
9599
internal const string Index = "index.html";
@@ -110,6 +114,10 @@ public static class PdfOutput
110114
public const string GenerateTaggedPdf = CrossCutting.GenerateTaggedPdf;
111115

112116
public const string MetaData = CrossCutting.MetaData;
117+
118+
public const string UserPassword = CrossCutting.UserPassword;
119+
120+
public const string OwnerPassword = CrossCutting.OwnerPassword;
113121
}
114122

115123
/// <summary>

0 commit comments

Comments
 (0)