Pkpass Generator for Wallet
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Configuration; | |
using System.IO; | |
using System.IO.Compression; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Http; | |
using System.Net.Http.Headers; | |
using System.Security.Cryptography; | |
using System.Security.Cryptography.Pkcs; | |
using System.Security.Cryptography.X509Certificates; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
using System.Threading.Tasks; | |
using Microsoft.Azure.WebJobs; | |
using Microsoft.Azure.WebJobs.Extensions.Http; | |
using Microsoft.Azure.WebJobs.Host; | |
using Newtonsoft.Json; | |
using Newtonsoft.Json.Linq; | |
namespace WalletPassGenerator | |
{ | |
public static class Generator | |
{ | |
private const string ForegroundColor = "rgb(255,255,255)"; | |
private const string BackgroundColor = "rgb(35,37,51)"; | |
private const string LabelColor = "rgb(112,95,173)"; | |
private const string AppleOid = "1.2.840.113635.100.4.14"; | |
[FunctionName("GeneratePass")] | |
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "pass/{eventId}")]HttpRequestMessage req, string eventId, TraceWriter log, ExecutionContext executionContext) | |
{ | |
try | |
{ | |
var eventData = FetchEventData(eventId); | |
var attendee = await req.Content.ReadAsAsync<Attendee>() ?? new Attendee {Email = "toto@toto.com", Name = "Toto Leheros"}; | |
var signatureData = CreateSignatureData(executionContext); | |
var memoryStream = new MemoryStream(); | |
using (var pkpass = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) | |
{ | |
var passJson = pkpass.CreateEntry("pass.json"); | |
var passJsonHash = await CreatePassJsonAsync(passJson, signatureData, eventData, attendee); | |
var manifest = new JObject(new JProperty("pass.json", passJsonHash)); | |
async Task AddImageAsync(string path) | |
{ | |
var hash = await ConvertFileToEntryAsync(pkpass.CreateEntry(path), Path.Combine(executionContext.FunctionAppDirectory, "Assets", path)); | |
manifest.Add(new JProperty(path, hash)); | |
} | |
await AddImageAsync("strip.png"); | |
await AddImageAsync("strip@2x.png"); | |
await AddImageAsync("icon.png"); | |
await AddImageAsync("icon@2x.png"); | |
await AddImageAsync("logo.png"); | |
await AddImageAsync("logo@2x.png"); | |
var manifestEntry = pkpass.CreateEntry("manifest.json"); | |
var dataToSign = await CreateManifestJsonAsync(manifestEntry, manifest); | |
await CreateSignatureAsync(pkpass.CreateEntry("signature"), signatureData, dataToSign); | |
} | |
memoryStream.Position = 0; | |
return new HttpResponseMessage(HttpStatusCode.OK) | |
{ | |
Content = new StreamContent(memoryStream) | |
{ | |
Headers = | |
{ | |
ContentType = new MediaTypeHeaderValue("application/vnd.apple.pkpass"), | |
ContentDisposition = new ContentDispositionHeaderValue("attachment") {FileName = $"pass-{eventId}.pkpass"} | |
} | |
} | |
}; | |
} | |
catch (Exception ex) | |
{ | |
log.Error("An error occured", ex); | |
throw; | |
} | |
} | |
private static SignatureData CreateSignatureData(ExecutionContext executionContext) | |
{ | |
var signingCertificate = new X509Certificate2(Path.Combine(executionContext.FunctionAppDirectory, ConfigurationManager.AppSettings["CertificatePath"]), ConfigurationManager.AppSettings["PrivateKeyPassword"]); | |
var teamIdentifier = Regex.Match(signingCertificate.Subject, @"OU=(?<teamId>\w+),").Groups["teamId"].Value; | |
return new SignatureData | |
{ | |
SigningCertificate = signingCertificate, | |
Authority = new X509Certificate2(Path.Combine(executionContext.FunctionAppDirectory, ConfigurationManager.AppSettings["CertificateAuthorityPath"])), | |
TeamIdentifier = teamIdentifier, | |
PassTypeIdentifier = ConfigurationManager.AppSettings["PassTypeIdentifier"] | |
}; | |
} | |
private static EventData FetchEventData(string eventId) | |
{ | |
return new EventData | |
{ | |
Id = "matinaleserverless052018", | |
Organizer = "Cellenza", | |
Type = "Matinale Cellenza", | |
Link = "http://click.cellenza.com/inscription-matinale-31-mai-architectures-modernes", | |
DateTime = new DateTimeOffset(2018, 05, 29, 09, 00, 00, TimeSpan.FromHours(2)), | |
Title = "Matinale Serverless", | |
SubTitle = "Serverless, microservices et containers", | |
Speaker = "Marius Zaharia, Michel Hubert", | |
Location = new EventLocation | |
{ | |
Address = "City Chateauform, 2 Avenue Vélasquez, 75008 PARIS", | |
Phone = "+33145631429", | |
Email = "contact@cellenza.com" | |
} | |
}; | |
} | |
private static async Task<string> CreatePassJsonAsync(ZipArchiveEntry archiveEntry, SignatureData signatureData, EventData eventData, Attendee attendee) | |
{ | |
var serial = string.Join(":", eventData.Id, GetHashForBytes(Encoding.UTF8.GetBytes(attendee.Email.ToLowerInvariant()))); | |
var passTemplate = new | |
{ | |
FormatVersion = 1, | |
PassTypeIdentifier = signatureData.PassTypeIdentifier, | |
SerialNumber = serial, | |
TeamIdentifier = signatureData.TeamIdentifier, | |
OrganizationName = eventData.Organizer, | |
Description = eventData.Type, | |
ForegroundColor = ForegroundColor, | |
BackgroundColor = BackgroundColor, | |
LabelColor = LabelColor, | |
Barcode = new | |
{ | |
Message = serial, | |
Format = "PKBarcodeFormatQR", | |
messageEncoding = "iso-8859-1" | |
}, | |
EventTicket = new | |
{ | |
HeaderFields = new object[] | |
{ | |
new | |
{ | |
Key = "date", | |
Label = "Date", | |
IsRelative = true, | |
Value = eventData.DateTime.ToString("yyyy-MM-ddTHH:mm:sszzz"), | |
DateStyle = "PKDateStyleShort", | |
TimeStyle = "PKDateStyleShort", | |
} | |
}, | |
PrimaryFields = new object[] | |
{ | |
new | |
{ | |
Key = "title", | |
Label = string.Empty, | |
Value = eventData.Title | |
} | |
}, | |
SecondaryFields = new object[] | |
{ | |
new | |
{ | |
Key = "subtitle", | |
Label = string.Empty, | |
Value = eventData.SubTitle, | |
TextAlignment = "PKTextAlignmentLeft" | |
} | |
}, | |
AuxiliaryFields = new object[] | |
{ | |
new | |
{ | |
Key = "speaker", | |
Label = "PRESENTE PAR", | |
Value = eventData.Speaker, | |
TextAlignment = "PKTextAlignmentLeft" | |
} | |
}, | |
BackFields = new object[] | |
{ | |
new | |
{ | |
Key = "site", | |
Label = "PLUS D'INFOS", | |
Value = eventData.Link | |
}, | |
new | |
{ | |
Key = "address", | |
Label = "ADRESSE", | |
Value = eventData.Location.Address | |
}, | |
new | |
{ | |
Key = "phone", | |
Label = "TELEPHONE", | |
Value = eventData.Location.Phone | |
}, | |
new | |
{ | |
Key = "email", | |
Label = "EMAIL", | |
Value = eventData.Location.Email | |
} | |
} | |
} | |
}; | |
var serializer = JsonSerializer.CreateDefault(new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver(), Formatting = Formatting.Indented }); | |
using (var entryStream = archiveEntry.Open()) | |
using (var memoryStream = new MemoryStream()) | |
using (var writer = new StreamWriter(memoryStream) { AutoFlush = true }) | |
{ | |
serializer.Serialize(writer, passTemplate); | |
memoryStream.Position = 0; | |
var hash = GetHashForStream(memoryStream); | |
memoryStream.Position = 0; | |
await memoryStream.CopyToAsync(entryStream); | |
return hash; | |
} | |
} | |
private static async Task<string> ConvertFileToEntryAsync(ZipArchiveEntry archiveEntry, string filePath) | |
{ | |
using (var fileStream = File.OpenRead(filePath)) | |
using (var entryStream = archiveEntry.Open()) | |
{ | |
var hash = GetHashForStream(fileStream); | |
fileStream.Position = 0; | |
await fileStream.CopyToAsync(entryStream); | |
return hash; | |
} | |
} | |
private static async Task<byte[]> CreateManifestJsonAsync(ZipArchiveEntry archiveEntry, JToken manifest) | |
{ | |
using (var memoryStream = new MemoryStream()) | |
using (var entryStream = archiveEntry.Open()) | |
using (var streamWriter = new StreamWriter(memoryStream) { AutoFlush = true }) | |
{ | |
await streamWriter.WriteAsync(manifest.ToString(Formatting.Indented)); | |
memoryStream.Position = 0; | |
await memoryStream.CopyToAsync(entryStream); | |
return memoryStream.ToArray(); | |
} | |
} | |
private static string GetHashForBytes(byte[] bytes) | |
{ | |
using (var sha1 = new SHA1Managed()) | |
{ | |
return string.Concat(sha1.ComputeHash(bytes).Select(x => x.ToString("x2"))); | |
} | |
} | |
private static string GetHashForStream(Stream stream) | |
{ | |
using (var sha1 = new SHA1Managed()) | |
{ | |
return string.Concat(sha1.ComputeHash(stream).Select(x => x.ToString("x2"))); | |
} | |
} | |
private static async Task CreateSignatureAsync(ZipArchiveEntry entry, SignatureData signatureData, byte[] dataToSign) | |
{ | |
var signedCms = new SignedCms(new ContentInfo(new Oid(AppleOid), dataToSign), true); | |
var cmsSigner = new CmsSigner(SubjectIdentifierType.SubjectKeyIdentifier, signatureData.SigningCertificate) { IncludeOption = X509IncludeOption.None }; | |
cmsSigner.SignedAttributes.Add(new Pkcs9SigningTime()); | |
cmsSigner.Certificates.Add(signatureData.SigningCertificate); | |
cmsSigner.Certificates.Add(signatureData.Authority); | |
signedCms.ComputeSignature(cmsSigner); | |
var bytes = signedCms.Encode(); | |
using (var stream = entry.Open()) | |
{ | |
await stream.WriteAsync(bytes, 0, bytes.Length); | |
} | |
} | |
} | |
public class SignatureData | |
{ | |
public X509Certificate2 SigningCertificate { get; set; } | |
public X509Certificate2 Authority { get; set; } | |
public string TeamIdentifier { get; set; } | |
public string PassTypeIdentifier { get; set; } | |
} | |
public class EventData | |
{ | |
public string Organizer { get; set; } | |
public string Type { get; set; } | |
public string Link { get; set; } | |
public DateTimeOffset DateTime { get; set; } | |
public string Title { get; set; } | |
public string SubTitle { get; set; } | |
public string Speaker { get; set; } | |
public EventLocation Location { get; set; } | |
public string Id { get; set; } | |
} | |
public class EventLocation | |
{ | |
public string Address { get; set; } | |
public string Phone { get; set; } | |
public string Email { get; set; } | |
} | |
public class Attendee | |
{ | |
public string Name { get; set; } | |
public string Email { get; set; } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment