Hatena::ブログ(Diary)

知らないけどきっとそう。 RSSフィード

 

2017-11-06 ジェネリックトレネ このエントリーを含むブックマーク

振動を検知して荷物の盗難を防ぐデバイス「トレネ」のクラウドファンディングが、目標金額を達成しています。

要素技術が、振動センサーと Bluetooth ということで、Raspberry Pi Zero W と少しのパーツで似た機能の実現を目指しました。盗られたことがわかるので、「トラレ」と呼ぶことにします。



緑色のミンティアカテキンミント)の中に収まっているのが Raspberry Pi で、ケーブル接続されている黒い物体は一般的なモバイルバッテリーです。(ミンティアから飛び出している赤外線 LED は今回関係ありません)

まずは振動センサーですが、15 円/個くらいの 水銀スイッチ - WikipediaRaspberry Pi の GPIO18 ピンと GND にはんだ付けしました。水銀スイッチは取り扱いを誤ると、環境に悪影響を及ぼすため 金属球で感知するタイプ もあります。GPIO18 がオフからオン、または逆に変化したとき、傾きが発生したとみなすことができます。

「トラレ」と iOS 端末の距離は、Raspberry PiBluetooth の RSSI 値を定期的に取得することで監視します。今回は GitHub - ewenchou/bluetooth-proximity: Bluetooth Proximity Detection using Python を利用しました。

iOS 端末が一定以上離れていて、Raspberry Pi が傾きを検知したとき、iOS 端末に通知をおこないます。本家「トレネ」では、本体が警告音を発する仕様ですが、誤動作で注目を集めるのは望むところではないので、そのようにしました。Bluetooth による通知といえば、一時期ブームだった iBeacon - Wikipedia という仕組みがあるため、それを用います。

これらを組み合わせて「トラレ」として動作させるために Raspberry Pi 上で実行する TRARE ? GitHub を作成しました。さらに、iOS 端末で iBeacon を受信するためのアプリインストールが必要です。iBeacon advertisement を受信できるアプリは数多くありますが、任意の UUID を指定でき、バックグラウンドでも通知可能な Find My Stuff - 鍵やお財布から車まで、あなたの所持品を即座に位置確認!を App Store で を採用しました。


f:id:asannou:20171105212011p:image:w590


Bluetooth が届く距離にいないと通知が受け取れないという懸念はあります。試した限りでは、壁を隔てた隣の部屋くらいであれば問題ないようです。圏外にいたとしても iBeacon advertisement は発信され続けるため、圏内に入った時点で通知されます。

Raspberry Pi Zero W は $10 なので、「トレネ」の予定価格 6,800 円と比較すると、コストパフォーマンスの高さが際立ちます。

2017-09-26 Siri に本当の愛を問う このエントリーを含むブックマーク

f:id:asannou:20170925214058g:image

  • カメラがドッタンバッタンしてるのと iMovie 編集丸出しなのはご容赦ください

f:id:asannou:20170925152130j:image:w320

begin remote

  name            SIGMA
  bits            16
  flags           SPACE_ENC
  eps             30
  aeps            100
  header          9094  4425
  one             642   1608
  zero            642   481
  ptrail          646
  pre_data_bits   16
  pre_data        0xAA55
  gap             39822
  toggle_bit_mask 0x0

  begin codes
    0 0xc837
    1 0xf00f
    2 0xf20d
    3 0x728d
    4 0xe01f
    5 0xe21d
    6 0x629d
    7 0xc03f
    8 0xc23d
    9 0x42bd
    A 0xca35
    B 0x4ab5
    E 0x7c83
  end codes

end remote
    • 曲番号を 3825A4 とした場合、以下のようにパディングと終端をつけて送信する
$ irsend SEND_ONCE SIGMA 3 8 2 5 A 0 4 E
    • これを Homebridge のスイッチに登録
{
  "accessories": [
    {
      "accessory": "CMD",
      "name": "ようこそジャパリパークへ",
      "on_cmd": "/usr/bin/irsend SEND_ONCE SIGMA 3 8 2 5 A 0 4 E",
      "off_cmd": "true"
    }
  ]
}
  • iOS 端末の HomeKit を設定

f:id:asannou:20170926013901p:image:w320

f:id:asannou:20170925152800j:image:w320

  • 愛はどこにある…

引用元: LIVE DAM STADIUM / ようこそジャパリパークへ / どうぶつビスケッツ×PPP

2017-08-03 サーバレスに OpenID Connect で AWS にサインイン このエントリーを含むブックマーク

準備

$ git clone https://github.com/asannou/aws-signin-with-oidc
$ git diff
diff --git a/terraform.tfvars b/terraform.tfvars
index 2d3d635..ec5d990 100644
--- a/terraform.tfvars
+++ b/terraform.tfvars
@@ -1,17 +1,17 @@
 aws_region = "us-east-1"

-s3_bucket = "bucketname"
+s3_bucket = "aws-signin-with-oidc"

 client_id = {
-  google = "000000000000-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com"
+  google = "689439588880-vmco4me6g34au0h43ao9tbkb3slgffht.apps.googleusercontent.com"
 }

 email = {
   dev = [
-    "dev@example.com",
+    "asannou@gmail.com",
   ]
   admin = [
-    "admin@example.com",
+    "asannou@gmail.com",
   ]
 }
  • terraform を適用します
