Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions models/Signature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { observable } from 'mobx';
import { BaseModel, persist, restore, toggle } from 'mobx-restful';

import { isServer } from './configuration';

export const buffer2hex = (buffer: ArrayBufferLike) =>
Array.from(new Uint8Array(buffer), x => x.toString(16).padStart(2, '0')).join('');

export class SignatureModel extends BaseModel {
algorithm = { name: 'ECDSA', namedCurve: 'P-384', hash: { name: 'SHA-256' } };

@persist()
@observable
accessor privateKey: CryptoKey | undefined;
Comment thread
TechQuery marked this conversation as resolved.

@persist()
@observable
accessor publicKey = '';

@persist()
@observable
accessor signatureMap = {} as Record<string, string>;

restored = !isServer() && restore(this, 'Signature');

@toggle('uploading')
async makeKeyPair() {
await this.restored;

if (this.publicKey) return this.publicKey;

const { publicKey, privateKey } = await crypto.subtle.generateKey(this.algorithm, true, [
'sign',
'verify',
]);
this.privateKey = privateKey;

const JWK = await crypto.subtle.exportKey('jwk', publicKey);

return (this.publicKey = btoa(JSON.stringify(JWK)));
}

@toggle('uploading')
async sign(value: string) {
await this.restored;

let signature = this.signatureMap[value];

if (signature) return signature;

if (!this.publicKey) await this.makeKeyPair();

const rawSignature = await crypto.subtle.sign(
this.algorithm,
this.privateKey!,
new TextEncoder().encode(value),
);
signature = buffer2hex(rawSignature);

this.signatureMap = { ...this.signatureMap, [value]: signature };

return signature;
}
Comment thread
TechQuery marked this conversation as resolved.
}
44 changes: 22 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@
},
"dependencies": {
"@giscus/react": "^3.1.0",
"@koa/router": "^15.2.0",
"@koa/router": "^15.3.0",
"@mdx-js/loader": "^3.1.1",
"@mdx-js/react": "^3.1.1",
"@next/mdx": "^16.1.4",
"core-js": "^3.47.0",
"@next/mdx": "^16.1.6",
"core-js": "^3.48.0",
"echarts-jsx": "^0.6.0",
"file-type": "^21.3.0",
"idea-react": "^2.0.0-rc.13",
Expand All @@ -28,64 +28,64 @@
"koa-jwt": "^4.0.4",
"koajax": "^3.1.2",
"license-filter": "^0.2.5",
"marked": "^17.0.1",
"marked": "^17.0.2",
"mime": "^4.1.0",
"mobx": "^6.15.0",
"mobx-github": "^0.6.2",
"mobx-i18n": "^0.7.2",
"mobx-lark": "^2.6.4",
"mobx-lark": "^2.6.5",
"mobx-react": "^9.2.1",
"mobx-react-helper": "^0.5.1",
"mobx-restful": "^2.1.4",
"mobx-restful-table": "^2.6.3",
"mobx-strapi": "^0.8.1",
"next": "^16.1.4",
"next": "^16.1.6",
"next-pwa": "^5.6.0",
"next-ssr-middleware": "^1.1.0",
"open-react-map": "^0.9.1",
"react": "^19.2.3",
"react": "^19.2.4",
"react-bootstrap": "^2.10.10",
"react-dom": "^19.2.3",
"react-dom": "^19.2.4",
"react-typed-component": "^1.0.6",
"remark-frontmatter": "^5.0.0",
"remark-mdx-frontmatter": "^5.2.0",
"web-utility": "^4.6.4",
"yaml": "^2.8.2"
},
"devDependencies": {
"@babel/plugin-proposal-decorators": "^7.28.6",
"@babel/plugin-proposal-decorators": "^7.29.0",
"@babel/plugin-transform-typescript": "^7.28.6",
"@babel/preset-react": "^7.28.5",
"@cspell/eslint-plugin": "^9.6.0",
"@eslint/js": "^9.39.2",
"@next/eslint-plugin-next": "^16.1.4",
"@cspell/eslint-plugin": "^9.6.4",
"@eslint/js": "^10.0.1",
"@next/eslint-plugin-next": "^16.1.6",
"@open-source-bazaar/china-ngo-database": "^0.6.0",
"@softonus/prettier-plugin-duplicate-remover": "^1.1.2",
"@stylistic/eslint-plugin": "^5.7.0",
"@stylistic/eslint-plugin": "^5.8.0",
"@types/eslint-config-prettier": "^6.11.3",
"@types/jsonwebtoken": "^9.0.10",
"@types/koa": "^3.0.1",
"@types/next-pwa": "^5.6.9",
"@types/node": "^24.10.9",
"@types/react": "^19.2.8",
"@types/node": "^24.10.13",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.4",
"eslint": "^10.0.0",
"eslint-config-next": "^16.1.6",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^17.0.0",
"globals": "^17.3.0",
"husky": "^9.1.7",
"jiti": "^2.6.1",
"less": "^4.5.1",
"less-loader": "^12.3.0",
"less-loader": "^12.3.1",
"lint-staged": "^16.2.7",
"next-with-less": "^3.0.1",
"prettier": "^3.8.0",
"prettier": "^3.8.1",
"prettier-plugin-css-order": "^2.2.0",
"sass": "^1.97.2",
"sass": "^1.97.3",
"typescript": "~5.9.3",
"typescript-eslint": "^8.53.1"
"typescript-eslint": "^8.55.0"
},
"resolutions": {
"mobx-react-helper": "$mobx-react-helper",
Expand Down
22 changes: 22 additions & 0 deletions pages/api/signature/[...slug].ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { createKoaRouter, withKoaRouter } from 'next-ssr-middleware';

export const config = { api: { bodyParser: false } };

const router = createKoaRouter(import.meta.url);

router.post('/verification', async context => {
const { algorithm, publicKey, value, signature } = Reflect.get(context.request, 'body');

const rawAlgorithm = JSON.parse(atob(algorithm)),
rawPublicKey = JSON.parse(atob(publicKey)),
rawSignature = Buffer.from(signature, 'hex'),
encodedValue = new TextEncoder().encode(value);

const key = await crypto.subtle.importKey('jwk', rawPublicKey, rawAlgorithm, true, ['verify']);
const verified = await crypto.subtle.verify(rawAlgorithm, key, rawSignature, encodedValue);

context.status = verified ? 200 : 400;
Comment thread
TechQuery marked this conversation as resolved.
context.body = {};
});
Comment thread
TechQuery marked this conversation as resolved.
Outdated

export default withKoaRouter(router);
60 changes: 60 additions & 0 deletions pages/signature.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { computed, observable } from 'mobx';
import { textJoin } from 'mobx-i18n';
import { observer } from 'mobx-react';
import { ObservedComponent } from 'mobx-react-helper';
import { compose, RouteProps, router } from 'next-ssr-middleware';
import { Container } from 'react-bootstrap';
import { buildURLData } from 'web-utility';

import { PageHead } from '../components/Layout/PageHead';
import { i18n, I18nContext } from '../models/Translation';
import { SignatureModel } from '../models/Signature';

export const getServerSideProps = compose(router);

@observer
export default class SignaturePage extends ObservedComponent<RouteProps, typeof i18n> {
static contextType = I18nContext;

@observable
accessor signatureStore = new SignatureModel();

@computed
get linkData() {
const { route } = this.observedProps;
const { valueName, algorithmName, publicKeyName, signatureName, value } = route.query,
{ algorithm, publicKey } = this.signatureStore;
const signature = this.signatureStore.signatureMap[value + ''] || {};
Comment thread
TechQuery marked this conversation as resolved.
Outdated

return buildURLData({
[valueName + '']: value,
[algorithmName + '']: btoa(JSON.stringify(algorithm)),
[publicKeyName + '']: publicKey,
[signatureName + '']: signature,
});
}
Comment on lines +23 to +35
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

当查询参数缺失时,linkData 会生成无意义的键名

valueNamealgorithmNamepublicKeyNamesignatureName 均可能为 undefined+ '' 强转后得到 "undefined" 作为 URL 参数名。建议对 iframeLink 及这些映射名参数做存在性检查,在参数缺失时返回空字符串或跳过构建。

🤖 Prompt for AI Agents
In `@pages/signature.tsx` around lines 23 - 35, linkData currently uses valueName,
algorithmName, publicKeyName, signatureName directly from
this.observedProps.route.query and concatenates '' which produces keys like
"undefined" when those query names are missing; update the linkData getter to
validate each query key (valueName, algorithmName, publicKeyName, signatureName)
before adding to the object passed to buildURLData — either skip entries whose
names are falsy or map them to an empty string, and ensure the signature lookup
this.signatureStore.signatureMap[value + ''] is only used if signatureName is
defined; this change should be made inside the linkData getter and affects the
object passed to buildURLData so no invalid "undefined" parameter names are
produced.


componentDidMount() {
const { value = '' } = this.props.route.query;

if (!value) this.signatureStore.makeKeyPair();
else this.signatureStore.sign(value + '');
}
Comment thread
TechQuery marked this conversation as resolved.

render() {
const { t } = this.observedContext,
{ value, iframeLink } = this.props.route.query;

const title = value ? textJoin(t('sign'), value + '') : t('generate_key_pair');

return (
<Container>
<PageHead title={title} />

<h1 className="my-5 text-truncate">{title}</h1>

<iframe className="border-0 w-100 vh-100" src={`${iframeLink}?${this.linkData}`} />
Comment thread
TechQuery marked this conversation as resolved.
Outdated
</Container>
);
}
}
Loading
Loading