`otpauth-migration://offline?data=~`からTOTP生成までをWebAPIで実装する

# TOTP
Time based One Time Password
二段階認証でよくあるコロコロ変わる6桁のやつ
SHA-1をハッシュ関数に使い、30秒毎に更新、6桁が一般的

セットアップの際に表示されるQRコードのURLは
`otpauth://`から始まり、6桁生成に必要な鍵と幾つかのオプションが含まれている

https://github.com/google/google-authenticator/wiki/Key-Uri-Format

## 実装

実装において、以下の記事が非常に参考になった
解説はこちらの記事を読んでほしい

https://qiita.com/waaaaRapy/items/8549890bda4f8ad33963

まずTOTP生成のコード全文がこちらである

```js
const
totp=async({
	_:{
		be4=x=>[x>>>24&255,x>>>16&255,x>>>8&255,x>>>0&255],
		b32de=w=>((
			d=[...'abcdefghijklmnopqrstuvwxyz234567'].reduce((a,x,i)=>(a[x]=i.toString(2).padStart(5,0),a),{}),
			b=w.toLowerCase().replace(/./g,x=>d[x]||'')
		)=>new Uint8Array([...Array(Math.ceil(b.length/8))].map((_,i)=>+`0b${b.slice(8*i++,8*i).padEnd(8,0)}`)))()
	}={},
	date:t=new Date,algorithm:h='SHA-1',digits:l=6,period:d=30,secret:{base32:_k='',raw:k=b32de(_k)}
}={})=>(w=>((w.slice(w=w.pop()&0xf,w+4).reduce((a,x)=>a<<8|x)&0x7fffffff)+'').slice(-l))(
	[...new Uint8Array(await crypto.subtle.sign('HMAC',
		await crypto.subtle.importKey('raw',k,{name:'HMAC',hash:h},0,['sign']),
		new Uint8Array([...be4((t=t.getTime()/1000/d)/2**32),...be4(t)])
	))]
);

```

オブジェクト1つを引数に取り、secret.base32またはsecret rawにStringもしくはArrayBuffer,TypedArray,DataViewを渡す
前述の`otpauth://`に含まれる鍵はbase32でエンコードされている

主題のHMAC-SHA-1はSubtleCryptoのsignを用いて生成できる

https://developer.mozilla.org/ja/docs/Web/API/SubtleCrypto/sign

digestではないので注意

signに使う鍵はgenerateKeyで生成するかimportKeyを用いてインポートする
importKeyは幾つかの鍵形式をサポートしているが今回はbase32にエンコードされたバイト列のためこれをデコードしてrawとして渡す
また、ハッシュ関数SHA-1の指定もここで行う

# `otpauth-migration://`
先述の`otpauth://`は1つのurlに1つの鍵しか含むことができない
GoogleAuthenticatorは複数の鍵を1つ、多くても数個のQRコードでエクスポート可能である
このQRコードに含まれる文字列が`otpauth-migration://offline?data=...`形式のURLである

`?data=`以降はbase64でエンコードされたProtocolBuffer Wire formatである

## ProtocolBuffer
googleが定めた同社サービスでよく用いられている効率的なデータ送受信のためのエコシステムである
エンコード及びデコードに用いられる`~.proto`定義ファイルとそのパーサや処理系、符号化された状態のWire formatの総称である

Wire formatの解説は以下の記事が非常に参考になった
解説はこちらを読んでほしい

https://qiita.com/nozmiz/items/5f56b5e8e280c22121de

### 実装

```js
const
depb=w=>[...{[Symbol.iterator]:(
	p=0,
	v=_=>[...{[Symbol.iterator]:d=>({next:_=>({done:d,value:d||(d=!(w[p]&0x80),w[p++]&0x7f)})})}].reduce((a,x,i)=>x<<(7*i)|a)
)=>({next:_=>w.length<=p?{done:1}:{value:((x=>({i:x>>>3,type:x=x&7,value:[
	(x=v())=>Object.assign(x,{s:(x+1)/(x&1?-2:2)|0}),_=>w.slice(p,p+=8),(l=v())=>w.slice(p,p+=l),,,_=>w.slice(p,p+=4)
][x]()}))(v()))}})}];
```

Uint8Arrayを受け取り、番号と種別とデータを含むオブジェクトの配列を返す
内包するデータがWire formatである場合はもう一度この関数を呼び出す前提の設計である

ループはイテレータを用いて実装した

https://qiita.com/McbeEringi/items/6105839835a8274d303a

## 実装

以下、urlをデコードするコードである

```js
const
migurl=w=>((
	td=new TextDecoder(),
	b32en=w=>(x=>[...Array(Math.ceil(x.length/5))].map(
		(_,i)=>'abcdefghijklmnopqrstuvwxyz234567'[+`0b${x.slice(5*i++,5*i).padEnd(5,0)}`]
	).join('').padEnd(Math.ceil(x.length/40)*8,'='))(w.reduce((a,x)=>a+x.toString(2).padStart(8,0),''))
)=>(
	w=new URL(w),w.href.match(/^[^?]+/)=='otpauth-migration://offline'&&(w=w.searchParams.get('data'))&&
	depb(new Uint8Array([...atob(decodeURIComponent(w))].map(x=>x.charCodeAt()))).reduce((a,x)=>(
		x.i==1?a.params.push(
			depb(x.value).reduce((a,x)=>(([,
				x=>a.secret={raw:x,base32:b32en(x)},
				x=>a.name=td.decode(x),
				x=>a.issuer=td.decode(x),
				x=>a.algorithm=[,'SHA-1','SHA-256','SHA-512','MD5'][x],
				x=>a.digits=[,6,8][x],
				x=>a.type=[,'HOTP','TOTP'][x],
				x=>a.conter=x
			][x.i]||(y=>a[x.i]=y))(x.value),a),{})
		):a[[,,'version','batch_size','batch_index','batch_id'][x.i]||x.i]=x.value,
		a
	),{params:[]})
))();
```

実装にあたって以下の`.proto`定義を参考にした

https://github.com/qistoph/otp_export/blob/master/OtpMigration.proto

`otpauth-migration://`urlを渡すとurlに含まれる情報を含むオブジェクトの配列を返す

# 動作検証

googleAuthenticatorで`helloworld234567`を鍵として入力して、これを`otpauth-migration://`形式のqrコードで出力した
得られたurlを用いてbunjs上で動作検証を行った

```js
console.log(await migurl(
	'otpauth-migration://offline?data=CjkKCjkWt1nRWPW%2Bd98SEGhlbGxvd29ybGQyMzQ1NjcgASgBMAJCEzlkZDNjMzE3MzI3MjAwMzcyMTIQAhgBIAA%3D'
).params.reduce(async(a,x)=>(x.key=await totp({...x,date:new Date('2024-11-30 23:29')}),[...await a,x]),[]));
```

```sh
bun run ./totp.mjs
```

![IMG_4028.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479627/93029146-4e25-8e71-f820-516dfa44d951.jpeg)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479627/2b2ca47d-c0ff-e2b4-7a97-7d65981e7724.png)

***:thumbs_up:***

これで追加のライブラリを用いずにクライアントのみでTOTPが生成可能である

コードは以下で公開している

- gitlab
  <https://gitlab.com/McbeEringi/petit/blob/main/totp.mjs>
  <https://gitlab.com/McbeEringi/petit/blob/main/protobuf.mjs>
- github(ミラー)
  <https://github.com/McbeEringi/petit/blob/main/totp.mjs>
  <https://github.com/McbeEringi/petit/blob/main/protobuf.mjs>