$ terraform apply
...
google = {
  admin = https://aws-signin-with-oidc.s3.amazonaws.com/google?role=arn:aws:iam::123456789012:role/Admin
  dev = https://aws-signin-with-oidc.s3.amazonaws.com/google?role=arn:aws:iam::123456789012:role/Dev
}
origin = https://aws-signin-with-oidc.s3.amazonaws.com

利用

f:id:asannou:20170803214548p:image:w656

    • できない場合は、反映が遅れている可能性があるため、少し待って再度試してください
  • AWS CLI を使用する際は、URL の末尾に &export=1 をつけて認証をおこなうと、ブラウザ環境変数が表示されます
    • 環境変数を設定すると、同等の権限で AWS CLI を実行することが可能です
$ export AWS_ACCESS_KEY_ID=ASIAIOSFODNN7EXAMPLE; export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY; export AWS_SESSION_TOKEN=AQoDYXdzEJr...
$ aws sts get-caller-identity
{
    "Account": "123456789012",
    "UserId": "AROAIOSFODNN7EXAMPLEU:asannou@gmail.com",
    "Arn": "arn:aws:sts::123456789012:assumed-role/Dev/asannou@gmail.com"
}

雑感

  • SAML であれば、ネイティブAWS コンソールへのサインインが提供されているが…
  • そういった事情により OpenID Connect 採用
    • Implicit Grant Flow なんて、バックエンドで検証しなきゃいけないし一生使わねえと思ってたけど、まるでサーバレスのために用意されていたかのよう
    • G Suite 環境では OAuth の濫用が懸念だったが、昨日解決した

2017-07-24 Vuls で EC2 をバルスされないために(実践編) このエントリーを含むブックマーク

これは Amazon EC2 Systems Manager Advent Calendar 2017 の -129 日目の記事です。

$ aws iam create-role --role-name EC2RoleSample --assume-role-policy-document '{"Version":"2012-10-17","Statement":{"Effect":"Allow","Principal":{"Service":"ec2.amazonaws.com"},"Action":"sts:AssumeRole"}}'
$ aws iam create-instance-profile --instance-profile-name EC2RoleSample
$ aws iam add-role-to-instance-profile --instance-profile-name EC2RoleSample --role-name EC2RoleSample
$ aws ec2 associate-iam-instance-profile --instance-id i-1234567890abcdef0 --iam-instance-profile Name=EC2RoleSample
    • EC2 インスタンスにアタッチされた IAM ロールに AmazonEC2RoleforSSM ポリシーをアタッチする
$ aws iam attach-role-policy --role-name EC2RoleSample --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM
[ec2-user ~]$ sudo yum install amazon-ssm-agent
[ec2-user ~]$ sudo start amazon-ssm-agent
  • 前提を満たしたら、やっていきます
$ terraform apply
var.vuls_roles
  Enter a value: []

var.vuls_users
  Enter a value: ["Bob"]

...
  • 晴れて IAM ユーザ "Bob" は EC2 インスタンス "i-1234567890abcdef0" に Vuls のためのユーザを作成できるようになりました
$ aws sts get-caller-identity --output text --query Arn
arn:aws:iam::123456789012:user/Bob
$ aws ssm send-command --document-name CreateVulsUser --instance-ids i-1234567890abcdef0 --parameters publickey="$(cat $HOME/.ssh/id_rsa.pub)" --output text --query Command.CommandId
854365f0-aefb-48eb-be4a-f2a0c6c2cf9e
  • 同時に実行結果として、インスタンスSSH ホスト公開鍵を取得できるので known_hosts に追加すれば安全に接続できます
$ aws ssm get-command-invocation --command-id 854365f0-aefb-48eb-be4a-f2a0c6c2cf9e --instance-id i-1234567890abcdef0 --output text --query StandardOutputContent | tee -a $HOME/.ssh/known_hosts
203.0.113.1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJKQ5+Aauvvb5iPFfYeQhcIegsich7SJ1Ji97mh3sGx3wvXAV53wuzn4ILSEn9ENtb6bXT/puLiCUi2bza2To24= root@ip-172-31-28-103
$ ssh -t vuls@203.0.113.1 'stty cols 1000; curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id'
i-1234567890abcdef0Connection to 203.0.113.1 closed.

2017-07-04 Vuls で EC2 をバルスされないために(簡易版) このエントリーを含むブックマーク

$ sudo iptables -I OUTPUT -d 169.254.169.254 -j REJECT --reject-with icmp-admin-prohibited
  • 禁止されていることを確認
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
curl: (7) Failed to connect to 169.254.169.254 port 80: ホストへの経路がありません
  • root ユーザ (UID 0) による通信を許可するルールを追加
$ sudo iptables -I OUTPUT -d 169.254.169.254 -m owner --uid-owner 0 -j ACCEPT
$ sudo curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
{
  ...
}
  • ec2-user ユーザ (UID 500) も許可します
