CNP prüfen: rumänische Personennummer in TypeScript und Zod

Dein Formular für Checkout, Lohnabrechnung oder Mietvertrag fragt nach dem CNP (cod numeric personal, die rumänische Personennummer), und ein Regex für 13 Ziffern akzeptiert jeden Tippfehler. Wer einen CNP prüfen will, muss mehr testen als die Länge: eine Ziffer für Geschlecht und Jahrhundert, ein Geburtsdatum, das existiert, einen Kreiscode und eine gewichtete Prüfziffer. In diesem Tutorial schreiben wir diesen Check in TypeScript, zerlegen den Code in seine Teile, machen daraus eine zod-Regel und ein React-Feld, das beim Tippen prüft, und klären am Ende, was du tun musst, bevor du auch nur einen CNP speicherst. Das Feld ist Masked Input aus Wingo UI, der Bibliothek, die wir bauen. Wir sind also nicht neutral. Der Validator ist reines TypeScript, das du überall einfügen kannst.
Wie ist der cod numeric personal aufgebaut?
Dreizehn Ziffern in sieben Teilen, geschrieben als S AA LL ZZ JJ NNN C. Der rumänische Wikipedia-Artikel zum CNP (opens in a new tab) beschreibt jeden davon. Hier sind sie am Beispiel 1960523123457, dem Beispielcode aus der Demo weiter unten:
Von 1 bis 8 ist die erste Ziffer bei Männern ungerade und bei Frauen gerade. 1 und 2 bedeuten: geboren in den 1900ern, 3 und 4 in den 1800ern, 5 und 6 in den 2000ern. 7 und 8 stehen für ausländische Einwohner, und bei ihnen ist das Jahrhundert nicht kodiert. Die englische Wikipedia führt außerdem 9 für ausländische Staatsangehörige; der rumänische Artikel hört bei 8 auf. Behandle 9 also als selten und akzeptier sie.
Die Kreiscodes sind 01 bis 39 für die Kreise in alphabetischer Reihenfolge (Alba bis Vrancea), 40 für Bukarest, 41 bis 46 für seine sechs Sektoren, 47 und 48 für die früheren Sektoren 7 und 8, die nur in alten Codes vorkommen, und 51 und 52 für Călărași und Giurgiu. Eine 49 oder 50 gibt es nicht.
Und seit Mai 2024 gibt es die 70. Codes, die Rumäniens neues IT-System für den Personenstand erzeugt, tragen bei jeder Registrierung 70 anstelle des Kreises, egal wo die Person geboren ist. Das Innenministerium erklärte im Mai 2024 (opens in a new tab), dass das mit Geburten begann, die ab dem 8. Mai im Kreis Ilfov und in sieben Städten registriert wurden, dazu mit den Geburtsurkunden frisch eingebürgerter Personen, und dass bestehende Codes sich nicht ändern. Auch der rumänische Wikipedia-Artikel führt die 70 inzwischen. Zwei Folgen für deinen Code: Eine Bereichsprüfung von 01 bis 52 lehnt jeden Code des neuen Systems ab, und aus einem neuen Code kannst du keinen Kreis mehr ablesen.
Wie funktioniert die CNP-Prüfziffer?
Die CNP-Prüfziffer ist eine gewichtete Summe mod 11. Multipliziere jede der ersten 12 Ziffern mit der Ziffer an derselben Stelle des Schlüssels 279146358279, addiere die Produkte und teile durch 11. Der Rest ist die Prüfziffer, nur dass ein Rest von 10 zu 1 wird. Hier ist das Beispiel von Hand gerechnet:
Die Produkte ergeben zusammen 260, und 260 mod 11 ist 7, die letzte Ziffer von 1960523123457.
Genau dort, wo aus 10 eine 1 wird, wird der Check schwächer. Die Reste 1 und 10 ergeben beide eine 1, also enden etwa 2 von 11 Codes auf 1, und ein Tippfehler, der die Summe vom einen zum anderen verschiebt, kommt durch. 1960523120031 ist gültig; änderst du die zwölfte Ziffer, besteht 1960523120021 ebenfalls.
Wir haben das mit der Funktion checkCnp weiter unten gemessen. Bei 20.000 zufälligen gültigen Codes haben wir jede Änderung einer einzelnen Ziffer und jede Vertauschung zweier benachbarter Ziffern ausprobiert. Die Prüfsumme allein übersah 1,7 % der Änderungen, alle wegen dieser doppelten 1, und 1,9 % der Vertauschungen. Mit dem zusätzlichen Check von Datum und Kreis sanken beide auf etwa 1,2 %. Von einer Million zufälliger 13-stelliger Strings bestanden 10 % die Prüfsumme und 0,14 % alle Checks. Etwa 1 von 80 Tippfehlern besteht also jeden Check, und deshalb zeigt das Formular unten der Person auch an, was in ihrem Code steht.
Wie validiere ich einen CNP in JavaScript oder TypeScript?
Führ die Checks der Reihe nach aus und sag, welcher fehlgeschlagen ist. So kann das Formular das Problem erklären, statt nur „ungültig“ anzuzeigen. Gib die zerlegten Teile aus demselben Durchlauf zurück, damit nichts den Code zweimal parst. Der Code ist TypeScript ohne Abhängigkeit; lass die Typen weg, und er läuft als reines JavaScript:
Vier Details, die oft schiefgehen, wenn du einen CNP in JavaScript validieren willst:
- Das Jahrhundert kommt aus der ersten Ziffer.
new Date(24, 5, 1)ist der 1. Juni 1924, weil JavaScript zweistellige Jahre auf die 1900er abbildet. Bau zuerst das volle Jahr und prüf, ob der Tag die Umwandlung hin und zurück übersteht, sonst rutscht der 29. Februar 1900 durch. - Ein Geburtsdatum kann nicht in der Zukunft liegen.
5300101123456ergibt den 1. Januar 2030. Vergleich mit dem heutigen Tag in Rumänien, nicht in UTC oder in der Zeitzone des Servers. - Die Kreisliste hat Lücken und ein neues Mitglied. 49 und 50 gibt es nicht, die 70 schon.
- Codes, die mit 7 bis 9 beginnen, haben kein Jahrhundert. Wir akzeptieren den Tag, wenn er in einem der beiden Jahrhunderte existiert, und geben
birthDate: nullzurück, statt zu raten. Rätst du falsch, wird aus einer 11-jährigen Einwohnerin eine 111-jährige.
Wie lese ich Geburtsdatum, Alter und Kreis aus einem CNP?
Hol sie dir aus info und mach daraus einen Satz, den die Person mit ihrem Ausweis vergleichen kann. Zeigst du einen gültigen Code so in Klartext an, fängst du den Fehler ab, den keine Prüfsumme findet: Jemand tippt einen gültigen Code ein, der dem Ehepartner gehört.
Ein Code mit Kreis 70 liest sich als „Männlich, geboren am 1. Juni 2024“, ohne Kreis, und das stimmt auch.
Wie schreibe ich eine zod-Regel für einen CNP?
Pack checkCnp in einen Check, der jedes Problem auf eine eigene Meldung abbildet, und entferne danach die Trennzeichen, damit die Datenbank die 13 Ziffern bekommt. Das Schema nutzt zod 4 (.check() und den Parameter error):
„Die Ziffern 2 bis 7 sind kein echtes Geburtsdatum“ sagt der Person, wo sie suchen muss. „Ungültiger CNP“ tut das nie. Wenn du die form-validation-Lib aus Wingo UI Pro nutzt: Sie hat Regeln für CUI und IBAN, aber keine für den CNP, also steht diese Regel daneben. Mit const v = createValidators() schreibst du z.object({ email: v.email(), iban: v.iban(), cnp }).
Wie baue ich ein CNP-Feld, das beim Tippen prüft?
Nimm ein maskiertes Feld mit Zifferntastatur und zeig das Ergebnis, sobald die 13. Ziffer drin ist. Das Preset cnp von Masked Input setzt inputMode="numeric" und autoComplete="off", begrenzt den Wert auf 13 Ziffern, entfernt Leerzeichen, Punkte und Bindestriche beim Einfügen und blendet ein Häkchen oder ein Warnsymbol ein, sobald der Code vollständig ist. Der rote Rahmen und aria-invalid warten, bis die Person das Feld verlässt. Der eigene Check des Presets akzeptiert jeden Kreis von 01 bis 52, lässt also 49 und 50 durch und lehnt 70 ab. Übergib validate={isValidCnp}, dann stimmt das Feld bei jedem Code mit der zod-Regel überein.
$ npx wingo-ui@latest add masked-inputBind es mit dem Controller von React Hook Form an (npm install react-hook-form @hookform/resolvers zod). Nimm Controller, nicht register: Das Input-Element hält den maskierten Text, und die rohen Ziffern kommen über onValueChange:
Das Feld nutzt auf Smartphones 16px große Schrift, damit iOS nicht hineinzoomt, und wird auf Touchscreens 44px hoch. Sein error ersetzt die Beschreibung, setzt aria-invalid und verknüpft die Meldung über aria-describedby. Als onSave übergibst du eine Server Action, die den Code mit dem Vault aus dem nächsten Abschnitt speichert; der CNP steckt dann im Body eines POST-Requests, nie in einer URL.
Solltest du CNPs überhaupt erheben und speichern?
Nur wenn ein Gesetz oder ein Vertrag einen verlangt. Ein CNP verrät ein Geburtsdatum und ein Geschlecht, bei Codes von vor 2024 auch einen Kreis. Wird er geleakt, wiegt das also schwerer als bei einer E-Mail-Adresse. Wenn du nur ein Alter brauchst, frag nach dem Geburtsdatum. Auch für B2C-Rechnungen über e-Factura brauchst du ihn nicht: Seit OUG 138/2024 darf ein Unternehmen 0000000000000 verwenden, wenn ein privater Käufer keine Steuernummer angibt, und es muss keine CNPs erheben (opens in a new tab). Akzeptier diese 13 Nullen, bevor du checkCnp aufrufst, wenn das Feld in eine Rechnung fließt.
$ npx wingo-ui@latest add invoicingWenn du ihn erhebst, gilt zusätzlich zur DSGVO das rumänische Gesetz 190/2018 (opens in a new tab). Sein Artikel 2 führt den CNP als nationale Identifikationsnummer, und Artikel 4 legt die Regeln für ihre Verarbeitung fest. Die Verarbeitung braucht eine Rechtsgrundlage nach DSGVO, und wenn diese das berechtigte Interesse ist, verlangt das Gesetz vier Schutzmaßnahmen: technische und organisatorische Maßnahmen für Datenminimierung und Sicherheit, einen Datenschutzbeauftragten, Speicherfristen mit Löschterminen und regelmäßige Schulungen für die Leute, die damit arbeiten. Das ist keine Rechtsberatung; dein Datenschutzbeauftragter entscheidet, was für dich gilt.
Was du technisch tun kannst: den Code von jedem Ort fernhalten, an dem er nicht sein muss. Fang beim Speichern an, denn ein Hash ist keine Anonymisierung. Der Raum gültiger Codes ist klein. Alle, die in den 1900ern geboren sind, passen in etwa 3,6 Milliarden Kandidaten: 36.524 Tage, 2 Geschlechtsziffern, 50 Kreiscodes und 999 laufende Nummern, die Prüfziffer jeweils berechnet. Auf einem Kern unseres Dev-Servers hat createHash von Node etwa 2 Millionen Codes pro Sekunde gehasht, also dauert das ganze Jahrhundert etwa eine halbe Stunde. Wer das Geburtsdatum kennt, hat nur noch 99.900 Kandidaten vor sich, etwa 50 Millisekunden Hashing.
Speichere also zwei Dinge: den Code verschlüsselt und einen Keyed Hash für Abfragen und Unique-Indizes. Der Schlüssel liegt nie in der Datenbank:
Die maskierte Form behält die laufende Nummer und die Prüfziffer und versteckt Geschlecht, Geburtsdatum und Kreis. Der Rest ist eine Checkliste:
- Nie in einer URL. Schick den Code im Body eines POST-Requests. Query-Strings landen in Access-Logs, im Browserverlauf und in Analytics-Tools.
- Nie in Logs oder Fehlerberichten. Schwärze
cnpin deinem Logger und im Event-Filter deines Error Trackers, und prüf, ob Session Replay das Feld maskiert. - Nie zurück an den Browser. Nach dem Speichern bekommt der Client
maskCnp, und nur eine Server-Route, die den vollen Code braucht, ruftdecryptCnpauf. - Ein Löschdatum. Leg fest, wann jeder gespeicherte Code gelöscht wird, und lass den Job laufen, der ihn löscht.
Solltest du stattdessen ein CNP-Validator-Paket nutzen?
Erst wenn du seine Kreistabelle gelesen hast. Der Code 70 ist neu, und ein Paket, das ihn nicht kennt, lehnt echte Codes ab. Stand Oktober 2026 ist das meistgeladene CNP-Paket auf npm, romanian-personal-identity-code-validator (opens in a new tab), bei Version 1.1.4 (veröffentlicht am 19. Mai 2024, MIT, 667 Downloads vom 1. bis 7. Oktober 2026). Es liest Geburtsdatum, Geschlecht, Geburtsort und Alter aus, hängt von Moment.js ab, und seiner Kreistabelle fehlt die 70, also lehnt es jeden Code des neuen Systems ab. Die 80 führt es dagegen, als im Ausland vergebenen Code. Das Modul ro.cnp von python-stdnum (opens in a new tab) akzeptiert die 70 und außerdem 80 bis 83, beschriftet mit „Unknown“. Für 80 bis 83 haben wir keine offizielle Quelle gefunden, also lässt der Code oben sie weg. Wenn der Code eines echten Kunden an einem davon scheitert, ergänze ihn.
lib/cnp.ts hat etwa 100 Zeilen ohne Abhängigkeit, und du kannst jede Regel darin lesen. Wenn sich das Format wieder ändert, bearbeitest du eine Tabelle, statt auf einen Maintainer zu warten.
Solltest du den Code kopieren oder installieren?
Kopier ihn, wenn du nur ein Feld brauchst: lib/cnp.ts, die zod-Regel und das kostenlose Input mit inputMode="numeric" reichen für ein Vertrags- oder Lohnformular. Masked Input, das Feld aus der Demo, gehört zu Wingo UI Pro, genau wie die invoicing-Lib, deren parseCnp und isValidCnp Geschlecht, Geburtsdatum und Kreis auslesen, die 70 akzeptieren und 0000000000000 als Käufer ohne CNP behandeln. Die Prüfung auf ein Geburtsdatum in der Zukunft lassen sie aus, also behalte checkCnp für das Formular. Installier das Feld mit npx wingo-ui@latest add masked-input.
Die anderen rumänischen Kennungen haben eigene Anleitungen: CUI prüfen für Firmen und IBAN prüfen in JavaScript für Bankkonten. Für den restlichen Aufbau des Formulars lies React-Formularkomponenten: barrierefreie Felder, zod und Mobile, oder stöbere in weiteren Komponenten-Guides.
Häufige Fragen
Wie prüfe ich einen rumänischen CNP?
Prüf, ob er 13 Ziffern hat, ob die erste Ziffer 1 bis 9 ist, ob die Ziffern 2 bis 7 ein echtes Geburtsdatum ergeben, das nicht in der Zukunft liegt, und ob die Ziffern 8 und 9 ein Kreiscode sind (01 bis 48, 51, 52 oder 70). Dann multiplizierst du die ersten 12 Ziffern mit 2, 7, 9, 1, 4, 6, 3, 5, 8, 2, 7, 9, addierst die Produkte und nimmst die Summe mod 11, wobei 10 zu 1 wird; das Ergebnis muss der letzten Ziffer entsprechen.
Was bedeutet der Kreiscode 70 in einem CNP?
Seit Mai 2024 tragen die Codes, die Rumäniens neues IT-System für den Personenstand erzeugt, 70 anstelle des Kreises, egal wo die Person geboren ist. Früher ausgegebene Codes behalten ihren Kreis, also lehnt ein Validator, der nur 01 bis 52 akzeptiert, jeden Code des neuen Systems ab.
Warum nutzt die CNP-Prüfziffer eine 1, wenn der Rest 10 ist?
Die Prüfziffer hat nur Platz für eine Ziffer, also wird ein Rest von 10 als 1 geschrieben. Der Preis: Die Reste 1 und 10 sehen gleich aus, also besteht ein Tippfehler, der die Summe zwischen den beiden verschiebt, den Check trotzdem; in unserem Test waren das 1,7 % der Tippfehler an einer einzelnen Ziffer.
Kann ich das Alter einer Person aus ihrem CNP ablesen?
Bei Codes, die mit 1 bis 6 beginnen, ja: Die erste Ziffer liefert das Jahrhundert, die Ziffern 2 bis 7 Jahr, Monat und Tag. Codes für ausländische Einwohner und ausländische Staatsangehörige beginnen mit 7, 8 oder 9 und enthalten kein Jahrhundert, also frag nach dem Geburtsdatum, wenn das Alter zählt.
Darf ich einen CNP in meiner Datenbank speichern?
Das rumänische Gesetz 190/2018 führt den CNP als nationale Identifikationsnummer: Du brauchst eine Rechtsgrundlage nach DSGVO, und wenn diese das berechtigte Interesse ist, außerdem Maßnahmen zu Datenminimierung und Sicherheit, einen Datenschutzbeauftragten, Speicherfristen und Schulungen für das Personal. Erheb ihn nur, wenn ein Gesetz oder ein Vertrag ihn verlangt, und frag vorher deinen Datenschutzbeauftragten.
Brauche ich den CNP des Kunden für eine B2C-Rechnung über e-Factura?
Nein. Seit OUG 138/2024 darf ein Unternehmen 0000000000000 verwenden, also dreizehn Nullen, wenn ein privater Käufer keine Steueridentifikationsnummer angibt, und es muss den CNP nicht erheben.
- CNP
- Rumänien
- Formularvalidierung
- Zod
- TypeScript
- Datenschutz


