Реализация алгоритма SimpleAuth в Java

Discussion in 'Разработка плагинов' started by fromgate, 2/1/16.

  1. This site uses cookies. By continuing to use this site, you are agreeing to our use of cookies. Learn More.
Dismiss Notice
We welcome you on our site. This site is devoted to the Nukkit project and all that is connected with him. Here you can communicate, download plugins, also many other things get acquainted! Register the account right now :3
  1. fromgate

    fromgate Administrator

    Messages:
    664
    Likes Received:
    186
    Насколько я понял в PocketMine активно использовался плагин SimpleAuth в качестве плагина авторизации.
    Поэтому было бы при переходе с PocketMine на Nukkit было бы логично использовать существующую пользовательскую базу.
    Однако, каждый кто попробует это сделать столкнётся с тем, что в SimpleAuth используется своеобразный алгоритм для хранения хэша пароля:

    Code:
     private function hash($salt, $password){
    return bin2hex(hash("sha512", $password . $salt, true) ^ hash("whirlpool", $salt . $password, true));
    }
    Чтобы было понятно. Хэш пароля генерируется с использованием соли. При этом, в качестве соли выступает имя пользователя (что само по себе бред - соль должна быть генерируемой). Кроме того, алгоритм использует не один какой-то алгоритм хэширования, а сразу два - формируются хэши по алгоритмам shat512 и whirlpool, а потом заксориваются друг на друга.

    Если Вы соберётесь писать собственный плагин авторизации - не используйте такого подхода. Я конечно, не профессионал в криптографии, но ясно что подобный алогритм не дает каких-то дополнительных преимуществ при формировании хэша. Устойчивее он не становится ;)

    Но если у Вас стоит задача проверять пароли SimpleAuth из плагина для Nukkit, то тогда Вы обнаружите, что в Java нет поддержки алгоритма Whirlpool и для этого надо либо воспользоваться сторонними методами либо писать его самому. Я воспользовался соответствующим классом из плагина xAuth и готовое решение у меня представлено в виде двух классов.

    Пользоваться ими достаточно просто - скопируйте их себе в проект (имя пакета измените на своё).

    Чтобы получить hash надо будет выполнить методо
    Code:
    String hash = SimpleAuthHasher.getHash("fromgate","12345password")
    
    Ну а вот сами классы:

    Code:
    package ru.nukkit.test;
    
    import java.security.MessageDigest;
    
    public class SimpleAuthHasher {
    
    [pre][code]public static String getHash(String userName, String password){
        byte[] hashSha = sha512(password+userName);
        byte[] hashWhpl = whirlpool(userName+password);
        if (hashSha==null) return null;
        byte[] result = xor(hashSha,hashWhpl);
        if (result == null) return null;
        return Whirlpool.display(result);
    }
    
    public static byte[] xor (byte [] arr1, byte [] arr2){
        if (arr1.length!=arr2.length) return null;
        byte [] rst = new byte[arr1.length];
        for (int i = 0; i<arr1.length;i++)
            rst [i] = (byte) (arr1[i]^arr2[i]);
        return  rst;
    }
    
    public static byte[] sha512 (String toHash) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-512");
            return digest.digest(toHash.getBytes("UTF-8"));
        } catch (Exception e){
        }
        return  null;
    }
    
    public static byte[] whirlpool (String toHash) {
        Whirlpool w = new Whirlpool();
        byte[] digest = new byte[64];
        w.NESSIEinit();
        w.NESSIEadd(toHash);
        w.NESSIEfinalize(digest);
        return digest;
    }
    [/pre]
    }
    [/code]

    Whirlpool.java — реализация хэширования алгоритма Whirlpool
    Code:
    package ru.nukkit.test;
    
    /**
    
    [list]
    [*]The Whirlpool hashing function.
    [*]
    [*]<P>
    [*]<b>References</b>
    [*]
    [*]<P>
    [*]The Whirlpool algorithm was developed by
    [*]<a href="mailto:pbarreto@scopus.com.br">Paulo S. L. M. Barreto</a> and
    [*]<a href="mailto:vincent.rijmen@cryptomathic.com">Vincent Rijmen</a>.
    [*]
    [*]See
    [*]P.S.L.M. Barreto, V. Rijmen,
    [*]``The Whirlpool hashing function,''
    [*]First NESSIE workshop, 2000 (tweaked version, 2003),
    [*][url="https://www.cosic.esat.kuleuven.ac.be/nessie/workshop/submissions/whirlpool.zip"]https://www.cosic.esat.kuleuven.ac.be/nessie/workshop/submissions/whirlpool.zip[/url]
    [*]
    [*]@author   Paulo S.L.M. Barreto
    [*]@author   Vincent Rijmen.
    [*]
    [*]@version 3.0 (2003.03.12)
    [*]
    [*]=============================================================================
    [*]
    [*]Differences from version 2.1:
    [*]
    [*][list]
    [*]Suboptimal diffusion matrix replaced by cir(1, 1, 4, 1, 8, 5, 2, 9).
    [/list]
    [*]
    [*]=============================================================================
    [*]
    [*]Differences from version 2.0:
    [*]
    [*][list]
    [*]Generation of ISO/IEC 10118-3 test vectors.
    [/list]
    [*][list]
    [*]Bug fix: nonzero carry was ignored when tallying the data length
    [/list]
    [*](this bug apparently only manifested itself when feeding data
    [*]in pieces rather than in a single chunk at once).
    [*]
    [*]Differences from version 1.0:
    [*]
    [*][list]
    [*]Original S-box replaced by the tweaked, hardware-efficient version.
    [/list]
    [*]
    [*]=============================================================================
    [*]
    [*]THIS SOFTWARE IS PROVIDED BY THE AUTHORS ''AS IS'' AND ANY EXPRESS
    [*]OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
    [*]WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    [*]ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
    [*]LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    [*]CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    [*]SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
    [*]BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
    [*]WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
    [*]OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
    [*]EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
    [*]*/
    [/list]
    import java.util.Arrays;
    
    class Whirlpool {
    
    [pre][code]/**
     * The message digest size (in bits)
     */
    public static final int DIGESTBITS = 512;
    
    /**
     * The message digest size (in bytes)
     */
    public static final int DIGESTBYTES = DIGESTBITS &gt;&gt;&gt; 3;
    
    /**
     * The number of rounds of the internal dedicated block cipher.
     */
    protected static final int R = 10;
    
    /**
     * The substitution box.
     */
    private static final String sbox =
            "\u1823\uc6E8\u87B8\u014F\u36A6\ud2F5\u796F\u9152" +
                    "\u60Bc\u9B8E\uA30c\u7B35\u1dE0\ud7c2\u2E4B\uFE57" +
                    "\u1577\u37E5\u9FF0\u4AdA\u58c9\u290A\uB1A0\u6B85" +
                    "\uBd5d\u10F4\ucB3E\u0567\uE427\u418B\uA77d\u95d8" +
                    "\uFBEE\u7c66\udd17\u479E\ucA2d\uBF07\uAd5A\u8333" +
                    "\u6302\uAA71\uc819\u49d9\uF2E3\u5B88\u9A26\u32B0" +
                    "\uE90F\ud580\uBEcd\u3448\uFF7A\u905F\u2068\u1AAE" +
                    "\uB454\u9322\u64F1\u7312\u4008\uc3Ec\udBA1\u8d3d" +
                    "\u9700\ucF2B\u7682\ud61B\uB5AF\u6A50\u45F3\u30EF" +
                    "\u3F55\uA2EA\u65BA\u2Fc0\udE1c\uFd4d\u9275\u068A" +
                    "\uB2E6\u0E1F\u62d4\uA896\uF9c5\u2559\u8472\u394c" +
                    "\u5E78\u388c\ud1A5\uE261\uB321\u9c1E\u43c7\uFc04" +
                    "\u5199\u6d0d\uFAdF\u7E24\u3BAB\ucE11\u8F4E\uB7EB" +
                    "\u3c81\u94F7\uB913\u2cd3\uE76E\uc403\u5644\u7FA9" +
                    "\u2ABB\uc153\udc0B\u9d6c\u3174\uF646\uAc89\u14E1" +
                    "\u163A\u6909\u70B6\ud0Ed\ucc42\u98A4\u285c\uF886";
    
    private static long[][] C = new long[8][256];
    private static long[]  rc = new long[R + 1];
    
    static {
        for (int x = 0; x &lt; 256; x++) {
            char c = sbox.charAt(x/2);
            long v1 = ((x &amp; 1) == 0) ? c &gt;&gt;&gt; 8 : c &amp; 0xff;
            long v2 = v1 &lt;&lt; 1;
            if (v2 &gt;= 0x100L) {
                v2 ^= 0x11dL;
            }
            long v4 = v2 &lt;&lt; 1;
            if (v4 &gt;= 0x100L) {
                v4 ^= 0x11dL;
            }
            long v5 = v4 ^ v1;
            long v8 = v4 &lt;&lt; 1;
            if (v8 &gt;= 0x100L) {
                v8 ^= 0x11dL;
            }
            long v9 = v8 ^ v1;
            /*
             * build the circulant table C[0][x] = S[x].[1, 1, 4, 1, 8, 5, 2, 9]:
             */
            C[0][x] =
                    (v1 &lt;&lt; 56) | (v1 &lt;&lt; 48) | (v4 &lt;&lt; 40) | (v1 &lt;&lt; 32) |
                            (v8 &lt;&lt; 24) | (v5 &lt;&lt; 16) | (v2 &lt;&lt;  8) | (v9    );
            /*
             * build the remaining circulant tables C[t][x] = C[0][x] rotr t
             */
            for (int t = 1; t &lt; 8; t++) {
                C[t][x] = (C[t - 1][x] &gt;&gt;&gt; 8) | ((C[t - 1][x] &lt;&lt; 56));
            }
        }
    
        /*
         * build the round constants:
         */
        rc[0] = 0L; /* not used (assigment kept only to properly initialize all variables) */
        for (int r = 1; r &lt;= R; r++) {
            int i = 8*(r - 1);
            rc[r] =
                    (C[0][i ] &amp; 0xff00000000000000L) ^
                            (C[1][i + 1] &amp; 0x00ff000000000000L) ^
                            (C[2][i + 2] &amp; 0x0000ff0000000000L) ^
                            (C[3][i + 3] &amp; 0x000000ff00000000L) ^
                            (C[4][i + 4] &amp; 0x00000000ff000000L) ^
                            (C[5][i + 5] &amp; 0x0000000000ff0000L) ^
                            (C[6][i + 6] &amp; 0x000000000000ff00L) ^
                            (C[7][i + 7] &amp; 0x00000000000000ffL);
        }
    }
    
    /**
     * Global number of hashed bits (256-bit counter).
     */
    protected byte[] bitLength = new byte[32];
    
    /**
     * Buffer of data to hash.
     */
    protected byte[] buffer = new byte[64];
    
    /**
     * Current number of bits on the buffer.
     */
    protected int bufferBits = 0;
    
    /**
     * Current (possibly incomplete) byte slot on the buffer.
     */
    protected int bufferPos = 0;
    
    /**
     * The hashing state.
     */
    protected long[] hash  = new long[8];
    protected long[] K   = new long[8]; // the round key
    protected long[] L   = new long[8];
    protected long[] block = new long[8]; // mu(buffer)
    protected long[] state = new long[8]; // the cipher state
    
    public Whirlpool() {
    }
    
    /**
     * The core Whirlpool transform.
     */
    protected void processBuffer() {
        /*
         * map the buffer to a block:
         */
        for (int i = 0, j = 0; i &lt; 8; i++, j += 8) {
            block[i] =
                    (((long)buffer[j    ]       ) &lt;&lt; 56) ^
                            (((long)buffer[j + 1] &amp; 0xffL) &lt;&lt; 48) ^
                            (((long)buffer[j + 2] &amp; 0xffL) &lt;&lt; 40) ^
                            (((long)buffer[j + 3] &amp; 0xffL) &lt;&lt; 32) ^
                            (((long)buffer[j + 4] &amp; 0xffL) &lt;&lt; 24) ^
                            (((long)buffer[j + 5] &amp; 0xffL) &lt;&lt; 16) ^
                            (((long)buffer[j + 6] &amp; 0xffL) &lt;&lt;  8) ^
                            (((long)buffer[j + 7] &amp; 0xffL)    );
        }
        /*
         * compute and apply K^0 to the cipher state:
         */
        for (int i = 0; i &lt; 8; i++) {
            state[i] = block[i] ^ (K[i] = hash[i]);
        }
        /*
         * iterate over all rounds:
         */
        for (int r = 1; r &lt;= R; r++) {
            /*
             * compute K^r from K^{r-1}:
             */
            for (int i = 0; i &lt; 8; i++) {
                L[i] = 0L;
                for (int t = 0, s = 56; t &lt; 8; t++, s -= 8) {
                    L[i] ^= C[t][(int)(K[(i - t) &amp; 7] &gt;&gt;&gt; s) &amp; 0xff];
                }
            }
            for (int i = 0; i &lt; 8; i++) {
                K[i] = L[i];
            }
            K[0] ^= rc[r];
            /*
             * apply the r-th round transformation:
             */
            for (int i = 0; i &lt; 8; i++) {
                L[i] = K[i];
                for (int t = 0, s = 56; t &lt; 8; t++, s -= 8) {
                    L[i] ^= C[t][(int)(state[(i - t) &amp; 7] &gt;&gt;&gt; s) &amp; 0xff];
                }
            }
            for (int i = 0; i &lt; 8; i++) {
                state[i] = L[i];
            }
        }
        /*
         * apply the Miyaguchi-Preneel compression function:
         */
        for (int i = 0; i &lt; 8; i++) {
            hash[i] ^= state[i] ^ block[i];
        }
    }
    
    /**
     * Initialize the hashing state.
     */
    public void NESSIEinit() {
        Arrays.fill(bitLength, (byte)0);
        bufferBits = bufferPos = 0;
        buffer[0] = 0; // it's only necessary to cleanup buffer[bufferPos].
        Arrays.fill(hash, 0L); // initial value
    }
    
    /**
     * Delivers input data to the hashing algorithm.
     *
     * @param   source      plaintext data to hash.
     * @param   sourceBits  how many bits of plaintext to process.
     *
     * This method maintains the invariant: bufferBits &lt; 512
     */
    public void NESSIEadd(byte[] source, long sourceBits) {
        /*
                           sourcePos
                           |
                           +-------+-------+-------
                              ||||||||||||||||||||| source
                           +-------+-------+-------
        +-------+-------+-------+-------+-------+-------
        ||||||||||||||||||||||                         buffer
        +-------+-------+-------+-------+-------+-------
                        |
                        bufferPos
        */
        int sourcePos = 0; // index of leftmost source byte containing data (1 to 8 bits).
        int sourceGap = (8 - ((int)sourceBits &amp; 7)) &amp; 7; // space on source[sourcePos].
        int bufferRem = bufferBits &amp; 7; // occupied bits on buffer[bufferPos].
        int b;
        // tally the length of the added data:
        long value = sourceBits;
        for (int i = 31, carry = 0; i &gt;= 0; i--) {
            carry += (bitLength[i] &amp; 0xff) + ((int)value &amp; 0xff);
            bitLength[i] = (byte)carry;
            carry &gt;&gt;&gt;= 8;
            value &gt;&gt;&gt;= 8;
        }
        // process data in chunks of 8 bits:
        while (sourceBits &gt; 8) { // at least source[sourcePos] and source[sourcePos+1] contain data.
            // take a byte from the source:
            b = ((source[sourcePos] &lt;&lt; sourceGap) &amp; 0xff) |
                    ((source[sourcePos + 1] &amp; 0xff) &gt;&gt;&gt; (8 - sourceGap));
            if (b &lt; 0 || b &gt;= 256) {
                throw new RuntimeException("LOGIC ERROR");
            }
            // process this byte:
            buffer[bufferPos++] |= b &gt;&gt;&gt; bufferRem;
            bufferBits += 8 - bufferRem; // bufferBits = 8*bufferPos;
            if (bufferBits == 512) {
                // process data block:
                processBuffer();
                // reset buffer:
                bufferBits = bufferPos = 0;
            }
            buffer[bufferPos] = (byte)((b &lt;&lt; (8 - bufferRem)) &amp; 0xff);
            bufferBits += bufferRem;
            // proceed to remaining data:
            sourceBits -= 8;
            sourcePos++;
        }
        // now 0 &lt;= sourceBits &lt;= 8;
        // furthermore, all data (if any is left) is in source[sourcePos].
        if (sourceBits &gt; 0) {
            b = (source[sourcePos] &lt;&lt; sourceGap) &amp; 0xff; // bits are left-justified on b.
            // process the remaining bits:
            buffer[bufferPos] |= b &gt;&gt;&gt; bufferRem;
        } else {
            b = 0;
        }
        if (bufferRem + sourceBits &lt; 8) {
            // all remaining data fits on buffer[bufferPos], and there still remains some space.
            bufferBits += sourceBits;
        } else {
            // buffer[bufferPos] is full:
            bufferPos++;
            bufferBits += 8 - bufferRem; // bufferBits = 8*bufferPos;
            sourceBits -= 8 - bufferRem;
            // now 0 &lt;= sourceBits &lt; 8; furthermore, all data is in source[sourcePos].
            if (bufferBits == 512) {
                // process data block:
                processBuffer();
                // reset buffer:
                bufferBits = bufferPos = 0;
            }
            buffer[bufferPos] = (byte)((b &lt;&lt; (8 - bufferRem)) &amp; 0xff);
            bufferBits += (int)sourceBits;
        }
    }
    
    /**
     * Get the hash value from the hashing state.
     *
     * This method uses the invariant: bufferBits &lt; 512
     */
    public void NESSIEfinalize(byte[] digest) {
        // append a '1'-bit:
        buffer[bufferPos] |= 0x80 &gt;&gt;&gt; (bufferBits &amp; 7);
        bufferPos++; // all remaining bits on the current byte are set to zero.
        // pad with zero bits to complete 512N + 256 bits:
        if (bufferPos &gt; 32) {
            while (bufferPos &lt; 64) {
                buffer[bufferPos++] = 0;
            }
            // process data block:
            processBuffer();
            // reset buffer:
            bufferPos = 0;
        }
        while (bufferPos &lt; 32) {
            buffer[bufferPos++] = 0;
        }
        // append bit length of hashed data:
        System.arraycopy(bitLength, 0, buffer, 32, 32);
        // process data block:
        processBuffer();
        // return the completed message digest:
        for (int i = 0, j = 0; i &lt; 8; i++, j += 8) {
            long h = hash[i];
            digest[j    ] = (byte)(h &gt;&gt;&gt; 56);
            digest[j + 1] = (byte)(h &gt;&gt;&gt; 48);
            digest[j + 2] = (byte)(h &gt;&gt;&gt; 40);
            digest[j + 3] = (byte)(h &gt;&gt;&gt; 32);
            digest[j + 4] = (byte)(h &gt;&gt;&gt; 24);
            digest[j + 5] = (byte)(h &gt;&gt;&gt; 16);
            digest[j + 6] = (byte)(h &gt;&gt;&gt;  8);
            digest[j + 7] = (byte)(h       );
        }
    }
    
    /**
     * Delivers string input data to the hashing algorithm.
     *
     * @param   source      plaintext data to hash (ASCII text string).
     *
     * This method maintains the invariant: bufferBits &lt; 512
     */
    public void NESSIEadd(String source) {
        if (source.length() &gt; 0) {
            byte[] data = new byte[source.length()];
            for (int i = 0; i &lt; source.length(); i++) {
                data[i] = (byte)source.charAt(i);
            }
            NESSIEadd(data, 8*data.length);
        }
    }
    
    public static String display(byte[] array) {
        char[] val = new char[2*array.length];
        //String hex = "0123456789ABCDEF";
        String hex = "0123456789abcdef";
        for (int i = 0; i &lt; array.length; i++) {
            int b = array[i] &amp; 0xff;
            val[2*i] = hex.charAt(b &gt;&gt;&gt; 4);
            val[2*i + 1] = hex.charAt(b &amp; 15);
        }
        return String.valueOf(val);
    }
    [/pre]
    }
    [/code]
     
  2. Tee7even

    Tee7even Nukkit Coders Team

    Messages:
    123
    Likes Received:
    27
    Плагины авторизации - это, конечно, хорошо, но Mojang все обещали в PE свою систему аккаунтов ввести, да вот никак пока не случается...
     
  3. fromgate

    fromgate Administrator

    Messages:
    664
    Likes Received:
    186
    Ну сейчас это актуально. А вот с внедрением системы Mojang актуальность снизится. Но если для MCW10E и MCPE будет разная система аккаунтов (MCW10E всё-таки уже привязан к Xbox Live), то плагины авторизации никуда не денутся...
     
  4. Tee7even

    Tee7even Nukkit Coders Team

    Messages:
    123
    Likes Received:
    27
    Если они решат ввести свою систему аккаунтов, то скорее всего на W10 Xbox Live будет использоваться только для мультиплеера с использованием этого самого Xbox Live. Не знаю как они решат, но так они это точно не оставят, иначе толку от ввода своей системы аккаунтов, как от ношения воды в решете.
     
  5. fromgate

    fromgate Administrator

    Messages:
    664
    Likes Received:
    186
    Ну почему же. Они вполне могут сделать авторизацию через Live и на адройдо-айфонах. Причем могут сделать её необходимой толькои при игре на Realms. Но как это будет - непонятно.
    Но вообще привязка к Live - логична, привязка к Mojang-аккаунтам тоже. Однаков MCW10E уже привязан к Live (я не могу сменить никнейм), поэтому я бы больше поставил на Live.
    Поживём - увидим.
     
  6. Tee7even

    Tee7even Nukkit Coders Team

    Messages:
    123
    Likes Received:
    27
    Через Live на Android и Apple устройствах? Звучит смешновато. А про "только ради Realms" они вроде говорили, что именно полностью собираются привязывать свои аккаунты, а не как при тестах Realms. Впрочем пытаясь нарыть пруф о том, я нашел и более интересные вещи, только это уже оффтоп.
    scn53HY08IU.jpg
     
  7. fromgate

    fromgate Administrator

    Messages:
    664
    Likes Received:
    186
    @Tee7even
    Главное, чтобы не "тяжеловато" ;)
    Я помню, как приходилось регистрироваться в GTA4 сразу через аккаунты RS Social Club и Microsoft Live, поэтому это меня вообще не удивит.
    Но, думаю, более вероятно что будет привязка к Mojang-аккаунтам. Поживём - увидим )
     
  8. Fi3iK(ZeleninGerman)

    Fi3iK(ZeleninGerman) Developer

    Messages:
    18
    Likes Received:
    4
    Minecraft:
    Fi3iK
    А не легче просто вносить пароли игроков в файл password.yml
    Тогда просто можно реализовать...
    Типо этого:
    nickname: password
    Я не понимаю, зачем шифровать?
    На случай "Отгонки данных"?
     
  9. xpyctum

    xpyctum Pioneer

    Messages:
    16
    Likes Received:
    1
    Minecraft:
    xpyctum
    Этот вопрос к Шогги, который создал SA.
     
  10. fromgate

    fromgate Administrator

    Messages:
    664
    Likes Received:
    186
    Именно на этот случай. Многие игроки используют один и тот же пароль к разным играм, аккаунтам и т.п.
    Утечка такой базы (по той или иной причине) в случае использования открытых паролей может привести к негативным результатам. Тогда как утечка хэшей - не так страшна - быстрого обратного решения по получению пароля из хэша не существует.
     
  11. fromgate

    fromgate Administrator

    Messages:
    664
    Likes Received:
    186
    Вопрос не в хранении хэшей - это вполне оправданно. Вопрос в том, что Шогги явно не удосужился изучить вопрос и написал код исходя из принципа "чем сложнее - тем страшнее".
     
  12. Fi3iK(ZeleninGerman)

    Fi3iK(ZeleninGerman) Developer

    Messages:
    18
    Likes Received:
    4
    Minecraft:
    Fi3iK
    Для администратора интереснее работать"открытыми" паролями;)
     
  13. fromgate

    fromgate Administrator

    Messages:
    664
    Likes Received:
    186
    Ага, а потом получать подозрения: "он знал мой пароль, значит аккаунт в стим увёл тоже он" ;) Н
     

Share This Page