$ sudo iptables -I OUTPUT -d 169.254.169.254 -m owner --uid-owner 500 -j ACCEPT
  • sudo なしで取れました
$ id
uid=500(ec2-user) gid=500(ec2-user) groups=500(ec2-user),10(wheel)
$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
{
  ...
}
  • 最終的なルールです
$ sudo iptables -L OUTPUT
Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination
ACCEPT     all  --  anywhere             instance-data.ap-northeast-1.compute.internal  owner UID match ec2-user
ACCEPT     all  --  anywhere             instance-data.ap-northeast-1.compute.internal  owner UID match root
REJECT     all  --  anywhere             instance-data.ap-northeast-1.compute.internal  reject-with icmp-admin-prohibited
[Jul  3 16:22:49] DEBUG [amazon] execResult: servername: amazon
  cmd: /usr/bin/ssh -tt -o StrictHostKeyChecking=yes -o LogLevel=quiet -o ConnectionAttempts=3 -o ConnectTimeout=10 -o ControlMaster=no -o ControlPath=none vuls-user@52.199.26.3 -p 22 -i /root/.ssh/id_rsa -o PasswordAuthentication=no stty cols 1000; curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id
  exitstatus: 0
  stdout: i-0bf0d78a268f80335
  stderr:
  err: %!s(<nil>)
[Jul  3 16:22:49]  INFO [localhost] (1/1) amazon is running on aws
[Jul  3 16:14:56] DEBUG [amazon] execResult: servername: amazon
  cmd: /usr/bin/ssh -tt -o StrictHostKeyChecking=yes -o LogLevel=quiet -o ConnectionAttempts=3 -o ConnectTimeout=10 -o ControlMaster=no -o ControlPath=none vuls-user@52.199.26.3 -p 22 -i /root/.ssh/id_rsa -o PasswordAuthentication=no stty cols 1000; curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id
  exitstatus: 7
  stdout: Warning: Transient problem: timeout Will retry in 1 seconds. 3 retries left.
curl: (7) Failed to connect to 169.254.169.254 port 80: No route to host

  stderr:
  err: %!s(<nil>)
[Jul  3 16:14:56]  INFO [localhost] (1/1) amazon is running on other

2017-06-28 Vuls で EC2 をバルスされないために このエントリーを含むブックマーク

  • 以下に Vuls の単純化した使用方法を示します
$ ssh-keygen -N '' -f $HOME/.ssh/id_rsa_vuls
$ cat $HOME/.ssh/id_rsa_vuls.pub
ssh-rsa ...
ec2-user$ sudo adduser -m vuls
ec2-user$ sudo su vuls
vuls$ mkdir -m 700 /home/vuls/.ssh
vuls$ echo 'ssh-rsa ...' > /home/vuls/.ssh/authorized_keys
  • Vuls の設定ファイルを作成
$ mkdir vuls
$ cat <<EOD > vuls/config.toml
[servers]

[servers.amazon]
host        = "203.0.113.1"
port        = "22"
user        = "vuls"
keyPath     = "/root/.ssh/id_rsa_vuls"
EOD
  • Vuls のスキャンを Docker で実行
$ docker run --rm -it -v $HOME/.ssh:/root/.ssh:ro -v $PWD:/vuls -v $PWD/vuls-log:/var/log/vuls vuls/vuls scan
$ for i in `seq 2002 $(date +"%Y")`; do docker run --rm -it -v $PWD:/vuls -v $PWD/go-cve-dictionary-log:/var/log/vuls vuls/go-cve-dictionary fetchnvd -years $i; done
  • Vuls のレポートを表示
$ docker run --rm -it -v $HOME/.ssh:/root/.ssh:ro -v $PWD:/vuls -v $PWD/vuls-log:/var/log/vuls vuls/vuls report
  • これによって、ユーザが認証情報を管理する必要がなくなるため、アプリケーションサーバとしての EC2 に、様々な権限を持つロールをアタッチするケースが考えられます
ec2-user$ curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name
$ ssh -t -i $HOME/.ssh/id_rsa_vuls vuls@203.0.113.1 'curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name'
{
  "Code" : "Success",
  "LastUpdated" : "2017-06-27T09:17:33Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASIAJHNLJH7SISLPSOMA",
  "SecretAccessKey" : "...",
  "Token" : "...",
  "Expiration" : "2017-06-27T15:39:38Z"
}Connection to 203.0.113.1 closed.
    • 取得できました
  • したがって、SSH 秘密鍵 "id_rsa_vuls" は、EC2 にアタッチされているロールによっては、高いレベルで保護する必要があります
  • しかし、その認識にギャップがあると思われ、次のように扱われる懸念があります
    • 脆弱性スキャンを自動実行させるため、秘密鍵パスフレーズを設定しない
    • 秘密鍵が置かれる、スキャン実行ホストのセキュリティレベルが低い、アクセス制御が徹底されていない
    • ロールによる権限を得られるべきではない外部のメンバー等が、スキャンを実行するために、秘密鍵へのアクセス権を持っている
  • vuls アカウントに設定することで、Vuls の実行のみが許可され、その他のコマンドは無視されます
vuls$ curl https://raw.githubusercontent.com/asannou/vuls-ssh-command/master/amazon-linux.sh > /home/vuls/.ssh/vuls-ssh-command.sh
vuls$ chmod +x /home/vuls/.ssh/vuls-ssh-command.sh
vuls$ echo 'command="/home/vuls/.ssh/vuls-ssh-command.sh" ssh-rsa ...' > /home/vuls/.ssh/authorized_keys
$ ssh -t -i $HOME/.ssh/id_rsa_vuls vuls@203.0.113.1 'curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role_name'
Connection to 203.0.113.1 closed.
  • たぶんバグあり、Amazon LinuxUbuntu しか用意していませんが、よろしければお使いください

2017-06-18 Slack ストーキング このエントリーを含むブックマーク

  • message event | Slack は、いくつかのサブタイプがあり、メッセージの変化を表現しています
$ node index.js | jq --unbuffered 'select(.subtype=="message_deleted")'
  • ユーザ @bsannou がチャンネル #twitter で "Despite the constant negative press covfefe" というメッセージを削除した様子です
{
  "type": "message",
  "deleted_ts": "1497783180.894743",
  "subtype": "message_deleted",
  "hidden": true,
  "channel": {
    "id": "C5VCH7HFV",
    "name": "#twitter"
  },
  "previous_message": {
    "type": "message",
    "user": {
      "id": "U5JPRSD6U",
      "name": "@bsannou"
    },
    "text": "Despite the constant negative press covfefe",
    "ts": "1497783180.894743"
  },
  "event_ts": "1497783204.895728",
  "ts": "1497783204.895728"
}
$ node index.js | jq --unbuffered 'select(.subtype=="message_changed")'
  • 様子です
{
  "type": "message",
  "message": {
    "type": "message",
    "user": {
      "id": "U5JPRSD6U",
      "name": "@bsannou"
    },
    "text": "あなたはとてもクサレ脳ミソですね",
    "edited": {
      "user": {
        "id": "U5JPRSD6U",
        "name": "@bsannou"
      },
      "ts": "1497188145.000000"
    },
    "ts": "1497188126.496907"
  },
  "subtype": "message_changed",
  "hidden": true,
  "channel": {
    "id": "C5JT9C849",
    "name": "#vip"
  },
  "previous_message": {
    "type": "message",
    "user": {
      "id": "U5JPRSD6U",
      "name": "@bsannou"
    },
    "text": "あなたはとてもド低能ですね",
    "ts": "1497188126.496907"
  },
  "event_ts": "1497188145.497849",
  "ts": "1497188145.497849"
}
  • 公式クライアントでは、削除や修正が常に適用された状態で表示されますが、一旦送信した情報は取り消せないので、留意していきましょう
$ node index.js | jq --unbuffered 'select(.presence=="active") | .now=now'
  • 様子
{
  "type": "presence_change",
  "presence": "active",
  "user": {
    "id": "U5JPRSD6U",
    "name": "@bsannou"
  },
  "now": 1497786620.436581
}
  • 深夜に active がいるとオッとなる
$ mkfifo fifo
$ node index.js < fifo | jq --unbuffered -c 'select(.presence=="active") | {"type":"message","text":"進捗どうですか?activeだから居ますよね??","channel":{"name":.user.name}}' > fifo
$ node index.js | jq --unbuffered 'select(.type=="user_typing")'
{
  "type": "user_typing",
  "channel": {
    "id": "C5JT9C849",
    "name": "#vip"
  },
  "user": {
    "id": "U5JPRSD6U",
    "name": "@bsannou"
  }
}
  • うざみ
$ mkfifo fifo
$ node index.js < fifo | jq --unbuffered -c 'select(.type=="user_typing") | {"type":"message","text":("ちょっと待って!今 "+.user.name+" が何か言おうとしてる!"),"channel":{"name":.channel.name}}' > fifo
  • 今日書きたいことはこれくらいです

2017-05-28 Node.js Stream からいつの間にか Slack クライアントができた このエントリーを含むブックマーク

  • Mastodon は観察専門なので、使用頻度の高い Slack を題材とします
let ws;
const url = "https://slack.com/api/rtm.connect?token=xoxp-0000000000-0000000000-000000000000-0123456789abcdef0123456789abcdef";
const stream = new require("stream").PassThrough();
require("https").get(url, res => {
  res.on("data", chunk => {
    const obj = JSON.parse(chunk);
    ws = new (require("ws"))(obj.url);
    ws.on("message", data => stream.write(data + "\n"));
  });
});
stream.pipe(process.stdout);
$ node index.js
{"type":"hello"}
{"type":"reconnect_url","url":"wss://mpmulti-0a1g.slack-msgs.com/websocket/ZvwORwLc1uFhgMHJAZva-04tZg2Lp5iJEclZ8vnOCctMTnJC64LQvb7MZvgJNifnscAGXvki2UAEcJpnlwFwCY031HGGe-NizIM5cZZKfdMQcO9kbXBhb8LkUKQnhR3bx-Z5hxefvQgsVQVgZwSFO9huhgXo3cBvjCc44YTZklY="}
{"type":"presence_change","presence":"active","user":"U5HCFCV5W"}
...
  • ここに、書き込むと WebSocket にデータを送る Writable Stream のコードを追加して stdin と繋ぎます
