惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

zodream梦想开源/个人编程日记

angular 21 升级使用 signals 方案笔记-zodream梦想开源/个人编程日记 文件解析笔记-zodream梦想开源/个人编程日记 密码本开发笔记之读写与保存-zodream梦想开源/个人编程日记 基于 SkiaSharp 的轮廓获取-zodream梦想开源/个人编程日记 SkiaSharp 把 pixel byte[] 转成 SKBitmap-zodream梦想开源/个人编程日记 nas 使用 Docker 安装 gogs-zodream梦想开源/个人编程日记 复制 android 手机中的文件到电脑-zodream梦想开源/个人编程日记 最新|个人日记-zodream梦想开源/个人编程日记 升级 SiteServer CMS 并迁移到 Linux 服务器-zodream梦想开源/个人编程日记 最新|个人日记-zodream梦想开源/个人编程日记 最新|个人日记-zodream梦想开源/个人编程日记 最新|个人日记-zodream梦想开源/个人编程日记 周报:寻找优质的周刊-zodream梦想开源/个人编程日记 开发日志:对Markdown的代码块新增引用来源支持-zodream梦想开源/个人编程日记 周报:怎么写技术类的教程文章-zodream梦想开源/个人编程日记 css display:flex 布局尺寸超出问题-zodream梦想开源/个人编程日记 周报:SEO优化的思考-zodream梦想开源/个人编程日记 Edge 浏览器不适用 Edge Image Viewer 打开图片 -zodream梦想开源/个人编程日记 SEO 学习笔记(一) 内容来源-zodream梦想开源/个人编程日记 PHP 实现双因素身份认证(2FA)-zodream梦想开源/个人编程日记 winui3 自定义标题栏-zodream梦想开源/个人编程日记 WPF MVVM 获取List 多选数据-zodream梦想开源/个人编程日记 Burp Suite 抓包-zodream梦想开源/个人编程日记 lnmp php集成环境安装包使用-zodream梦想开源/个人编程日记 js 进行在线编辑器开发-zodream梦想开源/个人编程日记 使用 indexnow 注意事项-zodream梦想开源/个人编程日记 Godot 使用字体图标 例如: Iconfont、FontAwesome-zodream梦想开源/个人编程日记 angular 15 对指定页面进行访问限制-zodream梦想开源/个人编程日记 CSS 使用 column-count 实现瀑布流出现内容分割的解决办法-zodream梦想开源/个人编程日记 angular 15 实现按下确认键,焦点移动到下一个表单或提交表单-zodream梦想开源/个人编程日记 input 确认按键事件在手机端不生效-zodream梦想开源/个人编程日记 C# 使用socket 进行通讯-zodream梦想开源/个人编程日记 Maui开发中Windows应用开启管理员权限-zodream梦想开源/个人编程日记 Maui 中自定义控件-zodream梦想开源/个人编程日记 TencentOS Server 3.1 安装 Nginx 1.23、PHP 8.2、MariaDB 10.11-zodream梦想开源/个人编程日记 angular 14 使用 ng-template 实现tree 结构显示-zodream梦想开源/个人编程日记 angular 14 替换 ComponentFactoryResolver 实现动态创建组件-zodream梦想开源/个人编程日记 c# 动态安装和卸载dll-zodream梦想开源/个人编程日记 慎用 CompositionTarget.Rendering-zodream梦想开源/个人编程日记 c# 重写 c++ 程序笔记:数据初始化-zodream梦想开源/个人编程日记 源码编译 aseprite-zodream梦想开源/个人编程日记 记录一下字符串分隔split各语言之间的不同-zodream梦想开源/个人编程日记 c# Gzip解码无头内容-zodream梦想开源/个人编程日记 Windows 10 查看内存占用-zodream梦想开源/个人编程日记 UWP 使用 win2d:加阴影-zodream梦想开源/个人编程日记 清除 PowerShell 历史记录-zodream梦想开源/个人编程日记 c# 调用 c++ 的dll-zodream梦想开源/个人编程日记 c# 重写 c++ 程序笔记:遍历-zodream梦想开源/个人编程日记 Net Core 与 UWP 共用类开发-zodream梦想开源/个人编程日记 hashcat(二)找回rar解压密码-zodream梦想开源/个人编程日记 Godot 学习笔记(一)-zodream梦想开源/个人编程日记 升级vue3记录-zodream梦想开源/个人编程日记 angular 12 显示数学公式-zodream梦想开源/个人编程日记 js 监听按键事件-zodream梦想开源/个人编程日记 angular 12 ng-deep 使用注意事项-zodream梦想开源/个人编程日记 angular 16 动态生成组件-zodream梦想开源/个人编程日记 angular 12 动画执行完成事件-zodream梦想开源/个人编程日记 angular 12 全局搜索组件-zodream梦想开源/个人编程日记 angular 12 中单例 Service 的使用-zodream梦想开源/个人编程日记 js 实现一个正则替换-zodream梦想开源/个人编程日记 uwp win2d 使用-zodream梦想开源/个人编程日记 UWP Custom Control自定义控件开发-zodream梦想开源/个人编程日记 UWP 读取应用内资源-zodream梦想开源/个人编程日记 gin 使用笔记(二)出错点-zodream梦想开源/个人编程日记 gin 使用笔记(一)基础-zodream梦想开源/个人编程日记 angular 关于自定义组件事件传递-zodream梦想开源/个人编程日记 angular 11 怎么获取 Content-Disposition-zodream梦想开源/个人编程日记 apache 使用gzip 压缩 js、css-zodream梦想开源/个人编程日记 angular 11 返回上一页保留页面数据的思考-zodream梦想开源/个人编程日记 一个简单的HTML音视频播放器-zodream梦想开源/个人编程日记 Net Core 实现一个简单的分页功能-zodream梦想开源/个人编程日记 关于内容中的 @用户 加 话题 的一些想法-zodream梦想开源/个人编程日记 Github Host 更改-zodream梦想开源/个人编程日记 OBS-Studio 等录屏软件录制显示器内容的黑屏的解决方法-zodream梦想开源/个人编程日记 angular 11 FormBuilder 中 FormGroup 和 FormArray 使用-zodream梦想开源/个人编程日记 angular 11 ngrx/effects 使用理解-zodream梦想开源/个人编程日记 angular 11 ngrx/store 使用理解-zodream梦想开源/个人编程日记 angular 10 直接获取表单值-zodream梦想开源/个人编程日记 angular 10 使用 tinymce 编辑器-zodream梦想开源/个人编程日记 htaccess 搭配 angular 10 放在二级目录-zodream梦想开源/个人编程日记 微信小程序跨页面传值-zodream梦想开源/个人编程日记 js 对 FileList 进行文件过滤上传-zodream梦想开源/个人编程日记 angular自定义表单组件支持 formControlName-zodream梦想开源/个人编程日记 基于不同形式的json响应处理-zodream梦想开源/个人编程日记 flutter CupertinoPicker 使用不显示-zodream梦想开源/个人编程日记 CC协议-zodream梦想开源/个人编程日记 flutter margin 负值实现-zodream梦想开源/个人编程日记 win10添加删除开机自启项-zodream梦想开源/个人编程日记 Wallpager Engine 删除记录-zodream梦想开源/个人编程日记 angular10教程之http 拦截器-zodream梦想开源/个人编程日记 dpl 文件-zodream梦想开源/个人编程日记 微信小程序开发记录(一)真机无法进入页面-zodream梦想开源/个人编程日记 flutter 跳转页面操作上一页-zodream梦想开源/个人编程日记 Regex Generator 使用指南-zodream梦想开源/个人编程日记 go init函数-zodream梦想开源/个人编程日记 angular 9 升级 angular 10-zodream梦想开源/个人编程日记 kotlin AndroidManifest 注意事项-zodream梦想开源/个人编程日记 对于zodream 框架的优化的思考-zodream梦想开源/个人编程日记 flutter 页面滚动条-zodream梦想开源/个人编程日记 flutter swiper 使用-zodream梦想开源/个人编程日记
php 接入 WebAuthn 登录-zodream梦想开源/个人编程日记
2023-10-14 · via zodream梦想开源/个人编程日记

php 接入 WebAuthn 登录

字节数据传输(关键问题)

解决方法:使用 base64 编解码;

接下来 需要解决 base64 算法不一致问题,

例如:浏览器自带的 js

window.btoa(val: string): string // base64 编码
window.atob(val: string): string // base64 解码

12

例如:php 自带的

base64_encode(val: string): string // base64 编码
base64_decode(val: string): string // base64 解码

12

实际,webAuthn 一些数据是 ArrayBuffer 所以默认的就不行了

js base64 处理

class Base64 {

    private static chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
    private static lookup = new Uint8Array(256);
    private static booted = false;

    private static ready() {
        if (this.booted) {
            return;
        }
        this.booted = true;
        for (let i = 0; i < this.chars.length; i++) {
            this.lookup[this.chars.charCodeAt(i)] = i;
        }
    }

    public static encode(arraybuffer: ArrayBuffer): string {
        const bytes = new Uint8Array(arraybuffer);
        const len = bytes.length;
        let base64 = '';

        for (let i = 0; i < len; i += 3) {
            base64 += this.chars[bytes[i] >> 2];
            base64 += this.chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
            base64 += this.chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
            base64 += this.chars[bytes[i + 2] & 63];
        }

        if (len % 3 === 2) {
            base64 = base64.substring(0, base64.length - 1);
        } else if (len % 3 === 1) {
            base64 = base64.substring(0, base64.length - 2);
        }

        return base64;
    }

    public static decode(base64: string): ArrayBuffer {
        const len = base64.length;
        const bufferLength = len * 0.75;
        const arraybuffer = new ArrayBuffer(bufferLength);
        const bytes = new Uint8Array(arraybuffer);

        let p = 0;
        for (let i = 0; i < len; i += 4) {
            const encoded1 = this.lookup[base64.charCodeAt(i)];
            const encoded2 = this.lookup[base64.charCodeAt(i + 1)];
            const encoded3 = this.lookup[base64.charCodeAt(i + 2)];
            const encoded4 = this.lookup[base64.charCodeAt(i + 3)];

            bytes[p++] = (encoded1 << 2) | (encoded2 >> 4);
            bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2);
            bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63);
        }

        return arraybuffer;
    }

    public static toBuffer(val: string): ArrayBuffer {
        const items: number[] = [];
        for (let i = 0; i < val.length; i++) {
            items.push(val.charCodeAt(i));
        }
        return new Uint8Array(items);
    }
}

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566

php 实际只要实现一个 base64 解码就行了,其他的通过字符串传给前端,前端在转成 ArrayBuffer


    public static function decodeBase64(string $base64): string {
        static $lookup = [];
        if (empty($lookup)) {
            $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
            for ($i = 0; $i < strlen($chars); $i ++) {
                $lookup[ord(substr($chars, $i, 1))] = $i;
            }
        }
        $len = strlen($base64);
        $maxLen = (int)floor($len * .75);
        $buffer = [];
        for ($i = 0; $i < $len; $i += 4) {
            $encoded1 = $lookup[ord(substr($base64, $i, 1))];
            $encoded2 = $lookup[ord(substr($base64, $i + 1, 1))];
            $encoded3 = $lookup[ord(substr($base64, $i + 2, 1))];
            $encoded4 = $lookup[ord(substr($base64, $i + 3, 1))];

            $buffer[] = chr(($encoded1 << 2) | ($encoded2 >> 4));
            $buffer[] = chr((($encoded2 & 15) << 4) | ($encoded3 >> 2));
            $buffer[] = chr((($encoded3 & 3) << 6) | ($encoded4 & 63));
        }
        if (count($buffer) > $maxLen) {
            array_splice($buffer, $maxLen);
        }
        return implode('', $buffer);
    }

123456789101112131415161718192021222324252627

准备工作

一个 php 的 CBOR 解码库

composer install spomky-labs/cbor-php

1

一个 php 的 pem 转换库, 因为从浏览器获取的公钥是没法直接通过 openssl_get_publickey 加载的

这些都可以通过一个库解决,不过依赖库比较多

composer install web-auth/webauthn-framework

1

步骤

  1. 第一步,注册 Passkey, 在已登录的情况下,点击一个按钮进行注册
<button class="register-webauth">注册Passkey</button>

1

$('.register-webauth').on('click',function() {
    if (!navigator.credentials) {
        return;
    }
    // 从后台获取注册需要数据,包含当前登录账户的id
    $.getJSON(baseUri + '/passkey/register_option', res => {
        const data = res.data;
        data.challenge = Base64.toBuffer(data.challenge);
        data.user.id = Base64.toBuffer(data.user.id);
        navigator.credentials.create({
            publicKey: data
        })
        .then((credential: any) => {
            const response = credential.response as AuthenticatorAttestationResponse;
            // 保存注册成功的 credentialId 和 公钥
            $.post(baseUri + '/passkey/register', {credential: {
                id: credential.id,
                clientDataJSON: Base64.encode(response.clientDataJSON),
                attestationObject: Base64.encode(response.attestationObject),
                publicKeyAlgorithm: response.getPublicKeyAlgorithm(),
            }}, res => {}, 'json');
        })
        .catch(console.error);
    });

}).toggle(!!navigator.credentials);

1234567891011121314151617181920212223242526

  1. 注册需要的数据

passkey/register_option

