diff --git a/apps/website/src/components/error-page.tsx b/apps/website/src/components/error-page.tsx index d7cf3a0e..f2ca376f 100644 --- a/apps/website/src/components/error-page.tsx +++ b/apps/website/src/components/error-page.tsx @@ -21,17 +21,6 @@ export const ErrorPage = (props: Props) => { ) - // todo!: design review, not in designs - case ERROR_CODES.UNVERIFIED_CONTENT: - return ( -
-
- - Unverified content. - -
- ) - case ERROR_CODES.INTERNAL_SERVER_ERROR: default: return ( diff --git a/apps/website/src/components/preview-page.tsx b/apps/website/src/components/preview-page.tsx index da29a55d..2ca361d1 100644 --- a/apps/website/src/components/preview-page.tsx +++ b/apps/website/src/components/preview-page.tsx @@ -43,25 +43,25 @@ type Type = 'community' | 'channel' | 'profile' type PreviewPageProps = { type: Type - unverifiedEncodedData?: string | null + encodedData?: string | null index?: boolean } & ( | { type: 'community' - unverifiedDecodedData?: ReturnType | null + decodedData?: ReturnType | null } | { type: 'channel' - unverifiedDecodedData?: ReturnType | null + decodedData?: ReturnType | null channelUuid?: string } | { type: 'profile' - unverifiedDecodedData?: ReturnType | null + decodedData?: ReturnType | null } ) -export type VerifiedData = +export type Data = | { type: 'community' info: CommunityInfo @@ -93,26 +93,26 @@ const JOIN_BUTTON_LABEL: Record = { } export function PreviewPage(props: PreviewPageProps) { - const { type, unverifiedDecodedData, unverifiedEncodedData } = props + const { type, decodedData, encodedData } = props const { asPath } = useRouter() const toast = useToast() // todo: default og image, not dynamic - // const ogImageUrl = getOgImageUrl(props.unverifiedDecodedData) + // const ogImageUrl = getOgImageUrl(props.decodedData) // todo?: pass meta, info as component // todo?: pass image, color as props const { publicKey, channelUuid: urlChannelUuid, - verifiedURLData, + data: urlData, errorCode: urlErrorCode, - } = useURLData(type, unverifiedDecodedData, unverifiedEncodedData) + } = useURLData(type, decodedData, encodedData) const { - data: verifiedWakuData, + data: wakuData, isLoading, status, refetch, @@ -120,7 +120,7 @@ export function PreviewPage(props: PreviewPageProps) { refetchOnWindowFocus: false, queryKey: [type], enabled: !!publicKey, - queryFn: async function ({ queryKey }): Promise { + queryFn: async function ({ queryKey }): Promise { const client = await getRequestClient() switch (queryKey[0]) { @@ -173,36 +173,35 @@ export function PreviewPage(props: PreviewPageProps) { return } - if (verifiedURLData) { + if (urlData) { toast.custom('Information just updated', ) } }, }) const loading = status === 'loading' || isLoading - const verifiedData: VerifiedData | undefined = - verifiedWakuData ?? verifiedURLData + const data: Data | undefined = wakuData ?? urlData const { avatarURL, bannerURL } = useMemo(() => { - if (!verifiedData) { + if (!data) { return {} } - const avatarURL = getAvatarURL(verifiedData) - const bannerURL = getBannerURL(verifiedData) + const avatarURL = getAvatarURL(data) + const bannerURL = getBannerURL(data) return { avatarURL, bannerURL } - }, [verifiedData]) + }, [data]) if (urlErrorCode) { return } - if (!loading && !verifiedData) { + if (!loading && !data) { return } - if ((loading && !verifiedData) || !verifiedData || !publicKey) { + if ((loading && !data) || !data || !publicKey) { return ( <>
@@ -317,7 +316,7 @@ export function PreviewPage(props: PreviewPageProps) { {/* todo: theme; based on user system settings */} {/* todo: (system or both?) install banner */}
@@ -336,54 +335,54 @@ export function PreviewPage(props: PreviewPageProps) { {/* HERO */}
- {verifiedData.type === 'community' && ( + {data.type === 'community' && ( )} - {verifiedData.type === 'channel' && ( + {data.type === 'channel' && ( )} - {verifiedData.type === 'profile' && ( + {data.type === 'profile' && ( )}

- {verifiedData.type === 'channel' && '#'} - {verifiedData.info.displayName} + {data.type === 'channel' && '#'} + {data.info.displayName}

- {verifiedData.info.description} + {data.info.description}

- {verifiedData.type === 'community' && ( + {data.type === 'community' && ( <>
- {formatNumber(verifiedData.info.membersCount)} + {formatNumber(data.info.membersCount)}
- {verifiedData.info.tags?.length > 0 && ( + {data.info.tags?.length > 0 && (
- {verifiedData.info.tags.map(tag => ( + {data.info.tags.map(tag => ( )} - {verifiedData.type === 'channel' && ( + {data.type === 'channel' && (
Channel in @@ -403,18 +402,18 @@ export function PreviewPage(props: PreviewPageProps) {
)} - {verifiedData.type === 'profile' && ( + {data.type === 'profile' && (

- {verifiedData.info.emojiHash} + {data.info.emojiHash}

)}
@@ -497,9 +496,7 @@ export function PreviewPage(props: PreviewPageProps) { !bannerURL ? { backgroundColor: - 'color' in verifiedData.info - ? verifiedData.info.color - : neutral[100], + 'color' in data.info ? data.info.color : neutral[100], } : undefined } @@ -527,14 +524,14 @@ const formatNumber = (n: number) => { return formatter.format(n) } -const getGradientStyles = (data: VerifiedData): CSSProperties => { +const getGradientStyles = (data: Data): CSSProperties => { return { // @ts-expect-error CSSProperties do not handle inline CSS variables '--gradient-color': 'color' in data.info ? data.info.color : neutral[100], } } -const getAvatarURL = (data: VerifiedData): string | undefined => { +const getAvatarURL = (data: Data): string | undefined => { let avatar: Uint8Array | undefined switch (data.type) { case 'community': @@ -560,7 +557,7 @@ const getAvatarURL = (data: VerifiedData): string | undefined => { return url } -const getBannerURL = (data: VerifiedData): string | undefined => { +const getBannerURL = (data: Data): string | undefined => { let banner: Uint8Array | undefined switch (data.type) { case 'community': diff --git a/apps/website/src/consts/error-codes.ts b/apps/website/src/consts/error-codes.ts index e0ab1eca..834df6e8 100644 --- a/apps/website/src/consts/error-codes.ts +++ b/apps/website/src/consts/error-codes.ts @@ -1,6 +1,5 @@ export const ERROR_CODES = { NOT_FOUND: 404, INTERNAL_SERVER_ERROR: 500, - UNVERIFIED_CONTENT: 600, INVALID_PUBLIC_KEY: 601, } diff --git a/apps/website/src/hooks/use-url-data.ts b/apps/website/src/hooks/use-url-data.ts index f8e7ae6e..296f2e10 100644 --- a/apps/website/src/hooks/use-url-data.ts +++ b/apps/website/src/hooks/use-url-data.ts @@ -7,13 +7,12 @@ import { indicesToTags, publicKeyToColorHash, publicKeyToEmojiHash, - verifyEncodedURLData, + recoverPublicKeyFromEncodedURLData, } from '@status-im/js' -import { decodeVerificationURLHash } from '@status-im/js/encode-url-hash' import { ERROR_CODES } from '@/consts/error-codes' -import type { VerifiedData } from '@/components/preview-page' +import type { Data } from '@/components/preview-page' import type { ChannelInfo, CommunityInfo, UserInfo } from '@status-im/js' import type { decodeChannelURLData, @@ -23,17 +22,17 @@ import type { export const useURLData = ( type: 'community' | 'channel' | 'profile', - unverifiedDecodedData: + decodedData: | ReturnType | ReturnType | ReturnType | undefined | null, - unverifiedEncodedData: string | undefined | null + encodedData: string | undefined | null ) => { const [publicKey, setPublicKey] = useState() const [channelUuid, setChannelUuid] = useState() - const [info, setInfo] = useState() + const [data, setData] = useState() const [error, setError] = useState() const compressPublicKey = type !== 'profile' @@ -46,9 +45,10 @@ export const useURLData = ( // return // } - if (!unverifiedDecodedData || !unverifiedEncodedData) { - const hash = window.location.hash.replace('#', '') + const hash = window.location.hash.replace('#', '') + // use provided public key + if (!decodedData || !encodedData) { if (!hash) { setError('NOT_FOUND') @@ -61,37 +61,39 @@ export const useURLData = ( }) setPublicKey(publicKey) + + return } catch (error) { console.error(error) setError('INVALID_PUBLIC_KEY') + + return } + } + + // recover public key + let deserializedPublicKey + try { + const recoveredPublicKey = recoverPublicKeyFromEncodedURLData( + encodedData, + hash + ) + deserializedPublicKey = deserializePublicKey(recoveredPublicKey, { + compress: compressPublicKey, + }) + + setPublicKey(deserializedPublicKey) + } catch (error) { + console.error(error) + setError('INVALID_PUBLIC_KEY') return } - const hash = window.location.hash.replace('#', '') - const { signature, publicKey } = decodeVerificationURLHash(hash) - - if (!signature || !publicKey) { - setError('UNVERIFIED_CONTENT') - - return - } - - if (!verifyEncodedURLData(unverifiedEncodedData, hash)) { - setError('UNVERIFIED_CONTENT') - - return - } - - const deserializedPublicKey = deserializePublicKey(publicKey, { - compress: compressPublicKey, - }) - - const verifiedDecodedData = unverifiedDecodedData + // map data switch (type) { case 'community': { - const data = verifiedDecodedData as Required< + const data = decodedData as Required< ReturnType > const info: CommunityInfo = { @@ -102,12 +104,12 @@ export const useURLData = ( tags: indicesToTags(data.tagIndices), } - setInfo({ type: 'community', info }) + setData({ type: 'community', info }) - break + return } case 'channel': { - const data = verifiedDecodedData as Required< + const data = decodedData as Required< ReturnType > const info: Omit & { @@ -120,13 +122,13 @@ export const useURLData = ( community: { displayName: data.community.displayName }, } - setInfo({ type: 'channel', info }) + setData({ type: 'channel', info }) setChannelUuid(data.uuid) - break + return } case 'profile': { - const data = verifiedDecodedData as Required< + const data = decodedData as Required< ReturnType > const info: UserInfo = { @@ -136,13 +138,11 @@ export const useURLData = ( emojiHash: publicKeyToEmojiHash(deserializedPublicKey), } - setInfo({ type: 'profile', info }) + setData({ type: 'profile', info }) - break + return } } - - setPublicKey(deserializedPublicKey) } catch (error) { console.error(error) setError('INTERNAL_SERVER_ERROR') @@ -152,7 +152,7 @@ export const useURLData = ( return { publicKey, channelUuid, - verifiedURLData: info, + data, errorCode: error ? ERROR_CODES[error] : undefined, } } diff --git a/apps/website/src/pages/c/[slug].tsx b/apps/website/src/pages/c/[slug].tsx index 4ff14bab..e077bb4d 100644 --- a/apps/website/src/pages/c/[slug].tsx +++ b/apps/website/src/pages/c/[slug].tsx @@ -15,8 +15,8 @@ export default function CommunityPreviewPage( return ( ) } diff --git a/apps/website/src/pages/cc/[slug].tsx b/apps/website/src/pages/cc/[slug].tsx index f6c816b8..b2c57b2a 100644 --- a/apps/website/src/pages/cc/[slug].tsx +++ b/apps/website/src/pages/cc/[slug].tsx @@ -13,8 +13,8 @@ export default function ChannelPreviewPage( return ( ) diff --git a/apps/website/src/pages/u/[slug].tsx b/apps/website/src/pages/u/[slug].tsx index 00c96bd3..91865de3 100644 --- a/apps/website/src/pages/u/[slug].tsx +++ b/apps/website/src/pages/u/[slug].tsx @@ -13,8 +13,8 @@ export default function UserPreviewPage( return ( ) } diff --git a/apps/website/src/server/ssr.ts b/apps/website/src/server/ssr.ts index df8ed22e..d9e1defd 100644 --- a/apps/website/src/server/ssr.ts +++ b/apps/website/src/server/ssr.ts @@ -13,17 +13,13 @@ type DecodeType = export type ServerSideProps> = { /** - * For verifying on client without decoding or re-encoding. - * - * Verification in general is done on encoded data, so it is not - * decoded, decompressed and deserialized unnecessarily if not to be - * displayed or othwerwise needed. + * For verifying on client without re-encoding. */ - uverifiedEncodedData: string | null + encodedData: string | null /** - * For instaneous preview even if the data is not verified yet. + * For instaneous preview of the content. */ - unverifiedDecodedData: T | null + decodedData: T | null channelUuid?: string } @@ -46,8 +42,8 @@ export function createGetServerSideProps(decodeURLData: DecodeType) { if (channelUuid) { const props: ServerSideProps = { channelUuid: channelUuid[0], - uverifiedEncodedData: null, - unverifiedDecodedData: null, + encodedData: null, + decodedData: null, } return { props } @@ -57,8 +53,8 @@ export function createGetServerSideProps(decodeURLData: DecodeType) { if (!encodedData) { const props: ServerSideProps = { - uverifiedEncodedData: null, - unverifiedDecodedData: null, + encodedData: null, + decodedData: null, } return { props } @@ -66,8 +62,8 @@ export function createGetServerSideProps(decodeURLData: DecodeType) { const decodedData = decodeURLData(encodedData) const props: ServerSideProps = { - uverifiedEncodedData: encodedData, - unverifiedDecodedData: decodedData || null, + encodedData, + decodedData: decodedData || null, } // fixme: set Cache-Control diff --git a/packages/status-js/src/index.ts b/packages/status-js/src/index.ts index 5d07bcf6..c8679441 100644 --- a/packages/status-js/src/index.ts +++ b/packages/status-js/src/index.ts @@ -22,4 +22,4 @@ export { createRequestClient } from './request-client/request-client' export { deserializePublicKey } from './utils/deserialize-public-key' export { publicKeyToColorHash } from './utils/public-key-to-color-hash' export { publicKeyToEmojiHash } from './utils/public-key-to-emoji-hash' -export { verifyEncodedURLData } from './utils/sign-url-data' +export { recoverPublicKeyFromEncodedURLData } from './utils/sign-url-data' diff --git a/packages/status-js/src/protos/url.proto b/packages/status-js/src/protos/url.proto index b7028e3e..a8a1c697 100644 --- a/packages/status-js/src/protos/url.proto +++ b/packages/status-js/src/protos/url.proto @@ -23,21 +23,11 @@ message User { string color = 3; } -message Verification { - string signature = 1; - string public_key = 2; -} - message URLData { // Community, Channel, or User bytes content = 1; } -message URLHash { - // Verification - bytes content = 1; -} - message URLParams { string encoded_url_data = 1; // Signature of encoded URL data diff --git a/packages/status-js/src/protos/url_pb.ts b/packages/status-js/src/protos/url_pb.ts index 9af218f1..b582b83a 100644 --- a/packages/status-js/src/protos/url_pb.ts +++ b/packages/status-js/src/protos/url_pb.ts @@ -267,61 +267,6 @@ export class User extends Message { } } -/** - * @generated from message Verification - */ -export class Verification extends Message { - /** - * @generated from field: string signature = 1; - */ - signature = '' - - /** - * @generated from field: string public_key = 2; - */ - publicKey = '' - - constructor(data?: PartialMessage) { - super() - proto3.util.initPartial(data, this) - } - - static readonly runtime = proto3 - static readonly typeName = 'Verification' - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: 'signature', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, - { no: 2, name: 'public_key', kind: 'scalar', T: 9 /* ScalarType.STRING */ }, - ]) - - static fromBinary( - bytes: Uint8Array, - options?: Partial - ): Verification { - return new Verification().fromBinary(bytes, options) - } - - static fromJson( - jsonValue: JsonValue, - options?: Partial - ): Verification { - return new Verification().fromJson(jsonValue, options) - } - - static fromJsonString( - jsonString: string, - options?: Partial - ): Verification { - return new Verification().fromJsonString(jsonString, options) - } - - static equals( - a: Verification | PlainMessage | undefined, - b: Verification | PlainMessage | undefined - ): boolean { - return proto3.util.equals(Verification, a, b) - } -} - /** * @generated from message URLData */ @@ -373,57 +318,6 @@ export class URLData extends Message { } } -/** - * @generated from message URLHash - */ -export class URLHash extends Message { - /** - * Verification - * - * @generated from field: bytes content = 1; - */ - content = new Uint8Array(0) - - constructor(data?: PartialMessage) { - super() - proto3.util.initPartial(data, this) - } - - static readonly runtime = proto3 - static readonly typeName = 'URLHash' - static readonly fields: FieldList = proto3.util.newFieldList(() => [ - { no: 1, name: 'content', kind: 'scalar', T: 12 /* ScalarType.BYTES */ }, - ]) - - static fromBinary( - bytes: Uint8Array, - options?: Partial - ): URLHash { - return new URLHash().fromBinary(bytes, options) - } - - static fromJson( - jsonValue: JsonValue, - options?: Partial - ): URLHash { - return new URLHash().fromJson(jsonValue, options) - } - - static fromJsonString( - jsonString: string, - options?: Partial - ): URLHash { - return new URLHash().fromJsonString(jsonString, options) - } - - static equals( - a: URLHash | PlainMessage | undefined, - b: URLHash | PlainMessage | undefined - ): boolean { - return proto3.util.equals(URLHash, a, b) - } -} - /** * @generated from message URLParams */ diff --git a/packages/status-js/src/utils/create-url.test.ts b/packages/status-js/src/utils/create-url.test.ts index 5aa3e8c3..b35d8555 100644 --- a/packages/status-js/src/utils/create-url.test.ts +++ b/packages/status-js/src/utils/create-url.test.ts @@ -66,7 +66,7 @@ describe('Create URLs', () => { ) ).toString() ).toBe( - 'https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#Co0BClhRbk8yaHc1dFZBRS1NRDVpOE1xNHNfb0dXZDByUkZtbE9iZ1JVTlFYdFVOd1AxaXhGdzkxNFk0LUJRcEYwOEtPcXBhVUxDaDdVQ3RsV1ItTzBZUDhNd0E9EjF6UTNzaFlTSHA3R29pWGFhdUpNbkRjandVMnlOamR6cFhMb3NBV2FwUFM0Q0Z4YzEx' + 'https://status.app/c/iyKACkQKB0Rvb2RsZXMSJ0NvbG9yaW5nIHRoZSB3b3JsZCB3aXRoIGpveSDigKIg4bSXIOKAohiYohsiByMxMzFEMkYqAwEhMwM=#QnO2hw5tVAE-MD5i8Mq4s_oGWd0rRFmlObgRUNQXtUNwP1ixFw914Y4-BQpF08KOqpaULCh7UCtlWR-O0YP8MwA=' ) }) @@ -122,7 +122,7 @@ describe('Create URLs', () => { ) ).toString() ).toBe( - 'https://status.app/cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#Co0BClg3YWVCLU02cElidnBTVkdNNFRlSmtLV1B5YTRZUkFIYnE0YW1MMGNIbFNCcFJLbjdfbHlSNGt4RURvMmhDNGtvcVBXWWVfYWsyUjljU1ZLU2lWX25OQUE9EjF6UTNzaFlTSHA3R29pWGFhdUpNbkRjandVMnlOamR6cFhMb3NBV2FwUFM0Q0Z4YzEx' + 'https://status.app/cc/G54AAKwObLdpiGjXnckYzRcOSq0QQAS_CURGfqVU42ceGHCObstUIknTTZDOKF3E8y2MSicncpO7fTskXnoACiPKeejvjtLTGWNxUhlT7fyQS7Jrr33UVHluxv_PLjV2ePGw5GQ33innzeK34pInIgUGs5RjdQifMVmURalxxQKwiuoY5zwIjixWWRHqjHM=#7aeB-M6pIbvpSVGM4TeJkKWPya4YRAHbq4amL0cHlSBpRKn7_lyR4kxEDo2hC4koqPWYe_ak2R9cSVKSiV_nNAA=' ) }) @@ -159,7 +159,7 @@ describe('Create URLs', () => { ) ).toString() ).toBe( - 'https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#Co0BClhMYlFVZEpESENLb2k4RHpvWXlYODlicEtyVGpWVjNTaHFIM0U2NGJEaWZKQjJHa2VkdExCZlZLQTAyUmJVZlgwNzRwYjlpM293R3dSZFM2eF9udHhyUUE9EjF6UTNzaHdRUGhSdURKU2pWR1ZCblRqQ2RnWHk1aTlXUWFlVlBkR0pENnlUYXJKUVNq' + 'https://status.app/u/G10A4B0JdgwyRww90WXtnP1oNH1ZLQNM0yX0Ja9YyAMjrqSZIYINOHCbFhrnKRAcPGStPxCMJDSZlGCKzmZrJcimHY8BbcXlORrElv_BbQEegnMDPx1g9C5VVNl0fE4y#LbQUdJDHCKoi8DzoYyX89bpKrTjVV3ShqH3E64bDifJB2GkedtLBfVKA02RbUfX074pb9i3owGwRdS6x_ntxrQA=' ) }) }) diff --git a/packages/status-js/src/utils/create-url.ts b/packages/status-js/src/utils/create-url.ts index 9dfa6412..024191f0 100644 --- a/packages/status-js/src/utils/create-url.ts +++ b/packages/status-js/src/utils/create-url.ts @@ -19,14 +19,12 @@ export async function createCommunityURLWithData( communityPrivateKey: Uint8Array | string ): Promise { const encodedURLData = encodeCommunityURLData(communityData) - const encodedVerificationURLHash = await signEncodedURLData( + const encodedSignature = await signEncodedURLData( encodedURLData, communityPrivateKey ) - return new URL( - `${BASE_URL}/c/${encodedURLData}#${encodedVerificationURLHash}` - ) + return new URL(`${BASE_URL}/c/${encodedURLData}#${encodedSignature}`) } export function createChannelURLWithChatKey( @@ -41,14 +39,12 @@ export async function createChannelURLWithData( communityPrivateKey: Uint8Array | string ): Promise { const encodedURLData = encodeChannelURLData(channelData) - const encodedVerificationURLHash = await signEncodedURLData( + const encodedSignature = await signEncodedURLData( encodedURLData, communityPrivateKey ) - return new URL( - `${BASE_URL}/cc/${encodedURLData}#${encodedVerificationURLHash}` - ) + return new URL(`${BASE_URL}/cc/${encodedURLData}#${encodedSignature}`) } export function createUserURLWithENS(ensName: string): URL { @@ -64,12 +60,10 @@ export async function createUserURLWithData( userPrivateKey: Uint8Array | string ): Promise { const encodedURLData = encodeUserURLData(userData) - const encodedVerificationURLHash = await signEncodedURLData( + const encodedSignature = await signEncodedURLData( encodedURLData, userPrivateKey ) - return new URL( - `${BASE_URL}/u/${encodedURLData}#${encodedVerificationURLHash}` - ) + return new URL(`${BASE_URL}/u/${encodedURLData}#${encodedSignature}`) } diff --git a/packages/status-js/src/utils/encode-url-hash.test.ts b/packages/status-js/src/utils/encode-url-hash.test.ts deleted file mode 100644 index 8f32538d..00000000 --- a/packages/status-js/src/utils/encode-url-hash.test.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { describe, expect, test } from 'vitest' - -import { - decodeVerificationURLHash, - encodeVerificationURLHash, -} from './encode-url-hash' - -describe('Encode URL hash', () => { - test('should encode and decode verification hash', () => { - const data = { - signature: - 'k-n7d-9Pcx6ht87F4riP5xAw1v7S-e1HGMRaeaO068Q3IF1Jo8xOyeMT9Yr3Wv349Z2CdBzylw8M83CgQhcMogA=', // not generated by the pk - publicKey: 'zQ3shUHp2rAM1yqBYeo6LhFbtrozG5mZeA6cRoGohsudtsieT', - } - - const encodedHash = encodeVerificationURLHash(data) - const decodedHash = decodeVerificationURLHash(encodedHash) - - expect(encodedHash).toBe( - 'Co0BClhrLW43ZC05UGN4Nmh0ODdGNHJpUDV4QXcxdjdTLWUxSEdNUmFlYU8wNjhRM0lGMUpvOHhPeWVNVDlZcjNXdjM0OVoyQ2RCenlsdzhNODNDZ1FoY01vZ0E9EjF6UTNzaFVIcDJyQU0xeXFCWWVvNkxoRmJ0cm96RzVtWmVBNmNSb0dvaHN1ZHRzaWVU' - ) - expect(decodedHash).toEqual(data) - }) -}) diff --git a/packages/status-js/src/utils/encode-url-hash.ts b/packages/status-js/src/utils/encode-url-hash.ts deleted file mode 100644 index 99d3468d..00000000 --- a/packages/status-js/src/utils/encode-url-hash.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { base64url } from '@scure/base' - -import { URLHash, Verification } from '../protos/url_pb' - -import type { PlainMessage } from '@bufbuild/protobuf' - -export type EncodedVerificationURLHash = string & { - _: 'EncodedVerificationURLHash' -} - -export function encodeVerificationURLHash( - data: PlainMessage -): EncodedVerificationURLHash { - return encodeURLHash( - new Verification(data).toBinary() - ) as EncodedVerificationURLHash -} - -export function decodeVerificationURLHash( - data: string -): PlainMessage { - const deserialized = decodeURLHash(data) - - return Verification.fromBinary( - deserialized.content - ).toJson() as PlainMessage -} - -function encodeURLHash(data: Uint8Array): string { - const serialized = new URLHash({ content: data }).toBinary() - const encoded = base64url.encode(serialized) - - return encoded -} - -function decodeURLHash(data: string): URLHash { - const decoded = base64url.decode(data) - const deserialized = URLHash.fromBinary(decoded) - - return deserialized -} diff --git a/packages/status-js/src/utils/sign-url-data.test.ts b/packages/status-js/src/utils/sign-url-data.test.ts index 5292b95c..c6c15585 100644 --- a/packages/status-js/src/utils/sign-url-data.test.ts +++ b/packages/status-js/src/utils/sign-url-data.test.ts @@ -1,7 +1,9 @@ import { expect, test } from 'vitest' -import { encodeVerificationURLHash } from './encode-url-hash' -import { signEncodedURLData, verifyEncodedURLData } from './sign-url-data' +import { + recoverPublicKeyFromEncodedURLData, + signEncodedURLData, +} from './sign-url-data' import type { EncodedURLData } from './encode-url-data' @@ -13,50 +15,41 @@ const publicKey = '0x04f9134866f2bd8f45f2bc7893c95a6b989378c370088c9a1a5a53eda2ebb8a1e8386921592b6bd56fc3573f03c46df3396cc42e2993cdc001855c858865d768a7' const encodedURLData = 'G74AgK0ObFNmYT-WC_Jcc9KfSjHXAQo9THKEEbgPaJoItceMES-bUxr2Tj9efv447rRefBIUg9CEsSFyjBOFTRdZ9PH2wUOW8hVNYqIje3BC96mZ8uFogqM6k7gCCJnMHy4ulsmsgHTdeh5dAzTNNuG8m9XB8oVeildTCKlRhINnTZh4kAl5sP8SzBB4V2_I41a8PKl3mcS0z_eF5gA=' as EncodedURLData +const encodedSignature = + 'k-n7d-9Pcx6ht87F4riP5xAw1v7S-e1HGMRaeaO068Q3IF1Jo8xOyeMT9Yr3Wv349Z2CdBzylw8M83CgQhcMogA=' -test('should verify URL data and correspoinding signature', async () => { - const encodedVerificationURLHash = await signEncodedURLData( - encodedURLData, - privateKey - ) - - expect(verifyEncodedURLData(encodedURLData, encodedVerificationURLHash)).toBe( - true +test('should sign URL data', async () => { + expect(await signEncodedURLData(encodedURLData, privateKey)).toBe( + encodedSignature ) }) -test('should not verify URL data and random signature', async () => { - const randomSignature = +test('should recover original public key from URL data', async () => { + expect( + await recoverPublicKeyFromEncodedURLData(encodedURLData, encodedSignature) + ).toBe(publicKey) +}) + +test('should not recover original public key from same URL data but changed signature', async () => { + const changedEncodedSignature = 'OyOgY6Zta8S7U4l5Bv_9E_7snALhixwvjxORVAVJ-YJk-tMSGgstOy5XEEQx25TQJIAtpWf8eHnEmV8V-GmouQA=' - const encodedVerificationURLHash = encodeVerificationURLHash({ - signature: randomSignature, - publicKey, - }) - - expect(verifyEncodedURLData(encodedURLData, encodedVerificationURLHash)).toBe( - false - ) - // see https://github.com/paulmillr/noble-secp256k1/issues/43#issuecomment-1020214968 - // expect(verifyEncodedURLData(randomSignature, encodedURLData)).toBe(false) + expect( + await recoverPublicKeyFromEncodedURLData( + encodedURLData, + changedEncodedSignature + ) + ).not.toBe(publicKey) }) -test('should not verify random URL data and random signature', async () => { - const randomEncodedURLData = +test('should not recover original public key from same signature but changed URL data', async () => { + const changedEncodedURLData = 'CyeACk0KHkxvcmVtIGlwc3VtIGRvbG9yIHNpdCBlZ2VzdGFzLhIYV2UgZG8gbm90IHN1cHBvcnQgQWxpY2UuGMCEPSIHIzQzNjBERioEAQIDBAM=' as EncodedURLData - const randomSignature = - 'k-n7d-9Pcx6ht87F4riP5xAw1v7S-e1HGMRaeaO068Q3IF1Jo8xOyeMT9Yr3Wv349Z2CdBzylw8M83CgQhcMogA=' - - const encodedVerificationURLHash = encodeVerificationURLHash({ - signature: randomSignature, - publicKey, - }) expect( - verifyEncodedURLData(randomEncodedURLData, encodedVerificationURLHash) - ).toBe(false) - // see https://github.com/paulmillr/noble-secp256k1/issues/43#issuecomment-1020214968 - // expect(verifyEncodedURLData(randomSignature, randomEncodedURLData)).toBe( - // false - // ) + await recoverPublicKeyFromEncodedURLData( + changedEncodedURLData, + encodedSignature + ) + ).not.toBe(publicKey) }) diff --git a/packages/status-js/src/utils/sign-url-data.ts b/packages/status-js/src/utils/sign-url-data.ts index 4d92abfc..7b06dfdd 100644 --- a/packages/status-js/src/utils/sign-url-data.ts +++ b/packages/status-js/src/utils/sign-url-data.ts @@ -1,51 +1,30 @@ import { base64url } from '@scure/base' -import { getPublicKey } from 'ethereum-cryptography/secp256k1' -import { bytesToHex } from 'ethereum-cryptography/utils' +import { toHex, utf8ToBytes as toBytes } from 'ethereum-cryptography/utils' -import { deserializePublicKey } from './deserialize-public-key' -import { - decodeVerificationURLHash, - encodeVerificationURLHash, -} from './encode-url-hash' -import { serializePublicKey } from './serialize-public-key' -import { signData, verifySignedData } from './sign-data' +import { recoverPublicKey } from './recover-public-key' +import { signData } from './sign-data' import type { EncodedURLData } from './encode-url-data' -import type { EncodedVerificationURLHash } from './encode-url-hash' export async function signEncodedURLData( encodedURLData: EncodedURLData, privateKey: Uint8Array | string -): Promise { +): Promise { const signature = await signData(encodedURLData, privateKey) - const encodedSignature = base64url.encode(signature) - const serializedPublicKey = serializePublicKey( - `0x${bytesToHex(getPublicKey(privateKey))}` - ) - return encodeVerificationURLHash({ - signature: encodedSignature, - publicKey: serializedPublicKey, - }) + return encodedSignature } -export function verifyEncodedURLData( +export function recoverPublicKeyFromEncodedURLData( encodedURLData: string, - encodedVerificationURLHash: string -): boolean { - const { signature, publicKey } = decodeVerificationURLHash( - encodedVerificationURLHash - ) - - const decodedSignature = base64url.decode(signature) - const deserializedPublicKey = deserializePublicKey(publicKey, { - compress: false, - }) - - return verifySignedData( + encodedSignature: string +): string { + const decodedSignature = base64url.decode(encodedSignature) + const recoveredPublicKey = recoverPublicKey( decodedSignature, - encodedURLData, - deserializedPublicKey + toBytes(encodedURLData) ) + + return `0x${toHex(recoveredPublicKey)}` } diff --git a/packages/status-js/vite.config.ts b/packages/status-js/vite.config.ts index ca0b8d09..cf703902 100644 --- a/packages/status-js/vite.config.ts +++ b/packages/status-js/vite.config.ts @@ -26,11 +26,7 @@ export default defineConfig(({ mode }) => { build: { target: 'es2020', lib: { - entry: [ - './src/index.ts', - './src/utils/encode-url-data.ts', - './src/utils/encode-url-hash.ts', - ], + entry: ['./src/index.ts', './src/utils/encode-url-data.ts'], fileName: (format, entryName) => { if (!['es'].includes(format)) { throw new Error(`Unexpected format: ${format}`)