const writable = new require("stream").Writable({
  write: (chunk, encoding, callback) => {
    ws.send(chunk.toString());
  }
});
process.stdin.pipe(writable);
  • stdin へチャンネルにメッセージを投稿するための JSON を入力します
$ node index.js
...
{"type":"message","channel":"C5JT9C849","text":"test"}
  • 直後に成功を表す JSON が送られてきたら、ブラウザ等から確認すると反映されていることがわかります
{"ok":true,"reply_to":0,"ts":"1495991511.232093","text":"test"}
  • 送られてくる JSON は channel や user の情報が "C5JT9C849" のような id となっていて扱いづらく、よって name に変換する Transform Stream を作って間にはさみます
const url = "https://slack.com/api/rtm.start?token=xoxp-0000000000-0000000000-000000000000-0123456789abcdef0123456789abcdef";
const stream = new require("stream").PassThrough();
require("https").get(url, res => { ... });
const transform = new require("stream").Transform({
  transform: function(chunk, encoding, callback) { ... }
});
stream.pipe(transform).pipe(process.stdout);
{"type":"message","user":{"id":"U5HCFCV5W","name":"@asannou"},"text":"test","ts":"1496083043.865560","channel":{"id":"C5JT9C849","name":"#vip"}}
{"type":"message","user":{"id":"USLACKBOT","name":"@slackbot"},"text":"Hello, I’m Slackbot. I try to be helpful. (But I’m still just a bot. Sorry!) *Type something* and hit _enter_ to send your message.","ts":"1495732144.332015","channel":{"id":"D5J44CUDS","name":"@slackbot"}}
{"type":"message","user":{"id":"USLACKBOT","name":"@slackbot"},"text":"Pleasure to meet you. Let me show you a couple things about Slack.","ts":"1495732157.337778","channel":{"id":"D5J44CUDS","name":"@slackbot"}}
170525170904.332015 [@slackbot] (@slackbot) Hello, I’m Slackbot. I try to be helpful. (But I’m still just a bot. Sorry!) *Type something* and hit _enter_ to send your message.
170525170917.337778 [@slackbot] (@slackbot) Pleasure to meet you. Let me show you a couple things about Slack.
  • タイムスタンプ部分 "%y%m%d%H%M%S.%6N" が苦しいですが、生 JSON よりはマシでしょう
    • 出力のすべては JSON であるがゆえに、特定のチャンネルをフィルタリングするなど、jq の能力が発揮されます
$ node index.js | jq -c --unbuffered 'select(.channel.name != "#general")'
  • 入力についても、手打ち JSON は死ぬので jq に頼ります
$ echo "test" | jq -R -c --unbuffered '{type:"message",channel:{name:"@slackbot"},text:.}'
{"type":"message","channel":{"name":"@slackbot"},"text":"test"}
  • 入力と出力を jq で挟むことにより、ちょっとだけまともな Slack クライアントになりました
$ jq -R -c --unbuffered '{type:"message",channel:{name:"@slackbot"},text:.}' | node index.js | jq --unbuffered -r -f format.jq
help!
170528175842.970929 [@slackbot] (@asannou) help!
170528175842.970933 [@slackbot] (@slackbot) I can help by answering simple questions about how Slack works. I'm just a bot, though! If you need more help, try our <https://get.slack.help/hc/en-us/|Help Center> for loads of useful information about Slack ― it's easy to search! Or simply type */feedback* followed by your question or comment, and a human person will get back to you. :smile:
$ ./rwlap.js "jq -R -c --unbuffered '{type:\"message\",channel:{name:\"@slackbot\"},text:.}' | node index.js | jq --unbuffered -r -f format.jq"
  • Readline は、他にもタブキーによる入力補完もサポートしているため、公式クライアントのような、ユーザ名の補完なども実現可能です
$ ./rwlap.js "jq -R -c --unbuffered '{type:\"message\",channel:{name:\"@slackbot\"},text:.}' | node index.js | jq --unbuffered -r -f format.jq | ./highlight.js asannou"

170528175842.970929 [@slackbot] (@asannou) help!

  • もちろん、同様のことができるコマンドで、代替してもかまいません

f:id:asannou:20170530115133g:image

  • 個人的に Slack の機能はこれで足りているので、しばらく使ってみようと思います

2017-04-25 Mastodon のタイムラインをターミナルに流す このエントリーを含むブックマーク

  • Node.js の Stream の学習を兼ねて、これを様々なターミナルに出力できるようにする
    • 「Stream を制するものは、 Node.js を制す」らしい
