0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

TOTP

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

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

実装

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

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

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を用いて生成できる

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の解説は以下の記事が非常に参考になった
解説はこちらを読んでほしい

実装

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である場合はもう一度この関数を呼び出す前提の設計である

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

実装

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

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定義を参考にした

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

動作検証

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

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]),[]));
bun run ./totp.mjs

IMG_4028.jpeg

image.png

:thumbs_up:

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?