Last active
August 20, 2022 21:47
-
-
Save quartorz/cbd04f26508ea5105f99210ca89ed89a to your computer and use it in GitHub Desktop.
UnityでJWT
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.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(); | |
} | |
} | |
} | |
} |
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.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}は未実装です"); | |
} | |
} | |
} | |
} |
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.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, | |
}; | |
} | |
} |
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.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