$ node -e 'new (require("ws"))("ws://friends.nico/api/v1/streaming/?access_token=...&stream=public").on("message", (data, flags) => console.log(data))'
{"event":"update","payload":"{\"id\":1920985,\"created_at\":\"2017-04-24T16:10:03.248Z\",\"in_reply_to_id\":null,\"in_reply_to_account_id\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"application\":{\"name\":\"Web\",\"website\":null},\"account\":{...
  • これを parse() して Stream を作成し transform して出力
const stream = new require("stream").Transform({
    objectMode: true,
    transform: function(chunk, encoding, callback) { ... }
});

new (require("ws"))("ws://friends.nico/api/v1/streaming/?access_token=...&stream=public")
    .on("message", (data, flags) => stream.write(JSON.parse(data)));

stream.pipe(process.stdout);

f:id:asannou:20170425023534g:image:w576

  • Stream は TCP Socket に pipe() でつなぐことができる
    • 標準入力の Stream を pipe() して、ポート 12345 に接続すると ping の結果が流れてくるサンプル
$ ping localhost | node -e 'process.stdin.resume();require("net").createServer(socket => process.stdin.pipe(socket)).listen(12345);'
$ telnet localhost 12345
Trying ::1...
Connected to localhost.
Escape character is '^]'.
64 bytes from 127.0.0.1: icmp_seq=16 ttl=64 time=0.065 ms
64 bytes from 127.0.0.1: icmp_seq=17 ttl=64 time=0.056 ms
64 bytes from 127.0.0.1: icmp_seq=18 ttl=64 time=0.095 ms
64 bytes from 127.0.0.1: icmp_seq=19 ttl=64 time=0.049 ms
64 bytes from 127.0.0.1: icmp_seq=20 ttl=64 time=0.054 ms
^]
telnet> Connection closed.
  • pipe() によって、何回でも Stream の分岐や合流が可能
    • クライアントが複数接続しても、それぞれに同じデータを送ることができる
    • resume() しておくことで pipe() されているものが何もない時のデータは捨てる
$ telnet tootcat.0j0.jp
$ nc tootcat.0j0.jp 7007
$ telnet tootcat.0j0.jp 7008
$ telnet tootcat.0j0.jp 7009
$ telnet tootcat.0j0.jp 7010

  • 未加工の JSON を受信できるようにした
