前提・実現したいこと
Twitter APIをKotlinで触っています。
OAuth 1.0のAPIでは oauth_signature
というものを生成する必要があり、以下のページの入力例を使って実際に生成してみました。
Creating a signature — Twitter Developers
発生している問題・エラーメッセージ
問題はsigning key
の値とsignature base string
があっているのもかかわらず、シグネチャの生成結果が異なるということです。
Text
1signing key: 2私のコードで生成された値: kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw&LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE 3このページに載っている正しい値: kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw&LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE 4 5signature base string: 6私のコードで生成された値: POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252b%2520Gentlemen%252c%2520a%2520signed%2520OAuth%2520request%2521 7このページに載っている正しい値: POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521 8 9signature: 10私のコードで生成された値: lzKEyPhir88fiRVm0hC22SpU6Ig= 11このページに載っている正しい値: hCtSmYh+iHYCEqBWrE7C7hYmtUk=
該当のソースコード
私のコードを以下に示します。
Kotlin
1import java.net.URLEncoder 2import java.util.* 3import javax.crypto.Mac 4import javax.crypto.spec.SecretKeySpec 5 6fun main() { 7 val apiSecretKey = "kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw" 8 val accessTokenSecret = "LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE" 9 val requestMethod = "POST" 10 val requestUrl = "https://api.twitter.com/1.1/statuses/update.json" 11 val parameters = mapOf( 12 "include_entities" to "true", 13 "oauth_consumer_key" to "xvz1evFS4wEEPTGEFPHBog", 14 "oauth_nonce" to "kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", 15 "oauth_signature_method" to "HMAC-SHA1", 16 "oauth_timestamp" to "1318622958", 17 "oauth_token" to "370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb", 18 "oauth_version" to "1.0", 19 "status" to "Hello%20Ladies%20%2b%20Gentlemen%2c%20a%20signed%20OAuth%20request%21" 20 ) 21 22 val signature = createSignature(apiSecretKey, accessTokenSecret, requestMethod, requestUrl, parameters) 23 val expected = "hCtSmYh+iHYCEqBWrE7C7hYmtUk=" 24 25 println(if (signature == expected) "正しい結果が得られました。" else "間違った結果が得られました。") 26} 27 28fun createSignature( 29 apiSecretKey: String, 30 accessTokenSecret: String, 31 requestMethod: String, 32 requestUrl: String, 33 parameters: Map<String, String> 34): String { 35 36 // キーを生成する。 37 val encodedApiSecretKey = URLEncoder.encode(apiSecretKey, Charsets.UTF_8) 38 val encodedAccessTokenSecret = URLEncoder.encode(accessTokenSecret, Charsets.UTF_8) 39 val key = "$encodedApiSecretKey&$encodedAccessTokenSecret" 40 println("key: $key") 41 42 // データを生成する。 43 val encodedMethod = URLEncoder.encode(requestMethod, Charsets.UTF_8) 44 val encodedUrl = URLEncoder.encode(requestUrl, Charsets.UTF_8) 45 val encodedParameters = URLEncoder.encode(parameters.asSequence().sortedBy { it.key }.map { "${it.key}=${it.value}" }.joinToString("&"), Charsets.UTF_8) 46 val data = "$encodedMethod&$encodedUrl&$encodedParameters" 47 println("data: $data") 48 49 // キーとデータからシグネチャを生成する。 50 val algorithm = "HmacSHA1" 51 val keySpec = SecretKeySpec(key.toByteArray(), algorithm) 52 val mac = Mac.getInstance(algorithm).also { it.init(keySpec) } 53 val digest = mac.doFinal(data.toByteArray()) 54 val signature = Base64.getEncoder().encodeToString(digest) 55 println("signature: $signature") 56 57 return signature 58}
実行結果も以下に示します。
Text
1key: kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw&LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE 2data: POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252b%2520Gentlemen%252c%2520a%2520signed%2520OAuth%2520request%2521 3signature: lzKEyPhir88fiRVm0hC22SpU6Ig= 4間違った結果が得られました。
試したこと
ためしに、公式ページの値をコピペして、それらからシグネチャを生成する処理のみを走らせてみました。
コードを以下に示します。
Kotlin
1package net.aridai.mykotlin 2 3import java.util.* 4import javax.crypto.Mac 5import javax.crypto.spec.SecretKeySpec 6 7fun main() { 8 9 val signingKey = "kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw&LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE" 10 val signatureBaseString = "POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521" 11 12 val algorithm = "HmacSHA1" 13 val keySpec = SecretKeySpec(signingKey.toByteArray(), algorithm) 14 val mac = Mac.getInstance(algorithm).also { it.init(keySpec) } 15 val digest = mac.doFinal(signatureBaseString.toByteArray()) 16 val signature = Base64.getEncoder().encodeToString(digest) 17 println("$signature") 18}
実行結果を以下に示します。
Text
1hCtSmYh+iHYCEqBWrE7C7hYmtUk=
結果より、シグネチャの生成処理自体は正しく動作をしていると言えます。
補足情報(FW/ツールのバージョンなど)
IntelliJ IDEA 2018.3.6 (Community Edition)
Build #IC-183.6156.11, built on March 25, 2019
JRE: 1.8.0_152-release-1343-b28 x86_64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
macOS 10.14.5
build.gradle.kts
を以下に示します。
Kotlin
1import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 3plugins { 4 java 5 kotlin("jvm") version "1.3.40" 6} 7 8group = "tadano.sakana" 9version = "1.0.0" 10 11repositories { 12 mavenCentral() 13} 14 15dependencies { 16 implementation(kotlin("stdlib-jdk8")) 17 implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1") 18 implementation("io.reactivex.rxjava2:rxjava:2.2.9") 19 implementation("io.reactivex.rxjava2:rxkotlin:2.3.0") 20 implementation("com.squareup.retrofit2:retrofit:2.6.0") 21 implementation("com.squareup.retrofit2:converter-moshi:2.6.0") 22 implementation("com.squareup.moshi:moshi:1.8.0") 23 implementation("com.squareup.moshi:moshi-kotlin:1.8.0") 24 implementation("com.squareup.okhttp3:okhttp:3.14.2") 25 26 testCompile("junit", "junit", "4.12") 27} 28 29configure<JavaPluginConvention> { 30 sourceCompatibility = JavaVersion.VERSION_1_8 31} 32tasks.withType<KotlinCompile> { 33 kotlinOptions.jvmTarget = "1.8" 34}
追記
次のコードで検証して見ると、大文字小文字が違っていました。
公式ページの値と比較する際にChromeの検索機能 (大文字小文字を区別しない) で比較していたので気づきませんでした。
Kotlin
1val expected = "長いので略" 2val actual = "長いので略" 3val lastIndex = min(expected.length, actual.length) 4 5for (i in (0..lastIndex)) { 6 val actualChar = actual[i] 7 val expectedChar = expected[i] 8 if (actualChar != expectedChar) 9 println("$i: $actualChar, $expectedChar") 10}
Text
1388: b, B 2407: c, C
この情報をもとにもう一度実装し直してみます。
ページに大文字小文字についての言及があったのかもしれないので。
追記2
Hello Ladies + Gentlemen, a signed OAuth request!
をパーセントエンコードするとHello%20Ladies%20%2B%20Gentlemen%2C%20a%20signed%20OAuth%20request%21
になると思いますが、
Creating a signature — Twitter Developers
ではHello%20Ladies%20%2b%20Gentlemen%2c%20a%20signed%20OAuth%20request%21
として扱われているように思えます。
+
が%2B
に変換されるはずですが、このページでは%2b
として扱われています。
また、,
も%2C
に変換されるはずですが、%2c
として扱われています。
しかし、Creating a signature — Twitter Developers で実際のシグネチャを求めていく解説の中では、正しい値である大文字のBと大文字のCの方として扱っているため、実行結果が異なるといったことにつながったのではないかと思います。
試しに、入力を修正して私のプログラムを実行してみました。
"status" to "Hello%20Ladies%20%2b%20Gentlemen%2c%20a%20signed%20OAuth%20request%21"
を
"status" to "Hello%20Ladies%20%2B%20Gentlemen%2C%20a%20signed%20OAuth%20request%21"
に直して実行すると、
Text
1key: kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw&LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE 2data: POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521 3signature: hCtSmYh+iHYCEqBWrE7C7hYmtUk= 4正しい結果が得られました。
というように正しく動作したように思えます。
この「解説ページが間違っている」という推測が正しいという裏付けが取れませんので、何か情報を持ってらっしゃる方は教えてください。
あなたの回答
tips
プレビュー