Skip to content

Instantly share code, notes, and snippets.

@quartorz
Last active August 20, 2022 21:47
Show Gist options
  • Save quartorz/cbd04f26508ea5105f99210ca89ed89a to your computer and use it in GitHub Desktop.
Save quartorz/cbd04f26508ea5105f99210ca89ed89a to your computer and use it in GitHub Desktop.
UnityでJWT
using System.Collections.Generic;
namespace Asn1
{
public enum Class : byte
{
Universal = 0x0,
Application = 0x1,
ContextSpecific = 0x2,
Private = 0x3,
}
public enum Tag : byte
{
Integer = 0x02,
OctetString = 0x04,
Null = 0x05,
Sequence = 0x10,
Set = 0x11,
// UtcTime = 0x17,
// GeneralizedTime = 0x18,
}
public abstract class Value
{
public byte[] value;
}
public class Integer : Value
{
}
public class OctetString : Value
{
}
public class Structured : Value
{
public Class @class;
public Tag tag;
public IEnumerable<Value> GetSubObjects()
{
var totalLength = 0;
while (totalLength < value.Length)
{
var (obj, length) = BerDecoder.Decode(value, totalLength);
yield return obj;
totalLength += length;
}
if (totalLength != value.Length)
{
throw new InvalidDataException();
}
}
}
}
using System;
using System.IO;
namespace Asn1
{
public static class BerDecoder
{
readonly ref struct Result<T>
{
public readonly int offset;
public readonly T value;
public Result(int offset, T value)
{
this.offset = offset;
this.value = value;
}
public void Deconstruct(out int offset, out T value)
{
offset = this.offset;
value = this.value;
}
}
static Result<(Class @class, bool isStructured, Tag tag)> ReadTag(ReadOnlySpan<byte> buffer)
{
var tag = buffer[0] & 0x1f;
if (tag == 0x1f)
{
throw new NotImplementedException("0x1eを超えるタグは未実装です");
}
return new Result<(Class, bool, Tag)>(1,
((Class)((buffer[0] & 0xc0) >> 6), (buffer[0] & 0x20) != 0, (Tag)tag));
}
static Result<int> ReadLength(ReadOnlySpan<byte> buffer)
{
var first = buffer[0];
if (first < 0x80)
{
return new Result<int>(1, first);
}
else if (first == 0xff)
{
throw new InvalidDataException("LENGTHの第1オクテットが0xff");
}
var numOfOctets = first & 0x7f;
switch (numOfOctets)
{
case 0:
throw new NotImplementedException("無限長には対応してません");
case > 4:
case 4 when (buffer[0] & 0x80) != 0:
throw new NotSupportedException("LENGTHがintで表せない");
}
var length = 0;
for (var i = 0; i < numOfOctets; ++i)
{
length *= 0x100;
length += buffer[1 + i];
}
return new Result<int>(1 + numOfOctets, length);
}
static Result<byte[]> ReadBytes(ReadOnlySpan<byte> buffer, int length)
{
if (length == 0)
{
return new Result<byte[]>(0, Array.Empty<byte>());
}
if (length > buffer.Length)
{
throw new InvalidDataException("LENGTHがデータ長より大きい");
}
return new Result<byte[]>(length, buffer[..length].ToArray());
}
public static (Value value, int length) Decode(byte[] bytes, int offset = 0)
{
ReadOnlySpan<byte> buffer = bytes[offset..];
var (tagOffset, (@class, isStructured, tag)) = ReadTag(buffer);
buffer = buffer[tagOffset..];
int lengthOffset;
int length;
(lengthOffset, length) = ReadLength(buffer);
buffer = buffer[lengthOffset..];
var objectSize = tagOffset + lengthOffset + length;
if (tag == Tag.Null)
{
if (length != 0)
{
throw new InvalidDataException("NULLのLENGTHが0じゃない");
}
return (null, objectSize);
}
var value = ReadBytes(buffer, length).value;
if (isStructured)
{
return (new Structured {@class = @class, tag = tag, value = value},
objectSize);
}
switch (tag)
{
case Tag.OctetString:
return (new OctetString {value = value}, objectSize);
case Tag.Integer:
return (new Integer {value = value}, objectSize);
case Tag.Sequence:
case Tag.Set:
throw new InvalidDataException("SEQUENCE, SETがstructuredじゃない");
default:
throw new NotImplementedException($"タグ{tag}は未実装です");
}
}
}
}
using System;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Asn1;
public static class RsaParameterDecoder
{
const string PRIVATE_KEY_PREFIX = "-----BEGIN PRIVATE KEY-----";
const string PRIVATE_KEY_SUFFIX = "-----END PRIVATE KEY-----";
public static RSAParameters Decode(string privateKey)
{
privateKey = privateKey.Trim();
if (!privateKey.StartsWith(PRIVATE_KEY_PREFIX) || !privateKey.EndsWith(PRIVATE_KEY_SUFFIX))
{
throw new ArgumentException("PKCS #8形式の秘密鍵じゃない", nameof(privateKey));
}
var base64 = privateKey.Substring(PRIVATE_KEY_PREFIX.Length,
privateKey.Length - PRIVATE_KEY_PREFIX.Length - PRIVATE_KEY_SUFFIX.Length);
var decoded = BerDecoder.Decode(Convert.FromBase64String(base64)).value;
if (decoded is not Structured {@class: Class.Universal, tag: Tag.Sequence} structured)
{
throw new InvalidDataException("秘密鍵の形式がおかしい");
}
var rsaPrivateKey = structured.GetSubObjects().ElementAt(2);
if (rsaPrivateKey is not OctetString keyString)
{
throw new InvalidDataException();
}
var keyObj = BerDecoder.Decode(keyString.value).value;
if (keyObj is not Structured {@class: Class.Universal, tag: Tag.Sequence} keySequence)
{
throw new InvalidDataException();
}
var keys = keySequence.GetSubObjects().Skip(1).Take(8).Select(x => (Integer)x).ToList();
return new RSAParameters
{
Modulus = keys[0].value,
Exponent = keys[1].value,
D = keys[2].value,
P = keys[3].value,
Q = keys[4].value,
DP = keys[5].value,
DQ = keys[6].value,
InverseQ = keys[7].value,
};
}
}
using System;
using System.Security.Cryptography;
using System.Text;
using UnityEditor;
using UnityEngine;
using Cysharp.Threading.Tasks;
using UnityEngine.Networking;
public class Sample : MonoBehaviour
{
[SerializeField] TextAsset _json;
[SerializeField] string _spreadsheetId;
[SerializeField] string _range;
class Header
{
public string alg;
public string typ;
}
class Payload
{
public string iss;
public string scope;
public string aud;
public long exp;
public long iat;
}
[Serializable]
class ServiceAccountKey
{
public string type;
public string project_id;
public string private_key_id;
public string private_key;
public string client_email;
public string client_id;
}
[Serializable]
class TokenResponse
{
public string access_token;
}
async UniTaskVoid Run()
{
var serviceAccountKey = JsonUtility.FromJson<ServiceAccountKey>(_json.text);
var headerEncoded = EncodeToBase64(new Header {alg = "RS256", typ = "JWT"});
var now = DateTimeOffset.Now.ToUnixTimeSeconds();
var payload = new Payload
{
iss = serviceAccountKey.client_email,
scope = "https://www.googleapis.com/auth/spreadsheets.readonly",
aud = "https://www.googleapis.com/oauth2/v3/token",
iat = now,
exp = now + 60 * 60,
};
var payloadEncoded = EncodeToBase64(payload);
var headerAndPayload = $"{headerEncoded}.{payloadEncoded}";
var token = $"{headerAndPayload}.{Convert.ToBase64String(SignData(headerAndPayload, serviceAccountKey.private_key))}";
using var request = new UnityWebRequest("https://www.googleapis.com/oauth2/v3/token");
request.uploadHandler = new UploadHandlerRaw(Encoding.UTF8.GetBytes(
$@"{{""assertion"":""{token}"",""grant_type"":""urn:ietf:params:oauth:grant-type:jwt-bearer""}}"));
request.uploadHandler.contentType = "application/json";
request.downloadHandler = new DownloadHandlerBuffer();
request.method = UnityWebRequest.kHttpVerbPOST;
await request.SendWebRequest();
if (request.result == UnityWebRequest.Result.Success)
{
var response = JsonUtility.FromJson<TokenResponse>(request.downloadHandler.text);
using var sheetReq =
UnityWebRequest.Get(
$"https://sheets.googleapis.com/v4/spreadsheets/{_spreadsheetId}/values/{_range}");
sheetReq.SetRequestHeader("Authorization", $"Bearer {response.access_token}");
await sheetReq.SendWebRequest();
Debug.Log(sheetReq.downloadHandler.text);
}
}
string EncodeToBase64(object obj)
{
var utf8Text = Encoding.UTF8.GetBytes(JsonUtility.ToJson(obj));
return Convert.ToBase64String(utf8Text);
}
byte[] SignData(string text, string privateKey)
{
var rsa = RSA.Create();
rsa.ImportParameters(RsaParameterDecoder.Decode(privateKey));
return rsa.SignData(Encoding.UTF8.GetBytes(text), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
[CustomEditor(typeof(Sample))]
class SampleEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (GUILayout.Button("Run"))
{
(target as Sample)!.Run().Forget();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment