feat: qr code uploading and decrypting
This commit is contained in:
parent
3df8a87c1c
commit
c9922481b1
5 changed files with 475 additions and 391 deletions
|
|
@ -12,8 +12,10 @@
|
|||
"dependencies": {
|
||||
"@auth/prisma-adapter": "2.7.2",
|
||||
"@prisma/client": "^6.5.0",
|
||||
"@trafficlunar/asmcrypto.js": "^1.0.2",
|
||||
"@yudiel/react-qr-scanner": "2.2.2-beta.2",
|
||||
"embla-carousel-react": "^8.5.2",
|
||||
"jsqr": "^1.4.0",
|
||||
"next": "15.2.4",
|
||||
"next-auth": "5.0.0-beta.25",
|
||||
"react": "^19.0.0",
|
||||
|
|
|
|||
796
pnpm-lock.yaml
796
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { Icon } from "@iconify/react";
|
||||
|
||||
|
|
@ -8,6 +8,10 @@ import TagSelector from "./submit/tag-selector";
|
|||
import QrUpload from "./submit/qr-upload";
|
||||
import QrScanner from "./submit/qr-scanner";
|
||||
|
||||
import { AES_CCM } from "@trafficlunar/asmcrypto.js";
|
||||
|
||||
const key = new Uint8Array([0x59, 0xfc, 0x81, 0x7e, 0x64, 0x46, 0xea, 0x61, 0x90, 0x34, 0x7b, 0x20, 0xe9, 0xbd, 0xce, 0x52]);
|
||||
|
||||
export default function SubmitForm() {
|
||||
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
|
||||
accept: {
|
||||
|
|
@ -18,6 +22,25 @@ export default function SubmitForm() {
|
|||
const [isQrScannerOpen, setIsQrScannerOpen] = useState(false);
|
||||
const [qrBytes, setQrBytes] = useState<Uint8Array>(new Uint8Array());
|
||||
|
||||
useEffect(() => {
|
||||
if (qrBytes.length == 0) return;
|
||||
|
||||
const decrypt = async () => {
|
||||
const nonce = qrBytes.subarray(0, 8);
|
||||
const content = qrBytes.subarray(8, 0x70);
|
||||
|
||||
const nonceWithZeros = new Uint8Array(12);
|
||||
nonceWithZeros.set(nonce, 0);
|
||||
|
||||
const decrypted = AES_CCM.decrypt(content, key, nonceWithZeros, undefined, 16);
|
||||
const result = new Uint8Array([...decrypted.subarray(0, 12), ...qrBytes.subarray(0, 8), ...decrypted.subarray(12, decrypted.length - 4)]);
|
||||
|
||||
console.log(result);
|
||||
};
|
||||
|
||||
decrypt();
|
||||
}, [qrBytes]);
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => e.preventDefault()} className="grid grid-cols-2">
|
||||
<div className="p-4">
|
||||
|
|
@ -66,7 +89,7 @@ export default function SubmitForm() {
|
|||
<fieldset className="border-t-2 border-b-2 border-black p-3 flex flex-col items-center gap-2">
|
||||
<legend className="px-2">QR Code</legend>
|
||||
|
||||
<QrUpload />
|
||||
<QrUpload setQrBytes={setQrBytes} />
|
||||
|
||||
<span>or</span>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,10 @@ export default function QrScanner({ isOpen, setIsOpen, setQrBytes }: Props) {
|
|||
}, [isOpen]);
|
||||
|
||||
const handleScan = (result: IDetectedBarcode[]) => {
|
||||
// todo: fix scan, use jsQR instead, data is wrong
|
||||
setIsOpen(false);
|
||||
|
||||
// Convert to bytes
|
||||
const encoder = new TextEncoder();
|
||||
const byteArray = encoder.encode(result[0].rawValue);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,43 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { FileWithPath, useDropzone } from "react-dropzone";
|
||||
import { Icon } from "@iconify/react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import jsQR from "jsqr";
|
||||
|
||||
export default function QrUpload() {
|
||||
const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
|
||||
interface Props {
|
||||
setQrBytes: React.Dispatch<React.SetStateAction<Uint8Array>>;
|
||||
}
|
||||
|
||||
export default function QrUpload({ setQrBytes }: Props) {
|
||||
const onDrop = useCallback((acceptedFiles: FileWithPath[]) => {
|
||||
acceptedFiles.forEach((file) => {
|
||||
// Scan QR code
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (event) => {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||
|
||||
const imageData = ctx.getImageData(0, 0, image.width, image.height);
|
||||
const decoded = jsQR(imageData.data, image.width, image.height);
|
||||
|
||||
setQrBytes(new Uint8Array(decoded?.binaryData!));
|
||||
};
|
||||
image.src = event.target!.result as string;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { getRootProps, getInputProps } = useDropzone({
|
||||
onDrop,
|
||||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp"],
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue