基本的に、操作をパーツに分割できます。
- どういうわけか最初の番号を「暗号化」して
n
、2 つの連続した番号が (非常に) 異なる結果をもたらすようにします
- ステップ 1 の結果から「人間が読める」コードを作成します。
ステップ 1 では、単純なブロック暗号を使用することをお勧めします (たとえば、任意のラウンド関数を使用したFeistel 暗号)。この質問も参照してください。
Feistel 暗号は複数のラウンドで動作します。各ラウンド中に、いくつかのラウンド関数が入力の半分に適用され、結果がxor
残りの半分で編集され、2 つの半分が交換されます。Feistel 暗号の優れた点は、ラウンド関数が双方向である必要がないことです (ラウンド関数への入力は各ラウンド後に変更されずに保持されるため、ラウンド関数の結果は復号化中に再構築できます)。したがって、好きなクレイジーな操作を選択できます:)。また、Feistel 暗号は対称であり、最初の要件を満たします。
C# での短い例
const int BITCOUNT = 30;
const int BITMASK = (1 << BITCOUNT/2) - 1;
static uint roundFunction(uint number) {
return (((number ^ 47894) + 25) << 1) & BITMASK;
}
static uint crypt(uint number) {
uint left = number >> (BITCOUNT/2);
uint right = number & BITMASK;
for (int round = 0; round < 10; ++round) {
left = left ^ roundFunction(right);
uint temp = left; left = right; right = temp;
}
return left | (right << (BITCOUNT/2));
}
(最後のラウンドの後、スワッピングがないことに注意してください。コードでは、スワッピングは結果の構築で単純に元に戻されます)
要件3と4を満たすこととは別に(関数はtotalであるため、入力が異なると出力が異なり、入力は非公式の定義に従って「完全にスクランブル」されます)、それは独自の逆でもあります(したがって、暗黙的に要件1を満たします)。つまり、入力ドメインのcrypt(crypt(x))==x
それぞれに対して(この実装では)。また、パフォーマンス要件の点で安価です。x
0..2^30-1
ステップ 2 では、結果を任意のベースにエンコードします。たとえば、30 ビットの数値をエンコードするには、32 文字のアルファベットの 6 つの「桁」を使用できます (したがって、6*5=30 ビットをエンコードできます)。
C# でのこのステップの例:
const string ALPHABET= "AG8FOLE2WVTCPY5ZH3NIUDBXSMQK7946";
static string couponCode(uint number) {
StringBuilder b = new StringBuilder();
for (int i=0; i<6; ++i) {
b.Append(ALPHABET[(int)number&((1 << 5)-1)]);
number = number >> 5;
}
return b.ToString();
}
static uint codeFromCoupon(string coupon) {
uint n = 0;
for (int i = 0; i < 6; ++i)
n = n | (((uint)ALPHABET.IndexOf(coupon[i])) << (5 * i));
return n;
}
入力 0 ~ 9 の場合、次のクーポン コードが生成されます
0 => 5VZNKB
1 => HL766Z
2 => TMGSEY
3 => P28L4W
4 => EM5EWD
5 => WIACCZ
6 => 8DEPDA
7 => OQE33A
8 => 4SEQ5A
9 => AVAXS5
このアプローチには 2 つの異なる内部「秘密」があることに注意してください。1 つ目はラウンド関数と使用されるラウンド数、2 つ目は暗号化された結果のエンコードに使用するアルファベットです。ただし、示されている実装は、暗号化の意味で決して安全ではないことに注意してください。
また、示されている関数は、可能なすべての 6 文字コード (アルファベット外の文字を含む) が一意の数値を生成するという意味で、全単射関数であることに注意してください。誰かがランダムなコードを入力するのを防ぐには、入力番号に対してある種の制限を定義する必要があります。たとえば、最初の 10.000 番号に対してのみクーポンを発行します。次に、ランダムなクーポン コードが有効になる確率は 10000/2^30=0.00001 になります (正しいクーポン コードを見つけるには、約 50000 回の試行が必要です)。さらに「セキュリティ」が必要な場合は、ビット サイズ/クーポン コードの長さを増やすことができます (以下を参照)。
編集:クーポンコードの長さを変更する
結果のクーポン コードの長さを変更するには、いくつかの計算が必要です。最初の (暗号化) ステップは、ビット数が偶数のビット文字列でのみ機能します (これは、Feistel 暗号が機能するために必要です)。
2 番目のステップでは、特定のアルファベットを使用してエンコードできるビット数は、選択したアルファベットの「サイズ」とクーポン コードの長さに依存します。この「エントロピー」はビット単位で与えられますが、一般に整数ではなく、偶数の整数ではありません。例えば:
30 文字のアルファベットを使用した 5 桁のコードは、30^5 の可能なコードになります。これは、ld(30^5)=24.53ビット/クーポン コードを意味します。
4 桁のコードの場合、簡単な解決策があります。32 文字のアルファベットを指定すると、*ld(32^4)=5*4=20* ビットをエンコードできます。したがって、BITCOUNT
を 20に設定し、コードの 2 番目の部分のループを(の代わりに)for
まで実行するように変更できます。4
6
5 桁のコードを生成するのは少しトリッキーで、どういうわけかアルゴリズムを「弱体化」させます: を 24 に設定して、30 文字のアルファベットから 5 桁のコードを生成するだけです (文字列BITCOUNT
から 2 文字を削除し、ループを実行させます)。まで)。ALPHABET
for
5
しかし、これはすべての可能な 5 桁コードを生成するわけではありません: 24 ビットでは、暗号化段階から 16,777,216 の可能な値しか取得できず、5 桁のコードは 24,300,000 の可能な数字をエンコードできるため、一部の可能なコードは決して生成されません。より具体的には、コードの最後の位置にアルファベットの文字が含まれることはありません。これは、有効なコードのセットを明白な方法で絞り込むため、欠点と見なすことができます。
クーポン コードをデコードするときは、まず関数を実行してからcodeFromCoupon
、結果のビット 25 が設定されているかどうかを確認する必要があります。これにより、すぐに拒否できる無効なコードがマークされます。実際には、アルゴリズムのすべての内部を公開することなく、コードの有効性を (クライアント側などで) 迅速にチェックできるため、これは利点でさえあることに注意してください。
ビット 25 が設定されていない場合は、crypt
関数を呼び出して元の番号を取得します。