Skip to content

Instantly share code, notes, and snippets.

@MathildeRoussel
Last active July 21, 2023 09:20
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save MathildeRoussel/5830bfbef2ac0706fd85336ddf4eec0f to your computer and use it in GitHub Desktop.
Save MathildeRoussel/5830bfbef2ac0706fd85336ddf4eec0f to your computer and use it in GitHub Desktop.
Pkpass Generator for Wallet
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