$ nc tootcat.0j0.jp 7006 | head -c 1000
{"id":6021698,"created_at":"2017-05-06T07:18:52.994Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"Web","website":null},"account":{"id":0,"username":"username","acct":"acct","display_name":"display_name","locked":false,"created_at":"2017-04-21T12:07:59.324Z","followers_count":0,"following_count":0,"statuses_count":1,"note":"note","url":"https://friends.nico/url","avatar":"avatar","avatar_static":"avatar_static","header":"header","header_static":"header_static","nico_url":"nico_url"},"media_attachments":[],"mentions":[],"tags":[],"uri":"tag:friends.nico,2017-05-06:objectId=6021698:objectType=Status","content":"content1\r\ncontent2","url":"url","reblogs_count":0,"favourites_count":0,"reblog":null}{...
$ nc tootcat.0j0.jp 7006 | docker run -i --rm -e "TZ=Asia/Tokyo" asannou/jq --unbuffered -r '.prefix = (.created_at | sub(".[0-9]*Z";"Z") | fromdate | strflocaltime("%H:%M")) | .prefix += " (" + .account.username + ") " | .prefix as $prefix | select(.uri | test("^tag:friends.nico,")) | .content | split("\r\n") | join("\r\n" + $prefix) | $prefix + .'
16:18 (username) content1
16:18 (username) content2
    • IRC
    • strflocaltime がまだ development version のみの機能だったので asannou/jq を作った
    • --unbuffered で滞りなく出力される

2017-03-06 5 月末に公開される SHAttered のコードを予想して新しい PDF を作る このエントリーを含むブックマーク

前回に引き続き https://shattered.it/ の件です。

Following Google’s vulnerability disclosure policy, we will wait 90 days before releasing code that allows anyone to create a pair of PDFs that hash to the same SHA-1 sum given two distinct images with some pre-conditions.

https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html

Google 曰く「誰にでも、いくつかの前提条件を満たすふたつの異なる画像から、同じ SHA-1 ハッシュPDF のペアが作成できるコードをリリースする」とのことです。ただし、脆弱性公開ポリシーに従い、リリースは発表から 90 日後です。

現在公開されている情報から、このコードがどのようなものか予想し、実証しようと思います。

縦に長くなったので、最初に成果物へリンクしておきます。

さて、SHA-1 ハッシュが同じで違う画像が表示される GooglePDF 1, PDF 2 にはどのような技巧が用いられているでしょうか。

%PDF-1.3
%....


1 0 obj
<</Width 2 0 R/Height 3 0 R/Type 4 0 R/Subtype 5 0 R/Filter 6 0 R/ColorSpace 7 0 R/Length 8 0 R/BitsPerComponent 8>>
stream
[...]
endstream
endobj

2 0 obj
1024
endobj

3 0 obj
740
endobj

4 0 obj
/XObject
endobj

5 0 obj
/Image
endobj

6 0 obj
/DCTDecode
endobj

7 0 obj
/DeviceRGB
endobj

8 0 obj
421385
endobj

[...]
%%EOF

PDF ファイルなので、一般的な PDF の構造があります。ここに特殊な箇所は見つかりません。この構造部分は PDF 1PDF 2 で完全に一致しています。

"1 0 obj" における stream [...] endstream 間のデータが、幅 1024 高さ 740 の Image として DCTDecode されて(つまり JPEG 画像)表示されます。強いて不自然な点をあげると、"/Width 1024/Height 740" と書かず "/Width 2 0 R/Height 3 0 R" のように "2 0 obj" と "3 0 obj" への参照となっていることです。これは後述する理由により、endstream より後方にあるデータは任意に書き換えが可能なため、画像のサイズを変更できるようにする工夫と窺えます。

stream 内の JPEG データを詳しく見る前に、通常の JPEG の構造を簡単に理解しておきましょう。

JPEG は、複数のセグメントで構成されています。セグメントは、マーカーとペイロードからできていて、マーカーがセグメントがどのような種類の情報かを表します。重要なマーカーは以下のものくらいです。

  • SOI(イメージ開始)
  • SOF(フレーム開始)
  • DHT(ハフマンテーブル定義)
  • DQT(量子化テーブル定義)
  • SOS(画像データ開始)
  • EOI(イメージ終了)

例えば https://upload.wikimedia.org/wikipedia/commons/thumb/e/e8/Lichtenstein.jpg/256px-Lichtenstein.jpg の構造は下記のとおりです。

SOI  0000
APP0 0010
DQT  0043
DQT  0043
SOF0 0011
DHT  001c
DHT  0040
DHT  001b
DHT  0030
SOS  000c
SCAN 4983
EOI  0000

マーカーの後ろにある数字は、ペイロードのバイト長です。SCAN はマーカーではなく画像データです。詳細は JPEG - Wikipedia を参照してください。

それでは、件の PDF に含まれる JPEG の構造を分析していきます。

PDF 1        PDF 2

SOI  0000    SOI  0000
COM  0024    COM  0024
    V            V
COM  0173    COM  017f    <--- (A)
[   |   ]    [   |   ]    <--- (B) collision blocks
    V            |
COM  00fc        |        <--- (C)
    |            V
    |        APP0 0010    <--- (D)
    |        DQT  0043
    |        DQT  0043
    |        SOF2 0011
    |        DHT  001e
    |        DHT  001d
    V        COM  0006
COM  27f4        V
    |        SOS  000c
    |        SCAN 27a6
    |        DHT  0038
    V        COM  0006
COM  218d        V
    |        SOS  0008
    |        SCAN 210b
    |        DHT  0070
    V        COM  0006
COM  9f9a        V
    |        SOS  0008
    |        SCAN 9f1e
    |        DHT  006a
    V        COM  0006
COM  659a        V
    |        SOS  0008
    |        SCAN 6519
    |        DHT  006f
    V        COM  0006
COM  ae2e        V
    |        SOS  0008
    |        SCAN adee
    |        DHT  002e
    V        COM  0006
COM  36d2        V
    |        SOS  0008
    |        SCAN 36c2
    V        COM  0006
COM  11b5        V
    |        SOS  000c
    |        SCAN 1172
    |        DHT  002d
    V        COM  0006
COM  2c5b        V
    |        SOS  0008
    |        SCAN 2c1b
    |        DHT  002e
    V        COM  0006
COM  2dce        V
    |        SOS  0008
    |        SCAN 2d8e
    |        DHT  002e
    V        COM  0006
COM  37d2        V
    |        SOS  0008
    |        SCAN 37c0
    |        EOI  0000
    V            X
APP0 0010        X
APP1 0040        X
APP13 0038       X
SOF0 0011        X
DHT  001f        X
DHT  00b5        X
DHT  001f        X
DHT  00b5        X
DQT  0043        X
DQT  0043        X
DRI  0004        X
SOS  000c        X
SCAN 39104       X
EOI  0000        X

COM というマーカーが頻出しますが、これはコメントを表します。コメントマーカーが現れると、そのペイロードの長さ分だけ、データが無視されるため、矢印でその範囲がスキップされることを表現しています。また、EOI の後ろにあるデータも、同様に無視されるので X とします。見ての通り、PDF 1 の COM に PDF 2 のデータが、PDF 2 の COM に PDF 1 のデータが入っているという、カドゥケウスの杖のような形をしていることが判明しました。

https://commons.wikimedia.org/wiki/File:Caduceus.svg

PDF 1PDF 2 で異なる部分は (A) と (B) のみで、残りは完全に一致しています。(A) において、コメントとみなす長さを 0x0173 か 0x017f かに変えることで、続きが (C) か (D) かに分かれ、異なる画像が表示されるという原理です。

(A) でコメント長を示す数値が異なるので、当然 SHA-1 ハッシュも違う値が計算されます。ですが、その直後の SHAttered による (B) collision blocks でそれが解消され、再びハッシュが等しくなります。そして (B) 以降は、どのようなデータを追加しても、ふたつのファイルの SHA-1 ハッシュは同一のままです。"1 0 obj" の stream が、後方の obj を参照していたのは、こういった理由からでした。

結論として、本稿の目的であるコードとは、(C) 以降にあるようなデータと同様の構造を持つ JPEG を生成するコードと言えるでしょう。

ということで、そのようなコードを(Perl で)実装し、それを使って作成した PDF のペアが https://asannou.github.io/shatterized-1.pdfhttps://asannou.github.io/shatterized-2.pdf です。

無論、ハッシュが一致しています。

$ shasum shatterized-1.pdf shatterized-2.pdf
5135c8373be5ceb5763406b307cd17d179fafbe2  shatterized-1.pdf
5135c8373be5ceb5763406b307cd17d179fafbe2  shatterized-2.pdf

また、画像のみの PDF だけでなく https://asannou.github.io/d.hatena.ne.jp-asannou-20170226-1.pdfhttps://asannou.github.io/d.hatena.ne.jp-asannou-20170226-2.pdf のように、一部の画像(ブックマーク数)が改ざんされているものも作ることができます。

$ shasum d.hatena.ne.jp-asannou-20170226-1.pdf d.hatena.ne.jp-asannou-20170226-2.pdf
6a2cd0570bc6d05cd4777a17925a2095e928582d  d.hatena.ne.jp-asannou-20170226-1.pdf
6a2cd0570bc6d05cd4777a17925a2095e928582d  d.hatena.ne.jp-asannou-20170226-2.pdf

残念ながら、この手法には欠点があります。それは COM のペイロードの長さの最大が 0xffff であるため、それより長いデータが存在すると、収まりきらないことです。例として https://upload.wikimedia.org/wikipedia/commons/e/e8/Lichtenstein.jpg の構造を示します。

SOI  0000
APP0 0010
DQT  0043
DQT  0043
SOF0 0011
DHT  001c
DHT  004a
DHT  001a
DHT  0032
SOS  000c
SCAN 79009
EOI  0000

SOS マーカーの後の画像データの長さが 0x79009 もあって、コメントに収まりません。おそらく、冒頭の「いくつかの前提条件を満たすふたつの異なる画像」というのは、このことを示唆しているのではないかと推測します。

しかしまだ、画像データを分割して、段階的に表示するプログレッシブ JPEG に変換する方法が残っています。やってみましょう。

$ jpegtran -progressive Lichtenstein.jpg > Lichtenstein_p.jpg
SOI  0000
APP0 0010
DQT  0043
DQT  0043
SOF2 0011
DHT  001b
DHT  0019
SOS  000c
SCAN c486
DHT  0034
SOS  0008
SCAN f1f2
DHT  002c
SOS  0008
SCAN 1203
DHT  002d
SOS  0008
SCAN 2110
DHT  0037
SOS  0008
SCAN 7195
DHT  002c
SOS  0008
SCAN 1729d
SOS  000c
SCAN 3718
DHT  0027
SOS  0008
SCAN 358d
DHT  0029
SOS  0008
SCAN 4499
DHT  002a
SOS  0008
SCAN 2c3fb
EOI  0000

確かに 10 分割されましたが、まだ 0xffff 以上の画像データが残っています。スキャンの方法をテキストファイルで詳細に指定し、分割数を増やすことができるので、それを試します。テキストファイルの仕様は https://github.com/mozilla/mozjpeg/blob/master/wizard.txt を参照してください。

$ cat <<EOD > scans.txt
0: 0-0,   0, 0;
1: 0-0,   0, 0;
2: 0-0,   0, 0;
0: 1-1,   0, 0;
0: 2-2,   0, 0;
0: 3-3,   0, 0;
0: 4-5,   0, 0;
1: 1-63,  0, 0;
2: 1-63,  0, 0;
0: 6-7,   0, 0;
0: 8-9,   0, 0;
0: 10-12, 0, 0;
0: 13-17, 0, 0;
0: 18-63, 0, 0;
EOD
$ jpegtran -scans scans.txt Lichtenstein.jpg > Lichtenstein_pp.jpg
SOI  0000
APP0 0010
DQT  0043
DQT  0043
SOF2 0011
DHT  001c
SOS  0008
SCAN b243
DHT  001a
SOS  0008
SCAN 1ffa
DHT  0019
SOS  0008
SCAN 1b35
DHT  0021
SOS  0008
SCAN 87b7
DHT  0020
SOS  0008
SCAN 8b6f
DHT  0021
SOS  0008
SCAN 5ac4
DHT  0029
SOS  0008
SCAN cf74
DHT  0038
SOS  0008
SCAN 6474
DHT  0032
SOS  0008
SCAN 4647
DHT  0027
SOS  0008
SCAN 9426
DHT  0026
SOS  0008
SCAN 9464
DHT  002a
SOS  0008
SCAN a9a7
DHT  002e
SOS  0008
SCAN bf73
DHT  003b
SOS  0008
SCAN dcd2
EOI  0000

無事成功しました。それでも画像データが巨大な場合は、最大個数に分割しても収まらないことがあると思います。その時は、画像の品質を下げる等を検討する必要があります。

In order to prevent this attack from active use, we’ve added protections for Gmail and GSuite users that detects our PDF collision technique.

https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html

最後に Gmailhttps://asannou.github.io/shatterized-1.pdfhttps://asannou.github.io/shatterized-2.pdf が検出されるかを確認しました。

f:id:asannou:20170306002735p:image:w535

手法で作られたファイルも、Gmail に添付して送ろうとするとエラーになりました。

Google がコードを公開したら、答え合わせをします。