
create or replace package util_line_api | |
as | |
/* | |
* チャネルアクセストークンv2.1を発行するURL | |
*/ | |
C_TOKEN_URL constant varchar2(160) := 'https://api.line.me/oauth2/v2.1/token'; | |
/** | |
* PKCSのフォーマットからアサーション署名キーとして登録する | |
* JSON Web Tokenを生成するのは大変なので、 | |
* その部分の処理を行うファンクションを定義。 | |
* | |
* 参照: https://developers.line.biz/ja/docs/messaging-api/generate-json-web-token/#create-an-assertion-signing-key | |
* useにsig、またはkey_opsに["verify"]のどちらを指定、とのことなのでuseを使うことにしています。 | |
* | |
* @param p_kty 鍵で使用されている暗号アルゴリズムファミリー。RSA必須。 | |
* @param p_alg 鍵で使用されるアルゴリズム。RS256必須。 | |
* @param p_use 鍵の用途。sig必須。 | |
* @param p_e 公開鍵を復元するための絶対値。概ね65537ですがデフォルト指定はしません。 | |
* @param p_n 公開鍵を復元するための暗号指数。 | |
* @return 生成されたJSON Web Key | |
*/ | |
function generate_json_web_key( | |
p_kty in varchar2 default 'RSA' | |
,p_alg in varchar2 default 'RS256' | |
,p_use in varchar2 default 'sig' | |
,p_e in number | |
,p_n in varchar2 | |
) | |
return varchar2; | |
/** | |
* LINE公式アカウントが受信したメッセージの署名を確認する。 | |
* | |
* 参照: https://developers.line.biz/ja/docs/messaging-api/receiving-messages/ | |
* | |
* @param p_channel_secret Messaging APIのチャネルのチャネルシークレット | |
* @param p_signature HTTPヘッダーx-line-signatureとして渡される署名の値 | |
* @param p_content 受信したメッセージ本体 | |
* @return 受信した署名とメッセージから生成した署名が一致したら真を返す。 | |
*/ | |
function verify_response_signature( | |
p_channel_secret in varchar2 | |
,p_signature in varchar2 | |
,p_content in blob | |
) | |
return boolean; | |
/** | |
* アサーション署名キーに対応する秘密鍵を使ってJWTを作る。 | |
* | |
* 元はGoogleのプロジェクトに登録したサービス・アカウントのキーより | |
* JWTを生成したコード。 | |
* | |
* 参照:https://developers.line.biz/ja/docs/messaging-api/generate-json-web-token/#generate-jwt | |
* | |
* @param p_secret アサーション署名キーに対応した秘密キー。PKCS#1またはPKCS#8 - PEM | |
* @param p_kid チャネルに登録したアサーション署名キーのキーID | |
* @param p_channel_id Messaging APIのチャネルID | |
* @param p_aud https://api.line.me/ 変更不可 | |
* @param p_iat トークンが有効になる開始時刻 - unixtimeではなくtimestamp型 - デフォルトは現在時刻 | |
* @param p_duration JWTの有効期限。秒で指定する。最大30分。 | |
* @param p_token_exp チャネルアクセストークンの有効期間。秒で指定する。最大30日。 | |
* @return 生成されたJSON Web Token | |
*/ | |
function generate_jwt( | |
p_secret in varchar2 | |
,p_kid in varchar2 -- アサーション署名キーのkid | |
,p_channel_id in varchar2 -- チャネルIDがissとsubの値になる | |
,p_aud in varchar2 default 'https://api.line.me/' | |
/* | |
* Googleを踏襲してp_iatとp_durationを引数とするが、LINEでは | |
* iatは指定せず iat + duration を計算してexpに設定する。 | |
*/ | |
,p_iat in timestamp default current_timestamp | |
,p_duration in number default 1800 -- 秒で指定する | |
/* | |
* token_expはLINEでチャネルアクセストークンを取得するときに使用される。 | |
*/ | |
,p_token_exp in number | |
) | |
return varchar2; | |
/** | |
* LINEのトークンURLを呼び出して、チャネルアクセストークンを取得する。 | |
* Web資格証明の静的IDが指定されてれば、取得したチャネルアクセストークンで更新する。 | |
* | |
* 参照:https://developers.line.biz/ja/docs/messaging-api/generate-json-web-token/#issue_a_channel_access_token_v2_1 | |
* | |
* @param p_jwt generare_jwtを呼び出して生成したJWT | |
* @param p_credential_static_id APEXに作成したWeb資格証明の静的ID | |
* @param p_expires_in 有効期限が切れるまでの秒数。出力値。 | |
* @param p_key_id チャネルアクセストークンを識別するキーID。出力値。 | |
* @return Authorizationヘッダーに指定するBearerで始まるチャネルアクセストークンの値 | |
*/ | |
function get_token( | |
p_jwt in varchar2 | |
,p_credential_static_id in varchar2 default null | |
,p_expires_in out number | |
,p_key_id out varchar2 | |
) | |
return varchar2; | |
end util_line_api; | |
/ | |
create or replace package body util_line_api | |
as | |
/* | |
* OracleのTIMESTAMP型のデータをUNIX時間に変換する。 | |
*/ | |
function unixtime(p_timestamp in timestamp) | |
return pls_integer | |
is | |
l_date date; | |
l_epoc number; | |
begin | |
l_date := sys_extract_utc(p_timestamp); | |
l_epoc := l_date - date'1970-01-01'; | |
return l_epoc * 24 * 60 * 60; | |
end unixtime; | |
/* BASE64のデコード */ | |
function from_base64(t in varchar2) return varchar2 is | |
begin | |
return utl_raw.cast_to_varchar2(utl_encode.base64_decode(utl_raw.cast_to_raw(t))); | |
end from_base64; | |
/* BASE64へのエンコード - RAWより */ | |
function to_base64_from_raw(t in raw) return varchar2 is | |
l_base64 varchar2(32767); | |
begin | |
l_base64 := utl_raw.cast_to_varchar2(utl_encode.base64_encode(t)); | |
l_base64 := replace(l_base64, chr(13)||chr(10), ''); | |
return l_base64; | |
end to_base64_from_raw; | |
/* BASE64へのエンコード - VARCHAR2 */ | |
function to_base64(t in varchar2) return varchar2 is | |
begin | |
return to_base64_from_raw(utl_raw.cast_to_raw(t)); | |
end to_base64; | |
/* 秘密鍵を一行にする。 */ | |
function convert_to_single_line( | |
p_string in varchar2 | |
) | |
return varchar2 | |
as | |
begin | |
return regexp_replace( | |
p_string | |
,'(-+((BEGIN|END) (RSA )?(PUBLIC|PRIVATE) KEY)-+\s?|\s)' | |
,'' | |
); | |
end convert_to_single_line; | |
/* | |
* 以下LINE向けの実装。 | |
*/ | |
function generate_json_web_key( | |
p_kty in varchar2 | |
,p_alg in varchar2 | |
,p_use in varchar2 | |
,p_e in number | |
,p_n in varchar2 | |
) | |
return varchar2 | |
as | |
l_jwt json_object_t; | |
l_result varchar2(32767); | |
l_e_raw raw(4); | |
l_e_str varchar2(16); | |
l_n_raw raw(256); | |
l_n_str varchar2(1028); | |
begin | |
l_jwt := json_object_t(); | |
l_jwt.put('kty',p_kty); | |
l_jwt.put('alg',p_alg); | |
l_jwt.put('use',p_use); | |
/* e */ | |
l_e_raw := hextoraw(trim(to_char(p_e,'XXXXXXX'))); | |
l_e_str := to_base64_from_raw(l_e_raw); | |
/* | |
* nと同様に本来であればtranslateが必要だがデータが短いので省略。 | |
*/ | |
l_jwt.put('e',l_e_str); | |
/* n */ | |
l_n_raw := hextoraw(p_n); | |
l_n_str := to_base64_from_raw(l_n_raw); | |
l_n_str := trim(translate(l_n_str, '+/=', '-_ ')); | |
l_jwt.put('n',l_n_str); | |
l_result := l_jwt.to_string(); | |
return l_result; | |
end generate_json_web_key; | |
function verify_response_signature( | |
p_channel_secret in varchar2 | |
,p_signature in varchar2 | |
,p_content in blob | |
) | |
return boolean | |
as | |
l_key raw(32); | |
l_mac raw(32); | |
l_content_signature varchar2(64); | |
begin | |
l_key := utl_raw.cast_to_raw(p_channel_secret); | |
l_mac := dbms_crypto.mac( | |
src => p_content | |
,typ => DBMS_CRYPTO.HMAC_SH256 | |
,key => l_key | |
); | |
l_content_signature := to_base64_from_raw(l_mac); | |
return p_signature = l_content_signature; | |
end verify_response_signature; | |
/* JWTを生成する実装 */ | |
function generate_jwt( | |
p_secret in varchar2 | |
,p_kid in varchar2 | |
,p_channel_id in varchar2 | |
,p_aud in varchar2 | |
,p_iat in timestamp | |
,p_duration in number -- second | |
,p_token_exp in number | |
) | |
return varchar2 | |
as | |
l_iat pls_integer; | |
l_exp pls_integer; | |
l_header_json json_object_t; | |
l_header_str varchar2(32767); | |
l_header_base64 varchar2(32767); -- 1st part of JWT | |
l_payload_json json_object_t; | |
l_payload_str varchar2(32767); | |
l_payload_base64 varchar2(32767); -- 2nd part of JWT | |
l_data varchar2(32767); | |
l_hmac_raw raw(32767); | |
l_hmac varchar2(32767); -- 3rd part of JWT | |
l_jwt varchar2(32767); | |
begin | |
/* iatとexpとなる値を求める。 */ | |
l_iat := unixtime(p_iat); | |
l_exp := l_iat + p_duration; | |
/* ヘッダーを手作業で作成し、BASE64でエンコードする。 */ | |
l_header_json := json_object_t(); | |
l_header_json.put('alg','RS256'); | |
l_header_json.put('typ','JWT'); | |
l_header_json.put('kid', p_kid); | |
l_header_str := l_header_json.to_string(); | |
l_header_base64 := to_base64(l_header_str); -- ヘッダー | |
/* ペイロードを手作業で作成し、BASE64でエンコードする。 */ | |
l_payload_json := json_object_t(); | |
if p_channel_id is not null then | |
l_payload_json.put('iss', p_channel_id); | |
l_payload_json.put('sub', p_channel_id); | |
end if; | |
if p_aud is not null then | |
l_payload_json.put('aud', p_aud); | |
end if; | |
l_payload_json.put('exp', l_exp); | |
l_payload_json.put('token_exp', p_token_exp); | |
l_payload_str := l_payload_json.to_string(); | |
l_payload_base64 := to_base64(l_payload_str); -- ペイロード | |
-- シグネチャを手作業で作成する。 | |
l_data := l_header_base64 || '.' || l_payload_base64; | |
l_hmac_raw := dbms_crypto.sign( | |
src => utl_i18n.string_to_raw(l_data,'AL32UTF8'), | |
prv_key => utl_i18n.string_to_raw(convert_to_single_line(p_secret),'AL32UTF8'), | |
pubkey_alg => DBMS_CRYPTO.KEY_TYPE_RSA, | |
sign_alg => DBMS_CRYPTO.SIGN_SHA256_RSA | |
); | |
l_hmac := to_base64_from_raw(l_hmac_raw); | |
l_hmac := trim(translate(l_hmac, '+/=', '-_ ')); -- HMAC | |
/* JSON Web Tokenを返す。 */ | |
l_jwt := l_header_base64 || '.' || l_payload_base64 || '.' || l_hmac; | |
return l_jwt; | |
end generate_jwt; | |
/* トークンを取得する実装 */ | |
function get_token( | |
p_jwt in varchar2 | |
,p_credential_static_id in varchar2 | |
,p_expires_in out number | |
,p_key_id out varchar2 | |
) | |
return varchar2 | |
as | |
l_token_clob clob; | |
l_token_json json_object_t; | |
l_token varchar2(32767); | |
l_request varchar2(32767); | |
l_length number; | |
begin | |
/* | |
* POSTで送信するリクエスト本体を作成する。 | |
*/ | |
l_request := 'grant_type=client_credentials&'; | |
l_request := l_request || 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&'; | |
l_request := l_request || 'client_assertion=' || p_jwt; | |
l_length := lengthb(l_request); | |
/* | |
* トークンURLの呼び出し。 | |
*/ | |
apex_web_service.clear_request_headers; | |
apex_web_service.set_request_headers('Content-Length',l_length,p_reset = false); | |
apex_web_service.set_request_headers('Content-Type', 'application/x-www-form-urlencoded',p_reset = false); | |
l_token_clob := apex_web_service.make_rest_request( | |
p_url => C_TOKEN_URL | |
,p_http_method => 'POST' | |
,p_body => l_request | |
); | |
if apex_web_service.g_status_code <> 200 then | |
raise_application_error(-20001,'Failed to get channel access token v2.1'); | |
end if; | |
-- dbms_output.put_line(l_token_clob); | |
l_token_json := json_object_t(l_token_clob); | |
l_token := l_token_json.get_string('token_type') || ' ' || l_token_json.get_string('access_token'); | |
p_expires_in := l_token_json.get_number('expires_in'); | |
p_key_id := l_token_json.get_string('key_id'); | |
/* | |
* Web資格証明の静的IDが指定されている場合は、アップデートする。 | |
*/ | |
if p_credential_static_id is not null then | |
apex_credential.set_persistent_credentials( | |
p_credential_static_id => p_credential_static_id | |
,p_username => 'Authorization' | |
,p_password => l_token | |
); | |
end if; | |
return l_token; | |
end get_token; | |
end util_line_api; | |
/ |
% openssl genrsa -out private.pem 2048
Generating RSA private key, 2048 bit long modulus
...................................................................+++++
.....+++++
e is 65537 (0x10001)
%
% openssl rsa -in private.pem -text -noout | grep publicExponent
publicExponent: 65537 (0x10001)
%
次にmodulusの値を取り出します。
% openssl rsa -in private.pem -modulus -noout
Modulus=BBABF46FC55DF62DCDAC79C00A978BF5B06041CEA20A19FD1CA95B0522ACE8CAD1CE81AC7F970307BD3191B20E8EB9DC356242A8304C6F2580E5C53EEACB53763033F436B7E7A1982435E6BBDC7927EC8E59F2EF5EA8FFCD76A2F66D290C68DFDEDDFE6CF1A8859700FCA25235C1662EC26C94A68379736F3C1F1492871D17D5A1507FC050AB83769FAF39D0E406DDCA695947142B010F5FF8AC99CFBA734FE63C965EA54C8A5CB61C60D34C35769F10C1443B3912830A0C112F03A9CAADC49956820B2E0A54095E5E6E5D6F769698FA8778C6264C8DE6C14704CA9BF0DC7B0BE63940497080618BFEEFC7854DCB6FF0262A968B2B7F6460244F9AFAE0295951
%
begin
dbms_output.put_line(
util_line_api.generate_json_web_key(
p_e => 65537
,p_n => 'Modulus=以降の値'
)
);
end;
# prefix: line
messages
signature vc4000
content blob
is_valid vc1 /nn /default Y
received_date date /default sysdate
create or replace function line_callback( | |
p_signature in varchar2 | |
,p_content in blob | |
) | |
return number | |
as | |
C_CHANNEL_SECRET constant varchar2(64) := 'Messaging APIのチャネルシークレット'; | |
begin | |
if not util_line_api.verify_response_signature(C_CHANNEL_SECRET, p_signature, p_content) then | |
/* | |
* 署名が正しくないので、それを記録する。それ以上の処理はせず、正常終了とする。エラーは返さない。 | |
*/ | |
insert into line_messages(signature, content, is_valid) values(p_signature, p_content, 'N'); | |
else | |
insert into line_messages(signature, content) values(p_signature, p_content); | |
end if; | |
return 200; | |
end line_callback; |
declare | |
l_sig varchar2(4000); | |
begin | |
l_sig := owa_util.get_cgi_env('x-line-signature'); | |
:status_code := line_callback(l_sig, :body); | |
end; |
declare | |
C_RSA_KEY constant varchar2(32767) := q'~ | |
-----BEGIN RSA PRIVATE KEY----- | |
MIIEpAIBAAKCAQEAv/U8xG3mIfYn2gGI39e4f5yG3w0aTCrOercbkVq+pR4fWVCe | |
アサーション署名キーの元となった秘密鍵のデータ | |
ZFsGzmTKr43RtEAX24VWsbZqve0sERVAyEwRZ6EYpryUpbNfJ2RHJg== | |
-----END RSA PRIVATE KEY-----~'; | |
l_jwt varchar2(32767); | |
l_token varchar2(32767); | |
l_expires_in number; | |
l_key_id varchar2(32767); | |
begin | |
/* JWTの生成 */ | |
l_jwt := util_line_api.generate_jwt( | |
p_secret => C_RSA_KEY | |
,p_kid => 'アサーション署名キーのID' | |
,p_channel_id => 'Messaging APIのチャネルID' | |
,p_token_exp => 3600 | |
); | |
/* トークンの取得 */ | |
l_token := util_line_api.get_token( | |
p_jwt => l_jwt | |
,p_credential_static_id => 'LINE_CHANNEL_ACCESS_TOKEN' | |
,p_expires_in => l_expires_in | |
,p_key_id => l_key_id | |
); | |
dbms_output.put_line(l_key_id); | |
end; |
declare | |
l_request clob; | |
l_response clob; | |
begin | |
l_request := q'~{ | |
"to": "あなたのユーザーID", | |
"messages":[ | |
{ | |
"type":"text", | |
"text":"Hello, world1" | |
}, | |
{ | |
"type":"text", | |
"text":"Hello, world2" | |
} | |
] | |
}~'; | |
apex_web_service.clear_request_headers; | |
apex_web_service.set_request_headers('Content-Type','application/json',p_reset = false); | |
l_response := apex_web_service.make_rest_request( | |
p_url => 'https://api.line.me/v2/bot/message/push' | |
,p_http_method => 'POST' | |
,p_body => l_request | |
,p_credential_static_id => 'LINE_CHANNEL_ACCESS_TOKEN' | |
); | |
dbms_output.put_line(l_response); | |
end; |