return [
    'challenge' => $challenge, // 随机的字符串,防止重复操作,需要保存,获取到 公钥后需要验证
    'rp' => [
        'name' => Option::value('site_title'),
        'id' => request()->host()
    ],
    'user' => [
        'id' => (string)$user->getIdentity(), // 用户id
        'name' => $user->email,               // 用户邮箱
        'displayName' => $user->name          // 显示的名
    ],
    'pubKeyCredParams' => [[
        'alg' => -7,                          // ES256 公钥类型
        'type' => 'public-key'
    ], [
        'type' => 'public-key',
        'alg' => -257                         // RS256 公钥类型,这个选项好像是必须的,不然可能不成功
    ]],
    'timeout' => $timeout * 1000,
    'excludeCredentials' => [],
    'attestation' => 'none',
    'authenticatorSelection' => [
        'authenticatorAttachment' => "platform",
        "residentKey" => "preferred",
        'requireResidentKey' => false,
        'userVerification' => 'preferred'
    ],
    'extensions' => [
        'credProps' => true
    ]
];

12345678910111213141516171819202122232425262728293031

  1. 保存注册成功的公钥

passkey/register

public static function register(array $credential): void {
    $clientDataJSON = Json::decode(base64_decode($credential['clientDataJSON']));
    if ($clientDataJSON['type'] !== 'webauthn.create') {
        throw new \Exception('type is error');
    }
    $challenge = base64_decode($clientDataJSON['challenge']);
    // TODO 验证临时

    $obj = static::parseAuthenticatorData($credential['attestationObject']);
    // 解码 attestationObject,获取 公钥
    if (empty($obj) || empty($obj['publicKey'])) {
        throw new Exception('attestation is error');
    }
    self::saveCredential($credential['id'], $obj['publicKey'],
        intval($credential['publicKeyAlgorithm']));
    // TODO 保存公钥
}

1234567891011121314151617

  1. 登录页添加,使用 Passkey 登录的按钮
<button class="login-webauth">Passkey 登录</button>

1

$('.login-webauth').on('click',function() {
    if (!navigator.credentials) {
        return;
    }
    // 从后台获取登录需要数据
    $.getJSON(baseUri + '/passkey/login_option', res => {
        const data = res.data;
        data.challenge = Base64.toBuffer(data.challenge);
        navigator.credentials.get({
            publicKey: data
        })
        .then((credential: any) => {
            const response = credential.response as AuthenticatorAssertionResponse;
            // 获取登录结果,验证数据有效,根据id登录
            $.post(baseUri + '/passkey/login', {
                credential: {
                    id: credential.id,
                    clientDataJSON: Base64.encode(response.clientDataJSON),
                    authenticatorData: Base64.encode(response.authenticatorData),
                    userHandle: Base64.encode(response.userHandle),
                    signature: Base64.encode(response.signature)
                },
                redirect_uri: $('[name=redirect_uri]').val()
            }, res => {}, 'json');
        })
        .catch(console.error);
    });

}).toggle(!!navigator.credentials);

1234567891011121314151617181920212223242526272829

  1. 登录需要的数据

passkey/login_option

return [
    'challenge' => $challenge, // 防止重复操作的随机的字符串
    'timeout' => $timeout * 1000,
    'rpId' => request()->host(),
    'allowCredentials' => [],
    'userVerification' => 'preferred'
];

1234567

  1. 验证登录数据,登录id

passkey/login

    public static function login(array $credential): void {
        $clientDataJSON = Json::decode(base64_decode($credential['clientDataJSON']));
        $challenge = base64_decode($clientDataJSON['challenge']);
        $key = sprintf('%s-%s', self::REGISTER_KEY, $challenge);
        if (!cache()->has($key)) {
            throw new \Exception('challenge is expired');
        }
        $userId = base64_decode($credential['userHandle']);
        // 可以验证 credentialId 的 hash 值是否一致 
        $signature = $credential['signature'];
        self::loadCredential(intval($userId), $credential['id'], $signature, $credential);
    }

    /**
     * 验证的登录数据
     * @param int $userId
     * @param string $credentialId
     * @param string $signature
     * @param array $credential
     * @return void
     * @throws \Exception
     */
    protected static function loadCredential(int $userId, string $credentialId,  string $signature, array $credential) {
        // 获取保存的 公钥
        $key = '';
        if (empty($key)) {
            throw new \Exception('验证失败');
        }
        $data = CBOR::decodeBase64($credential['authenticatorData']);
        $pkey = openssl_get_publickey($key);
        if (empty($pkey)) {
            throw new Exception('public key is error');
        }
        if (!openssl_verify($data.self::hash(CBOR::decodeBase64($credential['clientDataJSON'])),
            CBOR::decodeBase64($signature), $pkey, \OPENSSL_ALGO_SHA256)) {
            throw new \Exception('signature is error');
        }
        // TODO 登录
    }

    private static function hash(string $val): string {
        return \hash('sha256', $val, true);
    }

12345678910111213141516171819202122232425262728293031323334353637383940414243

源码

PassKey主文件

CBOR

PEM

转载请保留原文链接: https://zodream.cn/blog/id/246.html